Skip to content

Commit 46b446c

Browse files
scbeddmccoyp
andauthored
Enforce proper usage of py.typed file (Azure#24409)
*update verify_sdist.py to also validate the configuration of the package WRT py.typed. Enforces setup.py config, MANIFEST.in config, and checks the existence of a py.typed file at the correct folder depth. Co-authored-by: McCoy Patiño <39780829+mccoyp@users.noreply.github.com>
1 parent c29c3a1 commit 46b446c

11 files changed

+89
-22
lines changed

eng/tox/create_package_and_install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def discover_packages(setuppy_path, args):
5757

5858
def discover_prebuilt_package(dist_directory, setuppy_path, package_type):
5959
packages = []
60-
pkg_name, _, version = get_package_details(setuppy_path)
60+
pkg_name, _, version, _, _ = get_package_details(setuppy_path)
6161
if package_type == "wheel":
6262
prebuilt_package = find_whl(dist_directory, pkg_name, version)
6363
else:

eng/tox/import_all.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def should_run_import_all(package_name):
4141

4242
# get target package name from target package path
4343
pkg_dir = os.path.abspath(args.target_package)
44-
package_name, namespace, _ = get_package_details(os.path.join(pkg_dir, 'setup.py'))
44+
package_name, namespace, _, _, _ = get_package_details(os.path.join(pkg_dir, 'setup.py'))
4545

4646
if should_run_import_all(package_name):
4747
# import all modules from current package

eng/tox/install_dev_build_dependency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def install_dev_build_packages(pkg_name_to_exclude):
120120
if args.target_package:
121121
# get target package name from target package path
122122
pkg_dir = path.abspath(args.target_package)
123-
pkg_name, _, ver = get_package_details(path.join(pkg_dir, "setup.py"))
123+
pkg_name, _, ver, _, _ = get_package_details(path.join(pkg_dir, "setup.py"))
124124
install_dev_build_packages(pkg_name)
125125

126126

eng/tox/prep_sphinx_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def write_version(site_folder, version):
138138
args = parser.parse_args()
139139

140140
package_path = os.path.abspath(args.target_package)
141-
package_name, namespace, package_version = get_package_details(
141+
package_name, namespace, package_version, _, _ = get_package_details(
142142
os.path.join(package_path, "setup.py")
143143
)
144144

eng/tox/run_apistubgen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
def get_package_wheel_path(pkg_root):
2020
# parse setup.py to get package name and version
21-
pkg_name, _, version = get_package_details(os.path.join(pkg_root, "setup.py"))
21+
pkg_name, _, version, _, _ = get_package_details(os.path.join(pkg_root, "setup.py"))
2222
# Check if wheel is already built and available for current package
2323
prebuilt_dir = os.getenv("PREBUILT_WHEEL_DIR")
2424
if prebuilt_dir:

eng/tox/run_pylint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444

4545
pkg_dir = os.path.abspath(args.target_package)
46-
package_name, namespace, ver = get_package_details(os.path.join(pkg_dir, "setup.py"))
46+
package_name, namespace, ver, _, _ = get_package_details(os.path.join(pkg_dir, "setup.py"))
4747

4848
top_level_module = namespace.split('.')[0]
4949

eng/tox/run_sphinx_apidoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def mgmt_apidoc(working_directory, namespace):
117117
package_dir = os.path.abspath(args.package_root)
118118
output_directory = os.path.join(target_dir, "unzipped/docgen")
119119

120-
pkg_name, namespace, pkg_version = get_package_details(os.path.join(package_dir, 'setup.py'))
120+
pkg_name, namespace, pkg_version, _, _ = get_package_details(os.path.join(package_dir, 'setup.py'))
121121

122122
if should_build_docs(pkg_name):
123123
if is_mgmt_package(pkg_name):

eng/tox/run_sphinx_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def sphinx_build(target_dir, output_dir):
107107
target_dir = os.path.abspath(args.working_directory)
108108
package_dir = os.path.abspath(args.package_root)
109109

110-
package_name, _, pkg_version = get_package_details(os.path.join(package_dir, 'setup.py'))
110+
package_name, _, pkg_version, _, _ = get_package_details(os.path.join(package_dir, 'setup.py'))
111111

112112
if should_build_docs(package_name):
113113
sphinx_build(target_dir, output_dir)

eng/tox/tox_helper_tasks.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,22 @@ def setup(*args, **kwargs):
6060
package_name = kwargs["name"]
6161
# default namespace for the package
6262
name_space = package_name.replace("-", ".")
63+
64+
package_data = None
65+
if "package_data" in kwargs:
66+
package_data = kwargs["package_data"]
67+
68+
include_package_data = None
69+
if "include_package_data" in kwargs:
70+
include_package_data = kwargs["include_package_data"]
71+
6372
if "packages" in kwargs.keys():
6473
packages = kwargs["packages"]
6574
if packages:
6675
name_space = packages[0]
6776
logging.info("Namespaces found for package {0}: {1}".format(package_name, packages))
6877

69-
return package_name, name_space, kwargs["version"]
78+
return package_name, name_space, kwargs["version"], package_data, include_package_data
7079

7180

7281
def parse_req(req):

eng/tox/verify_sdist.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,34 @@
1111
import os
1212
import glob
1313
import shutil
14+
from unicodedata import name
15+
from xmlrpc.client import Boolean
1416
from tox_helper_tasks import (
1517
get_package_details,
1618
unzip_file_to_directory,
1719
)
1820
from verify_whl import cleanup, should_verify_package
21+
from typing import List, Mapping, Any
1922

2023
logging.getLogger().setLevel(logging.INFO)
2124

2225
ALLOWED_ROOT_DIRECTORIES = ["azure", "tests", "samples", "examples"]
2326

27+
EXCLUDED_PYTYPE_PACKAGES = ["azure-keyvault", "azure", "azure-common"]
2428

25-
def get_root_directories_in_source(package_dir):
26-
# find all allowed directories in source path
27-
source_folders = [
28-
d
29-
for d in os.listdir(package_dir)
30-
if os.path.isdir(d) and d in ALLOWED_ROOT_DIRECTORIES
31-
]
29+
30+
def get_root_directories_in_source(package_dir: str) -> List[str]:
31+
"""
32+
Find all allowed directories in source path.
33+
"""
34+
source_folders = [d for d in os.listdir(package_dir) if os.path.isdir(d) and d in ALLOWED_ROOT_DIRECTORIES]
3235
return source_folders
3336

3437

35-
def get_root_directories_in_sdist(dist_dir, version):
38+
def get_root_directories_in_sdist(dist_dir: str, version: str) -> List[str]:
39+
"""
40+
Given an unzipped sdist directory, extract which directories are present.
41+
"""
3642
# find sdist zip file
3743
# extract sdist and find list of directories in sdist
3844
path_to_zip = glob.glob(os.path.join(dist_dir, "*{}*.zip".format(version)))[0]
@@ -44,8 +50,10 @@ def get_root_directories_in_sdist(dist_dir, version):
4450
return sdist_folders
4551

4652

47-
def verify_sdist(package_dir, dist_dir, version):
48-
# This function compares the root directories in source against root directories sdist
53+
def verify_sdist(package_dir: str, dist_dir: str, version: str) -> bool:
54+
"""
55+
Compares the root directories in source against root directories present within a sdist.
56+
"""
4957

5058
source_folders = get_root_directories_in_source(package_dir)
5159
sdist_folders = get_root_directories_in_sdist(dist_dir, version)
@@ -63,9 +71,49 @@ def verify_sdist(package_dir, dist_dir, version):
6371
return True
6472

6573

74+
def verify_sdist_pytyped(
75+
pkg_dir: str, namespace: str, package_metadata: Mapping[str, Any], include_package_data: bool
76+
) -> bool:
77+
"""
78+
Takes a package directory and ensures that the setup.py within is correctly configured for py.typed files.
79+
"""
80+
result = True
81+
manifest_location = os.path.join(pkg_dir, "MANIFEST.in")
82+
83+
if include_package_data is None or False:
84+
logging.info(
85+
"Ensure that the setup.py present in directory {} has kwarg 'include_package_data' defined and set to 'True'."
86+
)
87+
result = False
88+
89+
if package_metadata:
90+
if not any([key for key in package_metadata if "py.typed" in str(package_metadata[key])]):
91+
logging.info(
92+
"At least one value in the package_metadata map should include a reference to the py.typed file."
93+
)
94+
result = False
95+
96+
if os.path.exists(manifest_location):
97+
with open(manifest_location, "r") as f:
98+
lines = f.readlines()
99+
result = any([include for include in lines if "py.typed" in include])
100+
101+
if not result:
102+
logging.info("Ensure that the MANIFEST.in includes at least one path that leads to a py.typed file.")
103+
104+
pytyped_file_path = os.path.join(pkg_dir, *namespace.split("."), "py.typed")
105+
if not os.path.exists(pytyped_file_path):
106+
logging.info(
107+
"The py.typed file must exist in the base namespace for your package. Traditionally this would mean the furthest depth, EG 'azure/storage/blob/py.typed'."
108+
)
109+
result = False
110+
111+
return result
112+
113+
66114
if __name__ == "__main__":
67115
parser = argparse.ArgumentParser(
68-
description="Verify directories included in whl and contents in manifest file"
116+
description="Verify directories included in sdist and contents in manifest file. Also ensures that py.typed configuration is correct within the setup.py."
69117
)
70118

71119
parser.add_argument(
@@ -88,7 +136,9 @@ def verify_sdist(package_dir, dist_dir, version):
88136

89137
# get target package name from target package path
90138
pkg_dir = os.path.abspath(args.target_package)
91-
pkg_name, _, ver = get_package_details(os.path.join(pkg_dir, "setup.py"))
139+
pkg_name, namespace, ver, package_data, include_package_data = get_package_details(
140+
os.path.join(pkg_dir, "setup.py")
141+
)
92142

93143
if should_verify_package(pkg_name):
94144
logging.info("Verifying sdist for package [%s]", pkg_name)
@@ -97,3 +147,11 @@ def verify_sdist(package_dir, dist_dir, version):
97147
else:
98148
logging.info("Failed to verify sdist for package [%s]", pkg_name)
99149
exit(1)
150+
151+
if pkg_name not in EXCLUDED_PYTYPE_PACKAGES and "-nspkg" not in pkg_name:
152+
logging.info("Verifying presence of py.typed: [%s]", pkg_name)
153+
if verify_sdist_pytyped(pkg_dir, namespace, package_data, include_package_data):
154+
logging.info("Py.typed setup.py kwargs are set properly: [%s]", pkg_name)
155+
else:
156+
logging.info("Verified py.typed [%s]. Check messages above.", pkg_name)
157+
exit(1)

0 commit comments

Comments
 (0)