Skip to content

Commit f837589

Browse files
authored
update verifywhl to check that one of homepage/repository exists (#43188)
* update verifywhl check to alias homepage/repository and validate project urls * black * check that homepage or repository link exists, don't check other project.urls * black
1 parent befebbf commit f837589

File tree

6 files changed

+72
-67
lines changed

6 files changed

+72
-67
lines changed

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]

0 commit comments

Comments
 (0)