Enable strict=True for zip() where safe (#4547)

* sub_alphas is derived directly from to_update so they're guaranteed to be of the same length.

* self.shapes is initialised as a direct copy of the mobject, guaranteed to be of same length.

* linspace in this case guarantees both arrays are of equal size (self.n_segments).

* Any transformation already requires that each datapoint in the first tuple has a corresponding datapoint in the second (ie same length)

* Replaced arange with linspace, eliminates risk of floating point errors and forces rgbas and offset to be the same size for strict=True

* all_arc_configs is either defined specifically by length of point_pairs or strictly forced to be the same length (n). In any case they'll always be the same length so strict=True works.

* There should always be an equal amount of start and end anchors; radius_list is defined directly from the length of vertex_group; both outer_vertices and inner_vertices posess n vertices.

* boundary_times always contains has an even length so both 'slices' in the zup function are the same length.

* colors_in_gradient is defined to be the same length as p_list_complete; labels and parts are seemingly user inputs with no guarantee of equal length; val_range is defined to be same lenght as self.bar_names; however there's no authentication that self.values has a fixed length after it's been defined ie user can append to the list creating a mismatch between len(self.values) and len(self.bars)

* In most cases here, the tuples are either defined to be of same length or manipulated to be by the align_data function. In the match_points function there is currently no validation to ensure both mobjects are the same length.

* Reverting _add_x_axis_labels() zip() function back
to strict=False due to failing test cases

* Reverted strict zip usage

* color_gradient is defined to be same length as p_list_complete & within _add_x_axis_labels we define val_range to be the same length as self.bar_names

* align_data and lock_matching_data have no validation to ensure tuples in the zip() function are of the same length. Every other time zip() is used here it is generally immediately manipulating or explicitly defining the tuples to be of same length

* All tuples in zip() functions here are either clearly the same size or manipulated to be the same size using the make_even function.

* The tuples in the zip() function will clearly be of equal length, the second tuple is simply a cyclic shift of the first.

* In the ingest_submobjects function arrays is a one to one mapping of attrs so they are guaranteed to have equal lengths.

* Every usage of zip() consists of tuples that are either manipulated to be equal size or defined to be equal size.

* the zip() function in bezier_remap will always consist of equal length tuples as current_number_of_curves is read directly from the shape of bezier_tuples and is used to dictate the size of split_factors.

* The zip() function color_gradient() will always consist of equal length tuples as floors is defined directly from alphas (which also defines alphas_mod1)

* The tuples in the zip() function in adjacent_n_tuples will always be the same length so strict=True.

* The find_intersection() contains a zip() function that has been set to strict=True. While it is technically possible to pass tuples to this function that are *not* the same length, this would result in generally unexpected behaviour anyway.

* Changed zip() function to have strict=True in __init__() as custom_labels is dependent on tick_range so guaranteed to have the same length.

* Several instances of zip() set to strict=True. In add_coordinates we have axis manipulated to be the same length as tick_range. In get_riemann_rectangles() we have colors dependent on of x_range_Array forcing them to be the same length. Finally in plot_line_graph()  it is clearly intended that all inputs used in the zip() function are of the same length (except possibly z which may not exist and will be made equal length to x); while it is not guaranteed they will be the same length this would cause unintended behaviour.

* zip() function bool changed to strict=True in all these test cases. Most test cases either a) hardcode two things to be the same length, b) verify things are the same length before the function or c) explicitly exist to check whether two things are the same length.
This commit is contained in:
Oll-iver 2026-02-11 09:39:25 +00:00 committed by GitHub
commit 8a5267a9ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 59 additions and 58 deletions

View file

@ -186,7 +186,7 @@ class AnimationGroup(Animation):
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
for anim_to_update, sub_alpha in zip(
to_update["anim"], sub_alphas, strict=False
to_update["anim"], sub_alphas, strict=True
):
anim_to_update.interpolate(sub_alpha)

View file

