From 1b7e19dfb26412b5e1683aede19e8531ce15b642 Mon Sep 17 00:00:00 2001 From: Michael Krasnyk Date: Sun, 7 May 2023 21:28:37 +0200 Subject: [PATCH] feat: split download and install steps --- python/extensions.bzl | 2 + python/poetry_deps.bzl | 144 ++++++++++++----- python/poetry_deps.py | 148 +++++++++++------- python/private/poetry_venv.bzl | 38 +++-- python/tests/BUILD | 13 ++ python/tests/poetry_deps_test.py | 125 +++++++++++++++ python/tests/poetry_venv_test.bzl | 5 +- .../resources/six-1.16.0-py2.py3-none-any.whl | Bin 0 -> 11053 bytes 8 files changed, 363 insertions(+), 112 deletions(-) create mode 100644 python/tests/poetry_deps_test.py create mode 100644 python/tests/resources/six-1.16.0-py2.py3-none-any.whl diff --git a/python/extensions.bzl b/python/extensions.bzl index 86d5682..d4411d0 100644 --- a/python/extensions.bzl +++ b/python/extensions.bzl @@ -7,6 +7,7 @@ def _poetry_impl(module_ctx): poetry_venv( name = attr.name, lock = attr.lock, + platforms = attr.platforms, ) poetry = module_extension( @@ -16,6 +17,7 @@ poetry = module_extension( attrs = { "name": attr.string(mandatory = True), "lock": attr.label(mandatory = True), + "platforms": attr.string_dict(), }, ), }, diff --git a/python/poetry_deps.bzl b/python/poetry_deps.bzl index ca6ffa8..afcd744 100644 --- a/python/poetry_deps.bzl +++ b/python/poetry_deps.bzl @@ -3,32 +3,38 @@ load("@bazel_skylib//lib:versions.bzl", "versions") load("//python:markers.bzl", "evaluate", "parse") # Environment Markers https://peps.python.org/pep-0508/#environment-markers -_INTERPRETER_MARKERS = { - "aarch64-apple-darwin": """{"platform_system": "Darwin", "platform_tag": ["macosx_11_0_arm64", "macosx_12_0_arm64"], "sys_platform": "darwin", "os_name": "posix"}""", - "aarch64-unknown-linux-gnu": """{"platform_system": "Linux", "platform_tag": "manylinux_2_17_arm64", "sys_platform": "linux", "os_name": "posix"}""", - "x86_64-apple-darwin": """{"platform_system": "Darwin", "platform_tag": "macosx_10_15_x86_64", "sys_platform": "darwin", "os_name": "posix"}""", - "x86_64-pc-windows-msvc": """{"platform_system": "Windows", "platform_tag": "win_amd64", "sys_platform": "win32", "os_name": "nt"}""", - "x86_64-unknown-linux-gnu": """{"platform_system": "Linux", "platform_tag": "manylinux_2_17_x86_64", "sys_platform": "linux", "os_name": "posix"}""", +# Platform tags https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag +_DEFAULT_PLATFORMS = { + "aarch64-apple-darwin": """{"os_name": "posix", "platform_system": "Darwin", "platform_tags": ["macosx_11_0_arm64", "macosx_12_0_arm64"], "sys_platform": "darwin"}""", + "aarch64-unknown-linux-gnu": """{"os_name": "posix", "platform_system": "Linux", "platform_tags": ["manylinux_2_17_arm64"], "sys_platform": "linux"}""", + "x86_64-apple-darwin": """{"os_name": "posix", "platform_system": "Darwin", "platform_tags": ["macosx_10_15_x86_64"], "sys_platform": "darwin"}""", + "x86_64-pc-windows-msvc": """{"os_name": "nt", "platform_system": "Windows", "platform_tags": ["win_amd64"], "sys_platform": "win32"}""", + "x86_64-unknown-linux-gnu": """{"os_name": "posix", "platform_system": "Linux", "platform_tags": ["manylinux_2_12_x86_64", "manylinux_2_17_x86_64"], "sys_platform": "linux"}""", } +def _get_python_version(interpreter): + parts = interpreter.split("_") + for index in range(len(parts) - 1): + if parts[index].endswith("python3") and parts[index + 1].isdigit(): + return "3.{}".format(parts[index + 1]) + + return "3" + def _derive_environment_markers(interpreter, interpreter_markers): tags = { "extra": "*", "implementation_name": "cpython", "platform_python_implementation": "CPython", + "platform_tags": [], + "python_version": _get_python_version(interpreter), } - parts = interpreter.split("_") - for index in range(len(parts) - 1): - if parts[index].endswith("python3") and parts[index + 1].isdigit(): - tags["python_version"] = "3.{}".format(parts[index + 1]) - break for fr, to in interpreter_markers.items(): if fr in interpreter: tags.update(**json.decode(to)) return fr, tags - return None, tags + return "default", tags def _include_dep(dep, markers, environment): if not markers: @@ -40,9 +46,9 @@ def _include_dep(dep, markers, environment): marker = markers[dep.label.name] return evaluate(parse(marker, environment)) -def _package_impl(ctx): +def _package_wheel_impl(ctx): """ - Rule to install a Python package. + Rule to download a Python package. Arguments: ctx: The rule context. @@ -53,7 +59,6 @@ def _package_impl(ctx): deps: label_list The package dependencies list. files: string_dict The dictionary of resolved file names with corresponding checksum. markers: string The JSON string with markers accordingly to PEP 508 – Dependency specification for Python Software Packages. - constraints: label_list The list of platform constraints (currently unused). Private attributes: _poetry_deps: @@ -67,14 +72,14 @@ def _package_impl(ctx): toolchain = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"] runtime_info = toolchain.py3_runtime - runtime_tag, tags = _derive_environment_markers(runtime_info.interpreter.path, ctx.attr.interpreter_markers) + runtime_tag, tags = _derive_environment_markers(runtime_info.interpreter.path, ctx.attr.platforms) python_version = tags["python_version"] - platform_tag = tags["platform_tag"] + platform_tags = tags["platform_tags"] output = ctx.actions.declare_directory("{}/{}/{}".format(python_version, runtime_tag, ctx.label.name)) arguments = [ - ctx.attr.name, - ctx.attr.version, + "download", + ctx.attr.constraint, "--python-version", python_version, "--output", @@ -83,24 +88,87 @@ def _package_impl(ctx): json.encode(ctx.attr.files), ] - if type(platform_tag) == "string": - arguments += ["--platform", platform_tag] - elif type(platform_tag) == "list": - for platform in tags["platform_tag"]: - arguments += ["--platform", platform] - else: - fail("platform_tag must be either a string o a list of strings") + for platform in platform_tags: + arguments += ["--platform", platform] + + ctx.actions.run( + outputs = [output], + mnemonic = "DownloadWheel", + progress_message = "Downloading package {} for Python {} {}".format(ctx.attr.constraint, python_version, runtime_tag), + arguments = arguments, + use_default_shell_env = True, + executable = ctx.executable._poetry_deps, + ) + + return [ + DefaultInfo(files = depset([output])), + ] - if ctx.attr.source_url: - arguments += [ - "--source-url", - ctx.attr.source_url, - ] +package_wheel = rule( + implementation = _package_wheel_impl, + attrs = { + "constraint": attr.string(mandatory = True, doc = "The package version constraint string"), + "description": attr.string(doc = "The package description"), + "files": attr.string_dict(doc = "The package resolved files"), + "platforms": attr.string_dict( + default = _DEFAULT_PLATFORMS, + doc = "The mapping of an interpter substring mapping to environment markers and platform tags as a JSON string. " + + "Default value corresponds to platforms defined at " + + "https://github.com/bazelbuild/rules_python/blob/23cf6b66/python/versions.bzl#L231-L277", + ), + "_poetry_deps": attr.label(default = ":poetry_deps", cfg = "exec", executable = True), + }, + toolchains = [ + "@bazel_tools//tools/python:toolchain_type", + ], +) + +def _package_impl(ctx): + """ + Rule to install a Python package. + + Arguments: + ctx: The rule context. + + Attributes: + deps: label_list The package dependencies list. + markers: string The JSON string with markers accordingly to PEP 508 – Dependency specification for Python Software Packages. + + Private attributes: + _poetry_deps: + + Returns: + The providers list or a tuple with a Poetry package. + + Required toolchains: + @bazel_tools//tools/python:toolchain_type + """ + + toolchain = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"] + runtime_info = toolchain.py3_runtime + runtime_tag, tags = _derive_environment_markers(runtime_info.interpreter.path, ctx.attr.platforms) + python_version = tags["python_version"] + platform_tags = tags["platform_tags"] + + output = ctx.actions.declare_directory("{}/{}/{}".format(python_version, runtime_tag, ctx.label.name)) + wheel_file = ctx.attr.wheel.files.to_list().pop() + arguments = [ + "install", + "url" if ctx.attr.url else "wheel", + ctx.attr.url if ctx.attr.url else wheel_file.path, + output.path, + "--python-version", + python_version, + ] + + for platform in platform_tags: + arguments += ["--platform", platform] ctx.actions.run( outputs = [output], + inputs = [] if ctx.attr.url else [wheel_file], mnemonic = "InstallWheel", - progress_message = "Installing Python package {} for Python {} {}".format(ctx.label.name, python_version, runtime_tag), + progress_message = "Installing package {} for Python {} {}".format(ctx.label.name, python_version, runtime_tag), arguments = arguments, use_default_shell_env = True, executable = ctx.executable._poetry_deps, @@ -120,15 +188,13 @@ package = rule( implementation = _package_impl, provides = [PyInfo], attrs = { - "version": attr.string(mandatory = True, doc = "The package exact version string"), - "description": attr.string(doc = "The package description"), "deps": attr.label_list(doc = "The package dependencies list"), - "files": attr.string_dict(doc = "The package resolved files"), + "wheel": attr.label(doc = "The package_wheel target"), + "url": attr.string(doc = "The source file URL"), "markers": attr.string(doc = "The JSON string with a dictionary of dependency markers accordingly to PEP 508"), - "source_url": attr.string(doc = "The source file URL"), - "interpreter_markers": attr.string_dict( - default = _INTERPRETER_MARKERS, - doc = "The mapping of an interpter substring mapping to environment markers as a JSON string. " + + "platforms": attr.string_dict( + default = _DEFAULT_PLATFORMS, + doc = "The mapping of an interpter substring mapping to environment markers and platform tags as a JSON string. " + "Default value corresponds to platforms defined at " + "https://github.com/bazelbuild/rules_python/blob/23cf6b66/python/versions.bzl#L231-L277", ), diff --git a/python/poetry_deps.py b/python/poetry_deps.py index 91941b8..f205e69 100644 --- a/python/poetry_deps.py +++ b/python/poetry_deps.py @@ -9,58 +9,56 @@ from pip._internal.commands import create_command from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Download and install a Poetry package") - parser.add_argument("name", type=str, help="Python package constraint") - parser.add_argument("version", type=str, help="Python package constraint") - parser.add_argument("--output", type=Path, default=Path(), help="package output directory") - parser.add_argument("--python-version", type=str, default=None, help="python version") - parser.add_argument("--platform", type=Path, nargs="*", help="platform") - parser.add_argument("--files", type=str, default="{}", help="files:hash dictionary") - parser.add_argument("--source-url", type=str, default="", help="source file URL") +_SHA256_PREFIX = "sha256:" - args = parser.parse_args() - output_pkg = args.output +def get_platform_args(args): + """Format Python platform tags and version arguments. - if (local_package_path := Path(args.version)).is_absolute() and local_package_path.exists(): - for item in local_package_path.iterdir(): - (output_pkg / item.name).symlink_to(item) - sys.exit(0) + Ref: https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag + """ + + platform_args = [] + + if args.platform: + platform_args = [f"--platform={platform}" for platform in args.platform] - platform_args = [f"--platform={platform}" for platform in args.platform] if args.python_version: platform_args.append(f"--python-version={args.python_version}") - # Pre-process - output_whl = output_pkg.parent / (output_pkg.name + "_whl") - output_whl.mkdir(parents=True, exist_ok=True) + return platform_args + + +def download(args): + if args.source_url is not None: + return 0 # Download wheel - download = create_command("download") + download_command = create_command("download") download_args = [ - args.source_url if args.source_url else f"{args.name}=={args.version}", - f"--destination-directory={os.fspath(output_whl)}", + args.constraint, + f"--destination-directory={os.fspath(args.output)}", "--no-cache-dir", "--no-dependencies", - "--only-binary=:all:", # TODO: in some cases CC compiler is needed + "--prefer-binary", "--disable-pip-version-check", "--quiet", - ] + ] + get_platform_args(args) + + if (retcode := download_command.main(download_args)) != 0: + return retcode - if retcode := download.main(download_args + platform_args): - logging.error(f"pip download returned {retcode}") - # TODO: proper handling of missing platforms - sys.exit(0) + # Check number of downloaded files and SHA256 sum + downloaded_files = [item for item in Path(args.output).iterdir() if item.is_file()] + if len(downloaded_files) != 1: + raise RuntimeError(f"downloaded {len(downloaded_files)} files but one is expected") - # Check SHA256 sum - downloaded_file = next((item for item in Path(output_whl).iterdir() if item.is_file()), None) + downloaded_file = downloaded_files.pop() package_files = json.loads(args.files) expected_hash = package_files[downloaded_file.name] - sha256_prefix = "sha256:" - if expected_hash.startswith(sha256_prefix): - expected_sha256sum = expected_hash.removeprefix(sha256_prefix) + if expected_hash.startswith(_SHA256_PREFIX): + expected_sha256sum = expected_hash.removeprefix(_SHA256_PREFIX) hasher = hashlib.sha256() data_buffer = bytearray(1024 * 1024) with open(downloaded_file, "rb") as stream: @@ -68,40 +66,76 @@ hasher.update(data_buffer[:bytes_read]) if hasher.hexdigest() != expected_sha256sum: - logging.error( - "downloaded file %s has SHA256 sum %s, but expected %s", - downloaded_file, - hasher.hexdigest(), - expected_sha256sum, + raise RuntimeError( + f"downloaded file {downloaded_file} has SHA256 sum {hasher.hexdigest()} " + + f"but expected {expected_sha256sum}" ) - sys.exit(1) + else: - logging.warning("unknown hash %s", expected_hash) + logging.warning("unknown hash type %s", expected_hash) - # Install wheel - install = create_command("install") - install_args = [ - os.fspath(downloaded_file), - f"--target={output_pkg.resolve()}", - "--no-cache-dir", - "--no-compile", - "--no-dependencies", - "--disable-pip-version-check", - "--use-pep517", - "--quiet", - ] + return 0 - if retcode := install.main(install_args + platform_args): - logging.error(f"pip install returned {retcode}") - # TODO: proper handling of CC toolchains, split download and install steps + +def install(args): + if args.kind == "url" and (local_package_path := Path(args.input)).is_absolute() and local_package_path.exists(): + for item in local_package_path.iterdir(): + (args.output / item.name).symlink_to(item) + else: + # Install wheel + install_command = create_command("install") + install_args = [ + args.input + if args.kind == "url" or Path(args.input).is_file() + else os.fspath(next((item for item in Path(args.input).iterdir() if item.is_file()))), + f"--target={args.output}", + "--no-cache-dir", + "--no-compile", + "--no-dependencies", + "--disable-pip-version-check", + "--use-pep517", + "--quiet", + ] + + if retcode := install_command.main(install_args + get_platform_args(args)): + logging.error(f"pip install returned {retcode}") + # TODO: proper handling of CC toolchains + return retcode # Clean-up some metadata files which may contain non-hermetic data - for direct_url_path in output_pkg.glob(f"*.dist-info/{DIRECT_URL_METADATA_NAME}"): + for direct_url_path in args.output.glob(f"*.dist-info/{DIRECT_URL_METADATA_NAME}"): direct_url_path.unlink() record_path = direct_url_path.parent / "RECORD" if record_path.exists(): - direct_url_line = f"{direct_url_path.relative_to(output_pkg)}," + direct_url_line = f"{direct_url_path.relative_to(args.output)}," with open(record_path) as record_file: records = record_file.readlines() with open(record_path, "wt") as record_file: record_file.writelines(record for record in records if not record.startswith(direct_url_line)) + + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Download and install a Poetry package") + subparsers = parser.add_subparsers(required=True) + + parser_download = subparsers.add_parser("download") + parser_download.set_defaults(func=download) + parser_download.add_argument("constraint", type=str, help="Python package constraint") + parser_download.add_argument("--output", type=Path, default=Path(), help="package output directory") + parser_download.add_argument("--python-version", type=str, default=None, help="python version") + parser_download.add_argument("--platform", type=str, nargs="*", action="extend", help="platform tag") + parser_download.add_argument("--files", type=str, default="{}", help="files:hash dictionary") + parser_download.add_argument("--source-url", type=Path, default=None, help="source file URL") + + parser_install = subparsers.add_parser("install") + parser_install.set_defaults(func=install) + parser_install.add_argument("kind", type=str, help="installation kind 'wheel' or 'url'") + parser_install.add_argument("input", type=str, help="wheel file or directory with a single wheel file") + parser_install.add_argument("output", type=Path, default=Path(), help="package output directory") + parser_install.add_argument("--python-version", type=str, default=None, help="python version") + parser_install.add_argument("--platform", type=str, nargs="*", action="extend", help="platform tag") + + args = parser.parse_args() + sys.exit(args.func(args)) diff --git a/python/private/poetry_venv.bzl b/python/private/poetry_venv.bzl index fffb1f1..3b0f751 100644 --- a/python/private/poetry_venv.bzl +++ b/python/private/poetry_venv.bzl @@ -1,9 +1,9 @@ -def parse_lock_file(data): +def parse_lock_file(data, platforms = None): _MARKERS = "markers = " - _SOURCE_URL = "url = " + _URL = "url = " result = "" for package_lines in data.split("[[package]]"): - section, name, version, description, files, deps, markers, source_url = "package", "", "", "", "", [], {}, "" + section, name, version, description, files, deps, markers, url = "package", "", "", "", "", [], {}, "" for line in package_lines.split("\n"): line = line.strip() if line == "[package.dependencies]": @@ -13,9 +13,9 @@ def parse_lock_file(data): elif line.startswith("["): section = "unknown" elif section == "package" and line.startswith("name = "): - name = line + name = line.replace("name = ", "").strip('",') elif section == "package" and line.startswith("version = "): - version = line + version = line.replace("version = ", "").strip('",') elif section == "package" and line.startswith("description = "): description = line elif section == "package" and line.startswith("{file = ") and ", hash = " in line: @@ -30,16 +30,22 @@ def parse_lock_file(data): if dep_marker[index - 1] != "\\" and dep_marker[index] == '"': markers[dep_name] = dep_marker[1:index] break - elif section == "source" and line.startswith(_SOURCE_URL): - source_url = line[len(_SOURCE_URL):] + elif section == "source" and line.startswith(_URL): + url = line[len(_URL):] if name: result += """ -package( - {name}, - {version},{description} +package_wheel( + name = "{name}_wheel", + constraint = "{name}=={version}",{description} files = {{{files} - }},{deps}{markers}{source_url} + }},{platforms} + visibility = [\"//visibility:public\"], +) + +package( + name = "{name}", + wheel = ":{name}_wheel",{deps}{markers}{url}{platforms} visibility = [\"//visibility:public\"], ) """.format( @@ -49,7 +55,8 @@ package( files = files, deps = "\n deps = [{}],".format(", ".join(deps)) if deps else "", markers = "\n markers = '''{}''',".format(json.encode(markers)) if markers else "", - source_url = "\n source_url = {},".format(source_url) if source_url else "", + url = "\n url = {},".format(url) if url else "", + platforms = "\n platforms = {},".format(platforms) if platforms else "", ) return result @@ -57,8 +64,8 @@ package( def _poetry_venv_impl(rctx): rules_repository = str(rctx.path(rctx.attr._self)).split("/")[-4] rules_repository = ("@@" if "~" in rules_repository else "@") + rules_repository - prefix = '''load("{name}//python:poetry_deps.bzl", "package")\n'''.format(name = rules_repository) - rctx.file("BUILD", prefix + parse_lock_file(rctx.read(rctx.attr.lock))) + prefix = '''load("{name}//python:poetry_deps.bzl", "package", "package_wheel")\n'''.format(name = rules_repository) + rctx.file("BUILD", prefix + parse_lock_file(rctx.read(rctx.attr.lock), rctx.attr.platforms)) rctx.file("WORKSPACE") poetry_venv = repository_rule( @@ -67,6 +74,9 @@ poetry_venv = repository_rule( allow_single_file = True, doc = "Poetry lock file", ), + "platforms": attr.string_list_dict( + doc = "The mapping of interpter substrings to Python platform tags and environment markers as a JSON string", + ), "_self": attr.label( allow_single_file = True, default = ":poetry_venv.bzl", diff --git a/python/tests/BUILD b/python/tests/BUILD index 8b43cd2..176b828 100644 --- a/python/tests/BUILD +++ b/python/tests/BUILD @@ -4,3 +4,16 @@ load(":poetry_venv_test.bzl", "poetry_venv_test_suite") markers_test_suite() poetry_venv_test_suite() + +py_test( + name = "poetry_deps_test", + srcs = [ + "poetry_deps_test.py", + ], + data = [ + "resources/six-1.16.0-py2.py3-none-any.whl", + ], + deps = [ + "//python:poetry_deps", + ], +) diff --git a/python/tests/poetry_deps_test.py b/python/tests/poetry_deps_test.py new file mode 100644 index 0000000..5db672a --- /dev/null +++ b/python/tests/poetry_deps_test.py @@ -0,0 +1,125 @@ +import glob +import json +import os +import shutil +import tempfile +import unittest +from pathlib import Path +from platform import python_version_tuple + +import python.poetry_deps as main + +TEST_TMPDIR = os.environ.get("TEST_TMPDIR", "/tmp") + + +def get_python_version(): + return ".".join(python_version_tuple()[:2]) + + +class DownloadArgs: + constraint = "six==1.16.0" + output = None + platform = None + python_version = get_python_version() + source_url = None + files = ( + """{"six-1.16.0-py2.py3-none-any.whl": """ + + """"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}""" + ) + + +class TestDownloadSubcommand(unittest.TestCase): + # TODO: add mocking six download + def test_download(self): + args = DownloadArgs() + for args.platform in (None, ["any"]): + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.output: + retcode = main.download(args) + self.assertEqual(retcode, 0) + + wheels = glob.glob(f"{args.output}/*.whl") + self.assertEqual(len(wheels), 1) + + def test_no_download_with_source_url(self): + args = DownloadArgs() + args.source_url = "/x" + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.output: + retcode = main.download(args) + self.assertEqual(retcode, 0) + + wheels = glob.glob(f"{args.output}/*") + self.assertEqual(len(wheels), 0) + + def test_wrong_python_version(self): + args = DownloadArgs() + args.python_version = "x" + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.output: + with self.assertRaisesRegex(SystemExit, "^2$"): + main.download(args) + + def test_wrong_files_keys(self): + args = DownloadArgs() + files = list(json.loads(args.files).keys()) + args.files = """{"x":"sha256:x"}""" + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.output: + with self.assertRaisesRegex(KeyError, files.pop()): + main.download(args) + + def test_wrong_files_hash(self): + args = DownloadArgs() + files = list(json.loads(args.files).keys()) + args.files = f"""{{"{files.pop()}":"sha256:x"}}""" + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.output: + with self.assertRaisesRegex(RuntimeError, "expected x"): + main.download(args) + + +class InstallArgs: + kind = "wheel" + input = "python/tests/resources/six-1.16.0-py2.py3-none-any.whl" + output = None + platform = None + python_version = get_python_version() + + +class TestInstallSubcommand(unittest.TestCase): + def test_install_from_file(self): + args = InstallArgs() + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as output_dir: + args.output = Path(output_dir) + + retcode = main.install(args) + self.assertEqual(retcode, 0) + + wheels = glob.glob(f"{args.output}/six*") + self.assertGreater(len(wheels), 0) + + def test_install_from_directory(self): + args = InstallArgs() + input_file = args.input + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as output_dir: + args.output = Path(output_dir) + + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as args.input: + shutil.copyfile(input_file, f"{args.input}/{Path(input_file).name}") + retcode = main.install(args) + self.assertEqual(retcode, 0) + + wheels = glob.glob(f"{args.output}/six*") + self.assertGreater(len(wheels), 0) + + def test_install_from_url(self): + args = InstallArgs() + with tempfile.TemporaryDirectory(prefix=f"{TEST_TMPDIR}/") as output_dir: + args.output = Path(output_dir) + + args.kind = "url" + retcode = main.install(args) + self.assertEqual(retcode, 0) + + wheels = glob.glob(f"{args.output}/six*") + self.assertGreater(len(wheels), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/poetry_venv_test.bzl b/python/tests/poetry_venv_test.bzl index b331fe3..5caeaba 100644 --- a/python/tests/poetry_venv_test.bzl +++ b/python/tests/poetry_venv_test.bzl @@ -108,11 +108,12 @@ tomli = {version = ">=1.0.0", markers = "python_version < \\"3.11\\""} """ build_content = parse_lock_file(lock) - asserts.true(env, """package(\n name = "click",\n version = "8.1.3",\n description = "Composable command line interface toolkit",""" in build_content) + asserts.true(env, """package_wheel(\n name = "click_wheel",\n constraint = "click==8.1.3",\n description = "Composable command line interface toolkit",""" in build_content) + asserts.true(env, """package(\n name = "click",\n wheel = ":click_wheel",""" in build_content) asserts.true(env, """"moto-4.1.6.tar.gz": "sha256:fdcc2731212ca050a28b2bc83e87628294bcbd55cb4f4c4692f972023fb1e7e6",""" in build_content) asserts.true(env, """deps = [":click", ":importlib-metadata", ":itsdangerous", ":jinja2", ":werkzeug", ":colorama", ":exceptiongroup", ":tomli"],""" in build_content) asserts.true(env, """markers = '''{"colorama":"platform_system == \\\\\\"Windows\\\\\\"","pywin32":"sys_platform == \\\\\\"win32\\\\\\""}''',""" in build_content) - asserts.true(env, """source_url = "/tmp/moto-4.1.6.tar.gz",""" in build_content) + asserts.true(env, """url = "/tmp/moto-4.1.6.tar.gz",""" in build_content) return unittest.end(env) parse_lock_file_test = unittest.make(_parse_lock_file_test_impl) diff --git a/python/tests/resources/six-1.16.0-py2.py3-none-any.whl b/python/tests/resources/six-1.16.0-py2.py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..fd942658a2f748ba433dd8632abb910a416e184f GIT binary patch literal 11053 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 literal 0 HcmV?d00001