diff --git a/docs/source/technical/source.rst b/docs/source/technical/source.rst index 6b69b491..44750bf2 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,13 +45,22 @@ 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. + +If you run it from source, and want 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 +Otherwise you can run novelWriter with uv_: + +.. code-block:: bash + + uv run novelwriter + .. note:: On Linux distros, the Qt library is usually split up into multiple packages. In some cases, @@ -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..4eb8c244 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 run generate the requirements file and install 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/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/setup/macos/build.sh b/setup/macos/build.sh index d9e4f601..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,6 +108,7 @@ 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" # Leave conda env diff --git a/setup/make_pip.sh b/setup/make_pip.sh index 681a3cb5..f7e82c65 100755 --- a/setup/make_pip.sh +++ b/setup/make_pip.sh @@ -1,24 +1,11 @@ #!/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 "================================================================================" @@ -30,7 +17,7 @@ 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/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 723da500..0136cf47 100644 --- a/utils/common.py +++ b/utils/common.py @@ -26,10 +26,24 @@ import sys from pathlib import Path +import tomllib + ROOT_DIR = Path(__file__).parent.parent SETUP_DIR = ROOT_DIR / "setup" +def extractReqs(groups: list[str]) -> list[str]: + """Generate requirements.txt file 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.