pywargame 0.3.1__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 (150) hide show
  1. pywargame/__init__.py +2 -0
  2. pywargame/common/__init__.py +3 -0
  3. pywargame/common/collector.py +87 -0
  4. pywargame/common/dicedraw.py +363 -0
  5. pywargame/common/drawdice.py +40 -0
  6. pywargame/common/singleton.py +22 -0
  7. pywargame/common/test.py +25 -0
  8. pywargame/common/verbose.py +59 -0
  9. pywargame/common/verboseguard.py +53 -0
  10. pywargame/cyberboard/__init__.py +18 -0
  11. pywargame/cyberboard/archive.py +283 -0
  12. pywargame/cyberboard/base.py +63 -0
  13. pywargame/cyberboard/board.py +462 -0
  14. pywargame/cyberboard/cell.py +200 -0
  15. pywargame/cyberboard/collect.py +49 -0
  16. pywargame/cyberboard/collectgbx0pwd.py +30 -0
  17. pywargame/cyberboard/collectgbxext.py +30 -0
  18. pywargame/cyberboard/collectgsnexp.py +32 -0
  19. pywargame/cyberboard/collectgsnext.py +30 -0
  20. pywargame/cyberboard/draw.py +396 -0
  21. pywargame/cyberboard/exporter.py +1132 -0
  22. pywargame/cyberboard/extractor.py +240 -0
  23. pywargame/cyberboard/features.py +17 -0
  24. pywargame/cyberboard/gamebox.py +81 -0
  25. pywargame/cyberboard/gbxexp.py +76 -0
  26. pywargame/cyberboard/gbxext.py +64 -0
  27. pywargame/cyberboard/gsnexp.py +147 -0
  28. pywargame/cyberboard/gsnext.py +59 -0
  29. pywargame/cyberboard/head.py +111 -0
  30. pywargame/cyberboard/image.py +76 -0
  31. pywargame/cyberboard/main.py +47 -0
  32. pywargame/cyberboard/mark.py +102 -0
  33. pywargame/cyberboard/palette.py +36 -0
  34. pywargame/cyberboard/piece.py +169 -0
  35. pywargame/cyberboard/player.py +36 -0
  36. pywargame/cyberboard/scenario.py +115 -0
  37. pywargame/cyberboard/testgrid.py +156 -0
  38. pywargame/cyberboard/tile.py +121 -0
  39. pywargame/cyberboard/tray.py +68 -0
  40. pywargame/cyberboard/windows.py +41 -0
  41. pywargame/cyberboard/zeropwd.py +45 -0
  42. pywargame/cyberboard.py +2728 -0
  43. pywargame/gbx0pwd.py +2776 -0
  44. pywargame/gbxextract.py +2795 -0
  45. pywargame/gsnexport.py +16499 -0
  46. pywargame/gsnextract.py +2790 -0
  47. pywargame/latex/__init__.py +2 -0
  48. pywargame/latex/collect.py +34 -0
  49. pywargame/latex/latexexporter.py +4010 -0
  50. pywargame/latex/main.py +184 -0
  51. pywargame/vassal/__init__.py +66 -0
  52. pywargame/vassal/base.py +139 -0
  53. pywargame/vassal/board.py +243 -0
  54. pywargame/vassal/buildfile.py +60 -0
  55. pywargame/vassal/chart.py +79 -0
  56. pywargame/vassal/chessclock.py +197 -0
  57. pywargame/vassal/collect.py +98 -0
  58. pywargame/vassal/collectpatch.py +28 -0
  59. pywargame/vassal/command.py +21 -0
  60. pywargame/vassal/documentation.py +322 -0
  61. pywargame/vassal/dumpcollect.py +28 -0
  62. pywargame/vassal/dumpvsav.py +28 -0
  63. pywargame/vassal/element.py +439 -0
  64. pywargame/vassal/exporter.py +89 -0
  65. pywargame/vassal/extension.py +101 -0
  66. pywargame/vassal/folder.py +103 -0
  67. pywargame/vassal/game.py +940 -0
  68. pywargame/vassal/gameelements.py +1091 -0
  69. pywargame/vassal/globalkey.py +127 -0
  70. pywargame/vassal/globalproperty.py +433 -0
  71. pywargame/vassal/grid.py +573 -0
  72. pywargame/vassal/map.py +1061 -0
  73. pywargame/vassal/mapelements.py +1020 -0
  74. pywargame/vassal/merge.py +57 -0
  75. pywargame/vassal/merger.py +460 -0
  76. pywargame/vassal/moduledata.py +275 -0
  77. pywargame/vassal/mrgcollect.py +31 -0
  78. pywargame/vassal/patch.py +44 -0
  79. pywargame/vassal/patchcollect.py +28 -0
  80. pywargame/vassal/player.py +83 -0
  81. pywargame/vassal/save.py +495 -0
  82. pywargame/vassal/skel.py +380 -0
  83. pywargame/vassal/trait.py +224 -0
  84. pywargame/vassal/traits/__init__.py +36 -0
  85. pywargame/vassal/traits/area.py +50 -0
  86. pywargame/vassal/traits/basic.py +35 -0
  87. pywargame/vassal/traits/calculatedproperty.py +22 -0
  88. pywargame/vassal/traits/cargo.py +29 -0
  89. pywargame/vassal/traits/click.py +41 -0
  90. pywargame/vassal/traits/clone.py +28 -0
  91. pywargame/vassal/traits/delete.py +24 -0
  92. pywargame/vassal/traits/deselect.py +32 -0
  93. pywargame/vassal/traits/dynamicproperty.py +112 -0
  94. pywargame/vassal/traits/globalcommand.py +55 -0
  95. pywargame/vassal/traits/globalhotkey.py +26 -0
  96. pywargame/vassal/traits/globalproperty.py +54 -0
  97. pywargame/vassal/traits/hide.py +67 -0
  98. pywargame/vassal/traits/label.py +76 -0
  99. pywargame/vassal/traits/layer.py +105 -0
  100. pywargame/vassal/traits/mark.py +20 -0
  101. pywargame/vassal/traits/mask.py +85 -0
  102. pywargame/vassal/traits/mat.py +26 -0
  103. pywargame/vassal/traits/moved.py +35 -0
  104. pywargame/vassal/traits/movefixed.py +51 -0
  105. pywargame/vassal/traits/nonrect.py +95 -0
  106. pywargame/vassal/traits/nostack.py +55 -0
  107. pywargame/vassal/traits/place.py +104 -0
  108. pywargame/vassal/traits/prototype.py +20 -0
  109. pywargame/vassal/traits/report.py +34 -0
  110. pywargame/vassal/traits/restrictaccess.py +28 -0
  111. pywargame/vassal/traits/restrictcommand.py +32 -0
  112. pywargame/vassal/traits/return.py +40 -0
  113. pywargame/vassal/traits/rotate.py +62 -0
  114. pywargame/vassal/traits/sendto.py +59 -0
  115. pywargame/vassal/traits/sheet.py +129 -0
  116. pywargame/vassal/traits/skel.py +9 -0
  117. pywargame/vassal/traits/stack.py +28 -0
  118. pywargame/vassal/traits/submenu.py +27 -0
  119. pywargame/vassal/traits/trail.py +61 -0
  120. pywargame/vassal/traits/trigger.py +72 -0
  121. pywargame/vassal/turn.py +272 -0
  122. pywargame/vassal/upgrade.py +191 -0
  123. pywargame/vassal/vmod.py +323 -0
  124. pywargame/vassal/vsav.py +100 -0
  125. pywargame/vassal/widget.py +358 -0
  126. pywargame/vassal/withtraits.py +634 -0
  127. pywargame/vassal/xml.py +4 -0
  128. pywargame/vassal/zone.py +399 -0
  129. pywargame/vassal.py +12500 -0
  130. pywargame/vmodpatch.py +12548 -0
  131. pywargame/vsavdump.py +12533 -0
  132. pywargame/vslmerge.py +13015 -0
  133. pywargame/wgexport.py +16689 -0
  134. pywargame/ztexport.py +14351 -0
  135. pywargame/zuntzu/__init__.py +5 -0
  136. pywargame/zuntzu/base.py +82 -0
  137. pywargame/zuntzu/collect.py +38 -0
  138. pywargame/zuntzu/countersheet.py +250 -0
  139. pywargame/zuntzu/dicehand.py +48 -0
  140. pywargame/zuntzu/exporter.py +936 -0
  141. pywargame/zuntzu/gamebox.py +154 -0
  142. pywargame/zuntzu/map.py +36 -0
  143. pywargame/zuntzu/piece.py +37 -0
  144. pywargame/zuntzu/scenario.py +208 -0
  145. pywargame/zuntzu/ztexp.py +115 -0
  146. pywargame-0.3.1.dist-info/METADATA +353 -0
  147. pywargame-0.3.1.dist-info/RECORD +150 -0
  148. pywargame-0.3.1.dist-info/WHEEL +5 -0
  149. pywargame-0.3.1.dist-info/licenses/LICENSE +5 -0
  150. pywargame-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python
