Skip to content

Commit 74a42a4

Browse files
AbrilRBSmemshardedczoido
authored
Stabilize PackagesList API (#18833)
* Document important PackagesList methods * wip * wip * proposal draft * wip * dirty, but to see if tests pass * fix test * remove print * review * walk()->items() + accessor * remove private ._data access * wip * wip * wip * wip * Some last changes to the decoumentation * Update conan/api/model/list.py Co-authored-by: Carlos Zoido <[email protected]> * Update conan/api/model/list.py Co-authored-by: Carlos Zoido <[email protected]> * Update test/integration/command/upload/test_upload_bundle.py Co-authored-by: Carlos Zoido <[email protected]> * review, normalized package list name * Remove last pkglist in help strings * Update conan/cli/commands/audit.py --------- Co-authored-by: memsharded <[email protected]> Co-authored-by: James <[email protected]> Co-authored-by: Carlos Zoido <[email protected]>
1 parent 9a8bf21 commit 74a42a4

File tree

21 files changed

+283
-184
lines changed

21 files changed

+283
-184
lines changed

conan/api/model/list.py

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import copy
12
import fnmatch
23
import json
34
import os
45
from json import JSONDecodeError
6+
from typing import Iterable
57

68
from conan.api.model import RecipeReference, PkgReference
9+
from conan.api.output import ConanOutput
710
from conan.errors import ConanException
811
from conan.internal.errors import NotFoundException
912
from conan.internal.model.version_range import VersionRange
@@ -104,7 +107,7 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None)
104107
)
105108

106109
mpkglist = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
107-
context=base_context)
110+
context=base_context)
108111
if context == "build-only":
109112
host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
110113
context="host")
@@ -150,11 +153,11 @@ def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None):
150153
continue
151154
pyref = RecipeReference.loads(pyref)
152155
if any(r == "*" or r == pyrecipe for r in recipes):
153-
cache_list.add_refs([pyref])
156+
cache_list.add_ref(pyref)
154157
pyremote = pyreq["remote"]
155158
if pyremote:
156159
remote_list = pkglist.lists.setdefault(pyremote, PackagesList())
157-
remote_list.add_refs([pyref])
160+
remote_list.add_ref(pyref)
158161

159162
recipe = node["recipe"]
160163
if recipe in (RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_VIRTUAL, RECIPE_PLATFORM):
@@ -165,109 +168,136 @@ def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None):
165168
ref.timestamp = node["rrev_timestamp"]
166169
recipe = recipe.lower()
167170
if any(r == "*" or r == recipe for r in recipes):
168-
cache_list.add_refs([ref])
171+
cache_list.add_ref(ref)
169172

170173
remote = node["remote"]
171174
if remote:
172175
remote_list = pkglist.lists.setdefault(remote, PackagesList())
173-
remote_list.add_refs([ref])
176+
remote_list.add_ref(ref)
174177
pref = PkgReference(ref, node["package_id"], node["prev"], node["prev_timestamp"])
175178
binary_remote = node["binary_remote"]
176179
if binary_remote:
177180
remote_list = pkglist.lists.setdefault(binary_remote, PackagesList())
178-
remote_list.add_refs([ref]) # Binary listed forces recipe listed
179-
remote_list.add_prefs(ref, [pref])
181+
remote_list.add_ref(ref) # Binary listed forces recipe listed
182+
remote_list.add_pref(pref)
180183

181184
binary = node["binary"]
182185
if binary in (BINARY_SKIP, BINARY_INVALID, BINARY_MISSING):
183186
continue
184187

185188
binary = binary.lower()
186189
if any(b == "*" or b == binary for b in binaries):
187-
cache_list.add_refs([ref]) # Binary listed forces recipe listed
188-
cache_list.add_prefs(ref, [pref])
189-
cache_list.add_configurations({pref: node["info"]})
190+
cache_list.add_ref(ref) # Binary listed forces recipe listed
191+
cache_list.add_pref(pref, node["info"])
190192
return pkglist
191193

192194

193195
class PackagesList:
194196
""" A collection of recipes, revisions and packages."""
195197
def __init__(self):
196-
self.recipes = {}
198+
self._data = {}
199+
200+
def __bool__(self):
201+
""" Whether the package list contains any recipe"""
202+
return bool(self._data)
197203

198204
def merge(self, other):
205+
assert isinstance(other, PackagesList)
199206
def recursive_dict_update(d, u): # TODO: repeated from conandata.py
200207
for k, v in u.items():
201208
if isinstance(v, dict):
202209
d[k] = recursive_dict_update(d.get(k, {}), v)
203210
else:
204211
d[k] = v
205212
return d
206-
recursive_dict_update(self.recipes, other.recipes)
213+
recursive_dict_update(self._data, other._data)
207214

