Merge ~cjwatson/launchpad:refactor-ci-build-upload-job into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 971e0361b7c2234c8ed97649cc46bc70ca29f625
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:refactor-ci-build-upload-job
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:ci-build-upload-require-package
Diff against target: 212 lines (+99/-53)
1 file modified
lib/lp/soyuz/model/archivejob.py (+99/-53)
Reviewer Review Type Date Requested Status
Andrey Fedoseev (community) Approve
Review via email: mp+426403@code.launchpad.net

Commit message

Refactor CIBuildUploadJob.run

Description of the change

The `CIBuildUploadJob.run` method was getting a bit long and unwieldy. Break it up with a couple of new helper methods.

`ScannedArtifact` isn't all that useful yet, but I'll be adding a couple more things to it soon so it seemed better to have this abstraction rather than a simple tuple.

To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

Looks good

A few notes:

1. I typically prefer to place higher-level methods above the lower-level methods that they call. In this case, the order would be:

  def run():
  def _scanArtifacts()
  def _scanFile()
  def _uploadPackages()

This follows the order in which the methods are called and gives the reader a nice top-down view.

2. I suggest adding type annotations to the new methods that you've extracted.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

1. I've always specifically preferred the opposite of that. This is partly because I learned rather less capable languages than Python first, but even in Python forward references are occasionally problematic (e.g. decorators must be declared before functions they decorate), and so I try to stick to backward references wherever possible. My mental model of programs tends to be more about building up from basic functions until I reach the overall goal that brings everything together, so I find that this suits me better when reading programs too. (Maybe it's because I used to be a mathematician, and this bottom-up order is how proofs are normally stated?)

