Repository: django/django
Issue category: Feature Request

Issue:
Improve `RuntimeError: Max post-process passes exceeded.` error
Description
		
(last modified by Michael)		
Having just spend 3 hours trying to debug a collect static issue, to help future travellers, I recommend printing the files that caused max depth to be exceeded, often when a file references itself it causes recursion, but the only clue it currently prints is 'All' which does not narrow down the problem. We can surely do better than that! The proposed change prints the problem files only that keep chaning and can't be resolved:
So instead of getting:
Post-processing 'All' failed!
We get the new and improved:
Post-processing 'jsapp/jsapp/notify.min.js' failed!
Or if more than one file:
Post-processing 'jsapp/jsapp/notify.min.js, jsapp/jsapp/somethingelse.min.js' failed!
I recommend changing from:
# contrib/staticfiles/storage.py line 313: in def post_process(self, paths, dry_run=False, **options):
		unresolved_paths = []
		for i in range(self.max_post_process_passes):
			substitutions = False
			for name, hashed_name, processed, subst in self._post_process(
				paths, adjustable_paths, hashed_files
			):
				# Overwrite since hashed_name may be newer.
				processed_adjustable_paths[name] = (name, hashed_name, processed)
				if subst and i == self.max_post_process_passes - 1:
					unresolved_paths.append(name)
				substitutions = substitutions or subst
			if not substitutions:
				break
		if substitutions:
			problem_paths_str = ", ".join(unresolved_paths) if unresolved_paths else "All"
			yield problem_paths_str, None, RuntimeError("Max post-process passes exceeded.")
I recommend changing to (see the comments on the right of the proposed changed lines 1/5 to 5/5):
# contrib/staticfiles/storage.py line 313: in def post_process(self, paths, dry_run=False, **options):
		unresolved_paths = []											# < -- add this line 1/5
		for i in range(self.max_post_process_passes):
			substitutions = False
			for name, hashed_name, processed, subst in self._post_process(
				paths, adjustable_paths, hashed_files
			):
				# Overwrite since hashed_name may be newer.
				processed_adjustable_paths[name] = (name, hashed_name, processed)
				if subst and i == self.max_post_process_passes - 1:	 # < -- add this line 2/5
					unresolved_paths.append(name)						# < -- add this line 3/5
				substitutions = substitutions or subst
			if not substitutions:
				break
		if substitutions:
			problem_paths_str = ", ".join(unresolved_paths) if unresolved_paths else "All"	 # < -- add this line 4/5
			yield problem_paths_str, None, RuntimeError("Max post-process passes exceeded.") # < -- change this line 5/5


Hints:
Adding the suggestion as a diff django/contrib/staticfiles/storage.py diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 04a5edbd30..5c41fd6828 100644 a b class HashedFilesMixin: 309309 310310 paths = {path: paths[path] for path in adjustable_paths} 311311 substitutions = False 312 312 unresolved_paths = [] 313313 for i in range(self.max_post_process_passes): 314314 substitutions = False 315315 for name, hashed_name, processed, subst in self._post_process( … … class HashedFilesMixin: 318318 # Overwrite since hashed_name may be newer. 319319 processed_adjustable_paths[name] = (name, hashed_name, processed) 320320 substitutions = substitutions or subst 321 if subst and i == self.max_post_process_passes - 1: 322 unresolved_paths.append(name) 321323 322324 if not substitutions: 323325 break 324326 325327 if substitutions: 326 yield "All", None, RuntimeError("Max post-process passes exceeded.") 328 problem_paths_str = ", ".join(unresolved_paths) if unresolved_paths else "All" 329 yield problem_paths_str, None, RuntimeError("Max post-process passes exceeded.") 327330 328331 # Store the processed paths 329332 self.hashed_files.update(hashed_files) Appreciate clear error messages so sounds good to me
1723434306

Patch:
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index 04a5edbd3086..dfc3137f76fd 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -308,22 +308,23 @@ def post_process(self, paths, dry_run=False, **options):
                 processed_adjustable_paths[name] = (name, hashed_name, processed)
 
         paths = {path: paths[path] for path in adjustable_paths}
-        substitutions = False
-
+        unresolved_paths = []
         for i in range(self.max_post_process_passes):
-            substitutions = False
+            unresolved_paths = []
             for name, hashed_name, processed, subst in self._post_process(
                 paths, adjustable_paths, hashed_files
             ):
                 # Overwrite since hashed_name may be newer.
                 processed_adjustable_paths[name] = (name, hashed_name, processed)
-                substitutions = substitutions or subst
+                if subst:
+                    unresolved_paths.append(name)
 
-            if not substitutions:
+            if not unresolved_paths:
                 break
 
-        if substitutions:
-            yield "All", None, RuntimeError("Max post-process passes exceeded.")
+        if unresolved_paths:
+            problem_paths = ", ".join(sorted(unresolved_paths))
+            yield problem_paths, None, RuntimeError("Max post-process passes exceeded.")
 
         # Store the processed paths
         self.hashed_files.update(hashed_files)


Test patch:
diff --git a/tests/staticfiles_tests/project/loop/baz.css b/tests/staticfiles_tests/project/loop/baz.css
new file mode 100644
index 000000000000..4021a1b1e6ca
--- /dev/null
+++ b/tests/staticfiles_tests/project/loop/baz.css
@@ -0,0 +1,3 @@
+body {
+    background-color: #fafafa;
+}
diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py
index d6ea03b7446a..9ca4d6255329 100644
--- a/tests/staticfiles_tests/test_storage.py
+++ b/tests/staticfiles_tests/test_storage.py
@@ -186,7 +186,9 @@ def test_import_loop(self):
         err = StringIO()
         with self.assertRaisesMessage(RuntimeError, "Max post-process passes exceeded"):
             call_command("collectstatic", interactive=False, verbosity=0, stderr=err)
-        self.assertEqual("Post-processing 'All' failed!\n\n", err.getvalue())
+        self.assertEqual(
+            "Post-processing 'bar.css, foo.css' failed!\n\n", err.getvalue()
+        )
         self.assertPostCondition()
 
     def test_post_processing(self):