From af5fa3068a5439af641c64f05e287757f6cb5c1a Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 26 Jul 2021 11:53:52 -0700 Subject: [PATCH 01/16] Adds `hex_digest` property to LockFileRequests # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../pants/backend/experimental/python/lockfile.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 25191b13824..76d9c149411 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -3,6 +3,7 @@ from __future__ import annotations +import hashlib import logging from dataclasses import dataclass from typing import cast @@ -93,6 +94,20 @@ def from_tool( description=f"Generate lockfile for {subsystem.options_scope}", ) + @property + def hex_digest(self) -> str: + """Produces a hex digest of this lockfile's inputs, which should uniquely specify the + resolution of this lockfile request. + + Inputs are definted as requirements and interpreter constraints. + """ + m = hashlib.sha256() + for requirement in self.requirements: + m.update(requirement.encode("utf-8")) + for constraint in self.interpreter_constraints: + m.update(str(constraint).encode("utf-8")) + return m.hexdigest() + @rule(desc="Generate lockfile", level=LogLevel.DEBUG) async def generate_lockfile( From 30c1e5193eac8d8dceae7cb20fcef4b124c06197 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 26 Jul 2021 11:54:18 -0700 Subject: [PATCH 02/16] Adds `lockfile_metadata` functions # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 76d9c149411..3b5f0706e3e 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -287,5 +287,42 @@ async def generate_all_tool_lockfiles( return ToolLockGoal(exit_code=0) +# Lockfile metadata for headers +def lockfile_metadata(invalidation_digest: str) -> bytes: + """Produces a metadata bytes object for including at the top of a lockfile. + + Currently, this only consists of an invalidation digest for the file, which is used when Pants + consumes the lockfile during builds. + """ + return ( + b""" +# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- +# invalidation digest: %(invalidation_digest)s +# --- END LOCKFILE METADATA --- + """ + % {b"invalidation_digest": invalidation_digest.encode("ascii")} + ).strip() + + +def read_lockfile_metadata(contents: bytes) -> dict[str, str]: + """Reads through `contents`, and returns the contents of the lockfile metadata block as a + dictionary.""" + + metadata = {} + + in_metadata_block = False + for line in contents.splitlines(): + line = line.strip() + if line == b"# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---": + in_metadata_block = True + elif line == b"# --- END LOCKFILE METADATA ---": + break + elif in_metadata_block: + key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) + metadata[key] = value + + return metadata + + def rules(): return collect_rules() From b1b656813b182c018030c7aeff12b42886db406e Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 26 Jul 2021 13:53:04 -0700 Subject: [PATCH 03/16] Adds the invalidation digest header to lockfiles [ci skip-rust] [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 3b5f0706e3e..6d771070cc1 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -16,7 +16,14 @@ from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.backend.python.util_rules.pex import PexRequest, PexRequirements, VenvPex, VenvPexProcess from pants.engine.addresses import Addresses -from pants.engine.fs import CreateDigest, Digest, FileContent, MergeDigests, Workspace +from pants.engine.fs import ( + CreateDigest, + Digest, + DigestContents, + FileContent, + MergeDigests, + Workspace, +) from pants.engine.goal import Goal, GoalSubsystem from pants.engine.process import ProcessResult from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule @@ -132,7 +139,9 @@ async def generate_lockfile( ), ) - result = await Get( + tmp_lockfile = f"{req.dest}.tmp" + + generated_lockfile = await Get( ProcessResult, # TODO(#12314): Figure out named_caches for pip-tools. The best would be to share # the cache between Pex and Pip. Next best is a dedicated named_cache. @@ -143,16 +152,25 @@ async def generate_lockfile( argv=[ "reqs.txt", "--generate-hashes", - f"--output-file={req.dest}", + f"--output-file={tmp_lockfile}", # NB: This allows pinning setuptools et al, which we must do. This will become # the default in a future version of pip-tools. "--allow-unsafe", ], input_digest=input_requirements, - output_files=(req.dest,), + output_files=(tmp_lockfile,), ), ) - return PythonLockfile(result.output_digest, req.dest) + + lockfile_contents = next( + iter(await Get(DigestContents, Digest, generated_lockfile.output_digest)) + ) + content_with_header = b"%b\n%b" % (lockfile_metadata(req.hex_digest), lockfile_contents.content) + complete_lockfile = await Get( + Digest, CreateDigest([FileContent(req.dest, content_with_header)]) + ) + + return PythonLockfile(complete_lockfile, req.dest) # -------------------------------------------------------------------------------------- @@ -309,7 +327,7 @@ def read_lockfile_metadata(contents: bytes) -> dict[str, str]: dictionary.""" metadata = {} - + in_metadata_block = False for line in contents.splitlines(): line = line.strip() @@ -320,7 +338,7 @@ def read_lockfile_metadata(contents: bytes) -> dict[str, str]: elif in_metadata_block: key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) metadata[key] = value - + return metadata From 10632a1474baeda7291cb37e47720a15fe84a064 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 26 Jul 2021 14:18:41 -0700 Subject: [PATCH 04/16] Change the intermediate filename for nicer-looking lockfiles [ci skip-rust] [ci skip-build-wheels] --- .../pants/backend/experimental/python/lockfile.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 6d771070cc1..de8b3be5ae3 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -120,8 +120,9 @@ def hex_digest(self) -> str: async def generate_lockfile( req: PythonLockfileRequest, pip_tools_subsystem: PipToolsSubsystem ) -> PythonLockfile: + reqs_filename = "reqs.txt" input_requirements = await Get( - Digest, CreateDigest([FileContent("reqs.txt", "\n".join(req.requirements).encode())]) + Digest, CreateDigest([FileContent(reqs_filename, "\n".join(req.requirements).encode())]) ) pip_compile_pex = await Get( @@ -139,8 +140,6 @@ async def generate_lockfile( ), ) - tmp_lockfile = f"{req.dest}.tmp" - generated_lockfile = await Get( ProcessResult, # TODO(#12314): Figure out named_caches for pip-tools. The best would be to share @@ -150,15 +149,15 @@ async def generate_lockfile( description=req.description, # TODO(#12314): Wire up all the pip options like indexes. argv=[ - "reqs.txt", + reqs_filename, "--generate-hashes", - f"--output-file={tmp_lockfile}", + f"--output-file={req.dest}", # NB: This allows pinning setuptools et al, which we must do. This will become # the default in a future version of pip-tools. "--allow-unsafe", ], input_digest=input_requirements, - output_files=(tmp_lockfile,), + output_files=(req.dest,), ), ) From 71f776f1ab6af219fee00a3aeade102987774910 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Mon, 26 Jul 2021 14:36:44 -0700 Subject: [PATCH 05/16] Factors out "begin lockfile metadata" into constants [ci skip-rust] [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index de8b3be5ae3..29544a1b9ff 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -304,6 +304,10 @@ async def generate_all_tool_lockfiles( return ToolLockGoal(exit_code=0) +BEGIN_LOCKFILE_HEADER = b"# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" +END_LOCKFILE_HEADER = b"# --- END LOCKFILE METADATA ---" + + # Lockfile metadata for headers def lockfile_metadata(invalidation_digest: str) -> bytes: """Produces a metadata bytes object for including at the top of a lockfile. @@ -313,11 +317,15 @@ def lockfile_metadata(invalidation_digest: str) -> bytes: """ return ( b""" -# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- +%(BEGIN_LOCKFILE_HEADER)b # invalidation digest: %(invalidation_digest)s -# --- END LOCKFILE METADATA --- +%(END_LOCKFILE_HEADER)b """ - % {b"invalidation_digest": invalidation_digest.encode("ascii")} + % { + b"BEGIN_LOCKFILE_HEADER": BEGIN_LOCKFILE_HEADER, + b"invalidation_digest": invalidation_digest.encode("ascii"), + b"END_LOCKFILE_HEADER": END_LOCKFILE_HEADER, + } ).strip() @@ -330,9 +338,9 @@ def read_lockfile_metadata(contents: bytes) -> dict[str, str]: in_metadata_block = False for line in contents.splitlines(): line = line.strip() - if line == b"# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---": + if line == BEGIN_LOCKFILE_HEADER: in_metadata_block = True - elif line == b"# --- END LOCKFILE METADATA ---": + elif line == END_LOCKFILE_HEADER: break elif in_metadata_block: key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) From 47af3f170385ea8422b3396708785abecda68336 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Tue, 27 Jul 2021 10:05:53 -0700 Subject: [PATCH 06/16] Adds unit tests for lockfile invalidation helpers [ci skip-rust] [ci skip-build-wheels] --- .../pants/backend/experimental/python/BUILD | 5 + .../backend/experimental/python/lockfile.py | 12 +- .../experimental/python/lockfile_test.py | 103 ++++++++++++++++++ 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/python/pants/backend/experimental/python/lockfile_test.py diff --git a/src/python/pants/backend/experimental/python/BUILD b/src/python/pants/backend/experimental/python/BUILD index 76e8c47837f..765402c9b99 100644 --- a/src/python/pants/backend/experimental/python/BUILD +++ b/src/python/pants/backend/experimental/python/BUILD @@ -2,3 +2,8 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). python_library() + +python_tests( + name = "tests", + timeout = 180, +) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 29544a1b9ff..810c0ffeafd 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -161,10 +161,10 @@ async def generate_lockfile( ), ) - lockfile_contents = next( - iter(await Get(DigestContents, Digest, generated_lockfile.output_digest)) - ) - content_with_header = b"%b\n%b" % (lockfile_metadata(req.hex_digest), lockfile_contents.content) + _lockfile_contents_iter = await Get(DigestContents, Digest, generated_lockfile.output_digest) + lockfile_contents = _lockfile_contents_iter[0] + + content_with_header = validated_lockfile_content(req, lockfile_contents.content) complete_lockfile = await Get( Digest, CreateDigest([FileContent(req.dest, content_with_header)]) ) @@ -309,6 +309,10 @@ async def generate_all_tool_lockfiles( # Lockfile metadata for headers +def validated_lockfile_content(req: PythonLockfileRequest, content: bytes) -> bytes: + return b"%b\n%b" % (lockfile_metadata(req.hex_digest), content) + + def lockfile_metadata(invalidation_digest: str) -> bytes: """Produces a metadata bytes object for including at the top of a lockfile. diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py new file mode 100644 index 00000000000..5ff1b149ac7 --- /dev/null +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -0,0 +1,103 @@ +# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from unittest.mock import MagicMock + +from pants.backend.experimental.python.lockfile import ( + PythonLockfileRequest, + lockfile_metadata, + read_lockfile_metadata, + validated_lockfile_content, +) +from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.util.ordered_set import FrozenOrderedSet + + +def test_metadata_round_trip(): + + val = "help_im_trapped_inside_a_unit_test_string" + output = read_lockfile_metadata(lockfile_metadata(val)) + assert val == output["invalidation digest"] + + +def test_validated_lockfile_content(): + + req = MagicMock(hex_digest="000faaafcacacaca") + content = b"""dave==3.1.4 \\ + --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ + """ + + line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] + output = b""" +# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- +# invalidation digest: 000faaafcacacaca +# --- END LOCKFILE METADATA --- +dave==3.1.4 \\ + --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ + """ + assert line_by_line(validated_lockfile_content(req, content)) == line_by_line(output) + + +def test_hex_digest_empty(): + + req = PythonLockfileRequest( + requirements=FrozenOrderedSet([]), + interpreter_constraints=InterpreterConstraints([]), + dest="lockfile.py", + description="empty", + ) + + assert req.hex_digest == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + +def test_hex_digest_empty_interpreter_constraints(): + + req = PythonLockfileRequest( + requirements=FrozenOrderedSet( + [ + "help", + "meim", + "trap", + "pedi", + "naun", + "itte", + ] + ), + interpreter_constraints=InterpreterConstraints([]), + dest="lockfile.py", + description="empty", + ) + + assert req.hex_digest == "9056af98e88b5f1ce893dcb7d7e189bd813ef4f5009f26594e1499be546fb3e1" + + +def test_hex_digest_empty_requirements(): + + req = PythonLockfileRequest( + requirements=FrozenOrderedSet([]), + interpreter_constraints=InterpreterConstraints( + ["stda", "tase", "tand", "itsd", "arka", "ndsc"] + ), + dest="lockfile.py", + description="empty", + ) + + assert req.hex_digest == "312bd499be026b8ecedb95a3b3e234bdea28e82c2d40ae1b2a435fc20971e2c2" + + +def test_hex_digest_both_specified(): + + req = PythonLockfileRequest( + requirements=FrozenOrderedSet(["aryi", "nher", "eple", "ases", "avem"]), + interpreter_constraints=InterpreterConstraints( + [ + "efro", + "mitq", + "uick", + ] + ), + dest="lockfile.py", + description="empty", + ) + + assert req.hex_digest == "fc71e036ed72f43b9e0f2dcd37050a9e21773a619f0172ab8b657be9ee502fb6" From fca64b543ac755feab79f3e8dbdc6b629234f110 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Tue, 27 Jul 2021 10:08:52 -0700 Subject: [PATCH 07/16] Adds comment to explain lambda # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- src/python/pants/backend/experimental/python/lockfile_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 5ff1b149ac7..2d5f339a481 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -27,7 +27,6 @@ def test_validated_lockfile_content(): --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ """ - line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] output = b""" # --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- # invalidation digest: 000faaafcacacaca @@ -35,6 +34,9 @@ def test_validated_lockfile_content(): dave==3.1.4 \\ --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ """ + + # Helper function to make the test case more resilient to reformatting + line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] assert line_by_line(validated_lockfile_content(req, content)) == line_by_line(output) From 148c29e86e218dfbb4d40649c3c7691a86d152c3 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Tue, 27 Jul 2021 12:09:21 -0700 Subject: [PATCH 08/16] Code review concerns # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 4 ++-- .../experimental/python/lockfile_test.py | 22 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 810c0ffeafd..c911f81945d 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -304,8 +304,8 @@ async def generate_all_tool_lockfiles( return ToolLockGoal(exit_code=0) -BEGIN_LOCKFILE_HEADER = b"# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" -END_LOCKFILE_HEADER = b"# --- END LOCKFILE METADATA ---" +BEGIN_LOCKFILE_HEADER = b"# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" +END_LOCKFILE_HEADER = b"# --- END PANTS LOCKFILE METADATA ---" # Lockfile metadata for headers diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 2d5f339a481..cbb7fbaf25e 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -13,24 +13,22 @@ from pants.util.ordered_set import FrozenOrderedSet -def test_metadata_round_trip(): - +def test_metadata_round_trip() -> None: val = "help_im_trapped_inside_a_unit_test_string" output = read_lockfile_metadata(lockfile_metadata(val)) assert val == output["invalidation digest"] -def test_validated_lockfile_content(): - +def test_validated_lockfile_content() -> None: req = MagicMock(hex_digest="000faaafcacacaca") content = b"""dave==3.1.4 \\ --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ """ output = b""" -# --- BEGIN LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- +# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE --- # invalidation digest: 000faaafcacacaca -# --- END LOCKFILE METADATA --- +# --- END PANTS LOCKFILE METADATA --- dave==3.1.4 \\ --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ """ @@ -40,8 +38,7 @@ def test_validated_lockfile_content(): assert line_by_line(validated_lockfile_content(req, content)) == line_by_line(output) -def test_hex_digest_empty(): - +def test_hex_digest_empty() -> None: req = PythonLockfileRequest( requirements=FrozenOrderedSet([]), interpreter_constraints=InterpreterConstraints([]), @@ -52,8 +49,7 @@ def test_hex_digest_empty(): assert req.hex_digest == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" -def test_hex_digest_empty_interpreter_constraints(): - +def test_hex_digest_empty_interpreter_constraints() -> None: req = PythonLockfileRequest( requirements=FrozenOrderedSet( [ @@ -73,8 +69,7 @@ def test_hex_digest_empty_interpreter_constraints(): assert req.hex_digest == "9056af98e88b5f1ce893dcb7d7e189bd813ef4f5009f26594e1499be546fb3e1" -def test_hex_digest_empty_requirements(): - +def test_hex_digest_empty_requirements() -> None: req = PythonLockfileRequest( requirements=FrozenOrderedSet([]), interpreter_constraints=InterpreterConstraints( @@ -87,8 +82,7 @@ def test_hex_digest_empty_requirements(): assert req.hex_digest == "312bd499be026b8ecedb95a3b3e234bdea28e82c2d40ae1b2a435fc20971e2c2" -def test_hex_digest_both_specified(): - +def test_hex_digest_both_specified() -> None: req = PythonLockfileRequest( requirements=FrozenOrderedSet(["aryi", "nher", "eple", "ases", "avem"]), interpreter_constraints=InterpreterConstraints( From 8a599a1d6df369a52f0c496595ad5357374aef4b Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 12:05:39 -0700 Subject: [PATCH 09/16] Factors out metadata functions into `lockfile_metadata.py`. This will be simpler, I promise. # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 52 +------------- .../experimental/python/lockfile_metadata.py | 68 +++++++++++++++++++ .../experimental/python/lockfile_test.py | 8 +-- 3 files changed, 73 insertions(+), 55 deletions(-) create mode 100644 src/python/pants/backend/experimental/python/lockfile_metadata.py diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index c911f81945d..d754bee5e39 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -33,6 +33,7 @@ from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet from pants.util.strutil import pluralize +from pants.backend.experimental.python.lockfile_metadata import lockfile_content_with_header logger = logging.getLogger(__name__) @@ -164,7 +165,7 @@ async def generate_lockfile( _lockfile_contents_iter = await Get(DigestContents, Digest, generated_lockfile.output_digest) lockfile_contents = _lockfile_contents_iter[0] - content_with_header = validated_lockfile_content(req, lockfile_contents.content) + content_with_header = lockfile_content_with_header(req, lockfile_contents.content) complete_lockfile = await Get( Digest, CreateDigest([FileContent(req.dest, content_with_header)]) ) @@ -304,54 +305,5 @@ async def generate_all_tool_lockfiles( return ToolLockGoal(exit_code=0) -BEGIN_LOCKFILE_HEADER = b"# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" -END_LOCKFILE_HEADER = b"# --- END PANTS LOCKFILE METADATA ---" - - -# Lockfile metadata for headers -def validated_lockfile_content(req: PythonLockfileRequest, content: bytes) -> bytes: - return b"%b\n%b" % (lockfile_metadata(req.hex_digest), content) - - -def lockfile_metadata(invalidation_digest: str) -> bytes: - """Produces a metadata bytes object for including at the top of a lockfile. - - Currently, this only consists of an invalidation digest for the file, which is used when Pants - consumes the lockfile during builds. - """ - return ( - b""" -%(BEGIN_LOCKFILE_HEADER)b -# invalidation digest: %(invalidation_digest)s -%(END_LOCKFILE_HEADER)b - """ - % { - b"BEGIN_LOCKFILE_HEADER": BEGIN_LOCKFILE_HEADER, - b"invalidation_digest": invalidation_digest.encode("ascii"), - b"END_LOCKFILE_HEADER": END_LOCKFILE_HEADER, - } - ).strip() - - -def read_lockfile_metadata(contents: bytes) -> dict[str, str]: - """Reads through `contents`, and returns the contents of the lockfile metadata block as a - dictionary.""" - - metadata = {} - - in_metadata_block = False - for line in contents.splitlines(): - line = line.strip() - if line == BEGIN_LOCKFILE_HEADER: - in_metadata_block = True - elif line == END_LOCKFILE_HEADER: - break - elif in_metadata_block: - key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) - metadata[key] = value - - return metadata - - def rules(): return collect_rules() diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata.py b/src/python/pants/backend/experimental/python/lockfile_metadata.py new file mode 100644 index 00000000000..86eeec31a43 --- /dev/null +++ b/src/python/pants/backend/experimental/python/lockfile_metadata.py @@ -0,0 +1,68 @@ +# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations +from typing import TYPE_CHECKING + +from dataclasses import dataclass + +if TYPE_CHECKING: + from pants.backend.experimental.python.lockfile import PythonLockfileRequest + + +BEGIN_LOCKFILE_HEADER = b"# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" +END_LOCKFILE_HEADER = b"# --- END PANTS LOCKFILE METADATA ---" + + +@dataclass +class LockfileMetadata: + invalidation_digest: str | None + + +# Lockfile metadata for headers +def lockfile_content_with_header(req: PythonLockfileRequest, content: bytes) -> bytes: + """ Returns a version of the lockfile with a pants metadata header prepended. """ + return b"%b\n%b" % (lockfile_metadata_header(req.hex_digest), content) + + +def lockfile_metadata_header(invalidation_digest: str) -> bytes: + """Produces a metadata bytes object for including at the top of a lockfile. + + Currently, this only consists of an invalidation digest for the file, which is used when Pants + consumes the lockfile during builds. + """ + return ( + b""" +%(BEGIN_LOCKFILE_HEADER)b +# invalidation digest: %(invalidation_digest)s +%(END_LOCKFILE_HEADER)b + """ + % { + b"BEGIN_LOCKFILE_HEADER": BEGIN_LOCKFILE_HEADER, + b"invalidation_digest": invalidation_digest.encode("ascii"), + b"END_LOCKFILE_HEADER": END_LOCKFILE_HEADER, + } + ).strip() + + +def read_lockfile_metadata(contents: bytes) -> LockfileMetadata: + """Reads through `contents`, and returns the contents of the lockfile metadata block as a + dictionary.""" + + metadata = {} + + in_metadata_block = False + for line in contents.splitlines(): + line = line.strip() + if line == BEGIN_LOCKFILE_HEADER: + in_metadata_block = True + elif line == END_LOCKFILE_HEADER: + break + elif in_metadata_block: + key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) + metadata[key] = value + + return LockfileMetadata( + invalidation_digest=metadata.get("invalidation digest") + ) + diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index cbb7fbaf25e..44dbaf8c899 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -5,18 +5,16 @@ from pants.backend.experimental.python.lockfile import ( PythonLockfileRequest, - lockfile_metadata, - read_lockfile_metadata, - validated_lockfile_content, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.util.ordered_set import FrozenOrderedSet +from pants.backend.experimental.python.lockfile_metadata import lockfile_metadata_header, read_lockfile_metadata, validated_lockfile_content def test_metadata_round_trip() -> None: val = "help_im_trapped_inside_a_unit_test_string" - output = read_lockfile_metadata(lockfile_metadata(val)) - assert val == output["invalidation digest"] + output = read_lockfile_metadata(lockfile_metadata_header(val)) + assert val == output.invalidation_digest def test_validated_lockfile_content() -> None: From 7066ab21680f471359c8f1c2a2d95a7b6f18e278 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 12:26:45 -0700 Subject: [PATCH 10/16] Improves unit tests per code review comments # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../experimental/python/lockfile_test.py | 73 +++++-------------- 1 file changed, 18 insertions(+), 55 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 44dbaf8c899..9adbbba3d57 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -1,6 +1,7 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import pytest from unittest.mock import MagicMock from pants.backend.experimental.python.lockfile import ( @@ -8,7 +9,7 @@ ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.util.ordered_set import FrozenOrderedSet -from pants.backend.experimental.python.lockfile_metadata import lockfile_metadata_header, read_lockfile_metadata, validated_lockfile_content +from pants.backend.experimental.python.lockfile_metadata import lockfile_metadata_header, read_lockfile_metadata, lockfile_content_with_header def test_metadata_round_trip() -> None: @@ -33,65 +34,27 @@ def test_validated_lockfile_content() -> None: # Helper function to make the test case more resilient to reformatting line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] - assert line_by_line(validated_lockfile_content(req, content)) == line_by_line(output) + assert line_by_line(lockfile_content_with_header(req, content)) == line_by_line(output) -def test_hex_digest_empty() -> None: - req = PythonLockfileRequest( - requirements=FrozenOrderedSet([]), - interpreter_constraints=InterpreterConstraints([]), - dest="lockfile.py", - description="empty", - ) - - assert req.hex_digest == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - - -def test_hex_digest_empty_interpreter_constraints() -> None: - req = PythonLockfileRequest( - requirements=FrozenOrderedSet( - [ - "help", - "meim", - "trap", - "pedi", - "naun", - "itte", - ] - ), - interpreter_constraints=InterpreterConstraints([]), - dest="lockfile.py", - description="empty", - ) +_interpreter_constraints = [">=3.7", "<3.10"] +_requirements = ["flake8-pantsbuild>=2.0,<3", "flake8-2020>=1.6.0,<1.7.0"] - assert req.hex_digest == "9056af98e88b5f1ce893dcb7d7e189bd813ef4f5009f26594e1499be546fb3e1" - - -def test_hex_digest_empty_requirements() -> None: - req = PythonLockfileRequest( - requirements=FrozenOrderedSet([]), - interpreter_constraints=InterpreterConstraints( - ["stda", "tase", "tand", "itsd", "arka", "ndsc"] - ), - dest="lockfile.py", - description="empty", - ) - - assert req.hex_digest == "312bd499be026b8ecedb95a3b3e234bdea28e82c2d40ae1b2a435fc20971e2c2" - - -def test_hex_digest_both_specified() -> None: +@pytest.mark.parametrize( + "requirements,interpreter_constraints,expected", + [ + ([], [], "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), + (_interpreter_constraints, [], "04a2a2691d10bde0a2320bf32e2d40c60d0db511613fabc71933137c87f61500"), + ([], _requirements, "4ffd0a2a29407ce3f6bf7bfca60fdfc6f7d6224adda3c62807eb86666edf93bf"), + (_interpreter_constraints, _requirements, "6c63e6595f2f6827b6c1b53b186a2fa2020942bbfe989e25059a493b32a8bf36"), + ], +) +def test_hex_digest(requirements, interpreter_constraints, expected) -> None: req = PythonLockfileRequest( - requirements=FrozenOrderedSet(["aryi", "nher", "eple", "ases", "avem"]), - interpreter_constraints=InterpreterConstraints( - [ - "efro", - "mitq", - "uick", - ] - ), + requirements=FrozenOrderedSet(requirements), + interpreter_constraints=InterpreterConstraints(interpreter_constraints), dest="lockfile.py", description="empty", ) - assert req.hex_digest == "fc71e036ed72f43b9e0f2dcd37050a9e21773a619f0172ab8b657be9ee502fb6" + assert req.hex_digest == expected From 4a489bb91cffb4919cff31fafa2975810a92a0b6 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 12:33:50 -0700 Subject: [PATCH 11/16] Fixes formatting issues # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 2 +- .../experimental/python/lockfile_metadata.py | 9 +++---- .../experimental/python/lockfile_test.py | 26 ++++++++++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index d754bee5e39..548b70daa7d 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from typing import cast +from pants.backend.experimental.python.lockfile_metadata import lockfile_content_with_header from pants.backend.python.subsystems.python_tool_base import ( PythonToolBase, PythonToolRequirementsBase, @@ -33,7 +34,6 @@ from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet from pants.util.strutil import pluralize -from pants.backend.experimental.python.lockfile_metadata import lockfile_content_with_header logger = logging.getLogger(__name__) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata.py b/src/python/pants/backend/experimental/python/lockfile_metadata.py index 86eeec31a43..f0b6381ea60 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). from __future__ import annotations -from typing import TYPE_CHECKING from dataclasses import dataclass +from typing import TYPE_CHECKING if TYPE_CHECKING: from pants.backend.experimental.python.lockfile import PythonLockfileRequest @@ -21,7 +21,7 @@ class LockfileMetadata: # Lockfile metadata for headers def lockfile_content_with_header(req: PythonLockfileRequest, content: bytes) -> bytes: - """ Returns a version of the lockfile with a pants metadata header prepended. """ + """Returns a version of the lockfile with a pants metadata header prepended.""" return b"%b\n%b" % (lockfile_metadata_header(req.hex_digest), content) @@ -62,7 +62,4 @@ def read_lockfile_metadata(contents: bytes) -> LockfileMetadata: key, value = (i.strip().decode("ascii") for i in line[1:].split(b":")) metadata[key] = value - return LockfileMetadata( - invalidation_digest=metadata.get("invalidation digest") - ) - + return LockfileMetadata(invalidation_digest=metadata.get("invalidation digest")) diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 9adbbba3d57..00b188af87c 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -1,19 +1,22 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -import pytest from unittest.mock import MagicMock -from pants.backend.experimental.python.lockfile import ( - PythonLockfileRequest, +import pytest + +from pants.backend.experimental.python.lockfile import PythonLockfileRequest +from pants.backend.experimental.python.lockfile_metadata import ( + lockfile_content_with_header, + lockfile_metadata_header, + read_lockfile_metadata, ) from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints from pants.util.ordered_set import FrozenOrderedSet -from pants.backend.experimental.python.lockfile_metadata import lockfile_metadata_header, read_lockfile_metadata, lockfile_content_with_header def test_metadata_round_trip() -> None: - val = "help_im_trapped_inside_a_unit_test_string" + val = "help_i_am_trapped_inside_a_unit_test_string" output = read_lockfile_metadata(lockfile_metadata_header(val)) assert val == output.invalidation_digest @@ -40,13 +43,22 @@ def test_validated_lockfile_content() -> None: _interpreter_constraints = [">=3.7", "<3.10"] _requirements = ["flake8-pantsbuild>=2.0,<3", "flake8-2020>=1.6.0,<1.7.0"] + @pytest.mark.parametrize( "requirements,interpreter_constraints,expected", [ ([], [], "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - (_interpreter_constraints, [], "04a2a2691d10bde0a2320bf32e2d40c60d0db511613fabc71933137c87f61500"), + ( + _interpreter_constraints, + [], + "04a2a2691d10bde0a2320bf32e2d40c60d0db511613fabc71933137c87f61500", + ), ([], _requirements, "4ffd0a2a29407ce3f6bf7bfca60fdfc6f7d6224adda3c62807eb86666edf93bf"), - (_interpreter_constraints, _requirements, "6c63e6595f2f6827b6c1b53b186a2fa2020942bbfe989e25059a493b32a8bf36"), + ( + _interpreter_constraints, + _requirements, + "6c63e6595f2f6827b6c1b53b186a2fa2020942bbfe989e25059a493b32a8bf36", + ), ], ) def test_hex_digest(requirements, interpreter_constraints, expected) -> None: From 08ee67b74293bf175ec1b3920de59af6366213d5 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 12:48:34 -0700 Subject: [PATCH 12/16] Removes cicrular dependency in `lockfile_metadata` [ci skip-rust] [ci skip-build-wheels] --- .../pants/backend/experimental/python/lockfile.py | 2 +- .../backend/experimental/python/lockfile_metadata.py | 11 +++-------- .../backend/experimental/python/lockfile_test.py | 7 +++---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index 548b70daa7d..a3e4c9667d4 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -165,7 +165,7 @@ async def generate_lockfile( _lockfile_contents_iter = await Get(DigestContents, Digest, generated_lockfile.output_digest) lockfile_contents = _lockfile_contents_iter[0] - content_with_header = lockfile_content_with_header(req, lockfile_contents.content) + content_with_header = lockfile_content_with_header(req.hex_digest, lockfile_contents.content) complete_lockfile = await Get( Digest, CreateDigest([FileContent(req.dest, content_with_header)]) ) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata.py b/src/python/pants/backend/experimental/python/lockfile_metadata.py index f0b6381ea60..db37d70ff11 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata.py @@ -4,11 +4,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pants.backend.experimental.python.lockfile import PythonLockfileRequest - BEGIN_LOCKFILE_HEADER = b"# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" END_LOCKFILE_HEADER = b"# --- END PANTS LOCKFILE METADATA ---" @@ -20,9 +15,9 @@ class LockfileMetadata: # Lockfile metadata for headers -def lockfile_content_with_header(req: PythonLockfileRequest, content: bytes) -> bytes: +def lockfile_content_with_header(invalidation_digest: str, content: bytes) -> bytes: """Returns a version of the lockfile with a pants metadata header prepended.""" - return b"%b\n%b" % (lockfile_metadata_header(req.hex_digest), content) + return b"%b\n%b" % (lockfile_metadata_header(invalidation_digest), content) def lockfile_metadata_header(invalidation_digest: str) -> bytes: @@ -47,7 +42,7 @@ def lockfile_metadata_header(invalidation_digest: str) -> bytes: def read_lockfile_metadata(contents: bytes) -> LockfileMetadata: """Reads through `contents`, and returns the contents of the lockfile metadata block as a - dictionary.""" + `LockfileMetadata` object.""" metadata = {} diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 00b188af87c..929c67b486b 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -1,8 +1,6 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from unittest.mock import MagicMock - import pytest from pants.backend.experimental.python.lockfile import PythonLockfileRequest @@ -22,7 +20,6 @@ def test_metadata_round_trip() -> None: def test_validated_lockfile_content() -> None: - req = MagicMock(hex_digest="000faaafcacacaca") content = b"""dave==3.1.4 \\ --hash=sha256:cab0c0c0c0c0dadacafec0c0c0c0cafedadabeefc0c0c0c0feedbeeffeedbeef \\ """ @@ -37,7 +34,9 @@ def test_validated_lockfile_content() -> None: # Helper function to make the test case more resilient to reformatting line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] - assert line_by_line(lockfile_content_with_header(req, content)) == line_by_line(output) + assert line_by_line(lockfile_content_with_header("000faaafcacacaca", content)) == line_by_line( + output + ) _interpreter_constraints = [">=3.7", "<3.10"] From 324dd0e8725a27432b7345eaa6c6acb773ebca92 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 12:56:30 -0700 Subject: [PATCH 13/16] Remove walrus # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- src/python/pants/backend/experimental/python/lockfile_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_test.py index 929c67b486b..1b4dd281be4 100644 --- a/src/python/pants/backend/experimental/python/lockfile_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_test.py @@ -33,7 +33,7 @@ def test_validated_lockfile_content() -> None: """ # Helper function to make the test case more resilient to reformatting - line_by_line = lambda b: [ii for i in b.splitlines() if (ii := i.strip())] + line_by_line = lambda b: [i for i in (j.strip() for j in b.splitlines()) if i] assert line_by_line(lockfile_content_with_header("000faaafcacacaca", content)) == line_by_line( output ) From ba02a9a70d135a146638f6e046571d5e17d54bca Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 28 Jul 2021 13:06:28 -0700 Subject: [PATCH 14/16] Renamed `lockfile_test.py` # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../python/{lockfile_test.py => lockfile_metadata_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/python/pants/backend/experimental/python/{lockfile_test.py => lockfile_metadata_test.py} (100%) diff --git a/src/python/pants/backend/experimental/python/lockfile_test.py b/src/python/pants/backend/experimental/python/lockfile_metadata_test.py similarity index 100% rename from src/python/pants/backend/experimental/python/lockfile_test.py rename to src/python/pants/backend/experimental/python/lockfile_metadata_test.py From 1d517fc11890d9e9c43c6885b664fa8b60b58db1 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 29 Jul 2021 14:34:59 -0700 Subject: [PATCH 15/16] Updates invalidation digest output # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../backend/experimental/python/lockfile.py | 13 +++----- .../experimental/python/lockfile_metadata.py | 19 ++++++++++- .../python/lockfile_metadata_test.py | 32 ++++++++++++------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile.py b/src/python/pants/backend/experimental/python/lockfile.py index a3e4c9667d4..45827104fcf 100644 --- a/src/python/pants/backend/experimental/python/lockfile.py +++ b/src/python/pants/backend/experimental/python/lockfile.py @@ -3,12 +3,14 @@ from __future__ import annotations -import hashlib import logging from dataclasses import dataclass from typing import cast -from pants.backend.experimental.python.lockfile_metadata import lockfile_content_with_header +from pants.backend.experimental.python.lockfile_metadata import ( + invalidation_digest, + lockfile_content_with_header, +) from pants.backend.python.subsystems.python_tool_base import ( PythonToolBase, PythonToolRequirementsBase, @@ -109,12 +111,7 @@ def hex_digest(self) -> str: Inputs are definted as requirements and interpreter constraints. """ - m = hashlib.sha256() - for requirement in self.requirements: - m.update(requirement.encode("utf-8")) - for constraint in self.interpreter_constraints: - m.update(str(constraint).encode("utf-8")) - return m.hexdigest() + return invalidation_digest(self.requirements, self.interpreter_constraints) @rule(desc="Generate lockfile", level=LogLevel.DEBUG) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata.py b/src/python/pants/backend/experimental/python/lockfile_metadata.py index db37d70ff11..29924f81d4e 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata.py @@ -3,8 +3,13 @@ from __future__ import annotations +import hashlib +import json from dataclasses import dataclass +from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints +from pants.util.ordered_set import FrozenOrderedSet + BEGIN_LOCKFILE_HEADER = b"# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---" END_LOCKFILE_HEADER = b"# --- END PANTS LOCKFILE METADATA ---" @@ -14,7 +19,19 @@ class LockfileMetadata: invalidation_digest: str | None -# Lockfile metadata for headers +def invalidation_digest( + requirements: FrozenOrderedSet[str], interpreter_constraints: InterpreterConstraints +) -> str: + """Returns an invalidation digest for the given requirements and interpreter constraints.""" + m = hashlib.sha256() + pres = { + "requirements": [str(i) for i in requirements], + "interpreter_constraints": [str(i) for i in interpreter_constraints], + } + m.update(json.dumps(pres).encode("utf-8")) + return m.hexdigest() + + def lockfile_content_with_header(invalidation_digest: str, content: bytes) -> bytes: """Returns a version of the lockfile with a pants metadata header prepended.""" return b"%b\n%b" % (lockfile_metadata_header(invalidation_digest), content) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata_test.py b/src/python/pants/backend/experimental/python/lockfile_metadata_test.py index 1b4dd281be4..3b324989fc8 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata_test.py @@ -3,8 +3,8 @@ import pytest -from pants.backend.experimental.python.lockfile import PythonLockfileRequest from pants.backend.experimental.python.lockfile_metadata import ( + invalidation_digest, lockfile_content_with_header, lockfile_metadata_header, read_lockfile_metadata, @@ -46,26 +46,36 @@ def test_validated_lockfile_content() -> None: @pytest.mark.parametrize( "requirements,interpreter_constraints,expected", [ - ([], [], "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), + ([], [], "51f5289473089f1de64ab760af3f03ff55cd769f25cce7ea82dd1ac88aac5ff4"), ( _interpreter_constraints, [], - "04a2a2691d10bde0a2320bf32e2d40c60d0db511613fabc71933137c87f61500", + "821e8eef80573c7d2460185da4d436b6a8c59e134f5f0758000be3c85e9819eb", ), - ([], _requirements, "4ffd0a2a29407ce3f6bf7bfca60fdfc6f7d6224adda3c62807eb86666edf93bf"), + ([], _requirements, "604fb99ed6d6d83ba2c4eb1230184dd7f279a446cda042e9e87099448f28dddb"), ( _interpreter_constraints, _requirements, - "6c63e6595f2f6827b6c1b53b186a2fa2020942bbfe989e25059a493b32a8bf36", + "9264a3b59a592d7eeac9cb4bbb4f5b2200907694bfe92b48757c99b1f71485f0", ), ], ) def test_hex_digest(requirements, interpreter_constraints, expected) -> None: - req = PythonLockfileRequest( - requirements=FrozenOrderedSet(requirements), - interpreter_constraints=InterpreterConstraints(interpreter_constraints), - dest="lockfile.py", - description="empty", + print( + invalidation_digest( + FrozenOrderedSet(requirements), InterpreterConstraints(interpreter_constraints) + ) ) + assert ( + invalidation_digest( + FrozenOrderedSet(requirements), InterpreterConstraints(interpreter_constraints) + ) + == expected + ) + - assert req.hex_digest == expected +def test_hash_depends_on_requirement_source(): + reqs = ["CPython"] + assert invalidation_digest( + FrozenOrderedSet(reqs), InterpreterConstraints([]) + ) != invalidation_digest(FrozenOrderedSet([]), InterpreterConstraints(reqs)) From 3694ae3904c5d502340ba6082513f70e8e5f4e84 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 30 Jul 2021 08:24:35 -0700 Subject: [PATCH 16/16] Resolve small nitpicks # Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels] --- .../pants/backend/experimental/python/lockfile_metadata.py | 2 +- .../backend/experimental/python/lockfile_metadata_test.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata.py b/src/python/pants/backend/experimental/python/lockfile_metadata.py index 29924f81d4e..cb9e79fe81f 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata.py @@ -25,7 +25,7 @@ def invalidation_digest( """Returns an invalidation digest for the given requirements and interpreter constraints.""" m = hashlib.sha256() pres = { - "requirements": [str(i) for i in requirements], + "requirements": list(requirements), "interpreter_constraints": [str(i) for i in interpreter_constraints], } m.update(json.dumps(pres).encode("utf-8")) diff --git a/src/python/pants/backend/experimental/python/lockfile_metadata_test.py b/src/python/pants/backend/experimental/python/lockfile_metadata_test.py index 3b324989fc8..582fe0d4279 100644 --- a/src/python/pants/backend/experimental/python/lockfile_metadata_test.py +++ b/src/python/pants/backend/experimental/python/lockfile_metadata_test.py @@ -61,11 +61,6 @@ def test_validated_lockfile_content() -> None: ], ) def test_hex_digest(requirements, interpreter_constraints, expected) -> None: - print( - invalidation_digest( - FrozenOrderedSet(requirements), InterpreterConstraints(interpreter_constraints) - ) - ) assert ( invalidation_digest( FrozenOrderedSet(requirements), InterpreterConstraints(interpreter_constraints) @@ -74,7 +69,7 @@ def test_hex_digest(requirements, interpreter_constraints, expected) -> None: ) -def test_hash_depends_on_requirement_source(): +def test_hash_depends_on_requirement_source() -> None: reqs = ["CPython"] assert invalidation_digest( FrozenOrderedSet(reqs), InterpreterConstraints([])