localstack-core 4.4.1.dev59__py3-none-any.whl → 4.4.1.dev65__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. localstack/aws/api/ec2/__init__.py +70 -2
  2. localstack/aws/api/s3/__init__.py +2 -0
  3. localstack/services/cloudformation/engine/v2/change_set_model.py +107 -146
  4. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +41 -22
  5. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +51 -23
  6. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +244 -123
  7. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +16 -1
  8. localstack/services/s3/provider.py +3 -2
  9. localstack/services/sns/provider.py +27 -9
  10. localstack/testing/pytest/cloudformation/fixtures.py +13 -1
  11. localstack/version.py +2 -2
  12. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/METADATA +3 -3
  13. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/RECORD +21 -21
  14. localstack_core-4.4.1.dev65.dist-info/plux.json +1 -0
  15. localstack_core-4.4.1.dev59.dist-info/plux.json +0 -1
  16. {localstack_core-4.4.1.dev59.data → localstack_core-4.4.1.dev65.data}/scripts/localstack +0 -0
  17. {localstack_core-4.4.1.dev59.data → localstack_core-4.4.1.dev65.data}/scripts/localstack-supervisor +0 -0
  18. {localstack_core-4.4.1.dev59.data → localstack_core-4.4.1.dev65.data}/scripts/localstack.bat +0 -0
  19. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/WHEEL +0 -0
  20. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/entry_points.txt +0 -0
  21. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/licenses/LICENSE.txt +0 -0
  22. {localstack_core-4.4.1.dev59.dist-info → localstack_core-4.4.1.dev65.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,15 @@ from __future__ import annotations
3
3
  import re
4
4
  from typing import Any, Final, Generic, Optional, TypeVar
5
5
 
6
+ from localstack.services.cloudformation.engine.transformers import (
7
+ Transformer,
8
+ execute_macro,
9
+ transformers,
10
+ )
6
11
  from localstack.services.cloudformation.engine.v2.change_set_model import (
7
12
  ChangeSetEntity,
8
13
  ChangeType,
14
+ Maybe,
9
15
  NodeArray,
10
16
  NodeCondition,
11
17
  NodeDependsOn,
@@ -20,16 +26,19 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
20
26
  NodeProperty,
21
27
  NodeResource,
22
28
  NodeTemplate,
29
+ Nothing,
23
30
  Scope,
24
31
  TerminalValue,
25
32
  TerminalValueCreated,
26
33
  TerminalValueModified,
27
34
  TerminalValueRemoved,
28
35
  TerminalValueUnchanged,
36
+ is_nothing,
29
37
  )
30
38
  from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
31
39
  ChangeSetModelVisitor,
32
40
  )
41
+ from localstack.services.cloudformation.stores import get_cloudformation_store
33
42
  from localstack.services.cloudformation.v2.entities import ChangeSet
34
43
  from localstack.utils.aws.arns import get_partition
35
44
  from localstack.utils.urls import localstack_host
@@ -52,10 +61,10 @@ TAfter = TypeVar("TAfter")
52
61
 
53
62
 
54
63
  class PreprocEntityDelta(Generic[TBefore, TAfter]):
55
- before: Optional[TBefore]
56
- after: Optional[TAfter]
64
+ before: Maybe[TBefore]
65
+ after: Maybe[TAfter]
57
66
 
58
- def __init__(self, before: Optional[TBefore] = None, after: Optional[TAfter] = None):
67
+ def __init__(self, before: Maybe[TBefore] = Nothing, after: Maybe[TAfter] = Nothing):
59
68
  self.before = before
60
69
  self.after = after
61
70
 
@@ -168,6 +177,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
168
177
  # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
169
178
  for node_resource in node_template.resources.resources:
170
179
  if node_resource.name == resource_name:
180
+ self.visit(node_resource)
171
181
  return node_resource
172
182
  raise RuntimeError(f"No resource '{resource_name}' was found")
173
183
 
@@ -177,6 +187,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
177
187
  # TODO: this could be improved with hashmap lookups if the Node contained bindings and not lists.
178
188
  for node_property in node_resource.properties.properties:
179
189
  if node_property.name == property_name:
190
+ self.visit(node_property)
180
191
  return node_property
181
192
  return None
182
193
 
@@ -189,11 +200,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
189
200
  # process the resource if this wasn't processed already. Ideally, values should only
190
201
  # be accessible through delta objects, to ensure computation is always complete at
