Skip to content

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

Merged
merged 3 commits into from
Oct 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions third_party/build/METADATA.tmpl
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).

3 changes: 3 additions & 0 deletions third_party/build/WHEEL.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Wheel-Version: 1.0
Generator: typeshed
Root-Is-Purelib: true
167 changes: 167 additions & 0 deletions third_party/build/build-dist.py
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>
Copy link
Member

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".

Copy link
Collaborator Author

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!

#
# 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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it amusing this looks like typescript :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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()