diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml index 291da081..945b0634 100644 --- a/.github/workflows/build_assets.yml +++ b/.github/workflows/build_assets.yml @@ -6,27 +6,28 @@ jobs: buildAssets: runs-on: ubuntu-latest steps: - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: "3.13" - architecture: x64 + - name: Checkout Source + uses: actions/checkout@v5 - - name: Install Packages (apt) + - name: Install System Packages run: | sudo apt update sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra - - name: Checkout Source - uses: actions/checkout@v5 + - name: Install UV and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.13" + enable-cache: true - - name: Install Packages (pip) - run: pip install -U -r requirements.txt -r docs/requirements.txt + - name: Sync UV + run: | + uv sync --no-dev --group docs - name: Build Assets run: | - python pkgutils.py build-assets - python pkgutils.py icons optional + uv run --no-sync pkgutils.py build-assets + uv run --no-sync pkgutils.py icons optional - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index 82a22f10..238ae27a 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -14,14 +14,18 @@ jobs: with: python-version: "3.13" architecture: x64 - - name: Install Packages (apt) + + - name: Checkout Source + uses: actions/checkout@v5 + + - name: Install System Packages run: | sudo apt update sudo apt install qttools5-dev-tools - - name: Checkout Source - uses: actions/checkout@v5 + - name: Build Assets run: python pkgutils.py qtlrelease + - name: Upload Artifacts uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index d91c58fd..f3d723f9 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -14,24 +14,29 @@ jobs: checkSyntax: runs-on: ubuntu-latest steps: - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: 3 - architecture: x64 - name: Checkout Source uses: actions/checkout@v5 - - name: Install Dependencies - run: pip install -r requirements.txt -r requirements-dev.txt + + - name: Install UV and Python + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Sync UV + run: | + uv sync --no-dev --group lint + - name: Ruff Check run: | - ruff --version - ruff check + uv run --no-sync ruff --version + uv run --no-sync ruff check + - name: Pyright Check run: | - pyright --version - pyright + uv run --no-sync pyright --version + uv run --no-sync pyright + - name: Isort Check run: | - isort --version - isort --check . + uv run --no-sync isort --version + uv run --no-sync isort --check . diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 46f9b437..1f84fa95 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -14,33 +14,44 @@ jobs: testLinux: strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: + - "3.11" + - "3.12" + - "3.13" + - "3.14" fail-fast: false runs-on: ubuntu-latest steps: - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - architecture: x64 - - name: Install Packages (apt) - run: | - sudo apt update - sudo apt install libenchant-2-dev qttools5-dev-tools - name: Checkout Source uses: actions/checkout@v5 - - name: Install Dependencies (pip) + + - name: Install System Packages run: | - pip install -U -r requirements.txt -r tests/requirements.txt - - name: Run Build Commands + sudo apt update + sudo apt install qttools5-dev-tools + + - name: Install UV and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Sync UV run: | - python pkgutils.py qtlrelease - python pkgutils.py sample + uv sync --no-dev --group test + + - name: Build Assets + run: | + uv run --no-sync pkgutils.py qtlrelease + uv run --no-sync pkgutils.py sample + - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - python -m pytest -v --cov=novelwriter --timeout=60 + uv run --no-sync coverage run -m pytest -v --timeout=60 + uv run --no-sync coverage xml + - name: Upload to Codecov uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml index 7ecb5116..d191ba4b 100644 --- a/.github/workflows/test_mac.yml +++ b/.github/workflows/test_mac.yml @@ -14,24 +14,30 @@ jobs: testMac: runs-on: macos-latest steps: - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: "3.13" - architecture: x64 - - name: Install Packages (brew) - run: | - brew install enchant - name: Checkout Source uses: actions/checkout@v5 - - name: Install Dependencies (pip) + + - name: Install System Packages run: | - pip install -U pyobjc -r requirements.txt -r tests/requirements.txt + brew install enchant + + - name: Install UV and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.13" + + - name: Sync UV + run: | + uv sync --no-dev --group test --group macos + - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - python -m pytest -v --cov=novelwriter --timeout=60 + uv run --no-sync coverage run -m pytest -v --timeout=60 + uv run --no-sync coverage xml + - name: Upload to Codecov uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml index 1cdca205..1cb2dbdd 100644 --- a/.github/workflows/test_win.yml +++ b/.github/workflows/test_win.yml @@ -14,20 +14,25 @@ jobs: testWin: runs-on: windows-latest steps: - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: "3.13" - architecture: x64 - name: Checkout Source uses: actions/checkout@v5 - - name: Install Dependencies (pip) + + - name: Install UV and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.13" + + - name: Sync UV run: | - pip install -U -r requirements.txt -r tests/requirements.txt + uv sync --no-dev --group test + - name: Run Tests run: | - python -m pytest -v --cov=novelwriter --timeout=60 + uv run --no-sync coverage run -m pytest -v --timeout=60 + uv run --no-sync coverage xml + - name: Upload to Codecov uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 833d4182..5b91bed7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ setup.iss /setup/macos/Info.plist /setup/windows/build .venv +/uv.lock # Translations /novelwriter/assets/i18n/*.qm diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 767647e7..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -docutils>=0.17.1 -pygments>=2.7 -sphinx-book-theme -sphinx-copybutton -sphinx-design -sphinx-favicon -sphinx-intl -sphinx>=5.0 diff --git a/docs/source/technical/source.rst b/docs/source/technical/source.rst index 6b69b491..c5bce486 100644 --- a/docs/source/technical/source.rst +++ b/docs/source/technical/source.rst @@ -7,6 +7,7 @@ Running from Source .. _GitHub: https://github.com/vkbo/novelWriter/releases .. _PyPi: https://pypi.org/project/novelWriter/ .. _Sphinx Docs: https://www.sphinx-doc.org/ +.. _uv: https://docs.astral.sh/uv/ This chapter describes various ways of running novelWriter directly from the source code, and how to build the various components like the translation files and documentation. @@ -28,6 +29,7 @@ by running: .. _docs_technical_source_depend: + Dependencies ============ @@ -43,11 +45,20 @@ The following Python packages are needed to run all features of novelWriter: If you want spell checking, you must install the ``PyEnchant`` package. The spell check library must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine. -If you install from PyPi, these dependencies should be installed automatically. If you install from -source, dependencies can still be installed from PyPi with: +If you install novelWriter from PyPi, these dependencies should be installed automatically. + +You can run novelWriter directly from source with uv_: .. code-block:: bash + uv run novelwriter + +If you prefer to install dependencies using ``pip``, you must first generate the +``requirements.txt`` file: + +.. code-block:: bash + + python pkgutils.py gen-req pip install -r requirements.txt .. note:: @@ -136,12 +147,14 @@ running: Building the Documentation ========================== -A local copy of this documentation can be generated as HTML. This requires installing some Python -packages from PyPi: +A local copy of this documentation can be generated as HTML. + +If you're using ``pip``, you must first generate the ``requirements.txt`` file: .. code-block:: bash - pip install -r docs/requirements.txt + python pkgutils.py gen-req docs + pip install -r requirements.txt The documentation can then be built from the root folder in the source code by running: @@ -149,6 +162,12 @@ The documentation can then be built from the root folder in the source code by r make -C docs html +Or you can run directly with uv_: + +.. code-block:: bash + + uv run make -C docs html + If successful, the documentation should be available in the ``docs/build/html`` folder and you can open the ``index.html`` file in your browser. diff --git a/docs/source/technical/tests.rst b/docs/source/technical/tests.rst index 56623ff4..532bd750 100644 --- a/docs/source/technical/tests.rst +++ b/docs/source/technical/tests.rst @@ -4,23 +4,12 @@ Running Tests ************* +.. _uv: https://docs.astral.sh/uv/ + The novelWriter source code is well covered by tests. The test framework used for the development is ``pytest`` with the use of an extension for Qt. -Dependencies -============ - -The dependencies for running the tests can be installed with: - -.. code-block:: bash - - pip install -r tests/requirements.txt - -This will install a couple of extra packages for coverage and test management. The minimum -requirement is ``pytest`` and ``pytest-qt``. - - Simple Test Run =============== @@ -28,19 +17,35 @@ To run the tests, you simply need to execute the following from the root of the .. code-block:: bash - pytest + uv run pytest + +This uses uv_. See below for manually installing dependencies using ``pip``. Since several of the tests involve opening up the novelWriter GUI, you may want to disable the GUI for the duration of the test run. Moving your mouse while the tests are running may otherwise interfere with the execution of some tests. -You can disable the renderring of the GUI by setting the flag ``QT_QPA_PLATFORM=offscreen``: +You can disable the rendering of the GUI by setting the flag ``QT_QPA_PLATFORM=offscreen``: .. code-block:: bash export QT_QPA_PLATFORM=offscreen pytest +Dependencies +------------ + +To generate the requirements file and install dependencies using ``pip``, run: + +.. code-block:: bash + + python pkgutils.py gen-req app test + pip install -r tests/requirements.txt + +This will install a couple of extra packages for coverage and test management. The minimum +requirement is ``pytest`` and ``pytest-qt``. + + Advanced Options ================ diff --git a/novelWriter.py b/novelWriter.py index 78c8db27..9758edee 100755 --- a/novelWriter.py +++ b/novelWriter.py @@ -6,14 +6,6 @@ novelWriter – Start Script import os import sys -try: - import PyQt6.QtCore # noqa: F401 - import PyQt6.QtGui # noqa: F401 - import PyQt6.QtWidgets # noqa: F401 -except Exception: - print("ERROR: Failed to load dependency PyQt6") - sys.exit(1) - os.curdir = os.path.abspath(os.path.dirname(__file__)) if __name__ == "__main__": diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index fa3dd30e..17c0298e 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -49,9 +49,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.8a2" -__hexversion__ = "0x020800a2" -__date__ = "2025-07-16" +__version__ = "2.8a3" +__hexversion__ = "0x020800a3" +__date__ = "2025-10-18" __status__ = "Stable" __domain__ = "novelwriter.io" @@ -206,9 +206,9 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Check Packages and Versions errorData = [] errorCode = 0 - if sys.hexversion < 0x030a00f0: + if sys.hexversion < 0x030b00f0: errorData.append( - f"At least Python 3.10 is required, found {CONFIG.verPyString}" + f"At least Python 3.11 is required, found {CONFIG.verPyString}" ) errorCode |= 0x04 if CONFIG.verQtValue < 0x060400: diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index b83de9b7..f3a026f1 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -615,7 +615,7 @@ class GuiTheme: lookup = f"{prefix}{name} {key}" keys.append(lookup) data[lookup] = (file.stem, name, mode == "dark", file) - except Exception: # noqa: PERF203 + except Exception: logger.error("Could not read file: %s", file) logException() diff --git a/pkgutils.py b/pkgutils.py index 29d6983f..fc49f94d 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -40,7 +40,10 @@ import utils.build_windows import utils.docs import utils.icon_themes -from utils.common import ROOT_DIR, SETUP_DIR, extractVersion, readFile, stripVersion, writeFile +from utils.common import ( + ROOT_DIR, SETUP_DIR, extractReqs, extractVersion, readFile, stripVersion, + writeFile +) OS_LINUX = sys.platform.startswith("linux") OS_DARWIN = sys.platform.startswith("darwin") @@ -61,7 +64,7 @@ def installPackages(args: argparse.Namespace) -> None: print("=======================") print("") - installQueue = ["pip", "-r requirements.txt"] + installQueue = ["pip", *extractReqs(["app"])] if args.mac: installQueue.append("pyobjc") elif args.win: @@ -130,6 +133,15 @@ def genMacOSPlist(args: argparse.Namespace) -> None: writeFile(outDir / "Info.plist", plistXML) +def genReqFiles(args: argparse.Namespace) -> None: + """Generate requirements.txt file from pyproject.toml.""" + select = [s.strip().lower() for s in args.groups] if args.groups else ["app"] + (ROOT_DIR / "requirements.txt").write_text( + "\n".join(extractReqs(select)), + encoding="utf-8" + ) + + if __name__ == "__main__": """Parse command line options and run the commands.""" parser = argparse.ArgumentParser( @@ -222,7 +234,7 @@ if __name__ == "__main__": cmdBuildHtmlDocs = parsers.add_parser( "docs-html", help="Build the HTML docs." ) - cmdBuildHtmlDocs.add_argument("lang", nargs="+") + cmdBuildHtmlDocs.add_argument("lang", nargs="+", help="Language codes to generate docs for.") cmdBuildHtmlDocs.set_defaults(func=utils.docs.buildHtmlDocs) # Build Sample @@ -295,10 +307,23 @@ if __name__ == "__main__": cmdBuildClean.set_defaults(func=cleanBuildDirs) # Generate MacOS PList File - cmdBuildMacOSPlist = parsers.add_parser( + cmdGenMacOSPlist = parsers.add_parser( "gen-plist", help="Generate an Info.plist for use in a MacOS Bundle." ) - cmdBuildMacOSPlist.set_defaults(func=genMacOSPlist) + cmdGenMacOSPlist.set_defaults(func=genMacOSPlist) + + # Generate Requirement File + cmdGenReq = parsers.add_parser( + "gen-req", help="Generate a requirements.txt file for pip." + ) + cmdGenReq.add_argument( + "groups", nargs="*", help=( + "Groups to generate for, or 'all' to generate for all groups. " + "Use 'app' to generate for just the core application. " + "Defaults to app dependencies." + ) + ) + cmdGenReq.set_defaults(func=genReqFiles) args = parser.parse_args() args.func(args) diff --git a/pyproject.toml b/pyproject.toml index f10c3529..17599847 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,16 +4,15 @@ build-backend = "setuptools.build_meta" [project] name = "novelWriter" -authors = [ - {name = "Veronica Berglyd Olsen", email = "code@vkbo.net"}, -] +authors = [{ name = "Veronica Berglyd Olsen", email = "code@vkbo.net" }] 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-files = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt"] +dynamic = ["version"] +requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -25,12 +24,32 @@ classifiers = [ "Natural Language :: English", "Topic :: Text Editors", ] -requires-python = ">=3.10" dependencies = [ "pyqt6>=6.4", - "pyenchant>=3.0.0", + "pyenchant>=3.3.0", # 3.3 is needed for MacOS AARCH64 builds ] -dynamic = ["version"] + +[dependency-groups] +dev = [ + { include-group = "build" }, + { include-group = "docs" }, + { include-group = "test" }, + { include-group = "lint" }, +] +build = ["build", "setuptools>=77.0.3"] +docs = [ + "docutils>=0.17.1", + "pygments>=2.7", + "sphinx-book-theme", + "sphinx-copybutton", + "sphinx-design", + "sphinx-favicon", + "sphinx-intl", + "sphinx>=5.0", +] +test = ["coverage>=7.2.0", "pytest-qt", "pytest-timeout", "pytest>=6.0.0"] +lint = ["isort", "pyright", "ruff"] +macos = ["pyobjc"] [project.urls] Homepage = "https://novelwriter.io" @@ -42,13 +61,13 @@ Issues = "https://github.com/vkbo/novelWriter/issues" novelwriter = "novelwriter:main" [tool.setuptools.dynamic] -version = {attr = "novelwriter.__version__"} +version = { attr = "novelwriter.__init__.__version__" } [tool.setuptools.packages.find] include = ["novelwriter*"] [tool.isort] -py_version="310" +py_version = "311" line_length = 99 wrap_length = 79 multi_line_output = 5 @@ -64,26 +83,26 @@ preview = true # Rules: https://docs.astral.sh/ruff/rules select = [ - "A", # flake8-builtins (A) - "ANN", # flake8-annotations (ANN) - "B", # flake8-bugbear (B) - "D", # pydocstyle (D) - "E", # pycodestyle (E) - "F", # Pyflakes (F) - "FA", # flake8-future-annotations (FA) - "PERF", # Perflint (PERF) - "PLC", # Pylint Convention (PLC) - "PLE", # Pylint Error (PLE) - "PLR17", # Refactor (PLR) - Only PLR17xx - "PLW", # Pylint Warning (PLW) - "Q", # flake8-quotes (Q) - "RET", # flake8-return (RET) - "RUF", # Ruff-specific rules (RUF) - "SLF", # flake8-self (SLF) - "SLOT", # flake8-slots (SLOT) - "TC", # flake8-type-checking (TC) - "UP", # pyupgrade (UP) - "W", # pycodestyle (W) + "A", # flake8-builtins (A) + "ANN", # flake8-annotations (ANN) + "B", # flake8-bugbear (B) + "D", # pydocstyle (D) + "E", # pycodestyle (E) + "F", # Pyflakes (F) + "FA", # flake8-future-annotations (FA) + "PERF", # Perflint (PERF) + "PLC", # Pylint Convention (PLC) + "PLE", # Pylint Error (PLE) + "PLR17", # Refactor (PLR) - Only PLR17xx + "PLW", # Pylint Warning (PLW) + "Q", # flake8-quotes (Q) + "RET", # flake8-return (RET) + "RUF", # Ruff-specific rules (RUF) + "SLF", # flake8-self (SLF) + "SLOT", # flake8-slots (SLOT) + "TC", # flake8-type-checking (TC) + "UP", # pyupgrade (UP) + "W", # pycodestyle (W) ] ignore = [ "ANN401", # any-type @@ -133,7 +152,7 @@ include = ["novelwriter"] exclude = ["**/__pycache__"] reportIncompatibleMethodOverride = false -pythonVersion = "3.10" +pythonVersion = "3.11" [tool.pytest.ini_options] log_level = "DEBUG" @@ -146,9 +165,8 @@ markers = [ [tool.coverage.run] branch = false +source = ["novelwriter"] [tool.coverage.report] precision = 2 -exclude_also = [ - "if TYPE_CHECKING:" -] +exclude_also = ["if TYPE_CHECKING:"] diff --git a/requirements-all.txt b/requirements-all.txt deleted file mode 100644 index d140ead6..00000000 --- a/requirements-all.txt +++ /dev/null @@ -1,5 +0,0 @@ --r requirements.txt --r requirements-dev.txt --r docs/requirements.txt --r setup/requirements.txt --r tests/requirements.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 794644e0..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -build -isort -pyright -ruff diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fa1cf957..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyqt6>=6.4 -pyenchant>=3.0.0 diff --git a/run_tests.py b/run_tests.py index b9687aa9..a2b8e937 100755 --- a/run_tests.py +++ b/run_tests.py @@ -3,6 +3,7 @@ import argparse import os +import shlex import subprocess import sys @@ -12,27 +13,35 @@ if __name__ == "__main__": parser.add_argument("-o", action="store_true", help="Run off screen") parser.add_argument("-r", action="store_true", help="Generate reports") parser.add_argument("-t", action="store_true", help="Generate terminal report") - parser.add_argument("-m", help="Test modules") - parser.add_argument("-k", help="Test filters") + parser.add_argument("-u", action="store_true", help="Generate uncovered terminal report") + parser.add_argument("-m", help="Test modules", metavar="MARKEXPR") + parser.add_argument("-k", help="Test filters", metavar="EXPRESSION") args = parser.parse_args() env = os.environ.copy() env["QT_SCALE_FACTOR"] = "1.0" - cmd = [sys.executable, "-m", "pytest", "-vv"] + if args.r or args.t or args.u: + cmd = ["coverage", "run", "-m"] + else: + cmd = [sys.executable, "-m"] + + cmd += ["pytest", "-vv"] if args.o: env["QT_QPA_PLATFORM"] = "offscreen" - if args.r or args.t: - cmd += ["--cov=novelwriter"] - if args.r: - cmd += ["--cov-report=xml", "--cov-report=html"] - if args.t: - cmd += ["--cov-report=term"] if args.m: cmd += ["-m", args.m] if args.k: cmd += ["-k", args.k] - print("Calling:", " ".join(cmd)) + print("Calling:", shlex.join(cmd)) subprocess.call(cmd, env=env) + + if args.r: + subprocess.call(["coverage", "xml"]) + subprocess.call(["coverage", "html"]) + if args.t and not args.u: + subprocess.call(["coverage", "report"]) + if args.u: + subprocess.call(["coverage", "report", "--skip-covered "]) diff --git a/setup/debian/control b/setup/debian/control index 24f0cd25..06d63fa8 100644 --- a/setup/debian/control +++ b/setup/debian/control @@ -4,24 +4,26 @@ Section: text Priority: optional Build-Depends: dh-python, + pybuild-plugin-pyproject, + python3-build, python3-setuptools, python3-all, debhelper (>= 9), - python3 (>=3.10), + python3 (>=3.11), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0), qt6-image-formats-plugins (>= 6.4) Standards-Version: 4.5.1 Homepage: https://novelwriter.io -X-Python3-Version: >= 3.10 +X-Python3-Version: >= 3.11 Package: novelwriter Architecture: all Depends: ${misc:Depends}, ${python3:Depends}, - python3 (>=3.10), + python3 (>=3.11), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0), diff --git a/setup/macos/build.sh b/setup/macos/build.sh index ca475fa1..cafa196a 100755 --- a/setup/macos/build.sh +++ b/setup/macos/build.sh @@ -1,7 +1,7 @@ #! /bin/bash if [[ -z "$1" || -z "$2" || -z "$3" ]]; then - echo "Not enouch input arguments" + echo "Not enough input arguments" exit 1 fi @@ -108,8 +108,8 @@ conda install -c conda-forge enchant hunspell-en --yes # Install dependencies echo "Installing Python dependencies ..." +python3 pkgutils.py gen-req pip install -r "$SRC_DIR/requirements.txt" -pip install pyenchant==3.3.0rc1 # Leave conda env conda deactivate diff --git a/setup/make_pip.sh b/setup/make_pip.sh index 681a3cb5..9d3c61b2 100755 --- a/setup/make_pip.sh +++ b/setup/make_pip.sh @@ -1,36 +1,23 @@ #!/bin/bash set -e -ENVPATH=/tmp/nwBuild - if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 fi -echo "" -echo " Create Python Env" -echo "================================================================================" -echo "" - -if [ ! -d $ENVPATH ]; then - python3 -m venv $ENVPATH -fi -source $ENVPATH/bin/activate -pip3 install -U build twine -r requirements.txt -r docs/requirements.txt - echo "" echo " Building Dependencies" echo "================================================================================" echo "" -python3 pkgutils.py build-assets -python3 pkgutils.py icons optional +uv run pkgutils.py build-assets +uv run pkgutils.py icons optional echo "" echo " Building Packages" echo "================================================================================" echo "" -python3 -m build +uv build mkdir -pv dist_upload cp -v dist/novelwriter-*.whl dist_upload/ cd dist_upload @@ -38,13 +25,6 @@ FILE=$(ls -t | head -1) shasum -a 256 $FILE | tee $FILE.sha256 cd .. -echo "" -echo " Checking Packages" -echo "================================================================================" -echo "" -twine check dist/* -deactivate - echo "" echo " Done!" echo "================================================================================" diff --git a/setup/make_release.sh b/setup/make_release.sh index 97438bd9..8c59a933 100755 --- a/setup/make_release.sh +++ b/setup/make_release.sh @@ -1,8 +1,6 @@ #!/bin/bash set -e -ENVPATH=/tmp/nwBuild - if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 @@ -12,18 +10,12 @@ echo "" echo " Building Dependencies" echo "================================================================================" echo "" -if [ ! -d $ENVPATH ]; then - python3 -m venv $ENVPATH -fi -source $ENVPATH/bin/activate -pip3 install -r requirements.txt -r docs/requirements.txt -python3 pkgutils.py build-assets -python3 pkgutils.py icons optional -deactivate +uv run pkgutils.py build-assets +uv run pkgutils.py icons optional echo "" echo " Building Linux Packages" echo "================================================================================" echo "" -python3 pkgutils.py build-deb --sign -python3 pkgutils.py build-ubuntu --sign +uv run pkgutils.py build-deb --sign +uv run pkgutils.py build-ubuntu --sign diff --git a/setup/requirements.txt b/setup/requirements.txt deleted file mode 100644 index 120a3f4f..00000000 --- a/setup/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools>=77.0.3 -twine diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 39f96e3c..00000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -coverage>=7.2.0 -pytest-cov -pytest-qt -pytest-timeout -pytest>=6.0.0 diff --git a/utils/build_debian.py b/utils/build_debian.py index 9ac0bd2e..892ee718 100644 --- a/utils/build_debian.py +++ b/utils/build_debian.py @@ -183,6 +183,7 @@ def launchpad(args: argparse.Namespace) -> None: ("24.04", "noble", True), ("25.04", "plucky", True), ("25.10", "questing", False), + ("26.04", "resolute", False), ] print("Building Ubuntu packages for:") diff --git a/utils/build_windows.py b/utils/build_windows.py index 523797da..debae0ef 100644 --- a/utils/build_windows.py +++ b/utils/build_windows.py @@ -30,7 +30,7 @@ import zipfile from pathlib import Path from utils.common import ( - ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile, + ROOT_DIR, SETUP_DIR, copySourceCode, extractReqs, extractVersion, readFile, removeRedundantQt, systemCall, writeFile ) @@ -45,7 +45,6 @@ def prepareCode(outDir: Path) -> None: files = [ ROOT_DIR / "CREDITS.md", ROOT_DIR / "LICENSE.md", - ROOT_DIR / "requirements.txt", SETUP_DIR / "iss_license.txt", SETUP_DIR / "windows" / "novelWriter.ico", SETUP_DIR / "windows" / "novelWriter.exe", @@ -85,7 +84,7 @@ def installRequirements(libDir: Path) -> None: """Install dependencies.""" print("Install dependencies ...") systemCall([ - sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "--target", libDir + sys.executable, "-m", "pip", "install", *extractReqs(["app"]), "--target", libDir ]) print("Done") print("") diff --git a/utils/common.py b/utils/common.py index 54528ba2..f5637285 100644 --- a/utils/common.py +++ b/utils/common.py @@ -23,6 +23,7 @@ from __future__ import annotations import shutil import subprocess import sys +import tomllib from pathlib import Path @@ -30,6 +31,18 @@ ROOT_DIR = Path(__file__).parent.parent SETUP_DIR = ROOT_DIR / "setup" +def extractReqs(groups: list[str]) -> list[str]: + """Extract dependency groups from pyproject.toml.""" + data = tomllib.loads((ROOT_DIR / "pyproject.toml").read_text(encoding="utf-8")) + reqs = [] + if "app" in groups or "all" in groups: + reqs += data["project"]["dependencies"] + for group in data["dependency-groups"]: + if group in groups or "all" in groups: + reqs += [d for d in data["dependency-groups"][group] if isinstance(d, str)] + return reqs + + def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]: """Extract the novelWriter version number without having to import anything else from the main package. @@ -92,14 +105,19 @@ def copySourceCode(dst: Path) -> None: def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None: """Copy files needed for packaging.""" - copyFiles = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt", "CREDITS.md", "pyproject.toml"] + copyFiles = [ + ROOT_DIR / "LICENSE.md", + SETUP_DIR / "LICENSE-Apache-2.0.txt", + ROOT_DIR / "CREDITS.md", + ROOT_DIR / "pyproject.toml", + ] for copyFile in copyFiles: - shutil.copyfile(copyFile, dst / copyFile) + shutil.copyfile(copyFile, dst / copyFile.name) print("Copied:", copyFile, flush=True) writeFile(dst / "MANIFEST.in", ( "include LICENSE.md\n" - "include setup/LICENSE-Apache-2.0.txt\n" + "include LICENSE-Apache-2.0.txt\n" "include CREDITS.md\n" "recursive-include novelwriter/assets *\n" ))