-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Build script for third-party distributions #2545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
Metadata-Version: 2.1 | ||
Name: $PACKAGE$-ts | ||
Version: $VERSION$ | ||
Summary: Type stubs for $PACKAGE$ | ||
Description-Content-Type: text/markdown | ||
Keywords: typehints typing type-hints typeshed | ||
Home-page: https://github.com/python/typeshed | ||
Classifier: Intended Audience :: Developers | ||
Classifier: License :: OSI Approved :: Apache Software License | ||
Classifier: Programming Language :: Python | ||
$TROVE_PY$ | ||
Classifier: Topic :: Software Development :: Libraries :: Python Modules | ||
Requires-Python: $PY_REQUIRES$ | ||
|
||
# Type stubs for $PACKAGE$ | ||
|
||
These type stubs are part of the [typeshed project](https://github.com/python/typeshed). | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Wheel-Version: 1.0 | ||
Generator: typeshed | ||
Root-Is-Purelib: true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# Build a stubs-only distribution for a third-party package. | ||
# | ||
# Usage: python3.7 build-dist.py <PACKAGE> | ||
# | ||
# The resulting wheel will be saved in third-party/build/dist. | ||
|
||
import datetime | ||
import os | ||
import re | ||
import shutil | ||
import subprocess | ||
import sys | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import List | ||
|
||
DIST_SUFFIX = "-ts" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find it amusing this looks like typescript :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, when I reviewed my suggestion before writing this script I wondered why I had chosen that suffix and whether I made a mistake because I had written a lot of typescript lately. Then I remembered that it was the abbreviation for typeshed. :) That said, I think we should get the pypi maintainers on board for a final decision for a suffix. |
||
MIN_PYTHON3_VERSION = (3, 4) | ||
MAX_PYTHON3_VERSION = (3, 7) | ||
EXCLUDED_PYTHON_VERSIONS = [(3, i) for i in range(MIN_PYTHON3_VERSION[1])] | ||
TROVE_PREFIX = "Classifier: Programming Language :: Python :: " | ||
|
||
pkg_version = "0." + datetime.datetime.now().strftime("%Y%m%d.%H%M") | ||
py_version_re = re.compile(r"^(2and3|\d+(\.\d+)?)$") | ||
|
||
base_dir = Path(__file__).parent | ||
root_dir = base_dir.parent.parent | ||
build_dir = base_dir / "build" | ||
dist_dir = base_dir / "dist" | ||
|
||
@dataclass | ||
class PackageInfo: | ||
path: Path | ||
name: str | ||
py_version: str | ||
is_module: bool | ||
|
||
@property | ||
def stub_name(self) -> str: | ||
return f"{self.name}-stubs" | ||
|
||
|
||
def py_version_to_wheel_tags(version: str) -> List[str]: | ||
if version == "2and3": | ||
return ["py2-none-any", "py3-none-any"] | ||
else: | ||
return [f"py{version}-none-any"] | ||
|
||
|
||
def py_version_to_requires(version: str) -> str: | ||
if version == "2": | ||
return ">= 2.7, < 3" | ||
elif version == "2and3": | ||
formatted = [f"!= {ma}.{mi}.*" for ma, mi in EXCLUDED_PYTHON_VERSIONS] | ||
return ">= 2.7, " + ", ".join(formatted) | ||
elif version == "3": | ||
return f">= {MIN_PYTHON3_VERSION[0]}.{MIN_PYTHON3_VERSION[1]}" | ||
else: | ||
return ">= " + version | ||
|
||
|
||
def py_version_to_trove(version: str) -> List[str]: | ||
py2_versions = ["2", "2.7"] | ||
if version == "2": | ||
versions = py2_versions + ["2 :: Only"] | ||
elif version == "2and3": | ||
versions = py2_versions + py3_versions() | ||
elif version == "3": | ||
versions = py3_versions() + ["3 :: Only"] | ||
else: | ||
versions = py3_versions(int(version[2:])) + ["3 :: Only"] | ||
return [TROVE_PREFIX + v for v in versions] | ||
|
||
def py3_versions(min_v: int = MIN_PYTHON3_VERSION[1]) -> List[str]: | ||
assert min_v <= MAX_PYTHON3_VERSION[1] | ||
r = range(min_v, MAX_PYTHON3_VERSION[1] + 1) | ||
return ["3"] + [f"3.{i}" for i in r] | ||
|
||
|
||
def parse_args() -> str: | ||
if len(sys.argv) != 2: | ||
print(f"Usage: {sys.argv[0]} PACKAGE", file=sys.stderr) | ||
sys.exit(1) | ||
return sys.argv[1] | ||
|
||
|
||
def find_package(name: str) -> PackageInfo: | ||
for dir_ in base_dir.parent.iterdir(): | ||
if not py_version_re.match(dir_.name): | ||
continue | ||
pkg_path = dir_ / name | ||
mod_path = dir_ / f"{name}.pyi" | ||
if pkg_path.is_dir(): | ||
return PackageInfo(pkg_path, name, dir_.name, is_module=False) | ||
elif mod_path.is_file(): | ||
return PackageInfo(mod_path, name, dir_.name, is_module=True) | ||
print(f"Stubs for package '{name}' not found", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
|
||
def build_distribution(package: PackageInfo) -> None: | ||
prepare_build_dir(package) | ||
pack_wheel(package) | ||
|
||
|
||
def prepare_build_dir(package: PackageInfo) -> None: | ||
shutil.rmtree(build_dir, ignore_errors=True) | ||
copy_package(package) | ||
pkg = (package.name + DIST_SUFFIX).replace("-", "_") | ||
dist_info_dir = build_dir / f"{pkg}-{pkg_version}.dist-info" | ||
os.mkdir(dist_info_dir) | ||
shutil.copy(root_dir / "LICENSE", dist_info_dir) | ||
create_wheel_file(package, base_dir / "WHEEL.tmpl", | ||
dist_info_dir / "WHEEL") | ||
create_metadata(package, base_dir / "METADATA.tmpl", | ||
dist_info_dir / "METADATA") | ||
|
||
|
||
def copy_package(package: PackageInfo) -> None: | ||
dest_dir = build_dir / package.stub_name | ||
if package.is_module: | ||
os.makedirs(dest_dir) | ||
shutil.copyfile(package.path, dest_dir / "__init__.pyi") | ||
else: | ||
shutil.copytree(package.path, dest_dir) | ||
|
||
|
||
def create_wheel_file(package: PackageInfo, src: Path, dest: Path) -> None: | ||
with open(dest, "w") as f: | ||
with open(src, "r") as src_f: | ||
for line in src_f: | ||
f.write(line) | ||
for tag in py_version_to_wheel_tags(package.py_version): | ||
f.write(f"Tag: {tag}\n") | ||
|
||
|
||
def create_metadata(package: PackageInfo, src: Path, dest: Path) -> None: | ||
py_requires = py_version_to_requires(package.py_version) | ||
with open(dest, "w") as f: | ||
with open(src, "r") as src_f: | ||
for line in src_f: | ||
if line.startswith("$TROVE_PY$"): | ||
for trove in py_version_to_trove(package.py_version): | ||
f.write(f"Classifier: {trove}\n") | ||
else: | ||
line = line.replace("$PACKAGE$", package.name) | ||
line = line.replace("$VERSION$", pkg_version) | ||
line = line.replace("$PY_REQUIRES$", py_requires) | ||
f.write(line) | ||
|
||
|
||
def pack_wheel(package: PackageInfo) -> None: | ||
os.makedirs(dist_dir, exist_ok=True) | ||
subprocess.run(["wheel", "pack", str(build_dir), | ||
"--dest-dir", str(dist_dir)]) | ||
|
||
|
||
def main() -> None: | ||
package_name = parse_args() | ||
package = find_package(package_name) | ||
build_distribution(package) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure requiring people to have a 3.7 install is a good idea, but then again it doesn't bother me too much. At minimum a version check/warning before importing anything would avoid people being confused by "no module named dataclasses".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eventually this script is meant to be run as part of the CI process, do I didn't worry about that too much. But the version check is a good idea!