@ -472,7 +472,7 @@ class SpiralIn(Animation):
def interpolate_mobject(self, alpha: float) -> None:
alpha = self.rate_func(alpha)
for original_shape, shape in zip(self.shapes, self.mobject, strict=False):
for original_shape, shape in zip(self.shapes, self.mobject, strict=True):
shape.restore()
fill_opacity = original_shape.get_fill_opacity()
stroke_opacity = original_shape.get_stroke_opacity()

View file

@ -349,7 +349,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
for stroke_width, time_width in zip(
np.linspace(0, max_stroke_width, self.n_segments),
np.linspace(max_time_width, 0, self.n_segments),
strict=False,
strict=True,
)
),
)

View file

@ -235,8 +235,8 @@ class Transform(Animation):
self.target_copy,
]
if config.renderer == RendererType.OPENGL:
return zip(*(mob.get_family() for mob in mobs), strict=False)
return zip(*(mob.family_members_with_points() for mob in mobs), strict=False)
return zip(*(mob.get_family() for mob in mobs), strict=True)
return zip(*(mob.family_members_with_points() for mob in mobs), strict=True)
def interpolate_submobject(
self,
@ -741,7 +741,7 @@ class CyclicReplace(Transform):
def create_target(self) -> Group:
target = self.group.copy()
cycled_targets = [target[-1], *target[:-1]]
for m1, m2 in zip(cycled_targets, self.group, strict=False):
for m1, m2 in zip(cycled_targets, self.group, strict=True):
m1.move_to(m2)
return target
@ -929,5 +929,5 @@ class FadeTransformPieces(FadeTransform):
"""Replaces the source submobjects by the target submobjects and sets
the opacity to 0.
"""
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=False):
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
super().ghost_to(sm0, sm1)

View file

@ -756,9 +756,8 @@ class Camera:
points = vmobject.get_gradient_start_and_end_points()
points = self.transform_points_pre_display(vmobject, points)
pat = cairo.LinearGradient(*it.chain(*(point[:2] for point in points)))
step = 1.0 / (len(rgbas) - 1)
offsets = np.arange(0, 1 + step, step)
for rgba, offset in zip(rgbas, offsets, strict=False):
offsets = np.linspace(0, 1, len(rgbas))
for rgba, offset in zip(rgbas, offsets, strict=True):
pat.add_color_stop_rgba(offset, *rgba[2::-1], rgba[3])
ctx.set_source(pat)
return self

View file

@ -1231,7 +1231,7 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
arcs = [
ArcBetweenPoints(*pair, **conf)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=False)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
]
super().__init__(**kwargs)

View file

@ -152,7 +152,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
# times, then .get_vertex_groups() splits it into N vertex groups.
group = []
for start, end in zip(
self.get_start_anchors(), self.get_end_anchors(), strict=False
self.get_start_anchors(), self.get_end_anchors(), strict=True
):
group.append(start)
@ -240,7 +240,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
radius_list = radius * ceil(len(vertex_group) / len(radius))
for current_radius, (v1, v2, v3) in zip(
radius_list, adjacent_n_tuples(vertex_group, 3), strict=False
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
):
vect1 = v2 - v1
vect2 = v3 - v2
@ -552,7 +552,7 @@ class Star(Polygon):
)
vertices: list[npt.NDArray] = []
for pair in zip(outer_vertices, inner_vertices, strict=False):
for pair in zip(outer_vertices, inner_vertices, strict=True):
vertices.extend(pair)
super().__init__(*vertices, **kwargs)

View file

@ -448,7 +448,7 @@ class CoordinateSystem:
zip(
tick_range,
axis.scaling.get_custom_labels(tick_range),
strict=False,
strict=True,
)
)
)
@ -1300,7 +1300,7 @@ class CoordinateSystem:
colors = color_gradient(color, len(x_range_array))
for x, color in zip(x_range_array, colors, strict=False):
for x, color in zip(x_range_array, colors, strict=True):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
@ -2362,7 +2362,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
vertices = [
self.coords_to_point(x, y, z)
for x, y, z in zip(x_values, y_values, z_values, strict=False)
for x, y, z in zip(x_values, y_values, z_values, strict=True)
]
graph.set_points_as_corners(vertices)
line_graph["line_graph"] = graph