208215
def keep_outer(self, other):
209-
if not self.recipes:
216+
assert isinstance(other, PackagesList)
217+
if not self._data:
210218
return
211219

212-
for ref, info in other.recipes.items():
213-
if self.recipes.get(ref, {}) == info:
214-
self.recipes.pop(ref)
220+
for ref, info in other._data.items():
221+
if self._data.get(ref, {}) == info:
222+
self._data.pop(ref)
215223

216224
def split(self):
217225
"""
218-
Returns a list of PackageList, splitted one per reference.
226+
Returns a list of PackageList, split one per reference.
219227
This can be useful to parallelize things like upload, parallelizing per-reference
220228
"""
221229
result = []
222-
for r, content in self.recipes.items():
230+
for r, content in self._data.items():
223231
subpkglist = PackagesList()
224-
subpkglist.recipes[r] = content
232+
subpkglist._data[r] = content
225233
result.append(subpkglist)
226234
return result
227235

228236
def only_recipes(self) -> None:
229237
""" Filter out all the packages and package revisions, keep only the recipes and
230-
recipe revisions in self.recipes.
238+
recipe revisions in self._data.
231239
"""
232-
for ref, ref_dict in self.recipes.items():
240+
for ref, ref_dict in self._data.items():
233241
for rrev_dict in ref_dict.get("revisions", {}).values():
234242
rrev_dict.pop("packages", None)
235243

236244
def add_refs(self, refs):
245+
ConanOutput().warning("PackagesLists.add_refs() non-public, non-documented method will be "
246+
"removed, use .add_ref() instead", warn_tag="deprecated")
237247
# RREVS alreday come in ASCENDING order, so upload does older revisions first
238248
for ref in refs:
239-
ref_dict = self.recipes.setdefault(str(ref), {})
240-
if ref.revision:
241-
revs_dict = ref_dict.setdefault("revisions", {})
242-
rev_dict = revs_dict.setdefault(ref.revision, {})
243-
if ref.timestamp:
244-
rev_dict["timestamp"] = ref.timestamp
249+
self.add_ref(ref)
250+
251+
def add_ref(self, ref: RecipeReference) -> None:
252+
"""
253+
Adds a new RecipeReference to a package list
254+
"""
255+
ref_dict = self._data.setdefault(str(ref), {})
256+
if ref.revision:
257+
revs_dict = ref_dict.setdefault("revisions", {})
258+
rev_dict = revs_dict.setdefault(ref.revision, {})
259+
if ref.timestamp:
260+
rev_dict["timestamp"] = ref.timestamp
245261

246262
def add_prefs(self, rrev, prefs):
263+
ConanOutput().warning("PackageLists.add_prefs() non-public, non-documented method will be "
264+
"removed, use .add_pref() instead", warn_tag="deprecated")
247265
# Prevs already come in ASCENDING order, so upload does older revisions first
248-
revs_dict = self.recipes[str(rrev)]["revisions"]
249-
rev_dict = revs_dict[rrev.revision]
250-
packages_dict = rev_dict.setdefault("packages", {})
266+
for p in prefs:
267+
self.add_pref(p)
251268

252-
for pref in prefs:
253-
package_dict = packages_dict.setdefault(pref.package_id, {})
254-
if pref.revision:
255-
prevs_dict = package_dict.setdefault("revisions", {})
256-
prev_dict = prevs_dict.setdefault(pref.revision, {})
257-
if pref.timestamp:
258-
prev_dict["timestamp"] = pref.timestamp
269+
def add_pref(self, pref: PkgReference, pkg_info: dict = None) -> None:
270+
"""
271+
Add a PkgReference to an already existing RecipeReference inside a package list
272+
"""
273+
# Prevs already come in ASCENDING order, so upload does older revisions first
274+
rev_dict = self.recipe_dict(pref.ref)
275+
packages_dict = rev_dict.setdefault("packages", {})
276+
package_dict = packages_dict.setdefault(pref.package_id, {})
277+
if pref.revision:
278+
prevs_dict = package_dict.setdefault("revisions", {})
279+
prev_dict = prevs_dict.setdefault(pref.revision, {})
280+
if pref.timestamp:
281+
prev_dict["timestamp"] = pref.timestamp
282+
if pkg_info is not None:
283+
package_dict["info"] = pkg_info
259284

