docs: def _container_candidates(self):
          """Generate container candidate list 
          
          Returns:
              tuple list: [(width1, height1), (width2, height2), ...] 
          """
          if not self._rectangles:
              return []
  
          if self._rotation:
              sides = sorted(side for rect in self._rectangles for side in rect)
              max_height = sum(max(r[0], r[1]) for r in self._rectangles)
              min_width = max(min(r[0], r[1]) for r in self._rectangles)
              max_width = max_height
          else:
              sides = sorted(r[0] for r in self._rectangles)
              max_height = sum(r[1] for r in self._rectangles)
              min_width = max(r[0] for r in self._rectangles)
              max_width = sum(sides)
          
          if self._max_width and self._max_width < max_width:
              max_width = self._max_width
  
          if self._max_height and self._max_height < max_height:
              max_height = self._max_height
  
          assert(max_width>min_width)
   
          # Generate initial container widths
          candidates = [max_width, min_width]
  
          width = 0
          for s in reversed(sides):
              width += s
              candidates.append(width)
          
          width = 0
          for s in sides:
              width += s
              candidates.append(width)
  
          candidates.append(max_width)
          candidates.append(min_width)
        
          # Remove duplicates and widths too big or small
          seen = set()
          seen_add = seen.add
          candidates = [x for x in candidates if not(x in seen or seen_add(x))] 
          candidates = [x for x in candidates if not(x>max_width or x<min_width)]
          
          # Remove candidates too small to fit all the rectangles
          min_area = sum(r[0]*r[1] for r in self._rectangles)
          return [(c, max_height) for c in candidates if c*max_height>=min_area]