1+ import copy
12import fnmatch
23import json
34import os
45from json import JSONDecodeError
6+ from typing import Iterable
57
68from conan .api .model import RecipeReference , PkgReference
9+ from conan .api .output import ConanOutput
710from conan .errors import ConanException
811from conan .internal .errors import NotFoundException
912from 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
193195class 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
0 commit comments