260285
def add_configurations(self, confs):
286+
ConanOutput().warning("PackageLists.add_configurations() non-public, non-documented method "
287+
"will be removed, use .add_pref() instead",
288+
warn_tag="deprecated")
261289
for pref, conf in confs.items():
262-
rev_dict = self.recipes[str(pref.ref)]["revisions"][pref.ref.revision]
290+
rev_dict = self.recipe_dict(pref.ref)
263291
try:
264292
rev_dict["packages"][pref.package_id]["info"] = conf
265293
except KeyError: # If package_id does not exist, do nothing, only add to existing prefs
266294
pass
267295

268296
def refs(self):
297+
ConanOutput().warning("PackageLists.refs() non-public, non-documented method will be "
298+
"removed, use .items() instead", warn_tag="deprecated")
269299
result = {}
270-
for ref, ref_dict in self.recipes.items():
300+
for ref, ref_dict in self._data.items():
271301
for rrev, rrev_dict in ref_dict.get("revisions", {}).items():
272302
t = rrev_dict.get("timestamp")
273303
recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this
@@ -276,8 +306,45 @@ def refs(self):
276306
result[recipe] = rrev_dict
277307
return result
278308

309+
def items(self) -> Iterable[tuple[RecipeReference, dict[PkgReference, dict]]]:
310+
""" Iterate the contents of the package list.
311+
312+
The first dictionary is the information directly belonging to the recipe-revision.
313+
The second dictionary contains PkgReference as keys, and a dictionary with the values
314+
belonging to that specific package reference (settings, options, etc.).
315+
"""
316+
for ref, ref_dict in self._data.items():
317+
for rrev, rrev_dict in ref_dict.get("revisions", {}).items():
318+
recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this
319+
t = rrev_dict.get("timestamp")
320+
if t is not None:
321+
recipe.timestamp = t
322+
packages = {}
323+
for package_id, pkg_info in rrev_dict.get("packages", {}).items():
324+
prevs = pkg_info.get("revisions", {})
325+
for prev, prev_info in prevs.items():
326+
t = prev_info.get("timestamp")
327+
pref = PkgReference(recipe, package_id, prev, t)
328+
packages[pref] = prev_info
329+
yield recipe, packages
330+
331+
def recipe_dict(self, ref: RecipeReference):
332+
""" Gives read/write access to the dictionary containing a specific RecipeReference
333+
information.
334+
"""
335+
return self._data[str(ref)]["revisions"][ref.revision]
336+
337+
def package_dict(self, pref: PkgReference):
338+
""" Gives read/write access to the dictionary containing a specific PkgReference
339+
information
340+
"""
341+
ref_dict = self.recipe_dict(pref.ref)
342+
return ref_dict["packages"][pref.package_id]["revisions"][pref.revision]
343+
279344
@staticmethod
280345
def prefs(ref, recipe_bundle):
346+
ConanOutput().warning("PackageLists.prefs() non-public, non-documented method will be "
347+
"removed, use .items() instead", warn_tag="deprecated")
281348
result = {}
282349
for package_id, pkg_bundle in recipe_bundle.get("packages", {}).items():
283350
prevs = pkg_bundle.get("revisions", {})
@@ -289,13 +356,13 @@ def prefs(ref, recipe_bundle):
289356

290357
def serialize(self):
291358
""" Serialize the instance to a dictionary."""
292-
return self.recipes.copy()
359+
return copy.deepcopy(self._data)
293360

294361
@staticmethod
295362
def deserialize(data):
296363
""" Loads the data from a serialized dictionary."""
297364
result = PackagesList()
298-
result.recipes = data
365+
result._data = copy.deepcopy(data)
299366
return result
300367

301368

conan/api/subapi/cache.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True,
109109
for f in backup_files:
110110
remove(f)
111111

112-
for ref, ref_bundle in package_list.refs().items():
112+
for ref, packages in package_list.items():
113113
ConanOutput(ref.repr_notime()).verbose("Cleaning recipe cache contents")
114114
ref_layout = cache.recipe_layout(ref)
115115
if source:
116116
rmdir(ref_layout.source())
117117
if download:
118118
rmdir(ref_layout.download_export())
119-
for pref, _ in package_list.prefs(ref, ref_bundle).items():
119+
for pref in packages:
120120
ConanOutput(pref).verbose("Cleaning package cache contents")
121121
pref_layout = cache.pkg_layout(pref)
122122
if build:
@@ -135,10 +135,11 @@ def save(self, package_list, tgz_path, no_source=False):
135135
compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int)
136136
tar_files: dict[str, str] = {} # {path_in_tar: abs_path}
137137