View file

@ -157,7 +157,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
else:
boundary_times = [self.t_min, self.t_max]
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=False):
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=True):
t_range = np.array(
[
*self.scaling.function(np.arange(t1, t2, self.t_step)),

View file

@ -262,7 +262,7 @@ class NumberLine(Line):
zip(
tick_range,
custom_labels,
strict=False,
strict=True,
)
),
)

View file

@ -107,7 +107,7 @@ class SampleSpace(Rectangle):
last_point = self.get_edge_center(-vect)
parts = VGroup()
for factor, color in zip(p_list_complete, colors_in_gradient, strict=False):
for factor, color in zip(p_list_complete, colors_in_gradient, strict=True):
part = SampleSpace()
part.set_fill(color, 1)
part.replace(self, stretch=True)
@ -368,7 +368,7 @@ class BarChart(Axes):
labels = VGroup()
for i, (value, bar_name) in enumerate(
zip(val_range, self.bar_names, strict=False)
zip(val_range, self.bar_names, strict=True)
):
# to accommodate negative bars, the label may need to be
# below or above the x_axis depending on the value of the bar

View file

@ -2016,7 +2016,7 @@ class Mobject:
mobs = self.family_members_with_points()
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors, strict=False):
for mob, color in zip(mobs, new_colors, strict=True):
mob.set_color(color, family=False)
return self
@ -2309,7 +2309,7 @@ class Mobject:
return Group(
*(
template.copy().pointwise_become_partial(self, a1, a2)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
)
)
@ -2502,7 +2502,7 @@ class Mobject:
x = VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
self.add(x)
"""
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
m2.next_to(m1, direction, buff, **kwargs)
if center:
self.center()
@ -2887,7 +2887,7 @@ class Mobject:
if not skip_point_alignment:
self.align_points(mobject)
# Recurse
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=False):
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=True):
m1.align_data(m2)
def get_point_mobject(self, center=None):
@ -2957,7 +2957,7 @@ class Mobject:
repeat_indices = (np.arange(target) * curr) // target
split_factors = [sum(repeat_indices == i) for i in range(curr)]
new_submobs = []
for submob, sf in zip(self.submobjects, split_factors, strict=False):
for submob, sf in zip(self.submobjects, split_factors, strict=True):
new_submobs.append(submob)
new_submobs.extend(submob.copy().fade(1) for _ in range(1, sf))
self.submobjects = new_submobs
@ -3169,7 +3169,7 @@ class Mobject:
mobject.move_to(self.get_center())
self.align_data(mobject, skip_point_alignment=True)
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
sm1.points = np.array(sm2.points)
sm1.interpolate_color(sm1, sm2, 1)
return self

View file

@ -1055,7 +1055,7 @@ class OpenGLMobject:
x = OpenGLVGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
self.add(x)
"""
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
@ -2190,7 +2190,7 @@ class OpenGLMobject:
# Color and opacity
if color is not None and opacity is not None:
rgbas: FloatRGBA_Array = np.array(
[[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities), strict=False)]
[[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities), strict=True)]
)
for mob in self.get_family(recurse):
mob.data[name] = rgbas.copy()
@ -2266,7 +2266,7 @@ class OpenGLMobject:
mobs = self.submobjects
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors, strict=False):
for mob, color in zip(mobs, new_colors, strict=True):
mob.set_color(color)
return self
@ -2481,7 +2481,7 @@ class OpenGLMobject:
return OpenGLGroup(
*(
template.copy().pointwise_become_partial(self, a1, a2)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
)
)
@ -2612,7 +2612,7 @@ class OpenGLMobject:
mob1.add_n_more_submobjects(max(0, n2 - n1))
mob2.add_n_more_submobjects(max(0, n1 - n2))
# Recurse
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=False):
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=True):
sm1.align_family(sm2)
return self
@ -2638,7 +2638,7 @@ class OpenGLMobject:
repeat_indices = (np.arange(target) * curr) // target
split_factors = [(repeat_indices == i).sum() for i in range(curr)]
new_submobs = []
for submob, sf in zip(self.submobjects, split_factors, strict=False):
for submob, sf in zip(self.submobjects, split_factors, strict=True):
new_submobs.append(submob)
for _ in range(1, sf):
new_submob = submob.copy()
@ -2780,7 +2780,7 @@ class OpenGLMobject:
mobject.move_to(self.get_center())
self.align_family(mobject)
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
sm1.set_data(sm2.data)
sm1.set_uniforms(sm2.uniforms)
self.refresh_bounding_box(recurse_down=True)

