Skip to content

Commit e608b2a

Browse files
authored
Build script for third-party distributions (#2545)
Part of #2491
1 parent da6e18c commit e608b2a

File tree

3 files changed

+188
-0
lines changed

3 files changed

+188
-0
lines changed

third_party/build/METADATA.tmpl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Metadata-Version: 2.1
2+
Name: $PACKAGE$-ts
3+
Version: $VERSION$
4+
Summary: Type stubs for $PACKAGE$
5+
Description-Content-Type: text/markdown
6+
Keywords: typehints typing type-hints typeshed
7+
Home-page: https://github.com/python/typeshed
8+
Classifier: Intended Audience :: Developers
9+
Classifier: License :: OSI Approved :: Apache Software License
10+
Classifier: Programming Language :: Python
11+
$TROVE_PY$
12+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
13+
Requires-Python: $PY_REQUIRES$
14+
15+
# Type stubs for $PACKAGE$
16+
17+
These type stubs are part of the [typeshed project](https://github.com/python/typeshed).
18+

third_party/build/WHEEL.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Wheel-Version: 1.0
2+
Generator: typeshed
3+
Root-Is-Purelib: true

third_party/build/build-dist.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
3+
# Build a stubs-only distribution for a third-party package.
4+
#
5+
# Usage: python3.7 build-dist.py <PACKAGE>
6+
#
7+
# The resulting wheel will be saved in third-party/build/dist.
8+
9+
import datetime
10+
import os
11+
import re
12+
import shutil
13+
import subprocess
14+
import sys
15+
from dataclasses import dataclass
16+
from pathlib import Path
17+
from typing import List
18+
19+
DIST_SUFFIX = "-ts"
20+
MIN_PYTHON3_VERSION = (3, 4)
21+
MAX_PYTHON3_VERSION = (3, 7)
22+
EXCLUDED_PYTHON_VERSIONS = [(3, i) for i in range(MIN_PYTHON3_VERSION[1])]
23+
TROVE_PREFIX = "Classifier: Programming Language :: Python :: "
24+
25+
pkg_version = "0." + datetime.datetime.now().strftime("%Y%m%d.%H%M")
26+
py_version_re = re.compile(r"^(2and3|\d+(\.\d+)?)$")
27+
28+
base_dir = Path(__file__).parent
29+
root_dir = base_dir.parent.parent
30+
build_dir = base_dir / "build"
31+
dist_dir = base_dir / "dist"
32+
33+
@dataclass
34+
class PackageInfo:
35+
path: Path
36+
name: str
37+
py_version: str
38+
is_module: bool
39+
40+
@property
41+
def stub_name(self) -> str:
42+
return f"{self.name}-stubs"
43+
44+
45+
def py_version_to_wheel_tags(version: str) -> List[str]:
46+
if version == "2and3":
47+
return ["py2-none-any", "py3-none-any"]
48+
else:
49+
return [f"py{version}-none-any"]
50+
51+
52+
def py_version_to_requires(version: str) -> str:
53+
if version == "2":
54+
return ">= 2.7, < 3"
55+
elif version == "2and3":
56+
formatted = [f"!= {ma}.{mi}.*" for ma, mi in EXCLUDED_PYTHON_VERSIONS]
57+
return ">= 2.7, " + ", ".join(formatted)
58+
elif version == "3":
59+
return f">= {MIN_PYTHON3_VERSION[0]}.{MIN_PYTHON3_VERSION[1]}"
60+
else:
61+
return ">= " + version
62+
63+
64+
def py_version_to_trove(version: str) -> List[str]:
65+
py2_versions = ["2", "2.7"]
66+
if version == "2":
67+
versions = py2_versions + ["2 :: Only"]
68+
elif version == "2and3":
69+
versions = py2_versions + py3_versions()
70+
elif version == "3":
71+
versions = py3_versions() + ["3 :: Only"]
72+
else:
73+
versions = py3_versions(int(version[2:])) + ["3 :: Only"]
74+
return [TROVE_PREFIX + v for v in versions]
75+
76+
def py3_versions(min_v: int = MIN_PYTHON3_VERSION[1]) -> List[str]:
77+
assert min_v <= MAX_PYTHON3_VERSION[1]
78+
r = range(min_v, MAX_PYTHON3_VERSION[1] + 1)
79+
return ["3"] + [f"3.{i}" for i in r]
80+
81+
82+
def parse_args() -> str:
83+
if len(sys.argv) != 2:
84+
print(f"Usage: {sys.argv[0]} PACKAGE", file=sys.stderr)
85+
sys.exit(1)
86+
return sys.argv[1]
87+
88+
89+
def find_package(name: str) -> PackageInfo:
90+
for dir_ in base_dir.parent.iterdir():
91+
if not py_version_re.match(dir_.name):
92+
continue
93+
pkg_path = dir_ / name
94+
mod_path = dir_ / f"{name}.pyi"
95+
if pkg_path.is_dir():
96+
return PackageInfo(pkg_path, name, dir_.name, is_module=False)
97+
elif mod_path.is_file():
98+
return PackageInfo(mod_path, name, dir_.name, is_module=True)
99+
print(f"Stubs for package '{name}' not found", file=sys.stderr)
100+
sys.exit(1)
101+
102+
103+
def build_distribution(package: PackageInfo) -> None:
104+
prepare_build_dir(package)
105+
pack_wheel(package)
106+
107+
108+
def prepare_build_dir(package: PackageInfo) -> None:
109+
shutil.rmtree(build_dir, ignore_errors=True)
110+
copy_package(package)
111+
pkg = (package.name + DIST_SUFFIX).replace("-", "_")
112+
dist_info_dir = build_dir / f"{pkg}-{pkg_version}.dist-info"
113+
os.mkdir(dist_info_dir)
114+
shutil.copy(root_dir / "LICENSE", dist_info_dir)
115+
create_wheel_file(package, base_dir / "WHEEL.tmpl",
116+
dist_info_dir / "WHEEL")
117+
create_metadata(package, base_dir / "METADATA.tmpl",
118+
dist_info_dir / "METADATA")
119+
120+
121+
def copy_package(package: PackageInfo) -> None:
122+
dest_dir = build_dir / package.stub_name
123+
if package.is_module:
124+
os.makedirs(dest_dir)
125+
shutil.copyfile(package.path, dest_dir / "__init__.pyi")
126+
else:
127+
shutil.copytree(package.path, dest_dir)
128+
129+
130+
def create_wheel_file(package: PackageInfo, src: Path, dest: Path) -> None:
131+
with open(dest, "w") as f:
132+
with open(src, "r") as src_f:
133+
for line in src_f:
134+
f.write(line)
135+
for tag in py_version_to_wheel_tags(package.py_version):
136+
f.write(f"Tag: {tag}\n")
137+
138+
139+
def create_metadata(package: PackageInfo, src: Path, dest: Path) -> None:
140+
py_requires = py_version_to_requires(package.py_version)
141+
with open(dest, "w") as f:
142+
with open(src, "r") as src_f:
143+
for line in src_f:
144+
if line.startswith("$TROVE_PY$"):
145+
for trove in py_version_to_trove(package.py_version):
146+
f.write(f"Classifier: {trove}\n")
147+
else:
148+
line = line.replace("$PACKAGE$", package.name)
149+
line = line.replace("$VERSION$", pkg_version)
150+
line = line.replace("$PY_REQUIRES$", py_requires)
151+
f.write(line)
152+
153+
154+
def pack_wheel(package: PackageInfo) -> None:
155+
os.makedirs(dist_dir, exist_ok=True)
156+
subprocess.run(["wheel", "pack", str(build_dir),
157+
"--dest-dir", str(dist_dir)])
158+
159+
160+
def main() -> None:
161+
package_name = parse_args()
162+
package = find_package(package_name)
163+
build_distribution(package)
164+
165+
166+
if __name__ == "__main__":
167+
main()

0 commit comments

Comments
 (0)