138-
for ref, ref_bundle in package_list.refs().items():
138+
for ref, packages in package_list.items():
139139
ref_layout = cache.recipe_layout(ref)
140140
recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder)
141141
recipe_folder = recipe_folder.replace("\\", "/") # make win paths portable
142+
ref_bundle = package_list.recipe_dict(ref)
142143
ref_bundle["recipe_folder"] = recipe_folder
143144
out.info(f"Saving {ref}: {recipe_folder}")
144145
# Package only selected folders, not DOWNLOAD one
@@ -152,19 +153,20 @@ def save(self, package_list, tgz_path, no_source=False):
152153
if os.path.exists(path):
153154
tar_files[f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}"] = path
154155

155-
for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
156+
for pref in packages:
156157
pref_layout = cache.pkg_layout(pref)
157158
pkg_folder = pref_layout.package()
158159
folder = os.path.relpath(pkg_folder, cache_folder)
159160
folder = folder.replace("\\", "/") # make win paths portable
160-
pref_bundle["package_folder"] = folder
161+
pkg_dict = package_list.package_dict(pref)
162+
pkg_dict["package_folder"] = folder
161163
out.info(f"Saving {pref}: {folder}")
162164
tar_files[folder] = os.path.join(cache_folder, folder)
163165

164166
if os.path.exists(pref_layout.metadata()):
165167
metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder)
166168
metadata_folder = metadata_folder.replace("\\", "/") # make paths portable
167-
pref_bundle["metadata_folder"] = metadata_folder
169+
pkg_dict["metadata_folder"] = metadata_folder
168170
out.info(f"Saving {pref} metadata: {metadata_folder}")
169171
tar_files[metadata_folder] = os.path.join(cache_folder, metadata_folder)
170172

@@ -194,7 +196,8 @@ def restore(self, path):
194196
# After unzipping the files, we need to update the DB that references these files
195197
out = ConanOutput()
196198
package_list = PackagesList.deserialize(json.loads(pkglist))
197-
for ref, ref_bundle in package_list.refs().items():
199+
for ref, packages in package_list.items():
200+
ref_bundle = package_list.recipe_dict(ref)
198201
ref.timestamp = revision_timestamp_now()
199202
ref_bundle["timestamp"] = ref.timestamp
200203
try:
@@ -207,8 +210,9 @@ def restore(self, path):
207210
# In the case of recipes, they are always "in place", so just checking it
208211
assert rel_path == recipe_folder, f"{rel_path}!={recipe_folder}"
209212
out.info(f"Restore: {ref} in {recipe_folder}")
210-
for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
213+
for pref in packages:
211214
pref.timestamp = revision_timestamp_now()
215+
pref_bundle = package_list.package_dict(pref)
212216
pref_bundle["timestamp"] = pref.timestamp
213217
try:
214218
pkg_layout = cache.pkg_layout(pref)

conan/api/subapi/download.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,21 @@ def download_full(self, package_list: PackagesList, remote: Remote,
8383
"""Download the recipes and packages specified in the ``package_list`` from the remote,
8484
parallelized based on ``core.download:parallel``"""
8585
def _download_pkglist(pkglist):
86-
for ref, recipe_bundle in pkglist.refs().items():
86+
for ref, packages in pkglist.items():
8787
self.recipe(ref, remote, metadata)
88-
recipe_bundle.pop("files", None)
89-
recipe_bundle.pop("upload-urls", None)
90-
for pref, pref_bundle in pkglist.prefs(ref, recipe_bundle).items():
88+
ref_dict = pkglist.recipe_dict(ref)
89+
ref_dict.pop("files", None)
90+
ref_dict.pop("upload-urls", None)
91+
for pref in packages:
9192
self.package(pref, remote, metadata)
92-
pref_bundle.pop("files", None)
93-
pref_bundle.pop("upload-urls", None)
93+
pkg_dict = pkglist.package_dict(pref)
94+
pkg_dict.pop("files", None)
95+
pkg_dict.pop("upload-urls", None)
9496

9597
t = time.time()
9698
parallel = self._conan_api.config.get("core.download:parallel", default=1, check_type=int)
9799
thread_pool = ThreadPool(parallel) if parallel > 1 else None
98-
if not thread_pool or len(package_list.refs()) <= 1:
100+
if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs
99101
_download_pkglist(package_list)
100102
else:
101103
ConanOutput().subtitle(f"Downloading with {parallel} parallel threads")

0 commit comments

Comments
 (0)