View file

@ -350,7 +350,7 @@ class OpenGLVMobject(OpenGLMobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -580,7 +580,7 @@ class OpenGLVMobject(OpenGLMobject):
new_points.extend(
[
partial_bezier_points(tup, a1, a2)
for a1, a2 in zip(alphas, alphas[1:], strict=False)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
],
)
else:
@ -769,7 +769,7 @@ class OpenGLVMobject(OpenGLMobject):
split_indices = [0, *split_indices, len(points)]
return [
points[i1:i2]
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppc
]
@ -1093,7 +1093,7 @@ class OpenGLVMobject(OpenGLMobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e, strict=False)))
return list(it.chain.from_iterable(zip(s, e, strict=True)))
def get_points_without_null_curves(self, atol=1e-9):
nppc = self.n_points_per_curve

View file

@ -132,7 +132,7 @@ class Polyhedron(VGroup):
"""Creates list of cyclic pairwise tuples."""
edges: list[tuple[int, int]] = []
for face in faces_list:
edges += zip(face, face[1:] + face[:1], strict=False)
edges += zip(face, face[1:] + face[:1], strict=True)
return edges
def create_faces(

View file

@ -199,7 +199,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
def ingest_submobjects(self) -> Self:
attrs = self.get_array_attrs()
arrays = list(map(self.get_merged_array, attrs))
for attr, array in zip(attrs, arrays, strict=False):
for attr, array in zip(attrs, arrays, strict=True):
setattr(self, attr, array)
self.submobjects = []
return self

View file

@ -236,7 +236,7 @@ class VMobject(Mobject):
rgbas: FloatRGBA_Array = np.array(
[
c.to_rgba_with_alpha(o)
for c, o in zip(*make_even(colors, opacities), strict=False)
for c, o in zip(*make_even(colors, opacities), strict=True)
],
)
@ -461,7 +461,7 @@ class VMobject(Mobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -1337,7 +1337,7 @@ class VMobject(Mobject):
split_indices = [0] + list(filtered) + [len(points)]
return (
points[i1:i2]
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppcc
)
@ -1691,7 +1691,7 @@ class VMobject(Mobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e, strict=False)))
return list(it.chain.from_iterable(zip(s, e, strict=True)))
def get_points_defining_boundary(self) -> Point3D_Array:
# Probably returns all anchors, but this is weird regarding the name of the method.

View file

@ -999,7 +999,7 @@ def bezier_remap(
new_tuples = np.empty((new_number_of_curves, nppc, dim))
index = 0
for curve, sf in zip(bezier_tuples, split_factors, strict=False):
for curve, sf in zip(bezier_tuples, split_factors, strict=True):
new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
sf, nppc, dim
)

View file

