All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
   9 from argparse import ArgumentParser, Namespace
 
  10 from concurrent.futures import ThreadPoolExecutor
 
  11 from functools import lru_cache, partial
 
  12 from pathlib import Path
 
  13 from typing import (  # type: ignore # typing can't see Literal
 
  23 from urllib.request import urlopen, urlretrieve
 
  25 PYPI_INSTANCE = "https://pypi.org/pypi"
 
  27     "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-{days}-days.json"
 
  29 INTERNAL_BLACK_REPO = f"{tempfile.gettempdir()}/__black"
 
  31 ArchiveKind = Union[tarfile.TarFile, zipfile.ZipFile]
 
  32 Days = Union[Literal[30], Literal[365]]
 
  34 subprocess.run = partial(subprocess.run, check=True)  # type: ignore
 
  35 # https://github.com/python/mypy/issues/1484
 
  38 class BlackVersion(NamedTuple):
 
  40     config: Optional[str] = None
 
  43 def get_pypi_download_url(package: str, version: Optional[str]) -> str:
 
  44     with urlopen(PYPI_INSTANCE + f"/{package}/json") as page:
 
  45         metadata = json.load(page)
 
  48         sources = metadata["urls"]
 
  50         if version in metadata["releases"]:
 
  51             sources = metadata["releases"][version]
 
  54                 f"No releases found with version ('{version}') tag. "
 
  55                 f"Found releases: {metadata['releases'].keys()}"
 
  58     for source in sources:
 
  59         if source["python_version"] == "source":
 
  62         raise ValueError(f"Couldn't find any sources for {package}")
 
  64     return cast(str, source["url"])
 
  67 def get_top_packages(days: Days) -> List[str]:
 
  68     with urlopen(PYPI_TOP_PACKAGES.format(days=days)) as page:
 
  69         result = json.load(page)
 
  71     return [package["project"] for package in result["rows"]]
 
  74 def get_package_source(package: str, version: Optional[str]) -> str:
 
  75     if package == "cpython":
 
  78         return f"https://github.com/python/cpython/archive/{version}.zip"
 
  79     elif package == "pypy":
 
  81             version = "branch/default"
 
  83             f"https://foss.heptapod.net/pypy/pypy/repository/{version}/archive.tar.bz2"
 
  86         return get_pypi_download_url(package, version)
 
  89 def get_archive_manager(local_file: str) -> ArchiveKind:
 
  90     if tarfile.is_tarfile(local_file):
 
  91         return tarfile.open(local_file)
 
  92     elif zipfile.is_zipfile(local_file):
 
  93         return zipfile.ZipFile(local_file)
 
  95         raise ValueError("Unknown archive kind.")
 
  98 def get_first_archive_member(archive: ArchiveKind) -> str:
 
  99     if isinstance(archive, tarfile.TarFile):
 
 100         return archive.getnames()[0]
 
 101     elif isinstance(archive, zipfile.ZipFile):
 
 102         return archive.namelist()[0]
 
 105 def download_and_extract(package: str, version: Optional[str], directory: Path) -> Path:
 
 106     source = get_package_source(package, version)
 
 108     local_file, _ = urlretrieve(source, directory / f"{package}-src")
 
 109     with get_archive_manager(local_file) as archive:
 
 110         archive.extractall(path=directory)
 
 111         result_dir = get_first_archive_member(archive)
 
 112     return directory / result_dir
 
 116     package: str, version: Optional[str], directory: Path
 
 119         return download_and_extract(package, version, directory)
 
 121         print(f"Caught an exception while downloading {package}.")
 
 122         traceback.print_exc()
 
 126 DEFAULT_SLICE = slice(None)  # for flake8
 
 129 def download_and_extract_top_packages(
 
 130     directory: Path, days: Days = 365, workers: int = 8, limit: slice = DEFAULT_SLICE,
 
 131 ) -> Generator[Path, None, None]:
 
 132     with ThreadPoolExecutor(max_workers=workers) as executor:
 
 133         bound_downloader = partial(get_package, version=None, directory=directory)
 
 134         for package in executor.map(bound_downloader, get_top_packages(days)[limit]):
 
 135             if package is not None:
 
 139 def git_create_repository(repo: Path) -> None:
 
 140     subprocess.run(["git", "init"], cwd=repo)
 
 141     git_add_and_commit(msg="Initial commit", repo=repo)
 
 144 def git_add_and_commit(msg: str, repo: Path) -> None:
 
 145     subprocess.run(["git", "add", "."], cwd=repo)
 
 146     subprocess.run(["git", "commit", "-m", msg, "--allow-empty"], cwd=repo)
 
 149 def git_switch_branch(
 
 150     branch: str, repo: Path, new: bool = False, from_branch: Optional[str] = None
 
 152     args = ["git", "checkout"]
 
 157         args.append(from_branch)
 
 158     subprocess.run(args, cwd=repo)
 
 161 def init_repos(options: Namespace) -> Tuple[Path, ...]:
 
 162     options.output.mkdir(exist_ok=True)
 
 164     if options.top_packages:
 
 165         source_directories = tuple(
 
 166             download_and_extract_top_packages(
 
 167                 directory=options.output,
 
 168                 workers=options.workers,
 
 169                 limit=slice(None, options.top_packages),
 
 173         source_directories = (
 
 174             download_and_extract(
 
 175                 package=options.pypi_package,
 
 176                 version=options.version,
 
 177                 directory=options.output,
 
 181     for source_directory in source_directories:
 
 182         git_create_repository(source_directory)
 
 184     if options.black_repo is None:
 
 186             ["git", "clone", "https://github.com/psf/black.git", INTERNAL_BLACK_REPO],
 
 189         options.black_repo = options.output / INTERNAL_BLACK_REPO
 
 191     return source_directories
 
 195 def black_runner(version: str, black_repo: Path) -> Path:
 
 196     directory = tempfile.TemporaryDirectory()
 
 197     venv.create(directory.name, with_pip=True)
 
 199     python = Path(directory.name) / "bin" / "python"
 
 200     subprocess.run([python, "-m", "pip", "install", "-e", black_repo])
 
 202     atexit.register(directory.cleanup)
 
 206 def format_repo_with_version(
 
 208     from_branch: Optional[str],
 
 210     black_version: BlackVersion,
 
 211     input_directory: Path,
 
 213     current_branch = f"black-{black_version.version}"
 
 214     git_switch_branch(black_version.version, repo=black_repo)
 
 215     git_switch_branch(current_branch, repo=repo, new=True, from_branch=from_branch)
 
 217     format_cmd: List[Union[Path, str]] = [
 
 218         black_runner(black_version.version, black_repo),
 
 219         (black_repo / "black.py").resolve(),
 
 222     if black_version.config:
 
 223         format_cmd.extend(["--config", input_directory / black_version.config])
 
 225     subprocess.run(format_cmd, cwd=repo, check=False)  # ensure the process
 
 226     # continuess to run even it can't format some files. Reporting those
 
 228     git_add_and_commit(f"Format with black:{black_version.version}", repo=repo)
 
 230     return current_branch
 
 233 def format_repos(repos: Tuple[Path, ...], options: Namespace) -> None:
 
 234     black_versions = tuple(
 
 235         BlackVersion(*version.split(":")) for version in options.versions
 
 240         for black_version in black_versions:
 
 241             from_branch = format_repo_with_version(
 
 243                 from_branch=from_branch,
 
 244                 black_repo=options.black_repo,
 
 245                 black_version=black_version,
 
 246                 input_directory=options.input,
 
 248         git_switch_branch("master", repo=repo)
 
 250     git_switch_branch("master", repo=options.black_repo)
 
 254     parser = ArgumentParser(
 
 255         description="""Black Gallery is a script that
 
 256     automates the process of applying different Black versions to a selected
 
 257     PyPI package and seeing the results between versions."""
 
 260     group = parser.add_mutually_exclusive_group(required=True)
 
 261     group.add_argument("-p", "--pypi-package", help="PyPI package to download.")
 
 263         "-t", "--top-packages", help="Top n PyPI packages to download.", type=int
 
 266     parser.add_argument("-b", "--black-repo", help="Black's Git repository.", type=Path)
 
 271             "Version for given PyPI package. Will be discarded if used with -t option."
 
 278             "Maximum number of threads to download with at the same time. "
 
 279             "Will be discarded if used with -p option."
 
 285         default=Path("/input"),
 
 287         help="Input directory to read configuration.",
 
 292         default=Path("/output"),
 
 294         help="Output directory to download and put result artifacts.",
 
 296     parser.add_argument("versions", nargs="*", default=("master",), help="")
 
 298     options = parser.parse_args()
 
 299     repos = init_repos(options)
 
 300     format_repos(repos, options)
 
 303 if __name__ == "__main__":