Skip to content

Commit 43e60cd

Browse files
author
Xiting Zhang
committed
Merge remote-tracking branch 'upstream/main'
2 parents 9ceca94 + 949e5b8 commit 43e60cd

File tree

47 files changed

+979
-899
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+979
-899
lines changed

eng/apiview_reqs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ tomli==2.2.1
1414
tomlkit==0.13.2
1515
typing_extensions==4.12.2
1616
wrapt==1.17.2
17-
apiview-stub-generator==0.3.22
17+
apiview-stub-generator==0.3.23

eng/common/scripts/Helpers/CommandInvocation-Helpers.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ function Invoke-LoggedCommand
5454
if($LastExitCode -notin $AllowedExitCodes)
5555
{
5656
LogError "Command failed to execute ($duration): $Command`n"
57+
58+
# This fix reproduces behavior that existed before
59+
# https://github.com/Azure/azure-sdk-tools/pull/12235
60+
# Before that change, if a command failed Write-Error was always
61+
# invoked in the failure case. Today, LogError only does Write-Error
62+
# when running locally (not in a CI environment)
63+
if ((Test-SupportsDevOpsLogging) -or (Test-SupportsGitHubLogging)) {
64+
Write-Error "Command failed to execute ($duration): $Command`n"
65+
}
5766
}
5867
else {
5968
Write-Host "Command succeeded ($duration)`n"

eng/tools/azure-sdk-tools/ci_tools/parsing/parse_functions.py

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -156,46 +156,31 @@ def _normalize_url_fields(pkg_info, metadata: Dict[str, Any]) -> None:
156156
# Homepage from PEP 566 style
157157
if pkg_info.home_page:
158158
metadata["homepage"] = pkg_info.home_page
159-
160-
# Handle project URLs (can be in various formats)
161-
if pkg_info.project_urls:
162-
metadata["project_urls"] = pkg_info.project_urls
163-
164-
# Try to extract homepage from project_urls if not already set
165-
if "homepage" not in metadata:
166-
homepage = _extract_homepage_from_project_urls(pkg_info.project_urls)
167-
if homepage:
168-
metadata["homepage"] = homepage
159+
elif pkg_info.project_urls: # If homepage not found, try to extract from project_urls
160+
if isinstance(pkg_info.project_urls, (list, tuple)):
161+
for url_entry in pkg_info.project_urls:
162+
if isinstance(url_entry, str) and "," in url_entry:
163+
# Format: "Label, https://example.com"
164+
label, url_value = url_entry.split(",", 1)
165+
label_lower = label.strip().lower()
166+
url_value = url_value.strip()
167+
if label_lower in ["homepage", "home-page", "home"]:
168+
metadata["homepage"] = url_value
169+
elif label_lower == "repository":
170+
metadata["repository"] = url_value
171+
elif isinstance(pkg_info.project_urls, dict):
172+
for key, value in pkg_info.project_urls.items():
173+
key_lower = key.lower()
174+
if key_lower in ["homepage", "home-page", "home"]:
175+
metadata["homepage"] = value
176+
elif key_lower == "repository":
177+
metadata["repository"] = value
169178

170179
# Download URL
171180
if hasattr(pkg_info, "download_url") and getattr(pkg_info, "download_url", None):
172181
metadata["download_url"] = pkg_info.download_url
173182

174183

175-
def _extract_homepage_from_project_urls(project_urls) -> Optional[str]:
176-
"""Extract homepage URL from project_urls in various formats."""
177-
if not project_urls:
178-
return None
179-
180-
# Handle different project_urls formats
181-
if isinstance(project_urls, (list, tuple)):
182-
for url_entry in project_urls:
183-
if isinstance(url_entry, str) and "," in url_entry:
184-
# Format: "Homepage, https://example.com"
185-
url_type, url_value = url_entry.split(",", 1)
186-
url_type = url_type.strip().lower()
187-
url_value = url_value.strip()
188-
if url_type in ["homepage", "home-page", "home", "website"]:
189-
return url_value
190-
elif isinstance(project_urls, dict):
191-
# Handle dictionary format
192-
for key, value in project_urls.items():
193-
if key.lower() in ["homepage", "home-page", "home", "website"]:
194-
return value
195-
196-
return None
197-
198-
199184
def _add_optional_fields(pkg_info, metadata: Dict[str, Any]) -> None:
200185
"""Add optional metadata fields that may be present."""
201186
optional_fields = ["obsoletes_dist", "provides_dist", "requires_external", "platform", "supported_platform"]

eng/tools/azure-sdk-tools/devtools_testutils/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
this = sys.modules[__name__]
2020
this.recording_ids = {}
2121

22+
2223
def locate_assets(current_test_file: str) -> str:
2324
"""Locate the test assets directory for the targeted testfile.
2425
@@ -75,6 +76,7 @@ def locate_assets(current_test_file: str) -> str:
7576

7677
raise FileNotFoundError(f"No matching breadcrumb file found for asset path {relative_asset_path}")
7778

79+
7880
def get_http_client(**kwargs):
7981
"""Returns a `urllib3` client that provides the test proxy's self-signed certificate if it's available.
8082

eng/tools/azure-sdk-tools/tests/integration/scenarios/pyproject_invalid_metadata/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "azure-storage-blob"
77
authors = [
8-
{email = "[email protected]"},
8+
{name = "Microsoft Corporation", email = "[email protected]"},
99
]
1010
description = "Microsoft Azure Blob Storage Client Library for Python"
1111
keywords = ["azure", "azure sdk"]
@@ -35,7 +35,7 @@ aio = [
3535
]
3636

3737
[project.urls]
38-
repository = "https://github.com/Azure/azure-sdk-for-python"
38+
source = "https://github.com/Azure/azure-sdk-for-python"
3939

4040
[tool.setuptools.dynamic]
4141
version = {attr = "azure.storage.blob._version.VERSION"}

eng/tools/azure-sdk-tools/tests/integration/scenarios/pyproject_metadata/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ aio = [
3535
]
3636

3737
[project.urls]
38-
homepage = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob"
3938
repository = "https://github.com/Azure/azure-sdk-for-python"
4039

4140
[tool.setuptools.dynamic]

eng/tools/azure-sdk-tools/tests/test_metadata_verification.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,15 @@ def test_verify_valid_metadata_passes(package_type, scenario_name, scenario_path
113113

114114

115115
@pytest.mark.parametrize(
116-
"package_type,scenario_name,scenario_path",
116+
"package_type,scenario_name,scenario_path,missing_keys",
117117
[
118-
("wheel", "stable", "pyproject_invalid_metadata_scenario"),
119-
("sdist", "stable", "pyproject_invalid_metadata_scenario"),
120-
("wheel", "beta", "pyproject_beta_invalid_metadata_scenario"),
121-
("sdist", "beta", "pyproject_beta_invalid_metadata_scenario"),
118+
("wheel", "stable", "pyproject_invalid_metadata_scenario", ["homepage", "repository"]),
119+
("sdist", "stable", "pyproject_invalid_metadata_scenario", ["homepage", "repository"]),
120+
("wheel", "beta", "pyproject_beta_invalid_metadata_scenario", ["author_email", "summary"]),
121+
("sdist", "beta", "pyproject_beta_invalid_metadata_scenario", ["author_email", "summary"]),
122122
],
123123
)
124-
def test_verify_invalid_metadata_fails_with_missing_keys(package_type, scenario_name, scenario_path, caplog):
124+
def test_verify_invalid_metadata_fails(package_type, scenario_name, scenario_path, missing_keys, caplog):
125125
"""Test that verify_whl/verify_sdist fails for scenarios with invalid metadata and reports missing author_name and homepage."""
126126
# Get the actual scenario path from globals
127127
actual_scenario_path = globals()[scenario_path]
@@ -154,22 +154,20 @@ def test_verify_invalid_metadata_fails_with_missing_keys(package_type, scenario_
154154
# Check that the error log contains information about missing keys
155155
error_logs = [record.message for record in caplog.records if record.levelname == "ERROR"]
156156

157-
# Different scenarios have different missing keys
158-
if scenario_name == "stable":
159-
# Stable scenario is missing author name and homepage
160-
expected_missing_keys = ["author", "homepage"]
161-
else: # beta scenario
162-
# Beta scenario is missing author email and description
163-
expected_missing_keys = ["author_email", "summary"]
164-
165-
# Check for either order of the missing keys
166-
missing_keys_pattern1 = f"Missing keys: {{'{expected_missing_keys[0]}', '{expected_missing_keys[1]}'}}"
167-
missing_keys_pattern2 = f"Missing keys: {{'{expected_missing_keys[1]}', '{expected_missing_keys[0]}'}}"
168-
has_missing_keys_error = any(missing_keys_pattern1 in msg or missing_keys_pattern2 in msg for msg in error_logs)
169-
170-
assert (
171-
has_missing_keys_error
172-
), f"Expected error log about missing keys '{expected_missing_keys[0]}' and '{expected_missing_keys[1]}' for {scenario_name} scenario, but got: {error_logs}"
157+
# Raise error if homepage AND repository not found in current version
158+
if "homepage" in missing_keys:
159+
assert f"Current metadata must contain at least one of: {missing_keys}" in error_logs
160+
# Otherwise, check for missing keys from prior version
161+
else:
162+
missing_keys_pattern1 = f"Missing keys: {{'{missing_keys[0]}', '{missing_keys[1]}'}}"
163+
missing_keys_pattern2 = f"Missing keys: {{'{missing_keys[1]}', '{missing_keys[0]}'}}"
164+
has_missing_keys_error = any(
165+
missing_keys_pattern1 in msg or missing_keys_pattern2 in msg for msg in error_logs
166+
)
167+
168+
assert (
169+
has_missing_keys_error
170+
), f"Expected error log about Missing keys: '{missing_keys[0]}' and '{missing_keys[1]}' for {scenario_name} scenario, but got: {error_logs}"
173171

174172
finally:
175173
# Cleanup dist directory

eng/tox/verify_whl.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,47 @@ def verify_prior_version_metadata(package_name: str, prior_version: str, current
125125
f"{package_name}=={prior_version}", "--dest", tmp_dir
126126
], check=True, capture_output=True)
127127
zip_files = glob.glob(os.path.join(tmp_dir, package_type))
128-
if not zip_files:
128+
# If no match and we're not constrained to wheel-only, attempt legacy sdist (zip) once.
129+
if not zip_files and package_type != "*.whl":
130+
zip_files = glob.glob(os.path.join(tmp_dir, "*.zip"))
131+
if not zip_files: # Still nothing -> treat as no prior artifact to compare.
129132
return True
130133

131134
prior_metadata: Dict[str, Any] = extract_package_metadata(zip_files[0])
132135
is_compatible = verify_metadata_compatibility(current_metadata, prior_metadata)
133-
if not is_compatible:
134-
missing_keys = set(prior_metadata.keys()) - set(current_metadata.keys())
135-
logging.error(f"Metadata compatibility failed for {package_name}. Missing keys: {missing_keys}")
136136
return is_compatible
137137
except Exception:
138138
return True
139139

140140

141141
def verify_metadata_compatibility(current_metadata: Dict[str, Any], prior_metadata: Dict[str, Any]) -> bool:
142-
"""Verify that all keys from prior version metadata are present in current version."""
143-
if not prior_metadata:
144-
return True
142+
"""Verify that all keys from prior version metadata are present in current version.
143+
144+
Special handling: homepage/repository keys are exempt from prior compatibility check,
145+
but current version must have at least one of them.
146+
"""
145147
if not current_metadata:
146148
return False
147-
return set(prior_metadata.keys()).issubset(set(current_metadata.keys()))
149+
# Check that current version has at least one homepage or repository URL
150+
repo_urls = ['homepage', 'repository']
151+
current_keys_lower = {k.lower() for k in current_metadata.keys()}
152+
if not any(key in current_keys_lower for key in repo_urls):
153+
logging.error(f"Current metadata must contain at least one of: {repo_urls}")
154+
return False
155+
156+
if not prior_metadata:
157+
return True
158+
159+
# For backward compatibility check, exclude homepage/repository from prior requirements
160+
prior_keys_filtered = {k for k in prior_metadata.keys() if k.lower() not in repo_urls}
161+
current_keys = set(current_metadata.keys())
162+
163+
is_compatible = prior_keys_filtered.issubset(current_keys)
164+
if not is_compatible:
165+
missing_keys = prior_keys_filtered - current_keys
166+
logging.error("Metadata compatibility failed. Missing keys: %s", missing_keys)
167+
return is_compatible
168+
148169

149170
def get_path_to_zip(dist_dir: str, version: str, package_type: str = "*.whl") -> str:
150171
return glob.glob(os.path.join(dist_dir, "**", "*{}{}".format(version, package_type)), recursive=True)[0]

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,9 +1444,11 @@ async def get_throughput(
14441444
return _deserialize_throughput(throughput=throughput_properties)
14451445

14461446
@distributed_trace_async
1447-
async def replace_throughput(
1447+
async def replace_throughput( # pylint: disable=unused-argument
14481448
self,
14491449
throughput: Union[int, ThroughputProperties],
1450+
*,
1451+
response_hook: Optional[Callable[[Mapping[str, Any], CosmosDict], None]] = None,
14501452
**kwargs: Any
14511453
) -> ThroughputProperties:
14521454
"""Replace the container's throughput.
@@ -1456,7 +1458,7 @@ async def replace_throughput(
14561458
:param throughput: The throughput to be set.
14571459
:type throughput: Union[int, ~azure.cosmos.ThroughputProperties]
14581460
:keyword response_hook: A callable invoked with the response metadata.
1459-
:paramtype response_hook: Callable[[Dict[str, str], Dict[str, Any]], None]
1461+
:paramtype response_hook: Callable[[Mapping[str, Any], CosmosDict], None]
14601462
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: No throughput properties exist for the container
14611463
or the throughput properties could not be updated.
14621464
:returns: ThroughputProperties for the container, updated with new throughput.

sdk/cosmos/azure-cosmos/azure/cosmos/container.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,9 +1625,11 @@ def get_throughput(
16251625
return _deserialize_throughput(throughput=throughput_properties)
16261626

16271627
@distributed_trace
1628-
def replace_throughput(
1628+
def replace_throughput( # pylint: disable=unused-argument
16291629
self,
16301630
throughput: Union[int, ThroughputProperties],
1631+
*,
1632+
response_hook: Optional[Callable[[Mapping[str, Any], CosmosDict], None]] = None,
16311633
**kwargs: Any
16321634
) -> ThroughputProperties:
16331635
"""Replace the container's throughput.
@@ -1636,6 +1638,8 @@ def replace_throughput(
16361638
16371639
:param throughput: The throughput to be set.
16381640
:type throughput: Union[int, ~azure.cosmos.ThroughputProperties]
1641+
:keyword response_hook: A callable invoked with the response metadata.
1642+
:paramtype response_hook: Callable[[Mapping[str, Any], CosmosDict], None]
16391643
:returns: ThroughputProperties for the container, updated with new throughput.
16401644
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: No throughput properties exist for the container
16411645
or the throughput properties could not be updated.

0 commit comments

Comments
 (0)