@ -1418,7 +1418,7 @@ def color_gradient(
floors[-1] = num_colors - 2
return [
rgb_to_color((rgbs[i] * (1 - alpha)) + (rgbs[i + 1] * alpha))
for i, alpha in zip(floors, alphas_mod1, strict=False)
for i, alpha in zip(floors, alphas_mod1, strict=True)
]

View file

@ -58,7 +58,7 @@ def adjacent_n_tuples(objects: Sequence[T], n: int) -> zip[tuple[T, ...]]:
>>> list(adjacent_n_tuples([1, 2, 3, 4], 3))
[(1, 2, 3), (2, 3, 4), (3, 4, 1), (4, 1, 2)]
"""
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=False)
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=True)
def adjacent_pairs(objects: Sequence[T]) -> zip[tuple[T, ...]]:

View file

@ -609,7 +609,7 @@ def find_intersection(
# algorithm from https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
result = []
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=False):
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=True):
normal = cross(v1, cross(v0, v1))
denom = max(np.dot(v0, normal), threshold)
result += [p0 + np.dot(p1 - p0, normal) / denom * v0]

View file

@ -90,7 +90,7 @@ def test_Polygram_get_vertex_groups():
for vertex_groups in vertex_groups_arr:
polygram = Polygram(*vertex_groups)
poly_vertex_groups = polygram.get_vertex_groups()
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=False):
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=True):
np.testing.assert_array_equal(poly_group, group)
# If polygram is a Polygram of a vertex group containing the start vertex N times,

View file

@ -62,7 +62,7 @@ def test_add_labels():
expected_label_length = 6
num_line = NumberLine(x_range=[-4, 4])
num_line.add_labels(
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
)
actual_label_length = len(num_line.labels)
assert actual_label_length == expected_label_length, (

View file

@ -108,7 +108,7 @@ def test_multi_part_tex_with_empty_parts():
for one_part_glyph, multi_part_glyph in zip(
one_part_fomula.family_members_with_points(),
multi_part_formula.family_members_with_points(),
strict=False,
strict=True,
):
np.testing.assert_allclose(one_part_glyph.points, multi_part_glyph.points)

View file

@ -369,7 +369,7 @@ def test_vdict_init():
# Test VDict made from a python dict
VDict({"a": VMobject(), "b": VMobject(), "c": VMobject()})
# Test VDict made using zip
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=False))
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=True))
# If the value is of type Mobject, must raise a TypeError
with pytest.raises(TypeError):
VDict({"a": Mobject()})

View file

@ -62,7 +62,7 @@ def test_add_labels():
expected_label_length = 6
num_line = NumberLine(x_range=[-4, 4])
num_line.add_labels(
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
)
actual_label_length = len(num_line.labels)
assert actual_label_length == expected_label_length, (

View file

@ -312,7 +312,7 @@ def test_vdict_init(using_opengl_renderer):
zip(
["a", "b", "c"],
[OpenGLVMobject(), OpenGLVMobject(), OpenGLVMobject()],
strict=False,
strict=True,
)
)
# If the value is of type OpenGLMobject, must raise a TypeError

View file

@ -32,7 +32,7 @@ def test_custom_coordinates(scene):
ax = Axes(x_range=[0, 10])
ax.add_coordinates(
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=False)),
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=True)),
)
scene.add(ax)

View file

@ -59,7 +59,7 @@ def test_vmobject_joint_types(scene):
]
)
lines = VGroup(*[angled_line.copy() for _ in range(len(LineJointType))])
for line, joint_type in zip(lines, LineJointType, strict=False):
for line, joint_type in zip(lines, LineJointType, strict=True):
line.joint_type = joint_type
lines.arrange(RIGHT, buff=1)

View file

@ -28,7 +28,9 @@ def _check_logs(reference_logfile_path: Path, generated_logfile_path: Path) -> N
msg_assert += f"\nPath of reference log: {reference_logfile}\nPath of generated logs: {generated_logfile}"
pytest.fail(msg_assert)
for index, ref, gen in zip(itertools.count(), reference_logs, generated_logs):
for index, ref, gen in zip(
itertools.count(), reference_logs, generated_logs, strict=False
):
# As they are string, we only need to check if they are equal. If they are not, we then compute a more precise difference, to debug.
if ref == gen:
continue

View file

@ -59,7 +59,7 @@ def check_video_data(path_control_data: Path, path_video_gen: Path) -> None:
f"expected {len(sec_index_exp)} sections ({', '.join([el['name'] for el in sec_index_exp])}), but {len(sec_index_gen)} ({', '.join([el['name'] for el in sec_index_gen])}) got generated (in '{path_sec_index_gen}')"
)
# check individual sections
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=False):
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=True):
assert_shallow_dict_compare(
sec_gen,
sec_exp,