@@ -1407,6 +1407,57 @@ def flat_deps(services, with_extends=False):
1407
1407
rec_deps (services , name )
1408
1408
1409
1409
1410
+ ###################
1411
+ # Override and reset tags
1412
+ ###################
1413
+
1414
+
1415
+ class OverrideTag (yaml .YAMLObject ):
1416
+ yaml_dumper = yaml .Dumper
1417
+ yaml_loader = yaml .SafeLoader
1418
+ yaml_tag = '!override'
1419
+
1420
+ def __init__ (self , value ):
1421
+ if len (value ) > 0 and isinstance (value [0 ], tuple ):
1422
+ self .value = {}
1423
+ # item is a tuple representing service's lower level key and value
1424
+ for item in value :
1425
+ # value can actually be a list, then all the elements from the list have to be
1426
+ # collected
1427
+ if isinstance (item [1 ].value , list ):
1428
+ self .value [item [0 ].value ] = [item .value for item in item [1 ].value ]
1429
+ else :
1430
+ self .value [item [0 ].value ] = item [1 ].value
1431
+ else :
1432
+ self .value = [item .value for item in value ]
1433
+
1434
+ @classmethod
1435
+ def from_yaml (cls , loader , node ):
1436
+ return OverrideTag (node .value )
1437
+
1438
+ @classmethod
1439
+ def to_yaml (cls , dumper , data ):
1440
+ return dumper .represent_scalar (cls .yaml_tag , data .value )
1441
+
1442
+
1443
+ class ResetTag (yaml .YAMLObject ):
1444
+ yaml_dumper = yaml .Dumper
1445
+ yaml_loader = yaml .SafeLoader
1446
+ yaml_tag = '!reset'
1447
+
1448
+ @classmethod
1449
+ def to_json (cls ):
1450
+ return cls .yaml_tag
1451
+
1452
+ @classmethod
1453
+ def from_yaml (cls , loader , node ):
1454
+ return ResetTag ()
1455
+
1456
+ @classmethod
1457
+ def to_yaml (cls , dumper , data ):
1458
+ return dumper .represent_scalar (cls .yaml_tag , '' )
1459
+
1460
+
1410
1461
async def wait_with_timeout (coro , timeout ):
1411
1462
"""
1412
1463
Asynchronously waits for the given coroutine to complete with a timeout.
@@ -1605,6 +1656,12 @@ async def volume_ls(self):
1605
1656
1606
1657
1607
1658
def normalize_service (service , sub_dir = "" ):
1659
+ if isinstance (service , ResetTag ):
1660
+ return service
1661
+
1662
+ if isinstance (service , OverrideTag ):
1663
+ service = service .value
1664
+
1608
1665
if "build" in service :
1609
1666
build = service ["build" ]
1610
1667
if isinstance (build , str ):
@@ -1708,6 +1765,8 @@ def rec_merge_one(target, source):
1708
1765
update target from source recursively
1709
1766
"""
1710
1767
done = set ()
1768
+ remove = set ()
1769
+
1711
1770
for key , value in source .items ():
1712
1771
if key in target :
1713
1772
continue
@@ -1717,15 +1776,37 @@ def rec_merge_one(target, source):
1717
1776
if key in done :
1718
1777
continue
1719
1778
if key not in source :
1779
+ if isinstance (value , ResetTag ):
1780
+ log ("INFO: Unneeded !reset found for [{key}]" )
1781
+ remove .add (key )
1782
+
1783
+ if isinstance (value , OverrideTag ):
1784
+ log ("INFO: Unneeded !override found for [{key}] with value '{value}'" )
1785
+ target [key ] = clone (value .value )
1786
+
1720
1787
continue
1788
+
1721
1789
value2 = source [key ]
1790
+
1791
+ if isinstance (value , ResetTag ) or isinstance (value2 , ResetTag ):
1792
+ remove .add (key )
1793
+ continue
1794
+
1795
+ if isinstance (value , OverrideTag ) or isinstance (value2 , OverrideTag ):
1796
+ target [key ] = (
1797
+ clone (value .value ) if isinstance (value , OverrideTag ) else clone (value2 .value )
1798
+ )
1799
+ continue
1800
+
1722
1801
if key in ("command" , "entrypoint" ):
1723
1802
target [key ] = clone (value2 )
1724
1803
continue
1804
+
1725
1805
if not isinstance (value2 , type (value )):
1726
1806
value_type = type (value )
1727
1807
value2_type = type (value2 )
1728
1808
raise ValueError (f"can't merge value of [{ key } ] of type { value_type } and { value2_type } " )
1809
+
1729
1810
if is_list (value2 ):
1730
1811
if key == "volumes" :
1731
1812
# clean duplicate mount targets
@@ -1742,6 +1823,10 @@ def rec_merge_one(target, source):
1742
1823
rec_merge_one (value , value2 )
1743
1824
else :
1744
1825
target [key ] = value2
1826
+
1827
+ for key in remove :
1828
+ del target [key ]
1829
+
1745
1830
return target
1746
1831
1747
1832
@@ -2027,10 +2112,13 @@ def _parse_compose_file(self):
2027
2112
content = rec_subs (content , self .environ )
2028
2113
if isinstance (services := content .get ('services' ), dict ):
2029
2114
for service in services .values ():
2030
- if 'extends' in service and (service_file := service ['extends' ].get ('file' )):
2031
- service ['extends' ]['file' ] = os .path .join (
2032
- os .path .dirname (filename ), service_file
2033
- )
2115
+ if not isinstance (service , OverrideTag ) and not isinstance (service , ResetTag ):
2116
+ if 'extends' in service and (
2117
+ service_file := service ['extends' ].get ('file' )
2118
+ ):
2119
+ service ['extends' ]['file' ] = os .path .join (
2120
+ os .path .dirname (filename ), service_file
2121
+ )
2034
2122
2035
2123
rec_merge (compose , content )
2036
2124
# If `include` is used, append included files to files
0 commit comments