191
202
  # every level.
192
- node_resource = self._get_node_resource_for(
203
+ _ = self._get_node_resource_for(
193
204
  resource_name=resource_logical_id, node_template=self._node_template
194
205
  )
195
- self.visit(node_resource)
196
-
197
206
  resolved_resource = resolved_resources.get(resource_logical_id)
198
207
  if resolved_resource is None:
199
208
  raise RuntimeError(
@@ -228,25 +237,27 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
228
237
  # TODO: another scenarios suggesting property lookups might be preferable.
229
238
  for mapping in mappings:
230
239
  if mapping.name == map_name:
240
+ self.visit(mapping)
231
241
  return mapping
232
- # TODO
233
- raise RuntimeError()
242
+ raise RuntimeError(f"Undefined '{map_name}' mapping")
234
243
 
235
- def _get_node_parameter_if_exists(self, parameter_name: str) -> Optional[NodeParameter]:
244
+ def _get_node_parameter_if_exists(self, parameter_name: str) -> Maybe[NodeParameter]:
236
245
  parameters: list[NodeParameter] = self._node_template.parameters.parameters
237
246
  # TODO: another scenarios suggesting property lookups might be preferable.
238
247
  for parameter in parameters:
239
248
  if parameter.name == parameter_name:
249
+ self.visit(parameter)
240
250
  return parameter
241
- return None
251
+ return Nothing
242
252
 
243
- def _get_node_condition_if_exists(self, condition_name: str) -> Optional[NodeCondition]:
253
+ def _get_node_condition_if_exists(self, condition_name: str) -> Maybe[NodeCondition]:
244
254
  conditions: list[NodeCondition] = self._node_template.conditions.conditions
245
255
  # TODO: another scenarios suggesting property lookups might be preferable.
246
256
  for condition in conditions:
247
257
  if condition.name == condition_name:
258
+ self.visit(condition)
248
259
  return condition
249
- return None
260
+ return Nothing
250
261
 
251
262
  def _resolve_condition(self, logical_id: str) -> PreprocEntityDelta:
252
263
  node_condition = self._get_node_condition_if_exists(condition_name=logical_id)
@@ -270,14 +281,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
270
281
  case "AWS::URLSuffix":
271
282
  return _AWS_URL_SUFFIX
272
283
  case "AWS::NoValue":
273
- # TODO: add support for NoValue, None cannot be used to communicate a Null value in preproc classes.
274
- raise NotImplementedError("The use of AWS:NoValue is currently unsupported")
275
- case "AWS::NotificationARNs":
276
- raise NotImplementedError(
277
- "The use of AWS::NotificationARNs is currently unsupported"
278
- )
284
+ return None
279
285
  case _:
280
- raise RuntimeError(f"Unknown pseudo parameter value '{pseudo_parameter_name}'")
286
+ raise RuntimeError(f"The use of '{pseudo_parameter_name}' is currently unsupported")
281
287
 
282
288
  def _resolve_reference(self, logical_id: str) -> PreprocEntityDelta:
283
289
  if logical_id in _PSEUDO_PARAMETERS:
@@ -313,11 +319,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
313
319
  return mapping_value_delta
314
320
 
315
321
  def visit(self, change_set_entity: ChangeSetEntity) -> PreprocEntityDelta:
316
- delta = self._processed.get(change_set_entity.scope)
317
- if delta is not None:
322
+ scope = change_set_entity.scope
323
+ if scope in self._processed:
324
+ delta = self._processed[scope]
318
325
  return delta
319
326
  delta = super().visit(change_set_entity=change_set_entity)
320
- self._processed[change_set_entity.scope] = delta
327
+ self._processed[scope] = delta
321
328
  return delta
322
329
 
323
330
  def visit_terminal_value_modified(
@@ -352,35 +359,35 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
352
359
  return PreprocEntityDelta(before=before_delta.before, after=after_delta.after)
353
360
 
354
361
  def visit_node_object(self, node_object: NodeObject) -> PreprocEntityDelta:
355
- before = dict()
356
- after = dict()
362
+ node_change_type = node_object.change_type
363
+ before = dict() if node_change_type != ChangeType.CREATED else Nothing
364
+ after = dict() if node_change_type != ChangeType.REMOVED else Nothing
357
365
  for name, change_set_entity in node_object.bindings.items():
358
366
  delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
359
- match change_set_entity.change_type:
360
- case ChangeType.MODIFIED:
361
- before[name] = delta.before
362
- after[name] = delta.after
363
- case ChangeType.CREATED:
364
- after[name] = delta.after
365
- case ChangeType.REMOVED:
366
- before[name] = delta.before
367
- case ChangeType.UNCHANGED:
368
- before[name] = delta.before
369
- after[name] = delta.before
367
+ delta_before = delta.before
368
+ delta_after = delta.after
369
+ if not is_nothing(before) and not is_nothing(delta_before) and delta_before is not None:
370
+ before[name] = delta_before
371
+ if not is_nothing(after) and not is_nothing(delta_after) and delta_after is not None:
372
+ after[name] = delta_after
370
373
  return PreprocEntityDelta(before=before, after=after)
371
374
 
372
375
  def visit_node_intrinsic_function_fn_get_att(
373
376
  self, node_intrinsic_function: NodeIntrinsicFunction
374
377
  ) -> PreprocEntityDelta:
375
- arguments_delta = self.visit(node_intrinsic_function.arguments)
376
378
  # TODO: validate the return value according to the spec.
377
- before_argument_list = arguments_delta.before
378
- after_argument_list = arguments_delta.after
379
-
380
- before = None
381
- if before_argument_list:
382
- before_logical_name_of_resource = before_argument_list[0]
383
- before_attribute_name = before_argument_list[1]
379
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
380
+ before_argument: Maybe[list[str]] = arguments_delta.before
381
+ if isinstance(before_argument, str):
382
+ before_argument = before_argument.split(".")
383
+ after_argument: Maybe[list[str]] = arguments_delta.after
384
+ if isinstance(after_argument, str):
385
+ after_argument = after_argument.split(".")
386
+
387
+ before = Nothing
388
+ if before_argument:
389
+ before_logical_name_of_resource = before_argument[0]
390
+ before_attribute_name = before_argument[1]
384
391
 
385
392
  before_node_resource = self._get_node_resource_for(
386
393
  resource_name=before_logical_name_of_resource, node_template=self._node_template
@@ -400,10 +407,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
400
407
  property_name=before_attribute_name,
401
408
  )
402
409
 
403
- after = None
404
- if after_argument_list:
405
- after_logical_name_of_resource = after_argument_list[0]
406
- after_attribute_name = after_argument_list[1]
410
+ after = Nothing
411
+ if after_argument:
412
+ after_logical_name_of_resource = after_argument[0]
413
+ after_attribute_name = after_argument[1]
407
414
  after_node_resource = self._get_node_resource_for(
408
415
  resource_name=after_logical_name_of_resource, node_template=self._node_template
409
416
  )
@@ -430,10 +437,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
430
437
  arguments_delta = self.visit(node_intrinsic_function.arguments)
431
438
  before_values = arguments_delta.before
432
439
  after_values = arguments_delta.after
433
- before = None
440
+ before = Nothing
434
441
  if before_values:
435
442
  before = before_values[0] == before_values[1]
436
- after = None
443
+ after = Nothing
437
444
  if after_values:
438
445
  after = after_values[0] == after_values[1]
439
446
  return PreprocEntityDelta(before=before, after=after)
@@ -442,6 +449,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
442
449
  self, node_intrinsic_function: NodeIntrinsicFunction
443
450
  ) -> PreprocEntityDelta:
444
451
  arguments_delta = self.visit(node_intrinsic_function.arguments)
452
+ arguments_before = arguments_delta.before
453
+ arguments_after = arguments_delta.after
445
454
 
446
455
  def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
447
456
  condition_name = args[0]
@@ -452,10 +461,14 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
452
461
  )
453
462
 
454
463
  # TODO: add support for this being created or removed.
455
- before_outcome_delta = _compute_delta_for_if_statement(arguments_delta.before)
456
- before = before_outcome_delta.before
457
- after_outcome_delta = _compute_delta_for_if_statement(arguments_delta.after)
458
- after = after_outcome_delta.after
464
+ before = Nothing
465
+ if not is_nothing(arguments_before):
466
+ before_outcome_delta = _compute_delta_for_if_statement(arguments_before)
467
+ before = before_outcome_delta.before
468
+ after = Nothing
469
+ if not is_nothing(arguments_after):
470
+ after_outcome_delta = _compute_delta_for_if_statement(arguments_after)
471
+ after = after_outcome_delta.after
459
472
  return PreprocEntityDelta(before=before, after=after)
460
473
 
461
474
  def visit_node_intrinsic_function_fn_not(
@@ -464,20 +477,89 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
464
477
  arguments_delta = self.visit(node_intrinsic_function.arguments)
465
478
  before_condition = arguments_delta.before
466
479
  after_condition = arguments_delta.after
467
- if before_condition:
480
+ before = Nothing
481
+ if not is_nothing(before_condition):
468
482
  before_condition_outcome = before_condition[0]
469
483
  before = not before_condition_outcome
470
- else:
471
- before = None
472
-
473
- if after_condition:
484
+ after = Nothing
485
+ if not is_nothing(after_condition):
474
486
  after_condition_outcome = after_condition[0]
475
487
  after = not after_condition_outcome
476
- else:
477
- after = None
478
488
  # Implicit change type computation.
479
489
  return PreprocEntityDelta(before=before, after=after)
480
490
 
491
+ def _compute_fn_transform(self, args: dict[str, Any]) -> Any:
492
+ # TODO: add typing to arguments before this level.
493
+ # TODO: add schema validation
494
+ # TODO: add support for other transform types
495
+
496
+ account_id = self._change_set.account_id
497
+ region_name = self._change_set.region_name
498
+ transform_name: str = args.get("Name")
499
+ if not isinstance(transform_name, str):
500
+ raise RuntimeError("Invalid or missing Fn::Transform 'Name' argument")
501
+ transform_parameters: dict = args.get("Parameters")
502
+ if not isinstance(transform_parameters, dict):
503
+ raise RuntimeError("Invalid or missing Fn::Transform 'Parameters' argument")
504
+
505
+ if transform_name in transformers:
506
+ # TODO: port and refactor this 'transformers' logic to this package.
507
+ builtin_transformer_class = transformers[transform_name]
508
+ builtin_transformer: Transformer = builtin_transformer_class()
509
+ transform_output: Any = builtin_transformer.transform(
510
+ account_id=account_id, region_name=region_name, parameters=transform_parameters
511
+ )
512
+ return transform_output
513
+
514
+ macros_store = get_cloudformation_store(
515
+ account_id=account_id, region_name=region_name
516
+ ).macros
517
+ if transform_name in macros_store:
518
+ # TODO: this formatting of stack parameters is odd but required to integrate with v1 execute_macro util.
519
+ # consider porting this utils and passing the plain list of parameters instead.
520
+ stack_parameters = {
521
+ parameter["ParameterKey"]: parameter
522
+ for parameter in self._change_set.stack.parameters
523
+ }
524
+ transform_output: Any = execute_macro(
525
+ account_id=account_id,
526
+ region_name=region_name,
527
+ parsed_template=dict(), # TODO: review the requirements for this argument.
528
+ macro=args, # TODO: review support for non dict bindings (v1).
529
+ stack_parameters=stack_parameters,
530
+ transformation_parameters=transform_parameters,
531
+ is_intrinsic=True,
532
+ )
533
+ return transform_output
534
+
535
+ raise RuntimeError(
536
+ f"Unsupported transform function '{transform_name}' in '{self._change_set.stack.stack_name}'"
537
+ )
538
+
539
+ def visit_node_intrinsic_function_fn_transform(
540
+ self, node_intrinsic_function: NodeIntrinsicFunction
541
+ ) -> PreprocEntityDelta:
542
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
543
+ arguments_before = arguments_delta.before
544
+ arguments_after = arguments_delta.after
545
+
546
+ # TODO: review the use of cache in self.precessed from the 'before' run to
547
+ # ensure changes to the lambda (such as after UpdateFunctionCode) do not
548
+ # generalise tot he before value at this depth (thus making it seems as
549
+ # though for this transformation before==after). Another options may be to
550
+ # have specialised caching for transformations.
551
+
552
+ # TODO: add tests to review the behaviour of CFN with changes to transformation
553
+ # function code and no changes to the template.
554
+
555
+ before = Nothing
556
+ if not is_nothing(arguments_before):
557
+ before = self._compute_fn_transform(args=arguments_before)
558
+ after = Nothing
559
+ if not is_nothing(arguments_after):
560
+ after = self._compute_fn_transform(args=arguments_after)
561
+ return PreprocEntityDelta(before=before, after=after)
562
+
481
563
  def visit_node_intrinsic_function_fn_sub(
482
564
  self, node_intrinsic_function: NodeIntrinsicFunction
483
565
  ) -> PreprocEntityDelta:
@@ -516,10 +598,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
516
598
  template_variable_value = sub_parameters[template_variable_name]
517
599
  else:
518
600
  try:
519
- reference_delta = self._resolve_reference(logical_id=template_variable_name)
601
+ resource_delta = self._resolve_reference(logical_id=template_variable_name)
520
602
  template_variable_value = (
521
- reference_delta.before if select_before else reference_delta.after
603
+ resource_delta.before if select_before else resource_delta.after
522
604
  )
605
+ if isinstance(template_variable_value, PreprocResource):
606
+ template_variable_value = template_variable_value.logical_id
523
607
  except RuntimeError:
524
608
  raise RuntimeError(
525
609
  f"Undefined variable name in Fn::Sub string template '{template_variable_name}'"
@@ -529,19 +613,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
529
613
  )
530
614
  return sub_string
531
615
 
532
- before = None
533
- if (
534
- isinstance(arguments_before, str)
535
- or isinstance(arguments_before, list)
536
- and len(arguments_before) == 2
537
- ):
616
+ before = Nothing
617
+ if not is_nothing(arguments_before):
538
618
  before = _compute_sub(args=arguments_before, select_before=True)
539
- after = None
540
- if (
541
- isinstance(arguments_after, str)
542
- or isinstance(arguments_after, list)
543
- and len(arguments_after) == 2
544
- ):
619
+ after = Nothing
620
+ if not is_nothing(arguments_after):
545
621
  after = _compute_sub(args=arguments_after)
546
622
  return PreprocEntityDelta(before=before, after=after)
547
623
 
@@ -558,18 +634,47 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
558
634
  delimiter: str = str(args[0])
559
635
  values: list[Any] = args[1]
560
636
  if not isinstance(values, list):
561
- raise RuntimeError("Invalid arguments list definition for Fn::Join")
637
+ raise RuntimeError(f"Invalid arguments list definition for Fn::Join: '{args}'")
562
638
  join_result = delimiter.join(map(str, values))
563
639
  return join_result
564
640
 
565
- before = None
641
+ before = Nothing
566
642
  if isinstance(arguments_before, list) and len(arguments_before) == 2:
567
643
  before = _compute_join(arguments_before)
568
- after = None
644
+ after = Nothing
569
645
  if isinstance(arguments_after, list) and len(arguments_after) == 2:
570
646
  after = _compute_join(arguments_after)
571
647
  return PreprocEntityDelta(before=before, after=after)
572
648
 
649
+ def visit_node_intrinsic_function_fn_select(
650
+ self, node_intrinsic_function: NodeIntrinsicFunction
651
+ ):
652
+ # TODO: add further support for schema validation
653
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
654
+ arguments_before = arguments_delta.before
655
+ arguments_after = arguments_delta.after
656
+
657
+ def _compute_fn_select(args: list[Any]) -> Any:
658
+ values: list[Any] = args[1]
659
+ if not isinstance(values, list) or not values:
660
+ raise RuntimeError(f"Invalid arguments list value for Fn::Select: '{values}'")
661
+ values_len = len(values)
662
+ index: int = int(args[0])
663
+ if not isinstance(index, int) or index < 0 or index > values_len:
664
+ raise RuntimeError(f"Invalid or out of range index value for Fn::Select: '{index}'")
665
+ selection = values[index]
666
+ return selection
667
+
668
+ before = Nothing
669
+ if not is_nothing(arguments_before):
670
+ before = _compute_fn_select(arguments_before)
671
+
672
+ after = Nothing
673
+ if not is_nothing(arguments_after):
674
+ after = _compute_fn_select(arguments_after)
675
+
676
+ return PreprocEntityDelta(before=before, after=after)
677
+
573
678
  def visit_node_intrinsic_function_fn_find_in_map(
574
679
  self, node_intrinsic_function: NodeIntrinsicFunction
575
680
  ) -> PreprocEntityDelta:
@@ -577,16 +682,14 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
577
682
  arguments_delta = self.visit(node_intrinsic_function.arguments)
578
683
  before_arguments = arguments_delta.before
579
684
  after_arguments = arguments_delta.after
685
+ before = Nothing
580
686
  if before_arguments:
581
687
  before_value_delta = self._resolve_mapping(*before_arguments)
582
688
  before = before_value_delta.before
583
- else:
584
- before = None
689
+ after = Nothing
585
690
  if after_arguments:
586
691
  after_value_delta = self._resolve_mapping(*after_arguments)
587
692
  after = after_value_delta.after
588
- else:
589
- after = None
590
693
  return PreprocEntityDelta(before=before, after=after)
591
694
 
592
695
  def visit_node_mapping(self, node_mapping: NodeMapping) -> PreprocEntityDelta:
@@ -641,15 +744,15 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
641
744
  after_logical_id = arguments_delta.after
642
745
 
643
746
  # TODO: extend this to support references to other types.
644
- before = None
645
- if before_logical_id is not None:
747
+ before = Nothing
748
+ if not is_nothing(before_logical_id):
646
749
  before_delta = self._resolve_reference(logical_id=before_logical_id)
647
750
  before = before_delta.before
648
751
  if isinstance(before, PreprocResource):
649
752
  before = before.physical_resource_id
650
753
 
651
- after = None
652
- if after_logical_id is not None:
754
+ after = Nothing
755
+ if not is_nothing(after_logical_id):
653
756
  after_delta = self._resolve_reference(logical_id=after_logical_id)
654
757
  after = after_delta.after
655
758
  if isinstance(after, PreprocResource):
@@ -658,14 +761,17 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
658
761
  return PreprocEntityDelta(before=before, after=after)
659
762
 
660
763
  def visit_node_array(self, node_array: NodeArray) -> PreprocEntityDelta:
661
- before = list()
662
- after = list()
764
+ node_change_type = node_array.change_type
765
+ before = list() if node_change_type != ChangeType.CREATED else Nothing
766
+ after = list() if node_change_type != ChangeType.REMOVED else Nothing
663
767
  for change_set_entity in node_array.array:
664
768
  delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
665
- if delta.before is not None:
666
- before.append(delta.before)
667
- if delta.after is not None:
668
- after.append(delta.after)
769
+ delta_before = delta.before
770
+ delta_after = delta.after
771
+ if not is_nothing(before) and not is_nothing(delta_before):
772
+ before.append(delta_before)
773
+ if not is_nothing(after) and not is_nothing(delta_after):
774
+ after.append(delta_after)
669
775
  return PreprocEntityDelta(before=before, after=after)
670
776
 
671
777
  def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
@@ -674,29 +780,44 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
674
780
  def visit_node_properties(
675
781
  self, node_properties: NodeProperties
676
782
  ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
677
- before_bindings: dict[str, Any] = dict()
678
- after_bindings: dict[str, Any] = dict()
783
+ node_change_type = node_properties.change_type
784
+ before_bindings = dict() if node_change_type != ChangeType.CREATED else Nothing
785
+ after_bindings = dict() if node_change_type != ChangeType.REMOVED else Nothing
679
786
  for node_property in node_properties.properties:
680
- delta = self.visit(node_property)
681
787
  property_name = node_property.name
682
- if node_property.change_type != ChangeType.CREATED:
683
- before_bindings[property_name] = delta.before
684
- if node_property.change_type != ChangeType.REMOVED:
685
- after_bindings[property_name] = delta.after
686
- before = PreprocProperties(properties=before_bindings)
687
- after = PreprocProperties(properties=after_bindings)
788
+ delta = self.visit(node_property)
789
+ delta_before = delta.before
790
+ delta_after = delta.after
791
+ if (
792
+ not is_nothing(before_bindings)
793
+ and not is_nothing(delta_before)
794
+ and delta_before is not None
795
+ ):
796
+ before_bindings[property_name] = delta_before
797
+ if (
798
+ not is_nothing(after_bindings)
799
+ and not is_nothing(delta_after)
800
+ and delta_after is not None
801
+ ):
802
+ after_bindings[property_name] = delta_after
803
+ before = Nothing
804
+ if not is_nothing(before_bindings):
805
+ before = PreprocProperties(properties=before_bindings)
806
+ after = Nothing
807
+ if not is_nothing(after_bindings):
808
+ after = PreprocProperties(properties=after_bindings)
688
809
  return PreprocEntityDelta(before=before, after=after)
689
810
 
690
811
  def _resolve_resource_condition_reference(self, reference: TerminalValue) -> PreprocEntityDelta:
691
812
  reference_delta = self.visit(reference)
692
813
  before_reference = reference_delta.before
693
- before = None
694
- if before_reference is not None:
814
+ before = Nothing
815
+ if isinstance(before_reference, str):
695
816
  before_delta = self._resolve_condition(logical_id=before_reference)
696
817
  before = before_delta.before
697
- after = None
818
+ after = Nothing
698
819
  after_reference = reference_delta.after
699
- if after_reference is not None:
820
+ if isinstance(after_reference, str):
700
821
  after_delta = self._resolve_condition(logical_id=after_reference)
701
822
  after = after_delta.after
702
823
  return PreprocEntityDelta(before=before, after=after)
@@ -705,19 +826,19 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
705
826
  self, node_resource: NodeResource
706
827
  ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
707
828
  change_type = node_resource.change_type
708
- condition_before = None
709
- condition_after = None
710
- if node_resource.condition_reference is not None:
829
+ condition_before = Nothing
830
+ condition_after = Nothing
831
+ if not is_nothing(node_resource.condition_reference):
711
832
  condition_delta = self._resolve_resource_condition_reference(
712
833
  node_resource.condition_reference
713
834
  )
714
835
  condition_before = condition_delta.before
715
836
  condition_after = condition_delta.after
716
837
 
717
- depends_on_before = None
718
- depends_on_after = None
719
- if node_resource.depends_on is not None:
720
- depends_on_delta = self.visit_node_depends_on(node_resource.depends_on)
838
+ depends_on_before = Nothing
839
+ depends_on_after = Nothing
840
+ if not is_nothing(node_resource.depends_on):
841
+ depends_on_delta = self.visit(node_resource.depends_on)
721
842
  depends_on_before = depends_on_delta.before
722
843
  depends_on_after = depends_on_delta.after
723
844
 
@@ -726,9 +847,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
726
847
  node_resource.properties
727
848
  )
728
849
 
729
- before = None
730
- after = None
731
- if change_type != ChangeType.CREATED and condition_before is None or condition_before:
850
+ before = Nothing
851
+ after = Nothing
852
+ if change_type != ChangeType.CREATED and is_nothing(condition_before) or condition_before:
732
853
  logical_resource_id = node_resource.name
733
854
  before_physical_resource_id = self._before_resource_physical_id(
734
855
  resource_logical_id=logical_resource_id
@@ -741,7 +862,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
741
862
  properties=properties_delta.before,
742
863
  depends_on=depends_on_before,
743
864
  )
744
- if change_type != ChangeType.REMOVED and condition_after is None or condition_after:
865
+ if change_type != ChangeType.REMOVED and is_nothing(condition_after) or condition_after:
745
866
  logical_resource_id = node_resource.name
746
867
  try:
747
868
  after_physical_resource_id = self._after_resource_physical_id(
@@ -765,8 +886,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
765
886
  change_type = node_output.change_type
766
887
  value_delta = self.visit(node_output.value)
767
888
 
768
- condition_delta = None
769
- if node_output.condition_reference is not None:
889
+ condition_delta = Nothing
890
+ if not is_nothing(node_output.condition_reference):
770
891
  condition_delta = self._resolve_resource_condition_reference(
771
892
  node_output.condition_reference
772
893
  )
@@ -777,11 +898,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
777
898
  elif condition_before and not condition_after:
778
899
  change_type = ChangeType.REMOVED
779
900
 
780
- export_delta = None
781
- if node_output.export is not None:
901
+ export_delta = Nothing
902
+ if not is_nothing(node_output.export):
782
903
  export_delta = self.visit(node_output.export)
783
904
 
784
- before: Optional[PreprocOutput] = None
905
+ before: Maybe[PreprocOutput] = Nothing
785
906
  if change_type != ChangeType.CREATED:
786
907
  before = PreprocOutput(
787
908
  name=node_output.name,
@@ -789,7 +910,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
789
910
  export=export_delta.before if export_delta else None,
790
911
  condition=condition_delta.before if condition_delta else None,
791
912
  )
792
- after: Optional[PreprocOutput] = None
913
+ after: Maybe[PreprocOutput] = Nothing
793
914
  if change_type != ChangeType.REMOVED:
794
915
  after = PreprocOutput(
795
916
  name=node_output.name,
@@ -808,8 +929,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
808
929
  output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
809
930
  output_before = output_delta.before
810
931
  output_after = output_delta.after
811
- if output_before:
932
+ if not is_nothing(output_before):
812
933
  before.append(output_before)
813
- if output_after:
934
+ if not is_nothing(output_after):
814
935
  after.append(output_after)
815
936
  return PreprocEntityDelta(before=before, after=after)