novelWriter 2.7.4__py3-none-any.whl → 2.8b1__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 (196) hide show
  1. novelwriter/__init__.py +8 -7
  2. novelwriter/assets/icons/font_awesome.icons +22 -4
  3. novelwriter/assets/icons/material_filled_normal.icons +20 -2
  4. novelwriter/assets/icons/material_filled_thin.icons +20 -2
  5. novelwriter/assets/icons/material_rounded_normal.icons +20 -2
  6. novelwriter/assets/icons/material_rounded_thin.icons +20 -2
  7. novelwriter/assets/icons/material_sharp_normal.icons +20 -2
  8. novelwriter/assets/icons/material_sharp_thin.icons +20 -2
  9. novelwriter/assets/icons/remix_filled.icons +20 -2
  10. novelwriter/assets/icons/remix_outline.icons +20 -2
  11. novelwriter/assets/images/welcome.webp +0 -0
  12. novelwriter/assets/manual.pdf +0 -0
  13. novelwriter/assets/manual_fr.pdf +0 -0
  14. novelwriter/assets/sample.zip +0 -0
  15. novelwriter/assets/text/credits_en.htm +61 -11
  16. novelwriter/assets/themes/aura.conf +97 -0
  17. novelwriter/assets/themes/aura_bright.conf +95 -0
  18. novelwriter/assets/themes/aura_soft.conf +97 -0
  19. novelwriter/assets/themes/b2t_garden_dark.conf +97 -0
  20. novelwriter/assets/themes/b2t_garden_light.conf +97 -0
  21. novelwriter/assets/themes/b2t_suburb_dark.conf +97 -0
  22. novelwriter/assets/themes/b2t_suburb_light.conf +97 -0
  23. novelwriter/assets/themes/b4t_classic_o_dark.conf +97 -0
  24. novelwriter/assets/themes/b4t_classic_o_light.conf +97 -0
  25. novelwriter/assets/themes/b4t_modern_c_dark.conf +97 -0
  26. novelwriter/assets/themes/b4t_modern_c_light.conf +97 -0
  27. novelwriter/assets/themes/blue_streak_dark.conf +97 -0
  28. novelwriter/assets/themes/blue_streak_light.conf +97 -0
  29. novelwriter/assets/themes/castle_day.conf +95 -0
  30. novelwriter/assets/themes/castle_night.conf +95 -0
  31. novelwriter/assets/themes/catppuccin_latte.conf +97 -0
  32. novelwriter/assets/themes/catppuccin_mocha.conf +97 -0
  33. novelwriter/assets/themes/chalky_soil.conf +95 -0
  34. novelwriter/assets/themes/chernozem.conf +95 -0
  35. novelwriter/assets/themes/cyberpunk_night.conf +88 -40
  36. novelwriter/assets/themes/default_dark.conf +89 -41
  37. novelwriter/assets/themes/default_light.conf +89 -41
  38. novelwriter/assets/themes/dracula.conf +91 -42
  39. novelwriter/assets/themes/espresso.conf +97 -0
  40. novelwriter/assets/themes/everforest_dark.conf +97 -0
  41. novelwriter/assets/themes/everforest_light.conf +97 -0
  42. novelwriter/assets/themes/floral_daydream.conf +95 -0
  43. novelwriter/assets/themes/floral_midnight.conf +95 -0
  44. novelwriter/assets/themes/full_moon.conf +95 -0
  45. novelwriter/assets/themes/grey_dark.conf +97 -0
  46. novelwriter/assets/themes/grey_light.conf +97 -0
  47. novelwriter/assets/themes/horizon_dark.conf +97 -0
  48. novelwriter/assets/themes/horizon_light.conf +97 -0
  49. novelwriter/assets/themes/jewel_case_dark.conf +95 -0
  50. novelwriter/assets/themes/jewel_case_light.conf +95 -0
  51. novelwriter/assets/themes/lcars.conf +97 -0
  52. novelwriter/assets/themes/light_owl.conf +117 -0
  53. novelwriter/assets/themes/new_moon.conf +97 -0
  54. novelwriter/assets/themes/night_owl.conf +117 -0
  55. novelwriter/assets/themes/noctis.conf +129 -0
  56. novelwriter/assets/themes/noctis_lux.conf +129 -0
  57. novelwriter/assets/themes/nord.conf +97 -0
  58. novelwriter/assets/themes/nordlicht.conf +95 -0
  59. novelwriter/assets/themes/otium_dark.conf +95 -0
  60. novelwriter/assets/themes/otium_light.conf +95 -0
  61. novelwriter/assets/themes/paragon.conf +96 -0
  62. novelwriter/assets/themes/primer_light.conf +97 -0
  63. novelwriter/assets/themes/primer_night.conf +97 -0
  64. novelwriter/assets/themes/rose_pine.conf +97 -0
  65. novelwriter/assets/themes/rose_pine_dawn.conf +97 -0
  66. novelwriter/assets/themes/ruby_day.conf +95 -0
  67. novelwriter/assets/themes/ruby_night.conf +95 -0
  68. novelwriter/assets/themes/selenium_dark.conf +95 -0
  69. novelwriter/assets/themes/selenium_light.conf +95 -0
  70. novelwriter/assets/themes/sepia_dark.conf +95 -0
  71. novelwriter/assets/themes/sepia_light.conf +95 -0
  72. novelwriter/assets/themes/snazzy.conf +102 -40
  73. novelwriter/assets/themes/solarized_dark.conf +108 -40
  74. novelwriter/assets/themes/solarized_light.conf +108 -40
  75. novelwriter/assets/themes/sultana_light.conf +95 -0
  76. novelwriter/assets/themes/sultana_night.conf +95 -0
  77. novelwriter/assets/themes/tango_dark.conf +111 -0
  78. novelwriter/assets/themes/tango_light.conf +111 -0
  79. novelwriter/assets/themes/tomorrow.conf +117 -0
  80. novelwriter/assets/themes/tomorrow_night.conf +117 -0
  81. novelwriter/assets/themes/tomorrow_night_blue.conf +117 -0
  82. novelwriter/assets/themes/tomorrow_night_bright.conf +117 -0
  83. novelwriter/assets/themes/tomorrow_night_eighties.conf +117 -0
  84. novelwriter/assets/themes/vivid_black_green.conf +97 -0
  85. novelwriter/assets/themes/vivid_black_red.conf +97 -0
  86. novelwriter/assets/themes/vivid_white_green.conf +97 -0
  87. novelwriter/assets/themes/vivid_white_red.conf +97 -0
  88. novelwriter/assets/themes/warpgate.conf +96 -0
  89. novelwriter/assets/themes/waterlily_dark.conf +95 -0
  90. novelwriter/assets/themes/waterlily_light.conf +95 -0
  91. novelwriter/common.py +47 -17
  92. novelwriter/config.py +57 -62
  93. novelwriter/constants.py +32 -6
  94. novelwriter/core/buildsettings.py +3 -23
  95. novelwriter/core/coretools.py +21 -25
  96. novelwriter/core/docbuild.py +4 -9
  97. novelwriter/core/document.py +2 -6
  98. novelwriter/core/index.py +33 -53
  99. novelwriter/core/indexdata.py +17 -22
  100. novelwriter/core/item.py +11 -35
  101. novelwriter/core/itemmodel.py +5 -21
  102. novelwriter/core/novelmodel.py +3 -7
  103. novelwriter/core/options.py +3 -4
  104. novelwriter/core/project.py +31 -21
  105. novelwriter/core/projectdata.py +2 -21
  106. novelwriter/core/projectxml.py +13 -21
  107. novelwriter/core/sessions.py +2 -4
  108. novelwriter/core/spellcheck.py +12 -13
  109. novelwriter/core/status.py +27 -20
  110. novelwriter/core/storage.py +5 -10
  111. novelwriter/core/tree.py +6 -15
  112. novelwriter/dialogs/about.py +9 -10
  113. novelwriter/dialogs/docmerge.py +17 -14
  114. novelwriter/dialogs/docsplit.py +18 -14
  115. novelwriter/dialogs/editlabel.py +15 -9
  116. novelwriter/dialogs/preferences.py +69 -68
  117. novelwriter/dialogs/projectsettings.py +88 -67
  118. novelwriter/dialogs/quotes.py +15 -10
  119. novelwriter/dialogs/wordlist.py +18 -21
  120. novelwriter/enum.py +75 -30
  121. novelwriter/error.py +6 -11
  122. novelwriter/extensions/configlayout.py +8 -34
  123. novelwriter/extensions/eventfilters.py +3 -3
  124. novelwriter/extensions/modified.py +87 -32
  125. novelwriter/extensions/novelselector.py +13 -12
  126. novelwriter/extensions/pagedsidebar.py +10 -18
  127. novelwriter/extensions/progressbars.py +5 -11
  128. novelwriter/extensions/statusled.py +3 -6
  129. novelwriter/extensions/switch.py +8 -11
  130. novelwriter/extensions/switchbox.py +2 -11
  131. novelwriter/extensions/versioninfo.py +6 -7
  132. novelwriter/formats/shared.py +10 -2
  133. novelwriter/formats/todocx.py +15 -37
  134. novelwriter/formats/tohtml.py +52 -61
  135. novelwriter/formats/tokenizer.py +33 -64
  136. novelwriter/formats/tomarkdown.py +4 -11
  137. novelwriter/formats/toodt.py +12 -71
  138. novelwriter/formats/toqdoc.py +11 -21
  139. novelwriter/formats/toraw.py +2 -6
  140. novelwriter/gui/doceditor.py +207 -245
  141. novelwriter/gui/dochighlight.py +142 -101
  142. novelwriter/gui/docviewer.py +53 -84
  143. novelwriter/gui/docviewerpanel.py +18 -41
  144. novelwriter/gui/editordocument.py +12 -17
  145. novelwriter/gui/itemdetails.py +5 -14
  146. novelwriter/gui/mainmenu.py +24 -32
  147. novelwriter/gui/noveltree.py +13 -51
  148. novelwriter/gui/outline.py +20 -61
  149. novelwriter/gui/projtree.py +40 -96
  150. novelwriter/gui/search.py +9 -24
  151. novelwriter/gui/sidebar.py +54 -22
  152. novelwriter/gui/statusbar.py +7 -22
  153. novelwriter/gui/theme.py +482 -368
  154. novelwriter/guimain.py +87 -101
  155. novelwriter/shared.py +79 -48
  156. novelwriter/splash.py +9 -5
  157. novelwriter/text/comments.py +1 -1
  158. novelwriter/text/counting.py +9 -5
  159. novelwriter/text/patterns.py +20 -15
  160. novelwriter/tools/dictionaries.py +18 -16
  161. novelwriter/tools/lipsum.py +15 -17
  162. novelwriter/tools/manusbuild.py +25 -45
  163. novelwriter/tools/manuscript.py +94 -95
  164. novelwriter/tools/manussettings.py +149 -104
  165. novelwriter/tools/noveldetails.py +10 -24
  166. novelwriter/tools/welcome.py +24 -72
  167. novelwriter/tools/writingstats.py +17 -26
  168. novelwriter/types.py +25 -13
  169. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/METADATA +7 -7
  170. novelwriter-2.8b1.dist-info/RECORD +212 -0
  171. novelwriter/assets/images/welcome-dark.jpg +0 -0
  172. novelwriter/assets/images/welcome-light.jpg +0 -0
  173. novelwriter/assets/syntax/cyberpunk_night.conf +0 -28
  174. novelwriter/assets/syntax/default_dark.conf +0 -42
  175. novelwriter/assets/syntax/default_light.conf +0 -42
  176. novelwriter/assets/syntax/dracula.conf +0 -44
  177. novelwriter/assets/syntax/grey_dark.conf +0 -29
  178. novelwriter/assets/syntax/grey_light.conf +0 -29
  179. novelwriter/assets/syntax/light_owl.conf +0 -49
  180. novelwriter/assets/syntax/night_owl.conf +0 -49
  181. novelwriter/assets/syntax/snazzy.conf +0 -42
  182. novelwriter/assets/syntax/solarized_dark.conf +0 -29
  183. novelwriter/assets/syntax/solarized_light.conf +0 -29
  184. novelwriter/assets/syntax/tango.conf +0 -39
  185. novelwriter/assets/syntax/tomorrow.conf +0 -49
  186. novelwriter/assets/syntax/tomorrow_night.conf +0 -49
  187. novelwriter/assets/syntax/tomorrow_night_blue.conf +0 -49
  188. novelwriter/assets/syntax/tomorrow_night_bright.conf +0 -49
  189. novelwriter/assets/syntax/tomorrow_night_eighties.conf +0 -49
  190. novelwriter/assets/themes/default.conf +0 -3
  191. novelwriter-2.7.4.dist-info/RECORD +0 -163
  192. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/WHEEL +0 -0
  193. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/entry_points.txt +0 -0
  194. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/LICENSE.md +0 -0
  195. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/setup/LICENSE-Apache-2.0.txt +0 -0
  196. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
1
+ [Main]
2
+ name = Waterlily Dark
3
+ mode = dark
4
+ author = Myian
5
+
6
+ [Base]
7
+ base = #1e2b33
8
+ default = #d0dee6
9
+ faded = #7b8e99
10
+ red = #ff7474
11
+ orange = #fca469
12
+ yellow = #f7eaa4
13
+ green = #9ae37f
14
+ cyan = #8ef3d1
15
+ blue = #71caeb
16
+ purple = #c89dfd
17
+
18
+ [Project]
19
+ root = default
20
+ folder = blue
21
+ file = purple
22
+ title = green
23
+ chapter = yellow
24
+ scene = #ff9fbd
25
+ note = faded
26
+ active = #ff9fbd
27
+ inactive = faded
28
+ disabled = #3e5366
29
+
30
+ [Icon]
31
+ tool = default
32
+ sidebar = default
33
+ accept = green
34
+ reject = red
35
+ action = blue
36
+ altaction = orange
37
+ apply = green
38
+ create = yellow
39
+ destroy = faded
40
+ reset = green
41
+ add = green
42
+ change = green
43
+ remove = red
44
+ shortcode = default
45
+ markdown = orange
46
+ systemio = yellow
47
+ info = blue
48
+ warning = orange
49
+ error = red
50
+
51
+ [Palette]
52
+ window = #151f25
53
+ windowtext = default
54
+ base = base
55
+ alternatebase = #3b515f
56
+ text = default
57
+ tooltipbase = #ffbad0
58
+ tooltiptext = base
59
+ button = base
60
+ buttontext = default
61
+ brighttext = base
62
+ highlight = #87bcc0
63
+ highlightedtext = base
64
+ link = blue
65
+ linkvisited = blue
66
+ accent = #ff9fbd
67
+
68
+ [GUI]
69
+ helptext = faded
70
+ fadedtext = faded
71
+ errortext = red
72
+
73
+ [Syntax]
74
+ background = base
75
+ text = default
76
+ line = #283842
77
+ link = blue
78
+ headertext = #e8eef1
79
+ headertag = faded
80
+ emphasis = green
81
+ whitespace = green:64
82
+ dialog = #ff9fbd
83
+ altdialog = orange
84
+ note = blue
85
+ hidden = faded
86
+ shortcode = purple
87
+ keyword = purple
88
+ tag = yellow
89
+ value = cyan
90
+ optional = faded
91
+ spellcheckline = #ff5d5d
92
+ errorline = #4ae698
93
+ replacetag = blue
94
+ modifier = purple
95
+ texthighlight = blue:64
@@ -0,0 +1,95 @@
1
+ [Main]
2
+ name = Waterlily Light
3
+ mode = light
4
+ author = Myian
5
+
6
+ [Base]
7
+ base = #f4fcfb
8
+ default = #435e66
9
+ faded = #91acab
10
+ red = #d36868
11
+ orange = #e08b63
12
+ yellow = #cab330
13
+ green = #62a729
14
+ cyan = #41a594
15
+ blue = #589dca
16
+ purple = #bd7fe7
17
+
18
+ [Project]
19
+ root = default
20
+ folder = blue
21
+ file = purple
22
+ title = green
23
+ chapter = yellow
24
+ scene = #e6859a
25
+ note = faded
26
+ active = #da7b8f
27
+ inactive = faded
28
+ disabled = #b6cac7
29
+
30
+ [Icon]
31
+ tool = default
32
+ sidebar = default
33
+ accept = green
34
+ reject = red
35
+ action = blue
36
+ altaction = orange
37
+ apply = green
38
+ create = yellow
39
+ destroy = faded
40
+ reset = green
41
+ add = green
42
+ change = green
43
+ remove = red
44
+ shortcode = default
45
+ markdown = orange
46
+ systemio = yellow
47
+ info = blue
48
+ warning = orange
49
+ error = red
50
+
51
+ [Palette]
52
+ window = #e2eeed
53
+ windowtext = default
54
+ base = base
55
+ alternatebase = #c7e0e0
56
+ text = default
57
+ tooltipbase = #ffdfe6
58
+ tooltiptext = #223f47
59
+ button = base
60
+ buttontext = default
61
+ brighttext = base
62
+ highlight = #4da1af
63
+ highlightedtext = base
64
+ link = blue
65
+ linkvisited = blue
66
+ accent = #e6859a
67
+
68
+ [GUI]
69
+ helptext = default:192
70
+ fadedtext = faded
71
+ errortext = red
72
+
73
+ [Syntax]
74
+ background = base
75
+ text = default
76
+ line = #e2eeed
77
+ link = #0b9caf
78
+ headertext = #57757e
79
+ headertag = #b6cac7
80
+ emphasis = #1e920e
81
+ whitespace = #1e920e64
82
+ dialog = #d15574
83
+ altdialog = #dd6b37
84
+ note = #0b9caf
85
+ hidden = #87a3a0
86
+ shortcode = #9a54c9
87
+ keyword = #9a54c9
88
+ tag = #c9a02e
89
+ value = #05af77
90
+ optional = #87a3a0
91
+ spellcheckline = #ff3e3e
92
+ errorline = #05af77
93
+ replacetag = #0b9caf
94
+ modifier = #9a54c9
95
+ texthighlight = cyan:64
novelwriter/common.py CHANGED
@@ -20,7 +20,7 @@ General Public License for more details.
20
20
 
21
21
  You should have received a copy of the GNU General Public License
22
22
  along with this program. If not, see <https://www.gnu.org/licenses/>.
23
- """
23
+ """ # noqa
24
24
  from __future__ import annotations
25
25
 
26
26
  import json
@@ -31,6 +31,7 @@ import xml.etree.ElementTree as ET
31
31
 
32
32
  from configparser import ConfigParser
33
33
  from datetime import datetime
34
+ from enum import Enum
34
35
  from pathlib import Path
35
36
  from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar
36
37
  from urllib.parse import urljoin
@@ -134,7 +135,7 @@ def checkPath(value: Any, default: Path) -> Path:
134
135
 
135
136
  def isHandle(value: Any) -> TypeGuard[str]:
136
137
  """Check if a string is a valid novelWriter handle.
137
- Note: This is case sensitive. Must be lower case!
138
+ Note: This is case sensitive. Must be lower case.
138
139
  """
139
140
  if not isinstance(value, str):
140
141
  return False
@@ -218,7 +219,7 @@ def firstFloat(*args: Any) -> float:
218
219
  ##
219
220
 
220
221
  def formatInt(value: int) -> str:
221
- """Formats an integer with k, M, G etc."""
222
+ """Format an integer with k, M, G etc."""
222
223
  if not isinstance(value, int):
223
224
  return "ERR"
224
225
 
@@ -444,6 +445,7 @@ def fontMatcher(font: QFont) -> QFont:
444
445
  default Qt font matching algorithm doesn't handle well changing
445
446
  application fonts at runtime.
446
447
  """
448
+ font.setStyleName(None) # Make sure no font style name is set from config, see #2502
447
449
  info = QFontInfo(font)
448
450
  if (famRequest := font.family()) != (famActual := info.family()):
449
451
  logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual)
@@ -462,21 +464,21 @@ def fontMatcher(font: QFont) -> QFont:
462
464
 
463
465
 
464
466
  def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
465
- """A replacement for Python lambdas that works for Qt slots."""
467
+ """A replacement for Python lambdas that works for Qt slots.""" # noqa: D401
466
468
  def wrapper(*a_: Any) -> None:
467
469
  func(*args, **kwargs)
468
470
  return wrapper
469
471
 
470
472
 
471
473
  def qtAddAction(parent: QWidget, label: str) -> QAction:
472
- """Helper to add action to widget and always return the action."""
474
+ """Helper to add action to widget and always return the action.""" # noqa: D401
473
475
  action = QAction(label, parent)
474
476
  parent.addAction(action)
475
477
  return action
476
478
 
477
479
 
478
480
  def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu:
479
- """Helper to add menu to menu and always return the menu."""
481
+ """Helper to add menu to menu and always return the menu.""" # noqa: D401
480
482
  menu = QMenu(label, parent)
481
483
  parent.addMenu(menu)
482
484
  return menu
@@ -485,7 +487,6 @@ def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu:
485
487
  def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
486
488
  """Encode handles into a mime data object."""
487
489
  mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
488
- return
489
490
 
490
491
 
491
492
  def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
@@ -493,6 +494,21 @@ def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
493
494
  return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
494
495
 
495
496
 
497
+ def utf16CharMap(text: str) -> list[int]:
498
+ """Compute mapping from Python string index to QString index.
499
+ Python strings are always one character per position in either
500
+ ASCII, UCS-2 or UCS-4. QStrings are in UTF-16, so wide characters
501
+ use 2 indices, and thus create an offset.
502
+ """
503
+ utf16Map = list(range(0, len(text) + 1))
504
+ offset = 0
505
+ for i, c in enumerate(text, 1):
506
+ if ord(c) > 0xffff:
507
+ offset += 1
508
+ utf16Map[i] = i + offset
509
+ return utf16Map
510
+
511
+
496
512
  ##
497
513
  # Encoder Functions
498
514
  ##
@@ -519,7 +535,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
519
535
  elif first in ("{", "["):
520
536
  n += 1
521
537
  indent = "\n"+" "*n
522
- if n > nmax and nmax > 0:
538
+ if n > nmax > 0:
523
539
  buffer.append(chunk)
524
540
  else:
525
541
  buffer.append(chunk[0] + indent + chunk[1:])
@@ -527,13 +543,13 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
527
543
  elif first in ("}", "]"):
528
544
  n -= 1
529
545
  indent = "\n"+" "*n
530
- if n >= nmax and nmax > 0:
546
+ if n >= nmax > 0:
531
547
  buffer.append(chunk)
532
548
  else:
533
549
  buffer.append(indent + chunk)
534
550
 
535
551
  elif first == ",":
536
- if n > nmax and nmax > 0:
552
+ if n > nmax > 0:
537
553
  buffer.append(chunk)
538
554
  else:
539
555
  buffer.append(chunk[0] + indent + chunk[1:].lstrip())
@@ -544,6 +560,12 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
544
560
  return "".join(buffer)
545
561
 
546
562
 
563
+ def jsonCombine(data: dict[str, str]) -> str:
564
+ """Combine multiple already packed JSON strings."""
565
+ payload = ",\n".join(f' "{k}": {v}' for k, v in data.items())
566
+ return f"{{\n{payload}\n}}\n"
567
+
568
+
547
569
  ##
548
570
  # XML Helpers
549
571
  ##
@@ -551,7 +573,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
551
573
  def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
552
574
  """A modified version of the XML indent function in the standard
553
575
  library. It behaves more closely to how the one from lxml does.
554
- """
576
+ """ # noqa: D401
555
577
  tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml
556
578
  if not isinstance(tree, ET.Element):
557
579
  return
@@ -581,8 +603,6 @@ def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
581
603
  if last is not None:
582
604
  last.tail = indentations[level]
583
605
 
584
- return
585
-
586
606
  if len(tree):
587
607
  indentChildren(tree, 0)
588
608
  tree.tail = "\n"
@@ -597,7 +617,7 @@ def xmlElement(
597
617
  attrib: dict | None = None,
598
618
  tail: str | None = None,
599
619
  ) -> ET.Element:
600
- """A custom implementation of Element with more arguments."""
620
+ """A custom implementation of Element with more arguments.""" # noqa: D401
601
621
  xSub = ET.Element(tag, attrib=attrib or {})
602
622
  if text is not None:
603
623
  if isinstance(text, bool):
@@ -617,7 +637,7 @@ def xmlSubElem(
617
637
  attrib: dict | None = None,
618
638
  tail: str | None = None,
619
639
  ) -> ET.Element:
620
- """A custom implementation of SubElement with more arguments."""
640
+ """A custom implementation of SubElement with more arguments.""" # noqa: D401
621
641
  xSub = ET.SubElement(parent, tag, attrib=attrib or {})
622
642
  if text is not None:
623
643
  if isinstance(text, bool):
@@ -648,6 +668,7 @@ def readTextFile(path: str | Path) -> str:
648
668
 
649
669
  def makeFileNameSafe(text: str) -> str:
650
670
  """Return a filename-safe string.
671
+
651
672
  See: https://unicode.org/reports/tr15/#Norm_Forms
652
673
  """
653
674
  text = unicodedata.normalize("NFKC", text).strip()
@@ -676,8 +697,11 @@ def openExternalPath(path: Path) -> bool:
676
697
  # Classes
677
698
  ##
678
699
 
700
+ _T_Enum = TypeVar("_T_Enum", bound=Enum)
701
+
702
+
679
703
  class NWConfigParser(ConfigParser):
680
- """Common: Adapted Config Parser
704
+ """Common: Adapted Config Parser.
681
705
 
682
706
  This is a subclass of the standard config parser that adds type safe
683
707
  helper functions, and support for lists. It also turns off
@@ -686,7 +710,6 @@ class NWConfigParser(ConfigParser):
686
710
 
687
711
  def __init__(self) -> None:
688
712
  super().__init__(interpolation=None)
689
- return
690
713
 
691
714
  def rdStr(self, section: str, option: str, default: str) -> str:
692
715
  """Read string value."""
@@ -737,3 +760,10 @@ class NWConfigParser(ConfigParser):
737
760
  for i in range(min(len(data), len(result))):
738
761
  result[i] = checkInt(data[i].strip(), result[i])
739
762
  return result
763
+
764
+ def rdEnum(self, section: str, option: str, default: _T_Enum) -> _T_Enum:
765
+ """Read enum value."""
766
+ if self.has_option(section, option):
767
+ data = self.get(section, option, fallback="")
768
+ return type(default).__members__.get(data.upper(), default)
769
+ return default