Skip to content

Commit 26cf736

Browse files
scbeddCopilotmccoyp
authored
Add Classifier Verification (#41610)
* enhance verify_whl tox environment to check that the classifier for the package matches the package version being released --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: McCoy Patiño <39780829+mccoyp@users.noreply.github.com>
1 parent bebf8d2 commit 26cf736

File tree

5 files changed

+95
-16
lines changed

5 files changed

+95
-16
lines changed

doc/eng_sys_checks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [Skipping a tox test environment at build queue time](#skipping-a-tox-test-environment-at-build-queue-time)
66
- [Skipping entire sections of builds](#skipping-entire-sections-of-builds)
77
- [The pyproject.toml](#the-pyprojecttoml)
8+
- [Coverage Enforcement](#coverage-enforcement)
89
- [Environment variables important to CI](#environment-variables-important-to-ci)
910
- [Atomic Overrides](#atomic-overrides)
1011
- [Enable test logging in CI pipelines](#enable-test-logging-in-ci-pipelines)
@@ -13,6 +14,7 @@
1314
- [Pyright](#pyright)
1415
- [Verifytypes](#verifytypes)
1516
- [Pylint](#pylint)
17+
- [Sphinx and docstring checker](#sphinx-and-docstring-checker)
1618
- [Bandit](#bandit)
1719
- [ApiStubGen](#apistubgen)
1820
- [black](#black)

eng/tox/verify_sdist.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
from typing import List, Mapping, Any
2121

2222
from ci_tools.parsing import ParsedSetup
23+
from ci_tools.functions import verify_package_classifiers
2324

2425
logging.getLogger().setLevel(logging.INFO)
2526

2627
ALLOWED_ROOT_DIRECTORIES = ["azure", "tests", "samples", "examples"]
2728

2829
EXCLUDED_PYTYPE_PACKAGES = ["azure-keyvault", "azure", "azure-common"]
2930

31+
EXCLUDED_CLASSIFICATION_PACKAGES = []
32+
3033

3134
def get_root_directories_in_source(package_dir: str) -> List[str]:
3235
"""
@@ -138,18 +141,34 @@ def verify_sdist_pytyped(
138141
pkg_dir = os.path.abspath(args.target_package)
139142
pkg_details = ParsedSetup.from_path(pkg_dir)
140143

144+
error_occurred = False
145+
141146
if should_verify_package(pkg_details.name):
142-
logging.info("Verifying sdist for package [%s]", pkg_details.name)
147+
logging.info(f"Verifying sdist for package {pkg_details.name}")
143148
if verify_sdist(pkg_dir, args.dist_dir, pkg_details.version):
144-
logging.info("Verified sdist for package [%s]", pkg_details.name)
149+
logging.info(f"Verified sdist for package {pkg_details.name}")
145150
else:
146-
logging.info("Failed to verify sdist for package [%s]", pkg_details.name)
147-
exit(1)
151+
logging.error(f"Failed to verify sdist for package {pkg_details.name}")
152+
error_occurred = True
148153

149154
if pkg_details.name not in EXCLUDED_PYTYPE_PACKAGES and "-nspkg" not in pkg_details.name and "-mgmt" not in pkg_details.name:
150-
logging.info("Verifying presence of py.typed: [%s]", pkg_details.name)
155+
logging.info(f"Verifying presence of py.typed: {pkg_details.name}")
151156
if verify_sdist_pytyped(pkg_dir, pkg_details.namespace, pkg_details.package_data, pkg_details.include_package_data):
152-
logging.info("Py.typed setup.py kwargs are set properly: [%s]", pkg_details.name)
157+
logging.info(f"Py.typed setup.py kwargs are set properly: {pkg_details.name}")
158+
else:
159+
logging.error(f"Py.typed verification failed for package {pkg_details.name}. Check messages above.")
160+
error_occurred = True
161+
162+
if pkg_details.name not in EXCLUDED_CLASSIFICATION_PACKAGES and "-nspkg" not in pkg_details.name:
163+
logging.info(f"Verifying package classifiers: {pkg_details.name}")
164+
165+
status, message = verify_package_classifiers(pkg_details.name, pkg_details.version, pkg_details.classifiers)
166+
if status:
167+
logging.info(f"Package classifiers are set properly: {pkg_details.name}")
153168
else:
154-
logging.info("Verified py.typed [%s]. Check messages above.", pkg_details.name)
155-
exit(1)
169+
logging.error(f"{message}")
170+
error_occurred = True
171+
172+
if error_occurred:
173+
logging.error(f"{pkg_details.name} failed sdist verification. Check outputs above.")
174+
exit(1)

sdk/textanalytics/azure-ai-textanalytics/LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
21+
SOFTWARE.

tools/azure-sdk-tools/ci_tools/functions.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pypi_tools.pypi import PyPIClient
1616

1717
import os, sys, platform, glob, re, logging
18-
from typing import List, Any, Optional
18+
from typing import List, Any, Optional, Tuple
1919

2020
INACTIVE_CLASSIFIER = "Development Status :: 7 - Inactive"
2121

@@ -42,10 +42,7 @@
4242
"azure-mgmt-core",
4343
]
4444

45-
TEST_COMPATIBILITY_MAP = {
46-
"azure-ai-ml": ">=3.7",
47-
"azure-ai-evaluation": ">=3.9, !=3.13.*"
48-
}
45+
TEST_COMPATIBILITY_MAP = {"azure-ai-ml": ">=3.7", "azure-ai-evaluation": ">=3.9, !=3.13.*"}
4946
TEST_PYTHON_DISTRO_INCOMPATIBILITY_MAP = {
5047
"azure-storage-blob": "pypy",
5148
"azure-storage-queue": "pypy",
@@ -697,7 +694,9 @@ def is_package_compatible(
697694
return True
698695

699696

700-
def get_total_coverage(coverage_file: str, coverage_config_file: str, package_name: str, repo_root: Optional[str] = None) -> Optional[float]:
697+
def get_total_coverage(
698+
coverage_file: str, coverage_config_file: str, package_name: str, repo_root: Optional[str] = None
699+
) -> Optional[float]:
701700
try:
702701
import coverage
703702
from coverage.exceptions import NoDataError
@@ -717,7 +716,9 @@ def get_total_coverage(coverage_file: str, coverage_config_file: str, package_na
717716
try:
718717
if repo_root:
719718
os.chdir(repo_root)
720-
logging.info(f"Running coverage report against \"{coverage_file}\" with \"{coverage_config_file}\" from \"{os.getcwd()}\".")
719+
logging.info(
720+
f'Running coverage report against "{coverage_file}" with "{coverage_config_file}" from "{os.getcwd()}".'
721+
)
721722
report = cov.report()
722723
except NoDataError as e:
723724
logging.info(f"Package {package_name} did not generate any coverage output: {e}")
@@ -891,3 +892,38 @@ def handle_incompatible_minimum_dev_reqs(
891892
cleansed_reqs.append(cleansed_dev_requirement_line)
892893

893894
return cleansed_reqs
895+
896+
897+
def verify_package_classifiers(package_name: str, package_version: str, package_classifiers: List[str]) -> Tuple[bool, Optional[str]]:
898+
"""
899+
Verify that the package classifiers match the expected classifiers.
900+
:param str package_name: The name of the package being verified. Used for detail in the error response.
901+
:param str package_version: The version of the package being verified.
902+
:param List[str] package_classifiers: The classifiers of the package being verified.
903+
:returns: A tuple, (x, y), where x is whether the package version matches its classifiers, and y is an error message or None.
904+
"""
905+
906+
dev_status = parse(package_version)
907+
908+
# gather all development‐status classifiers
909+
dev_classifiers = [c for c in package_classifiers if c.startswith("Development Status ::")]
910+
911+
# beta releases: enforce that only development status 4 is present
912+
if dev_status.is_prerelease:
913+
for c in dev_classifiers:
914+
if "4 - Beta" not in c:
915+
return False, f"{package_name} has version {package_version} and is a beta release, but has development status '{c}'. Expected 'Development Status :: 4 - Beta' ONLY."
916+
return True, None
917+
918+
# ga releases: all development statuses must be >= 5
919+
for c in dev_classifiers:
920+
try:
921+
# "Development Status :: 5 - Production/Stable"
922+
# or Development Status :: 6 - Mature
923+
# or Development Status :: 7 - Inactive
924+
num = int(c.split("::")[1].split("-")[0].strip())
925+
except (IndexError, ValueError):
926+
return False, f"{package_name} has version {package_version} and is a GA release, but failed to pull a status number from status '{c}'. Expecting format identical to 'Development Status :: 5 - Production/Stable'."
927+
if num < 5:
928+
return False, f"{package_name} has version {package_version} and is a GA release, but had development status '{c}'. Expecting a development classifier that is equal or greater than 'Development Status :: 5 - Production/Stable'."
929+
return True, None
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import pytest
2+
3+
from ci_tools.functions import verify_package_classifiers
4+
5+
6+
@pytest.mark.parametrize(
7+
"package_name, package_version, package_classifiers, expected_result",
8+
[
9+
("a", "1.0.0", ["Development Status :: 4 - Beta"], False),
10+
("b", "1.0.0", ["Development Status :: 5 - Production/Stable"], True),
11+
("b", "1.0.0a1", ["Development Status :: 5 - Production/Stable"], False),
12+
("b", "1.0.0a1", ["Development Status :: 4 - Beta"], True),
13+
("c", "1.0.0b1", ["Development Status :: 4 - Beta"], True),
14+
("c", "1.0.0b1", ["Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable"], False),
15+
("c", "1.0.0", ["Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable"], False),
16+
("c", "1.0.1", ["Development Status :: 7 - Inactive"], True),
17+
("c", "1.0.1b1", ["Development Status :: 7 - Inactive"], False),
18+
],
19+
)
20+
def test_classifier_enforcement(package_name, package_version, package_classifiers, expected_result):
21+
result = verify_package_classifiers(package_name, package_version, package_classifiers)
22+
assert result[0] is expected_result

0 commit comments

Comments
 (0)