Skip to content

Commit 889eb18

Browse files
Fix a bug in pipeweld for inserting into parallel steps with empty … (#1419)
* Fix a bug in `pipeweld` for inserting into parallel steps with empty series Enable `reportUnknownLambdaType` in `basedpyright` * Handle bug with a parallelism containing a multi-step series component * Avoid adding a successor into an empty parallelism * Fix empty components bug
1 parent c00d2f0 commit 889eb18

4 files changed

Lines changed: 97 additions & 21 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,14 @@ reportMissingTypeStubs = false
222222
reportPrivateUsage = false
223223
reportUnannotatedClassAttribute = false
224224
reportUnknownArgumentType = false
225-
reportUnknownLambdaType = false
226225
reportUnknownMemberType = false
227226
reportUnknownVariableType = false
228227
reportUnnecessaryIsInstance = false
229228
reportUnusedCallResult = false
230229

231230
[[tool.basedpyright.executionEnvironments]]
232231
root = "tests"
232+
reportUnknownLambdaType = false
233233
reportUnknownParameterType = false
234234
reportUnreachable = false
235235
reportUnusedFunction = false

src/usethis/_pipeweld/func.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,8 @@ def _get_instructions_for_insertion(
465465
466466
Args:
467467
component: The component to insert.
468-
after: The step to insert the new component after.
468+
after: The step to insert the new component after. None if the component should
469+
be inserted at the start of the pipeline.
469470
470471
Returns:
471472
A tuple containing the instructions to insert the new component and the endpoint
@@ -486,40 +487,51 @@ def _get_instructions_for_insertion(
486487
if len(component.root) == 0:
487488
return [], after
488489

489-
instructions: list[Instruction] = []
490-
endpoints = []
491-
min_idx = None
492-
min_endpoint = None
490+
instructions_per_sub: list[list[Instruction]] = []
491+
endpoints: list[str | None] = []
492+
min_idx: int | None = None
493+
min_endpoint: str | None = None
493494
for idx, subcomponent in enumerate(component.root):
494495
new_instructions, endpoint = _get_instructions_for_insertion(
495496
subcomponent,
496497
after=after,
497498
)
498-
if endpoint is not None and (
499-
min_endpoint is None or endpoint < min_endpoint
499+
if (
500+
new_instructions
501+
and endpoint is not None
502+
and (min_endpoint is None or endpoint < min_endpoint)
500503
):
501504
min_idx = idx
502505
min_endpoint = endpoint
503506

504507
endpoints.append(endpoint)
505-
instructions.extend(new_instructions)
508+
instructions_per_sub.append(new_instructions)
509+
if min_endpoint is None:
510+
# If the existing components are empty...
511+
min_endpoint = after
506512

507513
for idx in range(len(component.root)):
508-
if idx != min_idx:
509-
instructions[idx] = InsertParallel(
510-
after=instructions[idx].after, step=instructions[idx].step
514+
if idx != min_idx and instructions_per_sub[idx]:
515+
instructions_per_sub[idx][0] = InsertParallel(
516+
after=instructions_per_sub[idx][0].after,
517+
step=instructions_per_sub[idx][0].step,
511518
)
512519

513-
sorted_idxs = sorted(range(len(endpoints)), key=lambda k: endpoints[k])
520+
sorted_idxs = sorted(
521+
range(len(endpoints)),
522+
key=lambda k: (
523+
endpoints[k] is None,
524+
endpoints[k] if endpoints[k] is not None else "",
525+
),
526+
)
514527

515-
instructions = [instructions[idx] for idx in sorted_idxs]
528+
instructions = [
529+
inst for idx in sorted_idxs for inst in instructions_per_sub[idx]
530+
]
516531

517-
return instructions, min(endpoints)
532+
return instructions, min_endpoint
518533
elif isinstance(component, DepGroup):
519-
return _get_instructions_for_insertion(
520-
component.series,
521-
after=after,
522-
)
534+
return _get_instructions_for_insertion(component.series, after=after)
523535
else:
524536
assert_never(component)
525537

src/usethis/_tool/impl/base/import_linter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def config_spec(self) -> ConfigSpec:
4343
default=0,
4444
)
4545

46-
contracts: list[dict] = []
46+
contracts: list[dict[str, bool | str | list[str]]] = []
4747
for (
4848
layered_architecture_by_module
4949
) in layered_architecture_by_module_by_root_package.values():
@@ -63,7 +63,7 @@ def config_spec(self) -> ConfigSpec:
6363
):
6464
continue
6565

66-
layers = []
66+
layers: list[str] = []
6767
for layer in layered_architecture.layers:
6868
layers.append(" | ".join(sorted(layer)))
6969

tests/usethis/_integrations/ci/bitbucket/test_pipeweld.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,70 @@ def test_multiple(self):
726726
]
727727
assert endpoint == "A" # Alphabetical order wins tiebreaks
728728

729+
def test_contains_empty_series(self):
730+
"""A Parallel with an empty Series subcomponent produces a None endpoint
731+
for that branch; the overall endpoint should be the min non-None endpoint."""
732+
# Arrange
733+
component = parallel("B", series())
734+
after = None
735+
736+
# Act
737+
instructions, endpoint = _get_instructions_for_insertion(
738+
component, after=after
739+
)
740+
741+
# Assert
742+
assert instructions == [InsertSuccessor(step="B", after=None)]
743+
assert endpoint == "B"
744+
745+
def test_contains_multi_step_series(self):
746+
# Arrange
747+
component = parallel("A", series("B", "C"))
748+
after = "0"
749+
750+
# Act
751+
instructions, endpoint = _get_instructions_for_insertion(
752+
component, after=after
753+
)
754+
755+
# Assert
756+
assert instructions == [
757+
InsertSuccessor(step="A", after="0"),
758+
InsertParallel(step="B", after="0"),
759+
InsertSuccessor(step="C", after="B"),
760+
]
761+
assert endpoint == "A"
762+
763+
def test_empty_series_with_smaller_after(self):
764+
# Arrange
765+
component = parallel("C", series())
766+
after = "A"
767+
768+
# Act
769+
instructions, endpoint = _get_instructions_for_insertion(
770+
component, after=after
771+
)
772+
773+
# Assert
774+
assert instructions == [InsertSuccessor(step="C", after="A")]
775+
assert endpoint == "C"
776+
777+
def test_all_empty_branches_preserves_after(self):
778+
"""When all branches are empty, no instructions are produced but the
779+
endpoint should still be the original ``after`` value."""
780+
# Arrange
781+
component = parallel(series(), series())
782+
after = "X"
783+
784+
# Act
785+
instructions, endpoint = _get_instructions_for_insertion(
786+
component, after=after
787+
)
788+
789+
# Assert
790+
assert instructions == []
791+
assert endpoint == "X"
792+
729793
class TestDepGroup:
730794
def test_basic(self):
731795
# Arrange

0 commit comments

Comments
 (0)