1919 BaseModel ,
2020 Content ,
2121 Distribution ,
22+ ProgressReport ,
2223 Publication ,
2324 Remote ,
2425 Repository ,
@@ -394,6 +395,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
394395
395396 autopublish = models .BooleanField (default = False )
396397 allow_package_substitution = models .BooleanField (default = True )
398+ error_on_reject = models .BooleanField (default = True )
397399
398400 class Meta :
399401 default_related_name = "%(app_label)s_%(model_name)s"
@@ -423,10 +425,9 @@ def finalize_new_version(self, new_version):
423425 """
424426 Remove duplicate packages that have the same filename.
425427
426- When allow_package_substitution is False, reject any new version that would implicitly
427- replace existing content with different checksums (content substitution).
428-
429- Also checks newly added content against the repository's blocklist entries.
428+ Enforces package substitution and blocklist policies on newly added content.
429+ When error_on_reject is True (default), a ValidationError is raised and the version
430+ fails. When False, rejected packages are skipped and recorded in a progress report.
430431 """
431432 if not self .allow_package_substitution :
432433 self ._check_for_package_substitution (new_version )
@@ -436,52 +437,111 @@ def finalize_new_version(self, new_version):
436437
437438 def _check_for_package_substitution (self , new_version ):
438439 """
439- Raise a ValidationError if newly added packages would replace existing packages
440- that have the same filename but a different sha256 checksum.
440+ Handle packages that would replace existing packages with the same filename but a
441+ different sha256 checksum.
442+
443+ When error_on_reject is True, raise a ValidationError. When False, remove the
444+ newly added conflicting packages from the version and record them in a progress report.
441445 """
442446 qs = PythonPackageContent .objects .filter (pk__in = new_version .content )
443447 duplicates = collect_duplicates (qs , ("filename" ,))
444- if duplicates :
448+ if not duplicates :
449+ return
450+
451+ if self .error_on_reject :
445452 raise ValidationError (
446453 "Found duplicate packages being added with the same filename but different "
447454 "checksums. To allow this, set 'allow_package_substitution' to True on the "
448455 f"repository. Conflicting packages: { duplicates } "
449456 )
450457
458+ added_content = PythonPackageContent .objects .filter (
459+ pk__in = new_version .added (base_version = new_version .base_version )
460+ )
461+ added_pks = {str (pkg .pk ): pkg .filename for pkg in added_content .only ("pk" , "filename" )}
462+ to_remove_pks = []
463+ messages = []
464+ # Skip every newly added package in a conflicting filename group, including when
465+ # multiple new packages share a filename and none of them remain in the version.
466+ for dup in duplicates :
467+ for pk in dup .duplicate_pks :
468+ if pk in added_pks :
469+ to_remove_pks .append (pk )
470+ messages .append (f"{ added_pks [pk ]} ({ pk } )" )
471+
472+ if to_remove_pks :
473+ new_version .remove_content (PythonPackageContent .objects .filter (pk__in = to_remove_pks ))
474+ self ._report_rejected_packages (
475+ messages ,
476+ message = "Skipping packages rejected by package substitution policy" ,
477+ code = "python.reject.substitution" ,
478+ )
479+
451480 def _check_blocklist (self , new_version ):
452481 """
453482 Check newly added content in a repository version against the blocklist.
483+
484+ When error_on_reject is True, raise a ValidationError. When False, remove the
485+ blocklisted packages from the version and record them in a progress report.
454486 """
455487 added_content = PythonPackageContent .objects .filter (
456- pk__in = new_version .added ().values_list ("pk" , flat = True )
457- ).only ("filename" , "name_normalized" , "version" )
458- if added_content .exists ():
459- self .check_blocklist_for_packages (added_content )
488+ pk__in = new_version .added (base_version = new_version .base_version )
489+ ).only ("pk" , "filename" , "name_normalized" , "version" )
490+ if not added_content .exists ():
491+ return
492+
493+ blocked = self .find_blocklisted_packages (added_content )
494+ if not blocked :
495+ return
496+
497+ if self .error_on_reject :
498+ raise ValidationError (
499+ "Blocklisted packages cannot be added to this repository: {}" .format (
500+ ", " .join (pkg .filename for pkg in blocked )
501+ )
502+ )
460503
461- def check_blocklist_for_packages (self , packages ):
504+ new_version .remove_content (
505+ PythonPackageContent .objects .filter (pk__in = [p .pk for p in blocked ])
506+ )
507+ self ._report_rejected_packages (
508+ [f"{ pkg .filename } ({ pkg .pk } )" for pkg in blocked ],
509+ message = "Skipping packages rejected by blocklist policy" ,
510+ code = "python.reject.blocklist" ,
511+ )
512+
513+ def find_blocklisted_packages (self , packages ):
462514 """
463- Raise a ValidationError if any of the given packages match a blocklist entry.
515+ Return the packages from ` packages` that match a blocklist entry.
464516 """
465- entries = PythonBlocklistEntry .objects .filter (repository = self )
466- if not entries . exists () :
467- return
517+ entries = list ( PythonBlocklistEntry .objects .filter (repository = self ) )
518+ if not entries :
519+ return []
468520
469521 blocked = []
470522 for pkg in packages :
471523 for entry in entries :
472524 if entry .filename and entry .filename == pkg .filename :
473- blocked .append (pkg . filename )
525+ blocked .append (pkg )
474526 break
475527 if entry .name == pkg .name_normalized :
476528 if not entry .version or entry .version == pkg .version :
477- blocked .append (pkg . filename )
529+ blocked .append (pkg )
478530 break
479- if blocked :
480- raise ValidationError (
481- "Blocklisted packages cannot be added to this repository: {}" .format (
482- ", " .join (blocked )
483- )
484- )
531+ return blocked
532+
533+ def _report_rejected_packages (self , details , message , code ):
534+ """
535+ Record skipped packages in a task progress report.
536+ """
537+ log .info ("%s (%s package(s))" , message , len (details ))
538+ with ProgressReport (
539+ message = message ,
540+ code = code ,
541+ total = len (details ),
542+ suffix = "; " .join (details ),
543+ ) as pb :
544+ pb .increase_by (len (details ))
485545
486546
487547class PythonBlocklistEntry (BaseModel ):
0 commit comments