2
+ ## BEGIN_IMPORTS
3
+ from vassal import Merger
4
+ from common import Verbose
5
+ ## END_IMPORTS
6
+
7
+
8
+ # ====================================================================
9
+ if __name__ == '__main__':
10
+ from argparse import ArgumentParser, FileType
11
+
12
+ ap = ArgumentParser(description='Merge two modules or extensions')
13
+ ap.add_argument('input',type=FileType('r'),help='Input files',nargs='+')
14
+ ap.add_argument('-o','--output',type=FileType('w'),
15
+ help='Output file',nargs='?',default='Merged.vmod')
16
+ ap.add_argument('-V','--verbose',action='store_true',
17
+ help='Be verbose')
18
+ ap.add_argument('-p','--patch',type=FileType('r'),
19
+ help='Python patch script to execute after merging')
20
+ ap.add_argument('-O','--overwrite',action='store_true',
21
+ help='Overwrite elements, attributes, files, '
22
+ 'etc. with later specified content')
23
+ ap.add_argument('-K','--keep',action='store_false',dest='overwrite',
24
+ help='Keep first elements, attributes, files, etc.')
25
+ ap.add_argument('-S','--assume-same',action='store_true',
26
+ help='Assume all modules are same game')
27
+
28
+
29
+ args = ap.parse_args()
30
+
31
+ Verbose().setVerbose(args.verbose)
32
+
33
+ if len(args.input) < 2:
34
+ raise RuntimeError('At least two inputs must be given')
35
+
36
+ outname = args.output.name
37
+ args.output.close()
38
+
39
+ merger = Merger(outname,*args.input)
40
+
41
+ try:
42
+ merger.run(patch = args.patch,
43
+ overwrite = args.overwrite,
44
+ assume_same = args.assume_same)
45
+ except:
46
+ from pathlib import Path
47
+
48
+ op = Path(outname)
49
+ op.unlink(missing_ok=True)
50
+
51
+ raise
52
+
53
+
54
+
55
+ #
56
+ #
57
+ #
@@ -0,0 +1,460 @@
1
+ ## BEGIN_IMPORT
2
+ from common import VerboseGuard
3
+ from . base import *
4
+ from . element import Element
5
+ from . extension import *
6
+ from . vmod import *
7
+ from . buildfile import *
8
+ from . moduledata import *
9
+ ## END_IMPORT
10
+
11
+ class Merger:
12
+ def __init__(self,outname,*inputs):
13
+ self._outName = outname # Output name
14
+ self._inputs = list(inputs) # File-like objects
15
+ self._names = [i.name for i in self._inputs]
16
+
17
+ # Pop the first input, and open it as a module, and clone it
18
+ first = self._inputs.pop(0)
19
+ firstname = first.name
20
+ first.close()
21
+ with VMod(firstname, 'r') as vfirst:
22
+ self._dest = vfirst.clone(outname,
23
+ mode = 'a',
24
+ filter = self.filterCruft)
25
+
26
+ self._mergedData = {}
27
+ self._buildFile = BuildFile(self._dest.getBuildFile())
28
+ self._moduleData = ModuleData(self._dest.getModuleData())
29
+ self._extensions = None
30
+ self._mergedData[firstname] = self._moduleData._root
31
+ self._buildFileDest = (VMod.BUILD_FILE if
32
+ VMod.BUILD_FILE in self._dest._vmod.namelist()
33
+ else VMod.BUILD_FILE_SANS)
34
+
35
+ # ----------------------------------------------------------------
36
+ def filterCruft(self,f):
37
+ '''Filter special. Return true for files that should be
38
+ filtered out.
39
+
40
+ Some cruft left behind by MacOSX is explicitly
41
+ removed, and wrongly embedded module files are also
42
+ filtered out.
43
+
44
+ '''
45
+ if f.startswith('__MACOSX'):
46
+ return True
47
+ if f.endswith('.vmod'):
48
+ return True
49
+ if f.endswith('.DS_Store'):
50
+ return True
51
+ return False
52
+
53
+ # ----------------------------------------------------------------
54
+ def run(self,overwrite=True,assume_same=False,patch=None):
55
+ with VerboseGuard(f'Merging inputs into {self._outName}') as v:
56
+ try:
57
+ while self._inputs:
58
+ input = self._inputs.pop(0)
59
+ self._currentName = input.name
60
+ input.close()
61
+
62
+ self.mergeOne(overwrite=True,assume_same=assume_same)
63
+ except:
64
+ raise
65
+
66
+ #v(f'{"\n".join(self._dest.getFileNames())}')
67
+ self.documentMerge()
68
+ self.patch(patch)
69
+
70
+ v(f'Overrding updated files {self._buildFileDest}')
71
+ self._dest.replaceFiles(**{self._buildFileDest:
72
+ self._buildFile.encode(),
73
+ VMod.MODULE_DATA:
74
+ self._moduleData.encode()})
75
+
76
+ self.summary()
77
+
78
+ self._dest._vmod.close()
79
+
80
+ # ----------------------------------------------------------------
81
+ def documentMerge(self):
82
+ with VerboseGuard(f'Writing summary of merge') as v:
83
+ doc = self._buildFile.getGame().getDocumentation(single=True)
84
+ if not doc:
85
+ doc = self._buildFile.getGame().addDocumentation()
86
+ else:
87
+ doc = doc[0]
88
+
89
+ def li(n,d):
90
+ return f'<li><code>{n}</code></li>'
91
+
92
+ desc = f'''<html><body>
93
+ <h1>Merged modules and extensions</h1>
94
+ <p>
95
+ This module was created from other modules or extensions by
96
+ the Python script <code>vslmerge.py</code> available from
97
+ </p>
98
+ <pre>
99
+ htps://gitlab.com/wargames_tex/pywargame
100
+ </pre>
101
+ <h1>Merged files</h1>
102
+ <ul>
103
+ {"\n".join([li(n,d) for n,d in self._mergedData.items()])}
104
+ </ul>
105
+ <p>
106
+ See also the XML file <code>merged</code>, in the module
107
+ archive, for more information of the merged modules or
108
+ extensions
109
+ </p>
110
+ </body></html>;'''
111
+ self._dest.addFile('help/merged.html',desc)
112
+ doc.addHelpFile(title='Merged',fileName='help/merged.html')
113
+ v(f'Wrote merge information to help menu')
114
+
115
+ # ----------------------------------------------------------------
116
+ def summary(self):
117
+ with VerboseGuard(f'Writing summary XML to output') as v:
118
+ from pprint import pprint
119
+ #from xml.dom.minidom import Document
120
+ from pathlib import Path
121
+
122
+ doc = xmlns.Document()
123
+ lst = doc.createElement('MergeSummary')
124
+ doc.appendChild(lst)
125
+ for n,d in self._mergedData.items():
126
+ p = Path(n).name
127
+ v(f'{p}')
128
+
129
+ m = doc.createElement('Merged')
130
+ m.setAttribute('filename',p)
131
+ lst.appendChild(m)
132
+ m.appendChild(d.firstChild)
133
+
134
+ self._dest.addFile('merged',doc.toprettyxml())
135
+
136
+
137
+
138
+
139
+
140
+
141
+ # ----------------------------------------------------------------
142
+ def patch(self,patch):
143
+ if not patch:
144
+ return
145
+
146
+ with VerboseGuard(f'Patching merged module w/file {patch.name}') as v:
147
+ from importlib.util import spec_from_file_location, \
148
+ module_from_spec
149
+ from pathlib import Path
150
+ from sys import modules
151
+
152
+ p = Path(patch.name)
153
+
154
+
155
+ spec = spec_from_file_location(p.stem, p.absolute())
156
+ module = module_from_spec(spec)
157
+ spec.loader.exec_module(module)
158
+
159
+ modules[p.stem] = module
160
+
161
+ module.patch(self._buildFile,
162
+ self._moduleData,
163
+ self._dest,
164
+ Verbose())
165
+
166
+
167
+ # ----------------------------------------------------------------
168
+ def mergeOne(self,overwrite=True,assume_same=False):
169
+ with VerboseGuard(f'Merging from {self._currentName}') as v:
170
+ with VMod(self._currentName,'r') as self._current:
171
+
172
+ if self._current.isExtension():
173
+ self._mergedData[self._currentName] = \
174
+ self._current.getExtensionData()
175
+ else:
176
+ self._mergedData[self._currentName] = \
177
+ self._current.getModuleData()
178
+
179
+ v(f'About to merge files')
180
+ self.mergeFiles(overwrite=overwrite)
181
+
182
+ if self._current.isExtension():
183
+ v(f'About to merge extension data')
184
+ self.mergeExtension(overwrite=overwrite)
185
+ else:
186
+ v(f'About to merge module data')
187
+ self.mergeModuleData(overwrite=overwrite)
188
+
189
+ v(f'About to merge build files')
190
+ self.mergeBuildFile(overwrite=overwrite,
191
+ assume_same=assume_same)
192
+
193
+ # ----------------------------------------------------------------
194
+ def mergeFiles(self,overwrite=True):
195
+ specials = [VMod.BUILD_FILE,
196
+ VMod.BUILD_FILE_SANS,
197
+ VMod.MODULE_DATA,
198
+ VMod.EXTENSION_DATA]
199
+
200
+ def filter(f):
201
+ return self.filterCruft(f) or f in specials
202
+
203
+ with VerboseGuard(f'Merging files from {self._currentName}') as v:
204
+
205
+
206
+ currentFiles = {f for f in self._current.getFileNames()
207
+ if not filter(f)}
208
+ destFiles = {f for f in self._dest.getFileNames()
209
+ if not filter(f)}
210
+ unique = currentFiles - destFiles
211
+
212
+ v(f'Will add {len(unique)} unique files from '
213
+ f'{self._currentName} to destination')
214
+ uniqueContent = self._current.getFiles(*unique)
215
+
216
+ v(f'{f"\n{v.i}".join([f"{f}: {len(c)}"
217
+ for f,c in uniqueContent.items()])}')
218
+ self._dest.addFiles(**uniqueContent)
219
+
220
+ if not overwrite:
221
+ return
222
+
223
+ overlap = currentFiles.intersection(destFiles)
224
+ v(f'Will overwrite {len(overlap)} files in destination '
225
+ f'with files from {self._currentName}')
226
+
227
+
228
+ overlapContent = self._current.getFiles(*overlap)
229
+ v(f'{f"\n{v.i}".join([f"{f}: {c[:20]}..."
230
+ for f,c in overlapContent.items()])}')
231
+ self._dest.replaceFiles(**overlapContent)
232
+
233
+ # ----------------------------------------------------------------
234
+ def mergeModuleData(self,overwrite=True):
235
+ ''' Merge module data XML''';
236
+ with VerboseGuard(f'Merging module data from '
237
+ f'{self._currentName}') as v:
238
+ currentModuleDoc = self._current.getModuleData()
239
+ currentModuleData = ModuleData(currentModuleDoc)
240
+
241
+ # v(f'{currentModuleDoc.toprettyxml()}')
242
+ self.mergeElement(self._moduleData,
243
+ currentModuleData,
244
+ overwrite=overwrite)
245
+
246
+ # ----------------------------------------------------------------
247
+ def mergeBuildFile(self,
248
+ overwrite = True,
249
+ assume_same = False):
250
+ ''' Merge module data XML''';
251
+ with VerboseGuard(f'Merging build file from '
252
+ f'{self._currentName}') as v:
253
+ currentModuleDoc = self._current.getBuildFile()
254
+ currentBuildFile = BuildFile(currentModuleDoc)
255
+
256
+ if assume_same:
257
+ with VerboseGuard(f'Assuming same game') as v:
258
+ # We assume that the modules are modules of the
259
+ # same game. Thus, we want to change the name of
260
+ # the top-level element of subsequent modules to
261
+ # be the same as the destination name.
262
+ destGame = self._buildFile .getGame()
263
+ srcGame = currentBuildFile.getGame()
264
+ v(f'Destination game is {destGame["name"]}')
265
+ v(f'Current source game is {srcGame["name"]}')
266
+ if v: destGame.print()
267
+ if v: srcGame .print()
268
+ srcGame['name'] = destGame['name']
269
+
270
+ # v(f'{currentModuleDoc.toprettyxml()}')
271
+ self.mergeElement(self._buildFile,
272
+ currentBuildFile,
273
+ depth = 0,
274
+ overwrite = overwrite)
275
+
276
+ # ----------------------------------------------------------------
277
+ def mergeExtension(self,overwrite=True):
278
+ ''' Merge module data XML''';
279
+ with VerboseGuard(f'Merging extension from '
280
+ f'{self._currentName}') as v:
281
+ currentModuleDoc = self._current.getBuildFile()
282
+ currentBuildFile = Extension(parent=currentModuleDoc,
283
+ node=currentModuleDoc.firstChild)
284
+
285
+ if not self._extensions:
286
+ self._extensions = self._buildFile.getGame().addFolder(
287
+ name = 'MergedExtensions',
288
+ description = 'Extensions merged in')
289
+
290
+ # Mark extension as loaded
291
+ #
292
+ # Doesn't work because VASSAL.build.module.ModuleExtension
293
+ # does not have a default CTOR - sigh! Probably done on
294
+ # purpose, bit still annoying.
295
+ #
296
+ # oext = self._extensions.add(Extension)
297
+ # oext.setAttributes(
298
+ # anyModule = currentBuildFile['anyModule'],
299
+ # version = currentBuildFile['version'],
300
+ # description = currentBuildFile['description'],
301
+ # module = currentBuildFile['module'],
302
+ # moduleVersion = currentBuildFile['moduleVersion'],
303
+ # vassalVersion = currentBuildFile['vassalVersion'],
304
+ # nextPieceSlotId = currentBuildFile['nextPieceSlotId'],
305
+ # extensionId = currentBuildFile['extensionId'])
306
+
307
+
308
+ # Get all the extension elements specified
309
+ for k,ext in currentBuildFile.getExtensionElements().items():
310
+ v(f'Extension element: {k}/{ext}')
311
+ cur = self._buildFile
312
+ if ext.target == '':
313
+ print(f'Warning, no target specified for extension '
314
+ f'element, will assume top')
315
+ spec = [[cur._node.firstChild.tagName]]
316
+ else:
317
+ spec = ext.getSelect()
318
+
319
+ # From the unpacked target path, find the target
320
+ # element in destination.
321
+ for tn,*en in spec:
322
+ v(f' Looking for element w/tag={tn} and attributes={en}')
323
+
324
+ cs = Element.getTagClass(tn)
325
+ if not cs:
326
+ raise RuntimeError(f'Got no class for tag={tn}')
327
+
328
+ es = cur.getAllElements(cs,single=len(en)<1)
329
+ if not es:
330
+ raise RuntimeError(f'Got no elements w/tag={tn}')
331
+
332
+ if en:
333
+ tgt = None
334
+ unt = Element._make_unique(tn,*en)
335
+ for e in es:
336
+ #v(f' candidate: {e._unique()} ?= {unt}')
337
+ if e._unique() == unt:
338
+ tgt = e
339
+ break
340
+ else:
341
+ tgt = es[0]
342
+
343
+
344
+ if not tgt:
345
+ raise RuntimeError(f'Failed to find element w/tag='
346
+ f'{tn} and "name"={en}')
347
+
348
+ cur = tgt
349
+
350
+ if not cur:
351
+ raise RuntimeError(f'Failed to find element w/tag='
352
+ f'{tn} and "name"={en}')
353
+
354
+ # We have our target element. As we cannot specify
355
+ # changed attributes in extension elements, all we
356
+ # need to do is to merge in the child elements
357
+ v(f'Target element is {cur}')
358
+
359
+ self.mergeElement(cur,
360
+ ext,
361
+ depth = 0,
362
+ overwrite = overwrite,
363
+ skipAttributes = True)
364
+
365
+
366
+
367
+
368
+ # ----------------------------------------------------------------
369
+ def mergeElement(self,
370
+ dest,
371
+ src,
372
+ depth = 0,
373
+ overwrite = True,
374
+ skipAttributes = False):
375
+ '''Merge element src into dest under the policy 'overwrite'
376
+
377
+ This is a multi-stage process.
378
+
379
+ First, we check if there are attributes to merge, and then do that.
380
+ Second, we check if there are child nodes to merge, and then do that.
381
+ '''
382
+ with VerboseGuard(f'Merging element '
383
+ f'{src._unique()}') as v:
384
+ if not skipAttributes:
385
+ self.mergeAttributes(dest,src,overwrite)
386
+ self.mergeChildren (dest,src,
387
+ depth = depth,
388
+ overwrite = overwrite)
389
+
390
+ if overwrite and dest.hasText() and src.hasText():
391
+ # Perhaps append?
392
+ dest.setText(src.getText())
393
+ #dest.setText(dest.getText()+' '+src.getText())
394
+
395
+
396
+ # ----------------------------------------------------------------
397
+ def mergeAttributes(self,dest,src,overwrite=True):
398
+ with VerboseGuard(f'Merging attributes of elements '
399
+ f'{src._unique()}') as v:
400
+ srcAttributes = src.getAttributes()
401
+ destAttributes = dest.getAttributes()
402
+ srcNames = set(srcAttributes .keys()
403
+ if srcAttributes else [])
404
+
405
+ if srcAttributes is None:
406
+ return
407
+
408
+ destNames = set(destAttributes .keys()
409
+ if destAttributes else [])
410
+ unique = srcNames - destNames
411
+
412
+ v(f'Adding attributes {unique}')
413
+ dest.setAttributes(**{k:v for k,v in srcAttributes.items()
414
+ if k in unique})
415
+
416
+ if not overwrite:
417
+ return
418
+
419
+ overlap = srcNames.intersection(destNames)
420
+ v(f'Overwriting attributes {overlap}')
421
+ dest.setAttributes(**{k:v for k,v in srcAttributes.items()
422
+ if k in overlap})
423
+
424
+ # ----------------------------------------------------------------
425
+ def mergeChildren(self,dest,src,
426
+ overwrite = True,
427
+ depth = 0):
428
+ if depth > 20:
429
+ print(f'Maximum depth {depth} reached')
430
+ return
431
+
432
+ with VerboseGuard(f'Merging children of element '
433
+ f'{src._unique()}') as v:
434
+ srcChildren = set(src. getAllElements(cls=None))
435
+ destChildren = set(dest.getAllElements(cls=None))
436
+ unique = srcChildren - destChildren
437
+ overlap = destChildren.intersection(srcChildren)
438
+
439
+ with VerboseGuard(f'Adding {len(unique)} unique elements from '
440
+ f'{self._currentName} to destination {dest}') as vv:
441
+ for e in unique:
442
+ vv(f'Adding unique element {e._unique()}')
443
+ dest.append(e)
444
+ # print(dest._node.toprettyxml())
445
+
446
+ with VerboseGuard(f'Merging {len(overlap)} children from '
447
+ f'{self._currentName} into destination') as vv:
448
+ for e in overlap:
449
+ ds = [ee for ee in destChildren if ee == e]
450
+ if len(ds) != 1:
451
+ raise RuntimeError('Should not happen')
452
+ d = ds[0]
453
+ # Only apply `assume_same` on top-level
454
+ self.mergeElement(d,e,
455
+ depth = depth+1,
456
+ overwrite = overwrite)
457
+
458
+ #
459
+ # EOF
460
+ #