@@ -115,9 +115,9 @@ def err_types(self):
115115 return err_types
116116
117117 def __str__ (self ):
118- message = ["Following diffs found:\n " ]
119- for diff in self :
120- message . extend ([ " \n **** \n " , str ( diff ), " \n " ])
118+ message = ["Following diffs found:\n " ] + [
119+ f" \n **** { diff . __class__ . __name__ } \n { str ( diff ) } \n " for diff in self
120+ ]
121121 return "" .join (message )
122122
123123
@@ -192,43 +192,58 @@ def compare_asn_lists(left_asns, right_asns):
192192 if left_duplicates :
193193 try :
194194 check_duplicate_products (
195- left_asns , product_names = left_product_names , dup_names = left_duplicates
195+ left_asns , product_names = left_product_names , dup_names = left_duplicates , pfx = "Left "
196196 )
197197 except MultiDiffError as dup_errors :
198198 diffs .extend (dup_errors )
199199 if right_duplicates :
200200 try :
201201 check_duplicate_products (
202- right_asns , product_names = right_product_names , dup_names = right_duplicates
202+ right_asns ,
203+ product_names = right_product_names ,
204+ dup_names = right_duplicates ,
205+ pfx = "Right " ,
203206 )
204207 except MultiDiffError as dup_errors :
205208 diffs .extend (dup_errors )
206209
207210 # Ensure that the product name lists are the same.
211+ errmsg_not_in = []
208212 left_not_right = sorted (left_product_names - right_product_names )
213+ if left_not_right :
214+ errmsg_not_in .append (
215+ "Products in left but not right:\n " + "\n " .join (left_not_right )
216+ )
209217 right_not_left = sorted (right_product_names - left_product_names )
210- if left_not_right or right_not_left :
211- left_msg = "Products in left but not right:\n " + "\n " .join (left_not_right )
212- right_msg = "Products in right but not left:\n " + "\n " .join (right_not_left )
213- diffs .append (DifferentProductSetsError (f"{ left_msg } \n { right_msg } \n " ))
218+ if right_not_left :
219+ errmsg_not_in .append (
220+ "Products in right but not left:\n " + "\n " .join (right_not_left )
221+ )
222+ if errmsg_not_in :
223+ diffs .append (DifferentProductSetsError ("\n " .join (errmsg_not_in )))
214224
215225 # Compare like product associations
226+ skip_top_level_checks = False
216227 left_asns_by_product = {asn ["products" ][0 ]["name" ]: asn for asn in left_asns }
217228 right_asns_by_product = {asn ["products" ][0 ]["name" ]: asn for asn in right_asns }
218- for product_name in left_product_names :
229+ for product_name in sorted (left_product_names ):
230+ if product_name not in right_asns_by_product :
231+ continue
219232 try :
220- compare_asns (left_asns_by_product [product_name ], right_asns_by_product [product_name ])
233+ compare_asns (
234+ left_asns_by_product [product_name ],
235+ right_asns_by_product [product_name ],
236+ skip_top_level_checks = skip_top_level_checks ,
237+ )
221238 except MultiDiffError as compare_diffs :
222239 diffs .extend (compare_diffs )
223- except KeyError :
224- # Most likely due to a previous error. Ignore
225- pass
240+ skip_top_level_checks = True
226241
227242 if diffs :
228- raise diffs
243+ raise diffs from None
229244
230245
231- def compare_asns (left , right ):
246+ def compare_asns (left , right , skip_top_level_checks = False ):
232247 """
233248 Compare two associations.
234249
@@ -240,30 +255,15 @@ def compare_asns(left, right):
240255 left, right : dict
241256 Two, individual, associations to compare.
242257
243- Raises
244- ------
245- MultiDiffError
246- If there is a difference.
247- """
248- _compare_asns (left , right )
249-
250-
251- def _compare_asns (left , right ):
252- """
253- Compare two associations.
254-
255- This comparison will include metadata such as
256- ``asn_type`` and membership.
257-
258- Parameters
259- ----------
260- left, right : dict
261- Two, individual, associations to compare.
258+ skip_top_level_checks : bool
259+ Skip checks for ``asn_type`` and ``asn_id``.
260+ Set to `True` when this is called in a loop for subsequent products
261+ to avoid duplicate exceptions.
262262
263263 Raises
264264 ------
265265 MultiDiffError
266- If there are differences. The message will contain
266+ If there are differences, the message will contain
267267 all the differences.
268268
269269 Notes
@@ -288,18 +288,17 @@ def _compare_asns(left, right):
288288 diffs = MultiDiffError ()
289289
290290 # Assert that the same result type is the same.
291- if left ["asn_type" ] != right ["asn_type" ]:
291+ if ( not skip_top_level_checks ) and ( left ["asn_type" ] != right ["asn_type" ]) :
292292 diffs .append (
293- TypeMismatchError ("Type mismatch { } != {}" . format ( left [ "asn_type" ], right [" asn_type" ]) )
293+ TypeMismatchError (f"ASN type mismatch: { left [ 'asn_type' ] } != { right [' asn_type' ] } " )
294294 )
295295
296296 # Assert that the level of association candidate is the same.
297297 # Cannot guarantee value, but that the 'a'/'c'/'o' levels are similar.
298- if left ["asn_id" ][0 ] != right ["asn_id" ][0 ]:
298+ if ( not skip_top_level_checks ) and ( left ["asn_id" ][0 ] != right ["asn_id" ][0 ]) :
299299 diffs .append (
300300 CandidateLevelError (
301- f"Candidate level mismatch left '{ left ['asn_id' ][0 ]} ' != "
302- f"right '{ right ['asn_id' ][0 ]} '"
301+ f"Candidate level mismatch: { left ['asn_id' ][0 ]} != { right ['asn_id' ][0 ]} "
303302 )
304303 )
305304
@@ -310,7 +309,7 @@ def _compare_asns(left, right):
310309 diffs .extend (compare_diffs )
311310
312311 if diffs :
313- raise diffs
312+ raise diffs from None
314313
315314
316315def compare_membership (left , right ):
@@ -339,9 +338,9 @@ def compare_membership(left, right):
339338 )
340339 )
341340
342- for _left_idx , left_product in enumerate ( products_left ) :
341+ for left_product in products_left :
343342 left_product_name = components (left_product ["name" ])
344- for _right_idx , right_product in enumerate ( products_right ) :
343+ for right_product in products_right :
345344 if components (right_product ["name" ]) != left_product_name :
346345 continue
347346 try :
@@ -357,7 +356,7 @@ def compare_membership(left, right):
357356 diffs .append (DifferentProductSetsError (f"Right has { len (products_right )} left over" ))
358357
359358 if diffs :
360- raise diffs
359+ raise diffs from None
361360
362361
363362def compare_product_membership (left , right , strict_expname = True ):
@@ -423,76 +422,95 @@ def munge_expname(expname):
423422 except DuplicateMembersError as dup_member_error :
424423 diffs .append (dup_member_error )
425424
426- if len (right ["members" ]) != len (left ["members" ]):
425+ len_left = len (left ["members" ])
426+ len_right = len (right ["members" ])
427+ if len_right != len_left :
427428 diffs .append (
428429 MemberLengthDifferenceError (
429- "Product Member length differs:"
430- " Left Product {left_product_name} len {left_len} != "
431- " Right Product {right_product_name} len {right_len}"
432- "" .format (
433- left_product_name = left ["name" ],
434- left_len = len (left ["members" ]),
435- right_product_name = right ["name" ],
436- right_len = len (right ["members" ]),
437- )
430+ "Product Member length differs:\n "
431+ f" Left Product : { left ['name' ]} ({ len_left } )\n "
432+ f" Right Product: { right ['name' ]} ({ len_right } )"
438433 )
439434 )
435+ err_pfx = ""
436+ else :
437+ good_pfx = (
438+ "Comparing these products:\n "
439+ f" Left Product : { left ['name' ]} ({ len_left } )\n "
440+ f" Right Product: { right ['name' ]} ({ len_right } )\n \n "
441+ )
442+ err_pfx = good_pfx
440443
441444 members_right = copy (right ["members" ])
442445 left_unaccounted_members = []
443446 for left_member in left ["members" ]:
447+ left_expname = left_member ["expname" ]
448+ left_exptype = left_member ["exptype" ]
444449 for right_member in members_right :
445- if munge_expname (left_member ["expname" ]) != munge_expname (right_member ["expname" ]):
450+ right_expname = right_member ["expname" ]
451+ right_exptype = right_member ["exptype" ]
452+ if munge_expname (left_expname ) != munge_expname (right_expname ):
446453 continue
447454
448- if left_member [ "exptype" ] != right_member [ "exptype" ] :
455+ if left_exptype != right_exptype :
449456 diffs .append (
450457 MemberMismatchError (
451- "Left {left_expname}:{left_exptype}"
452- " != Right {right_expname}:{right_exptype}"
453- "" .format (
454- left_expname = left_member ["expname" ],
455- left_exptype = left_member ["exptype" ],
456- right_expname = right_member ["expname" ],
457- right_exptype = right_member ["exptype" ],
458- )
458+ f"{ err_pfx } "
459+ f" Left { left_expname } : { left_exptype } \n "
460+ f" Right { right_expname } : { right_exptype } "
459461 )
460462 )
463+ err_pfx = ""
461464
462465 members_right .remove (right_member )
463466 break
464467 else :
465468 left_unaccounted_members .append (left_member )
466469
467- if len (left_unaccounted_members ):
468- diffs .append (
469- UnaccountedMembersError (
470- f"Left has { len (left_unaccounted_members )} unaccounted members. "
471- f"Members are { left_unaccounted_members } "
472- )
470+ def pprint_mems (unaccounted_members , indent = 8 ):
471+ s = []
472+ spc = " " * indent
473+ for m in unaccounted_members :
474+ s .append (f"{ spc } { m ['expname' ]} ({ m ['exptype' ]} )" )
475+ return "\n " .join (s )
476+
477+ errmsg = []
478+ unaccounted_left = len (left_unaccounted_members )
479+ if unaccounted_left :
480+ errmsg .append (
481+ f" Left has { unaccounted_left } unaccounted members.\n "
482+ f"{ pprint_mems (left_unaccounted_members , indent = 8 )} "
473483 )
474484
475- if len (members_right ) != 0 :
476- diffs . append (
477- UnaccountedMembersError (
478- f" Right has { len ( members_right ) } unaccounted members. Members are { members_right } "
479- )
485+ unaccounted_right = len (members_right )
486+ if unaccounted_right != 0 :
487+ errmsg . append (
488+ f" Right has { unaccounted_right } unaccounted members.\n "
489+ f" { pprint_mems ( members_right , indent = 8 ) } "
480490 )
481491
492+ if errmsg :
493+ diffs .append (UnaccountedMembersError (err_pfx + "\n " .join (errmsg )))
494+ err_pfx = ""
495+
482496 # Check if one is a subset of the other.
483497 err_types = diffs .err_types
484- is_subset = (
498+ if (
485499 (len (diffs ) == 2 )
486500 and (MemberLengthDifferenceError in err_types )
487501 and (UnaccountedMembersError in err_types )
488- )
489- if is_subset :
490- diffs = MultiDiffError (
491- [SubsetError (f"Products are subsets: { left ['name' ]} { right ['name' ]} " )]
492- )
502+ ): # is_subset
503+ if unaccounted_left > 0 :
504+ errmsg = f"{ err_pfx } Right is a subset of left"
505+ err_pfx = ""
506+ else : # unaccounted_right > 0
507+ errmsg = f"{ err_pfx } Left is a subset of right"
508+ # Uncomment if more error handling added below:
509+ # err_pfx = ""
510+ diffs .append (SubsetError (errmsg ))
493511
494512 if diffs :
495- raise diffs
513+ raise diffs from None
496514
497515
498516def check_duplicate_members (product ):
@@ -511,19 +529,16 @@ def check_duplicate_members(product):
511529 MultiDiffError
512530 If the product has duplicate members.
513531 """
514- seen = set ()
515- dups = []
516- for expname in [member ["expname" ] for member in product ["members" ]]:
517- if expname in seen :
518- dups .append (expname )
519- else :
520- seen .add (expname )
532+ prod_count = Counter ([member ["expname" ] for member in product ["members" ]])
533+ dups = sorted (item for item , count in prod_count .items () if count > 1 )
521534
522535 if dups :
523- raise DuplicateMembersError (f"Product { product ['name' ]} has duplicate members { dups } " )
536+ raise DuplicateMembersError (
537+ f"Product { product ['name' ]} has duplicate members: { ', ' .join (dups )} "
538+ ) from None
524539
525540
526- def check_duplicate_products (asns , product_names = None , dup_names = None ):
541+ def check_duplicate_products (asns , product_names = None , dup_names = None , pfx = "" ):
527542 """
528543 Check for duplicate products in a list of associations.
529544
@@ -561,6 +576,9 @@ def check_duplicate_products(asns, product_names=None, dup_names=None):
561576 Duplicate product names in the given associations.
562577 If None, will be generated internally.
563578
579+ pfx : str
580+ Prefix to error message. Particularly useful during left/right diff.
581+
564582 Raises
565583 ------
566584 MultiDiffError
@@ -600,11 +618,12 @@ def check_duplicate_products(asns, product_names=None, dup_names=None):
600618 # Associations are exactly the same. Pure duplicate.
601619 diffs .append (
602620 DuplicateProductError (
603- f"Associations share product name { product } " , asns = [current_asn , asn ]
621+ f"{ pfx } Associations has duplicate product name: { product } " ,
622+ asns = [current_asn , asn ],
604623 )
605624 )
606625 if diffs :
607- raise diffs
626+ raise diffs from None
608627
609628
610629# #########
@@ -683,6 +702,9 @@ def separate_products(asn):
683702 separated : [Association[, ...]]
684703 The list of separated associations.
685704 """
705+ if len (asn ["products" ]) == 1 : # noop
706+ return [asn ]
707+
686708 separated = []
687709 for product in asn ["products" ]:
688710 new_asn = copy (asn )
0 commit comments