2. Yes, I expect to need some reminding of this until I really get into the habit, sorry! Done.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Not that it adds any value, but I never cared about method ordering, as I usually keep my classes short and I have IDE support for jumping from callers to implementation and back again.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/soyuz/model/archivejob.py b/lib/lp/soyuz/model/archivejob.py
2index e2bf402..d92edd3 100644
3--- a/lib/lp/soyuz/model/archivejob.py
4+++ b/lib/lp/soyuz/model/archivejob.py
5@@ -1,12 +1,19 @@
6 # Copyright 2010-2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9+from collections import OrderedDict
10 import io
11 import json
12 import logging
13 import os.path
14 import tarfile
15 import tempfile
16+from typing import (
17+ Any,
18+ Dict,
19+ Iterable,
20+ List,
21+ )
22 import zipfile
23
24 from lazr.delegates import delegate_to
25@@ -27,7 +34,10 @@ import zstandard
26
27 from lp.code.enums import RevisionStatusArtifactType
28 from lp.code.interfaces.cibuild import ICIBuildSet
29-from lp.code.interfaces.revisionstatus import IRevisionStatusArtifactSet
30+from lp.code.interfaces.revisionstatus import (
31+ IRevisionStatusArtifact,
32+ IRevisionStatusArtifactSet,
33+ )
34 from lp.registry.interfaces.distributionsourcepackage import (
35 IDistributionSourcePackage,
36 )
37@@ -202,6 +212,15 @@ class ScanException(Exception):
38 """A CI build upload job failed to scan a file."""
39
40
41+class ScannedArtifact:
42+
43+ def __init__(
44+ self, *, artifact: IRevisionStatusArtifact, metadata: Dict[str, Any]
45+ ):
46+ self.artifact = artifact
47+ self.metadata = metadata
48+
49+
50 @implementer(ICIBuildUploadJob)
51 @provider(ICIBuildUploadJobSource)
52 class CIBuildUploadJob(ArchiveJobDerived):
53@@ -307,7 +326,9 @@ class CIBuildUploadJob(ArchiveJobDerived):
54 def target_channel(self):
55 return self.metadata["target_channel"]
56
57- def _scanCondaMetadata(self, index, about):
58+ def _scanCondaMetadata(
59+ self, index: Dict[Any, Any], about: Dict[Any, Any]
60+ ) -> Dict[str, Any]:
61 return {
62 "name": index["name"],
63 "version": index["version"],
64@@ -322,7 +343,7 @@ class CIBuildUploadJob(ArchiveJobDerived):
65 "user_defined_fields": [("subdir", index["subdir"])],
66 }
67
68- def _scanFile(self, path):
69+ def _scanFile(self, path: str) -> Dict[str, Any]:
70 # XXX cjwatson 2022-06-10: We should probably start splitting this
71 # up some more.
72 name = os.path.basename(path)
73@@ -375,26 +396,23 @@ class CIBuildUploadJob(ArchiveJobDerived):
74 else:
75 return None
76
77- def run(self):
78- """See `IRunnableJob`."""
79- build_target = self.ci_build.git_repository.target
80- if not IDistributionSourcePackage.providedBy(build_target):
81- # This should be caught by `Archive.uploadCIBuild`, but check it
82- # here as well just in case.
83- logger.warning(
84- "Source CI build is for %s, which is not a package",
85- repr(build_target))
86- return
87+ def _scanArtifacts(
88+ self, artifacts: Iterable[IRevisionStatusArtifact]
89+ ) -> List[ScannedArtifact]:
90+ """Scan an iterable of `RevisionStatusArtifact`s for metadata.
91+
92+ Skips log artifacts, artifacts we don't understand, and artifacts
93+ not relevant to the target archive's repository format.
94+
95+ Returns a list of `ScannedArtifact`s containing metadata for
96+ relevant artifacts.
97+ """
98+ allowed_binary_formats = (
99+ self.binary_format_by_repository_format.get(
100+ self.archive.repository_format, set()))
101+ scanned = []
102 with tempfile.TemporaryDirectory(prefix="ci-build-copy-job") as tmpdir:
103- releases = {
104- (release.binarypackagename, release.binpackageformat): release
105- for release in self.ci_build.binarypackages}
106- allowed_binary_formats = (
107- self.binary_format_by_repository_format.get(
108- self.archive.repository_format, set()))
109- binaries = {}
110- for artifact in getUtility(
111- IRevisionStatusArtifactSet).findByCIBuild(self.ci_build):
112+ for artifact in artifacts:
113 if artifact.artifact_type == RevisionStatusArtifactType.LOG:
114 continue
115 name = artifact.library_file.filename
116@@ -403,37 +421,65 @@ class CIBuildUploadJob(ArchiveJobDerived):
117 copy_and_close(artifact.library_file, open(contents, "wb"))
118 metadata = self._scanFile(contents)
119 if metadata is None:
120- logger.info("No upload handler for %s" % name)
121+ logger.info("No upload handler for %s", name)
122 continue
123- binpackageformat = metadata["binpackageformat"]
124- if binpackageformat not in allowed_binary_formats:
125+ if metadata["binpackageformat"] not in allowed_binary_formats:
126 logger.info(
127- "Skipping %s (not relevant to %s archives)" % (
128- name, self.archive.repository_format))
129+ "Skipping %s (not relevant to %s archives)",
130+ name, self.archive.repository_format)
131 continue
132- logger.info(
133- "Uploading %s to %s %s (%s)" % (
134- name, self.archive.reference,
135- self.target_distroseries.getSuite(self.target_pocket),
136- self.target_channel))
137- metadata["binarypackagename"] = bpn = (
138- getUtility(IBinaryPackageNameSet).ensure(metadata["name"]))
139- del metadata["name"]
140- filetype = self.filetype_by_format[binpackageformat]
141- bpr = releases.get((bpn, binpackageformat))
142- if bpr is None:
143- bpr = self.ci_build.createBinaryPackageRelease(**metadata)
144- for bpf in bpr.files:
145- if (bpf.libraryfile == artifact.library_file and
146- bpf.filetype == filetype):
147- break
148- else:
149- bpr.addFile(artifact.library_file, filetype=filetype)
150- # The publishBinaries interface was designed for .debs,
151- # which need extra per-binary "override" information
152- # (component, etc.). None of this is relevant here.
153- binaries[bpr] = (None, None, None, None)
154- if binaries:
155- getUtility(IPublishingSet).publishBinaries(
156- self.archive, self.target_distroseries, self.target_pocket,
157- binaries, channel=self.target_channel)
158+ scanned.append(
159+ ScannedArtifact(artifact=artifact, metadata=metadata))
160+ return scanned
161+
162+ def _uploadBinaries(self, scanned: Iterable[ScannedArtifact]) -> None:
163+ """Upload an iterable of `ScannedArtifact`s to an archive."""
164+ releases = {
165+ (release.binarypackagename, release.binpackageformat): release
166+ for release in self.ci_build.binarypackages}
167+ binaries = OrderedDict()
168+ for scanned_artifact in scanned:
169+ library_file = scanned_artifact.artifact.library_file
170+ metadata = dict(scanned_artifact.metadata)
171+ binpackageformat = metadata["binpackageformat"]
172+ logger.info(
173+ "Uploading %s to %s %s (%s)",
174+ library_file.filename, self.archive.reference,
175+ self.target_distroseries.getSuite(self.target_pocket),
176+ self.target_channel)
177+ metadata["binarypackagename"] = bpn = (
178+ getUtility(IBinaryPackageNameSet).ensure(metadata["name"]))
179+ del metadata["name"]
180+ filetype = self.filetype_by_format[binpackageformat]
181+ bpr = releases.get((bpn, binpackageformat))
182+ if bpr is None:
183+ bpr = self.ci_build.createBinaryPackageRelease(**metadata)
184+ for bpf in bpr.files:
185+ if (bpf.libraryfile == library_file and
186+ bpf.filetype == filetype):
187+ break
188+ else:
189+ bpr.addFile(library_file, filetype=filetype)
190+ # The publishBinaries interface was designed for .debs,
191+ # which need extra per-binary "override" information
192+ # (component, etc.). None of this is relevant here.
193+ binaries[bpr] = (None, None, None, None)
194+ getUtility(IPublishingSet).publishBinaries(
195+ self.archive, self.target_distroseries, self.target_pocket,
196+ binaries, channel=self.target_channel)
197+
198+ def run(self) -> None:
199+ """See `IRunnableJob`."""
200+ build_target = self.ci_build.git_repository.target
201+ if not IDistributionSourcePackage.providedBy(build_target):
202+ # This should be caught by `Archive.uploadCIBuild`, but check it
203+ # here as well just in case.
204+ logger.warning(
205+ "Source CI build is for %s, which is not a package",
206+ repr(build_target))
207+ return
208+ artifacts = getUtility(IRevisionStatusArtifactSet).findByCIBuild(
209+ self.ci_build)
210+ scanned = self._scanArtifacts(artifacts)
211+ if scanned:
212+ self._uploadBinaries(scanned)

Subscribers

People subscribed via source and target branches

to status/vote changes: