Merge 2.7.5 (#2524)
This commit is contained in:
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
buildWin64:
|
buildWin64:
|
||||||
needs: buildAssets
|
needs: buildAssets
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
- name: Python Setup
|
- name: Python Setup
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# novelWriter Changelog
|
# novelWriter Changelog
|
||||||
|
|
||||||
|
## Version 2.7.5 [2025-09-14]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a patch release that fixes an issue related to crashes when using the completer menu under
|
||||||
|
certain conditions, and improves positioning of the input box for CJK languages.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fixes an issue where the app would crash of deleting the `@` character with the completer menu
|
||||||
|
visible and the text margins of the editor set to "justified". This is likely crashing due to
|
||||||
|
some unhandled corner case in the Qt library, but the implementation of the completer menu in
|
||||||
|
novelWriter uses a small hack to bypass some intended behaviour of the menu. Extra steps have
|
||||||
|
been added to the implementation that seems to avoid the crash. Issue #2510. PR #2511.
|
||||||
|
* Fixes an issue where the input box that shows up when typing CJK languages were covering the text
|
||||||
|
due to an incorrect offset of the box location. The incorrect offset is caused by the text
|
||||||
|
margins not being taken into account. Fix by @Euophrys based on solution by @Jack-name.
|
||||||
|
Issues #2267 and #2517. PR #2518.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
## Version 2.7.4 [2025-07-15]
|
## Version 2.7.4 [2025-07-15]
|
||||||
|
|
||||||
### Release Notes
|
### Release Notes
|
||||||
|
|||||||
@@ -39,13 +39,14 @@ from enum import Enum, IntFlag
|
|||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
QObject, QPoint, QRect, QRegularExpression, QRunnable, Qt, QTimer,
|
||||||
pyqtSlot
|
QVariant, pyqtSignal, pyqtSlot
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent,
|
QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent,
|
||||||
QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QShortcut,
|
QInputMethodEvent, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
|
||||||
QTextBlock, QTextCursor, QTextDocument, QTextFormat, QTextOption
|
QResizeEvent, QShortcut, QTextBlock, QTextCursor, QTextDocument,
|
||||||
|
QTextFormat, QTextOption
|
||||||
)
|
)
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
|
QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
|
||||||
@@ -74,9 +75,9 @@ from novelwriter.text.counting import standardCounter
|
|||||||
from novelwriter.tools.lipsum import GuiLipsum
|
from novelwriter.tools.lipsum import GuiLipsum
|
||||||
from novelwriter.types import (
|
from novelwriter.types import (
|
||||||
QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
|
QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
|
||||||
QtAlignRight, QtKeepAnchor, QtModCtrl, QtModNone, QtModShift, QtMouseLeft,
|
QtAlignRight, QtImCursorRectangle, QtKeepAnchor, QtModCtrl, QtModNone,
|
||||||
QtMoveAnchor, QtMoveLeft, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded,
|
QtModShift, QtMouseLeft, QtMoveAnchor, QtMoveLeft, QtMoveRight,
|
||||||
QtTransparent
|
QtScrollAlwaysOff, QtScrollAsNeeded, QtTransparent
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -154,7 +155,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
# Completer
|
# Completer
|
||||||
self._completer = CommandCompleter(self)
|
self._completer = CommandCompleter(self)
|
||||||
self._completer.complete.connect(self._insertCompletion)
|
self._completer.insertText.connect(self._insertCompletion)
|
||||||
|
|
||||||
# Create Custom Document
|
# Create Custom Document
|
||||||
self._qDocument = GuiTextDocument(self)
|
self._qDocument = GuiTextDocument(self)
|
||||||
@@ -905,7 +906,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
##
|
##
|
||||||
# Document Events and Maintenance
|
# Events and Overloads
|
||||||
##
|
##
|
||||||
|
|
||||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||||
@@ -1003,6 +1004,26 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.updateDocMargins()
|
self.updateDocMargins()
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
|
|
||||||
|
def inputMethodEvent(self, event: QInputMethodEvent) -> None:
|
||||||
|
"""Handle text being input from CJK input methods."""
|
||||||
|
super().inputMethodEvent(event)
|
||||||
|
if event.commitString():
|
||||||
|
# See issues #2267 and #2517
|
||||||
|
self.ensureCursorVisible()
|
||||||
|
self._completerToCursor()
|
||||||
|
|
||||||
|
def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QRect | QVariant:
|
||||||
|
"""Adjust completion windows for CJK input methods to consider
|
||||||
|
the viewport margins.
|
||||||
|
"""
|
||||||
|
if query == QtImCursorRectangle:
|
||||||
|
# See issues #2267 and #2517
|
||||||
|
vM = self.viewportMargins()
|
||||||
|
rect = self.cursorRect()
|
||||||
|
rect.translate(vM.left(), vM.top())
|
||||||
|
return rect
|
||||||
|
return super().inputMethodQuery(query)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Slots
|
# Public Slots
|
||||||
##
|
##
|
||||||
@@ -1062,24 +1083,20 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
if (block := self._qDocument.findBlock(pos)).isValid():
|
if (block := self._qDocument.findBlock(pos)).isValid():
|
||||||
text = block.text()
|
text = block.text()
|
||||||
|
|
||||||
if text and text[0] in "@%" and added + removed == 1:
|
if text and text[0] in "@%" and added + removed == 1:
|
||||||
# Only run on single character changes, or it will trigger
|
# Only run on single character changes, or it will trigger
|
||||||
# at unwanted times when other changes are made to the document
|
# at unwanted times when other changes are made to the document
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
bPos = cursor.positionInBlock()
|
bPos = cursor.positionInBlock()
|
||||||
if bPos > 0 and (viewport := self.viewport()):
|
if bPos > 0:
|
||||||
if text[0] == "@":
|
if text[0] == "@":
|
||||||
show = self._completer.updateMetaText(text, bPos)
|
show = self._completer.updateMetaText(text, bPos)
|
||||||
else:
|
else:
|
||||||
show = self._completer.updateCommentText(text, bPos)
|
show = self._completer.updateCommentText(text, bPos)
|
||||||
if show:
|
if show:
|
||||||
point = self.cursorRect().bottomRight()
|
|
||||||
self._completer.move(viewport.mapToGlobal(point))
|
|
||||||
self._completer.show()
|
self._completer.show()
|
||||||
else:
|
self._completerToCursor()
|
||||||
self._completer.close()
|
|
||||||
else:
|
|
||||||
self._completer.close()
|
|
||||||
|
|
||||||
if self._doReplace and added == 1:
|
if self._doReplace and added == 1:
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
@@ -1104,7 +1121,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
cursor.setPosition(check, QtMoveAnchor)
|
cursor.setPosition(check, QtMoveAnchor)
|
||||||
cursor.setPosition(check + length, QtKeepAnchor)
|
cursor.setPosition(check + length, QtKeepAnchor)
|
||||||
cursor.insertText(text)
|
cursor.insertText(text)
|
||||||
self._completer.hide()
|
self._completer.close()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _openContextFromCursor(self) -> None:
|
def _openContextFromCursor(self) -> None:
|
||||||
@@ -1866,6 +1883,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def _completerToCursor(self) -> None:
|
||||||
|
"""Make sure the completer menu is positioned by the cursor."""
|
||||||
|
if self._completer.isVisible() and (viewport := self.viewport()):
|
||||||
|
point = self.cursorRect().bottomLeft()
|
||||||
|
self._completer.move(viewport.mapToGlobal(point))
|
||||||
|
|
||||||
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
|
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
|
||||||
"""Slot for the spell check context menu triggering the
|
"""Slot for the spell check context menu triggering the
|
||||||
replacement of a word with the word from the dictionary.
|
replacement of a word with the word from the dictionary.
|
||||||
@@ -2050,10 +2073,13 @@ class CommandCompleter(QMenu):
|
|||||||
called on every keystroke on a line starting with @ or %.
|
called on every keystroke on a line starting with @ or %.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
complete = pyqtSignal(int, int, str)
|
__slots__ = ("_parent",)
|
||||||
|
|
||||||
|
insertText = pyqtSignal(int, int, str)
|
||||||
|
|
||||||
def __init__(self, parent: QWidget) -> None:
|
def __init__(self, parent: QWidget) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
def updateMetaText(self, text: str, pos: int) -> bool:
|
def updateMetaText(self, text: str, pos: int) -> bool:
|
||||||
"""Update the menu options based on the line of text."""
|
"""Update the menu options based on the line of text."""
|
||||||
@@ -2139,14 +2165,14 @@ class CommandCompleter(QMenu):
|
|||||||
|
|
||||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||||
"""Capture keypresses and forward most of them to the editor."""
|
"""Capture keypresses and forward most of them to the editor."""
|
||||||
parent = self.parent()
|
|
||||||
if event.key() in (
|
if event.key() in (
|
||||||
Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return,
|
Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return,
|
||||||
Qt.Key.Key_Enter, Qt.Key.Key_Escape
|
Qt.Key.Key_Enter, Qt.Key.Key_Escape
|
||||||
):
|
):
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
elif isinstance(parent, GuiDocEditor):
|
else:
|
||||||
parent.keyPressEvent(event)
|
self.close() # Close to release the event lock before forwarding the key press (#2510)
|
||||||
|
self._parent.keyPressEvent(event)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
@@ -2154,7 +2180,7 @@ class CommandCompleter(QMenu):
|
|||||||
|
|
||||||
def _emitComplete(self, pos: int, length: int, value: str) -> None:
|
def _emitComplete(self, pos: int, length: int, value: str) -> None:
|
||||||
"""Emit the signal to indicate a selection has been made."""
|
"""Emit the signal to indicate a selection has been made."""
|
||||||
self.complete.emit(pos, length, value)
|
self.insertText.emit(pos, length, value)
|
||||||
|
|
||||||
|
|
||||||
class BackgroundWordCounter(QRunnable):
|
class BackgroundWordCounter(QRunnable):
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ QtMoveAnchor = QTextCursor.MoveMode.MoveAnchor
|
|||||||
QtMoveLeft = QTextCursor.MoveOperation.Left
|
QtMoveLeft = QTextCursor.MoveOperation.Left
|
||||||
QtMoveRight = QTextCursor.MoveOperation.Right
|
QtMoveRight = QTextCursor.MoveOperation.Right
|
||||||
|
|
||||||
|
QtImCursorRectangle = Qt.InputMethodQuery.ImCursorRectangle
|
||||||
|
|
||||||
# Size Policy
|
# Size Policy
|
||||||
|
|
||||||
QtSizeExpanding = QSizePolicy.Policy.Expanding
|
QtSizeExpanding = QSizePolicy.Policy.Expanding
|
||||||
|
|||||||
+1
-2
@@ -260,8 +260,7 @@ if __name__ == "__main__":
|
|||||||
cmdBuildUbuntu = parsers.add_parser(
|
cmdBuildUbuntu = parsers.add_parser(
|
||||||
"build-ubuntu", help=(
|
"build-ubuntu", help=(
|
||||||
"Build a .deb package for Debian and Ubuntu. "
|
"Build a .deb package for Debian and Ubuntu. "
|
||||||
"Add --sign to sign package. "
|
"Add --sign to sign package."
|
||||||
"Add --first to set build number to 0."
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cmdBuildUbuntu.add_argument("--sign", action="store_true", help="Sign the package.")
|
cmdBuildUbuntu.add_argument("--sign", action="store_true", help="Sign the package.")
|
||||||
|
|||||||
+1
-4
@@ -10,10 +10,7 @@ authors = [
|
|||||||
description = "A plain text editor for planning and writing novels"
|
description = "A plain text editor for planning and writing novels"
|
||||||
readme = {file = "setup/description_pypi.md", content-type = "text/markdown"}
|
readme = {file = "setup/description_pypi.md", content-type = "text/markdown"}
|
||||||
license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"
|
license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"
|
||||||
license-files = [
|
license-files = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt"]
|
||||||
"LICENSE.md",
|
|
||||||
"setup/LICENSE-Apache-2.0.txt",
|
|
||||||
]
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import pytest
|
|||||||
from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl
|
from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent,
|
QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent,
|
||||||
QDropEvent, QFont, QMouseEvent, QTextBlock, QTextCursor, QTextDocument,
|
QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor,
|
||||||
QTextOption
|
QTextDocument, QTextOption
|
||||||
)
|
)
|
||||||
from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit
|
from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit
|
||||||
|
|
||||||
@@ -1953,6 +1953,19 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
|
|||||||
"%Note.Consistency: \n"
|
"%Note.Consistency: \n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# CJK completer reposition (#2267 and #2517)
|
||||||
|
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
|
||||||
|
assert completer.isVisible() is True
|
||||||
|
completer.move(0, 0)
|
||||||
|
assert completer.pos().x() == 0 # Completer menu at 0
|
||||||
|
assert completer.pos().y() == 0 # Completer menu at 0
|
||||||
|
|
||||||
|
event = QInputMethodEvent()
|
||||||
|
event.setCommitString("Text")
|
||||||
|
docEditor.inputMethodEvent(event)
|
||||||
|
assert completer.pos().x() > 0 # Completer should have moved
|
||||||
|
assert completer.pos().y() > 0 # Completer should have moved
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
|
|||||||
|
|
||||||
def makeDebianPackage(
|
def makeDebianPackage(
|
||||||
signKey: str | None = None, sourceBuild: bool = False, distName: str = "unstable",
|
signKey: str | None = None, sourceBuild: bool = False, distName: str = "unstable",
|
||||||
buildName: str = "", forLaunchpad: bool = False
|
buildName: str = "", forLaunchpad: bool = False, oldLicense: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build a Debian package."""
|
"""Build a Debian package."""
|
||||||
print("")
|
print("")
|
||||||
@@ -96,7 +96,7 @@ def makeDebianPackage(
|
|||||||
print("Copying or generating additional files ...")
|
print("Copying or generating additional files ...")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
copyPackageFiles(outDir, setupPy=True)
|
copyPackageFiles(outDir, oldLicense=oldLicense)
|
||||||
|
|
||||||
# Copy/Write Debian Files
|
# Copy/Write Debian Files
|
||||||
# =======================
|
# =======================
|
||||||
@@ -180,14 +180,14 @@ def launchpad(args: argparse.Namespace) -> None:
|
|||||||
bldNum = "0"
|
bldNum = "0"
|
||||||
|
|
||||||
distLoop = [
|
distLoop = [
|
||||||
("24.04", "noble"),
|
("24.04", "noble", True),
|
||||||
("25.04", "plucky"),
|
("25.04", "plucky", True),
|
||||||
("25.10", "questing"),
|
("25.10", "questing", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
print("Building Ubuntu packages for:")
|
print("Building Ubuntu packages for:")
|
||||||
print("")
|
print("")
|
||||||
for distNum, codeName in distLoop:
|
for distNum, codeName, _ in distLoop:
|
||||||
print(f" * Ubuntu {distNum} {codeName.title()}")
|
print(f" * Ubuntu {distNum} {codeName.title()}")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ def launchpad(args: argparse.Namespace) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
dputCmd = []
|
dputCmd = []
|
||||||
for distNum, codeName in distLoop:
|
for distNum, codeName, oldLicense in distLoop:
|
||||||
buildName = f"ubuntu{distNum}.{bldNum}"
|
buildName = f"ubuntu{distNum}.{bldNum}"
|
||||||
dCmd = makeDebianPackage(
|
dCmd = makeDebianPackage(
|
||||||
signKey=signKey,
|
signKey=signKey,
|
||||||
@@ -205,6 +205,7 @@ def launchpad(args: argparse.Namespace) -> None:
|
|||||||
distName=codeName,
|
distName=codeName,
|
||||||
buildName=buildName,
|
buildName=buildName,
|
||||||
forLaunchpad=True,
|
forLaunchpad=True,
|
||||||
|
oldLicense=oldLicense,
|
||||||
)
|
)
|
||||||
dputCmd.append(dCmd)
|
dputCmd.append(dCmd)
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -90,27 +90,31 @@ def copySourceCode(dst: Path) -> None:
|
|||||||
print("Copied:", relSrc, flush=True)
|
print("Copied:", relSrc, flush=True)
|
||||||
|
|
||||||
|
|
||||||
def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
|
def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None:
|
||||||
"""Copy files needed for packaging."""
|
"""Copy files needed for packaging."""
|
||||||
copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
|
copyFiles = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt", "CREDITS.md", "pyproject.toml"]
|
||||||
for copyFile in copyFiles:
|
for copyFile in copyFiles:
|
||||||
shutil.copyfile(copyFile, dst / copyFile)
|
shutil.copyfile(copyFile, dst / copyFile)
|
||||||
print("Copied:", copyFile, flush=True)
|
print("Copied:", copyFile, flush=True)
|
||||||
|
|
||||||
writeFile(dst / "MANIFEST.in", (
|
writeFile(dst / "MANIFEST.in", (
|
||||||
"include LICENSE.md\n"
|
"include LICENSE.md\n"
|
||||||
|
"include setup/LICENSE-Apache-2.0.txt\n"
|
||||||
"include CREDITS.md\n"
|
"include CREDITS.md\n"
|
||||||
"recursive-include novelwriter/assets *\n"
|
"recursive-include novelwriter/assets *\n"
|
||||||
))
|
))
|
||||||
|
|
||||||
if setupPy:
|
|
||||||
writeFile(dst / "setup.py", (
|
|
||||||
"import setuptools\n"
|
|
||||||
"setuptools.setup()\n"
|
|
||||||
))
|
|
||||||
|
|
||||||
text = readFile(ROOT_DIR / "pyproject.toml")
|
text = readFile(ROOT_DIR / "pyproject.toml")
|
||||||
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
|
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
|
||||||
|
if oldLicense:
|
||||||
|
new = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("license = "):
|
||||||
|
line = 'license = {text = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"}'
|
||||||
|
if line.startswith("license-files = "):
|
||||||
|
continue
|
||||||
|
new.append(line)
|
||||||
|
text = "\n".join(new)
|
||||||
writeFile(dst / "pyproject.toml", text)
|
writeFile(dst / "pyproject.toml", text)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user