diff --git a/.github/workflows/packagerbuddy.yaml b/.github/workflows/packagerbuddy.yaml index 29cc644..75c06f3 100644 --- a/.github/workflows/packagerbuddy.yaml +++ b/.github/workflows/packagerbuddy.yaml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.6] + python-version: [3.8, 3.11] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -15,10 +15,7 @@ jobs: - name: Install run: | - pip install -e .[dev] + make install-dev - name: Lint run: | - ./runcheck - - name: Test - run: | - ./runtests + make check diff --git a/.gitignore b/.gitignore index 1c29065..5fe434e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # files *.pyc .coverage +.DS_Store # directories .vscode/ @@ -8,3 +9,5 @@ __pycache__/ build/ dist/ *.egg-info/ +.env/ +.ruff_cache/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55476e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,92 @@ +.PHONY: install install-dev clean check format test + +## Install for production +install: + @echo ">> Installing dependencies" + python -m pip install --upgrade pip + python -m pip install -e . + +## Install for development +install-dev: install + python -m pip install -e ".[dev]" + +## Delete all temporary files +clean: + rm -rf .pytest_cache + rm -rf **/.pytest_cache + rm -rf __pycache__ + rm -rf **/__pycache__ + rm -rf build + rm -rf dist + +## Run checks +check: + ruff check . + black --check . + +## Format files using black +format: + ruff . --fix + black . + +test: + pytest -vv --disable-warnings --no-header --cov=packagerbuddy --cov-branch --cov-report=term-missing ./tests + +################################################################################# +# Self Documenting Commands # +################################################################################# + +.DEFAULT_GOAL := help + +# Inspired by +# sed script explained: +# /^##/: +# * save line in hold space +# * purge line +# * Loop: +# * append newline + line to hold space +# * go to next line +# * if line starts with doc comment, strip comment character off and loop +# * remove target prerequisites +# * append hold space (+ newline) to line +# * replace newline plus comments by `---` +# * print line +# Separate expressions are necessary because labels cannot be delimited by +# semicolon; see +.PHONY: help +help: + @echo "$$(tput bold)Available commands:$$(tput sgr0)" + @sed -n -e "/^## / { \ + h; \ + s/.*//; \ + :doc" \ + -e "H; \ + n; \ + s/^## //; \ + t doc" \ + -e "s/:.*//; \ + G; \ + s/\\n## /---/; \ + s/\\n/ /g; \ + p; \ + }" ${MAKEFILE_LIST} \ + | awk -F '---' \ + -v ncol=$$(tput cols) \ + -v indent=19 \ + -v col_on="$$(tput setaf 6)" \ + -v col_off="$$(tput sgr0)" \ + '{ \ + printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ + n = split($$2, words, " "); \ + line_length = ncol - indent; \ + for (i = 1; i <= n; i++) { \ + line_length -= length(words[i]) + 1; \ + if (line_length <= 0) { \ + line_length = ncol - indent - length(words[i]) - 1; \ + printf "\n%*s ", -indent, " "; \ + } \ + printf "%s ", words[i]; \ + } \ + printf "\n"; \ + }' \ + | more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars') diff --git a/README.md b/README.md index 777a5db..5044327 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # PackagerBuddy - -![](https://github.com/cedricduriau/packagerbuddy/workflows/Build/badge.svg?branch=master) -[![Platform](https://img.shields.io/badge/Platform-linux--64-lightgrey.svg)](https://img.shields.io/badge/Platform-linux--64-lightgrey.svg) -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Python](https://img.shields.io/badge/Python-2.7%20|%203.6-blue.svg)](https://img.shields.io/badge/Python-2.7%20|%203.6-blue.svg) +[![platform](https://img.shields.io/badge/platform-linux--x64-lightgrey.svg)](https://img.shields.io/badge/platform-linux--x64-lightgrey.svg) +[![platform](https://img.shields.io/badge/platform-darwin--arm64-lightgrey.svg)](https://img.shields.io/badge/platform-darwin--arm64-lightgrey.svg) +[![license: GPL v3](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://img.shields.io/badge/Python-3.8+-blue.svg) +[![coverage](https://img.shields.io/badge/coverage-90%25-brightgreen.svg)](https://img.shields.io/badge/coverage-90%25-brightgreen) ## Overview -PackagerBuddy is a JSON config based software packager written entirely in Python for Linux. +PackagerBuddy is a JSON config based software packager written entirely in Python. Use Cases @@ -20,39 +20,51 @@ of itself. If you wish to install the current master, use the following command: -`pip install git+git://github.com/cedricduriau/packagerbuddy.git` +```sh +# latest master +pip install git+git://github.com/cedricduriau/packagerbuddy.git -Or a specific release version: +# specific version +pip install git+git://github.com/cedricduriau/packagerbuddy.git@{RELEASE} +``` -`pip install git+git://github.com/cedricduriau/packagerbuddy.git@{RELEASE}` +## Usage +### Setup -This will create all default directories and copy the default configuration file that ships with the repository. (see [Configure](#Configure)) +The setup command will create all directories required to function. By default these are installed in the user home directory. To change the default location, see see [Configure](#Configure). -## Usage +```sh +packagerbuddy setup +``` ### Add software The add command requires two arguments. The `software` argument used as alias to interact with, and the `url` argument which needs to be an url containing a version placeholder. -``` -packagerbuddy add --software codium --url https://github.com/VSCodium/vscodium/releases/download/{version}/VSCodium-linux-x64-{version}.tar.gz +```sh +packagerbuddy add --software codium --url https://github.com/VSCodium/vscodium/releases/download/{version}/VSCodium-darwin-arm64-{version}.zip ``` ### Remove software The remove command requires a single argument, the `software` argument, which needs to match an already added software. To list the available software packages, see `avail` command below. -``` +```sh packagerbuddy remove --software codium ``` -### Install software -The `install` command requires two arguments. The `software` argument which needs to match an alias in the software config and the `version` argument which needs to form an existing download url. If the requested software version has already been installed, the install will stop. If you wish to force an install -again, the `force` flag covers this feature. +### List available software to install +The `avail` command prints all software names that are present in the config, supported by PackgerBuddy. +```sh +packagerbuddy avail ``` -packagerbuddy install --software codium --version 1.44.0 -packagerbuddy install --software codium --version 1.44.0 --force + +### Install software +The `install` command requires two arguments. The `software` argument which needs to match an alias in the software config and the `version` argument which needs to form an existing download url. If the requested software version has already been installed, the install will stop. + +```sh +packagerbuddy install --software codium --version 1.85.2.2401 ``` Installing consists of five steps: @@ -60,19 +72,13 @@ Installing consists of five steps: 1. Download the software from the url in the configs to the designated download directory. 2. Unpack the downloaded content. 3. Install/move the unpacked content to the designated install directory. -4. Create a package file inside the installed directory. -5. Run the post install script from the designated scripts directory. +4. Run the post install script from the designated scripts directory. ### List installed software The `list` command prints all installed software. PackagerBuddy knows the difference between ordinary directories and software it installed thanks to a package file which is written out at install time. -``` -packagerbuddy list -``` -### List software available to install -The `avail` command prints all software names that are present in the config, supported by PackgerBuddy. -``` -packagerbuddy avail +```sh +packagerbuddy list ``` ### Uninstalling @@ -80,16 +86,12 @@ The `uninstall` command, well, does exactly that. It checks if the given softwar The `version` argument is optional. If it is passed, only given version will be removed. If it is not passed, **all** versions will be uninstalled of given software. Beware of this feature. -If you're the kind of person that prepares a batch of commands before running them and then feed those to a terminal, there is also a `--dry-run` flag for you to enjoy. -``` +```sh # uninstall all versions packagerbuddy uninstall --software codium # uninstall specific version -packagerbuddy uninstall --software codium --version 1.44.0 - -# dry run, execute without removing -packagerbuddy uninstall --software codium --dry-run +packagerbuddy uninstall --software codium --version 1.85.2.2401 ``` ## Configure @@ -99,19 +101,16 @@ packagerbuddy uninstall --software codium --dry-run * `PB_CONFIG` : Path of the software config. * default: custom file in the user home. (`~/.packagerbuddy/config/software.json`) * `PB_DOWNLOAD` : Directory the software will be downloaded to. - * default: custom directory in the user home. (`~/.packagerbuddy/source`) + * default: custom directory in the user home. (`~/.packagerbuddy/downloaded`) * `PB_INSTALL`: Directory the software will be installed in. * default: custom directory in the user home. (`~/.packagerbuddy/installed`) * `PB_SCRIPTS`: Directory of the post install scripts. * default: custom directory in the user home. (`~/.packagerbuddy/scripts`) - ### Examples If you want to try out the example shipping with the repository, run following commands from the root of this repo: -* `cp -R ./examples/* ~/.packagerbuddy/` - -This will allow you to install three software packages. One of them is `code` (Visual Studio Code), which has a post install script. -Check out the `code` post install script for its inner working. -Check out the [vscode release notes](https://code.visualstudio.com/updates) for a valid version number to install. +```sh +cp -R ./darwin-arm64/examples/* ~/.packagerbuddy/ +``` diff --git a/bin/packagerbuddy b/bin/packagerbuddy deleted file mode 100755 index 6202047..0000000 --- a/bin/packagerbuddy +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python - -# stdlib modules -from __future__ import absolute_import -import os -import sys -import argparse - -# tool modules -from packagerbuddy import packagerbuddy - - -def _build_parser(): - """ - Builds the command line interface. - - :rtype: argparse.ArgumentParser - """ - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers() - - # ========================================================================= - # add - # ========================================================================= - help = "add a software configuration" - parser_add = subparsers.add_parser("add", help=help) - parser_add.set_defaults(action="add") - - # required arguments - req_args = parser_add.add_argument_group("required arguments") - - help = "name of the software" - req_args.add_argument("-s", "--software", help=help) - - help = "download url template" - req_args.add_argument("-u", "--url", help=help) - - # ========================================================================= - # remove - # ========================================================================= - help = "remove a software configuration" - parser_remove = subparsers.add_parser("remove", help=help) - parser_remove.set_defaults(action="remove") - - # required arguments - req_args = parser_remove.add_argument_group("required arguments") - req_args.add_argument("-s", "--software", help="name of the software") - - # ========================================================================= - # install - # ========================================================================= - help = "installs a software version" - parser_install = subparsers.add_parser("install", help=help) - parser_install.set_defaults(action="install") - - # required arguments - req_args = parser_install.add_argument_group("required arguments") - - help = "name of the software to install" - req_args.add_argument("-s", "--software", required=True, help=help) - - help = "version of the software to install" - req_args.add_argument("-v", "--version", required=True, help=help) - - help = "force the install procedure again" - parser_install.add_argument("-f", "--force", action="store_true", help=help) - - # ========================================================================= - # list - # ========================================================================= - help = "shows all installed software versions" - parser_list = subparsers.add_parser("list", help=help) - parser_list.set_defaults(action="list") - - # ========================================================================= - # avail - # ========================================================================= - help = "shows all available software packages to install" - parser_avail = subparsers.add_parser("avail", help=help) - parser_avail.set_defaults(action="avail") - - # ========================================================================= - # uninstall - # ========================================================================= - help = "uninstall one or all versions of a software" - parser_uninstall = subparsers.add_parser("uninstall", help=help) - parser_uninstall.set_defaults(action="uninstall") - - # required arguments - req_args = parser_uninstall.add_argument_group("required arguments") - - help = "name of the software to uninstall" - req_args.add_argument("-s", "--software", required=True, help=help) - - # optional arguments - help = "version of the version to uninstall" - parser_uninstall.add_argument("-v", "--version", required=False, help=help) - - help = "perform a trial run with no changes made" - parser_uninstall.add_argument("--dry-run", action="store_true", help=help) - - # ========================================================================= - # parser settings - # ========================================================================= - # usage - usages = [] - usages.append(parser_install.format_usage()) - usages.append(parser_list.format_usage()) - usages.append(parser_avail.format_usage()) - usages.append(parser_uninstall.format_usage()) - - usage = os.path.basename(__file__) + " [COMMAND] [OPTIONS]\n" - parser.usage = usage + "".join(usages) - - # description - parser.description = "JSON config based software packager." - - return parser - - -if __name__ == "__main__": - parser = _build_parser() - namespace = parser.parse_args() - - try: - action = namespace.action - except AttributeError: - action = None - print("No action provided, see actions using -h/--help.") - sys.exit() - - if action == "install": - packagerbuddy.install(namespace.software, - namespace.version, - force=namespace.force) - elif action == "list": - msg = "No installed software found." - installed = packagerbuddy.get_installed_software() - if installed: - msg = "\n".join(["- " + i for i in installed]) - print(msg) - elif action == "avail": - msg = "No software configs available." - - try: - config = packagerbuddy.get_config() - names = sorted(config.keys()) - msg = "\n".join(["- " + k for k in names]) - except Exception: - pass - - print(msg) - elif action == "uninstall": - packagerbuddy.uninstall(namespace.software, - version=namespace.version, - dry_run=namespace.dry_run) - elif action == "add": - packagerbuddy.add_software(namespace.software, namespace.url) - elif action == "remove": - packagerbuddy.remove_software(namespace.software) diff --git a/examples/darwin-arm64/config/software.json b/examples/darwin-arm64/config/software.json new file mode 100644 index 0000000..23fb4d5 --- /dev/null +++ b/examples/darwin-arm64/config/software.json @@ -0,0 +1,3 @@ +{ + "codium": "https://github.com/VSCodium/vscodium/releases/download/{version}/VSCodium-darwin-arm64-{version}.zip" +} \ No newline at end of file diff --git a/examples/darwin-arm64/scripts/codium b/examples/darwin-arm64/scripts/codium new file mode 100755 index 0000000..a435089 --- /dev/null +++ b/examples/darwin-arm64/scripts/codium @@ -0,0 +1,54 @@ +#!/usr/bin/env sh + +# ============================================================================== +# Visual Studio Codium post install script +# ============================================================================== +# This script will do the following: +# 1. create a symlink of the install dir without a version number in the name +# 2. create a bin directory inside your user home +# 3. create a symlink of the code executable inside your user home bin dir called "vscode" +# +# WARNING: Any existing symlink will be remove to relink with current version of the software. +# ============================================================================== + +# ============================================================================== +# arguments passed by packagerbuddy +# ============================================================================== +software="$1" +version="$2" + +# ============================================================================== +# create version-less symlink +# ============================================================================== +link="$(dirname "$PWD")"/$1 + +# remove existing link +if [ \( -e $link \) ] +then + rm $link +fi + +ln -s $PWD $link + +# ============================================================================== +# ensure user home bin exists +# ============================================================================== +usr_bin=~/bin + +if [ ! \( -d $usr_bin \) ] +then + mkdir $usr_bin +fi + +# ============================================================================== +# create symlink to user home bin +# ============================================================================== +src_exec=$link/$software +dst_exec=$usr_bin/$software + +if [ \( -e $dst_exec \) ] +then + rm $dst_exec +fi + +ln -s $src_exec $dst_exec diff --git a/examples/config/software.json b/examples/linux-x64/config/software.json similarity index 99% rename from examples/config/software.json rename to examples/linux-x64/config/software.json index a78fd67..f58e1ec 100644 --- a/examples/config/software.json +++ b/examples/linux-x64/config/software.json @@ -3,4 +3,4 @@ "pycharm-community": "https://download.jetbrains.com/python/pycharm-community-{version}.tar.gz", "code": "https://vscode-update.azurewebsites.net/{version}/linux-x64/stable", "codium": "https://github.com/VSCodium/vscodium/releases/download/{version}/VSCodium-linux-x64-{version}.tar.gz" -} +} \ No newline at end of file diff --git a/examples/scripts/code b/examples/linux-x64/scripts/code similarity index 98% rename from examples/scripts/code rename to examples/linux-x64/scripts/code index 428e33a..a553f66 100755 --- a/examples/scripts/code +++ b/examples/linux-x64/scripts/code @@ -51,4 +51,4 @@ then rm $dst_exec fi -ln -s $src_exec $dst_exec +ln -s $src_exec $dst_exec \ No newline at end of file diff --git a/examples/scripts/codium b/examples/linux-x64/scripts/codium similarity index 100% rename from examples/scripts/codium rename to examples/linux-x64/scripts/codium diff --git a/examples/scripts/modules b/examples/linux-x64/scripts/modules similarity index 98% rename from examples/scripts/modules rename to examples/linux-x64/scripts/modules index 07e9ca6..9c098e2 100755 --- a/examples/scripts/modules +++ b/examples/linux-x64/scripts/modules @@ -63,4 +63,4 @@ then sudo rm $dst_module_csh fi -sudo ln -s $src_module_csh $dst_module_csh +sudo ln -s $src_module_csh $dst_module_csh \ No newline at end of file diff --git a/packagerbuddy.module b/packagerbuddy.module deleted file mode 100644 index 4a82bcc..0000000 --- a/packagerbuddy.module +++ /dev/null @@ -1,8 +0,0 @@ -#%Module1.0 - -set currentdir [file dirname $ModulesCurrentModulefile] -conflict packagerbuddy - -prepend-path PATH $currentdir/bin -prepend-path PYTHONPATH $currentdir/python -setenv PB_CONFIG $currentdir/examples/config/software.json diff --git a/tests/test_packagerbuddy/__init__.py b/packagerbuddy/__init__.py similarity index 100% rename from tests/test_packagerbuddy/__init__.py rename to packagerbuddy/__init__.py diff --git a/packagerbuddy/cli.py b/packagerbuddy/cli.py new file mode 100755 index 0000000..48eacce --- /dev/null +++ b/packagerbuddy/cli.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python + +# stdlib +import argparse +import os + +# package +from packagerbuddy import configutils, downloadutils, installutils, scriptutils, settings + + +# ============================================================================== +# actions +# ============================================================================== +def setup(): + dirs = [ + settings.DIR_CONFIG, + settings.DIR_DOWNLOAD, + settings.DIR_INSTALL, + settings.DIR_SCRIPTS, + ] + for d in dirs: + if not os.path.exists(d): + print(f"creating directory {d}") + os.makedirs(d) + + if not os.path.exists(settings.FILE_CONFIG): + print(f"creating file {settings.FILE_CONFIG}") + configutils.dump({}) + + +def list_available_software() -> None: + config = configutils.load() + available = sorted(config.keys()) + print("\n".join(available)) + + +def add_software(software: str, url: str) -> None: + if not software.strip(): + print("no software provided") + exit(1) + + if not url.strip(): + print("no url provided") + exit(1) + + config = configutils.load() + + if configutils.is_software_configured(config, software): + print("software already configured") + exit(1) + + if r"{version}" not in url: + print(r"no {version} format string found in url") + exit(1) + + configutils.add_software(config, software, url) + + +def remove_software(software: str) -> None: + if not software.strip(): + print("no software provided") + exit(1) + + config = configutils.load() + if not configutils.is_software_configured(config, software): + print("software not found") + exit(1) + + configutils.remove_software(config, software) + + +def download_software(software: str, version: str) -> None: + if not software.strip(): + print("no software provided") + exit(1) + + config = configutils.load() + if not configutils.is_software_configured(config, software): + print("software not found") + exit(1) + + archive = downloadutils.find_archive(software, version) + if archive is not None: + print(archive) + return + + archive = downloadutils.download(software, version, config) + print(archive) + + +def install_software(software: str, version: str) -> None: + if not software.strip(): + print("no software provided") + exit(1) + + config = configutils.load() + if not configutils.is_software_configured(config, software): + print("software not found") + exit(1) + + if installutils.is_software_installed(software, version): + dir_install = installutils.build_install_path(software, version) + print(dir_install) + return + + archive = downloadutils.find_archive(software, version) + if archive is None: + archive = downloadutils.download(software, version, config) + + dir_temp = installutils.build_temporary_install_path(software, version) + os.makedirs(dir_temp, exist_ok=True) + + installutils.unarchive(archive, dir_temp) + + dir_install = installutils.build_install_path(software, version) + os.makedirs(dir_install, exist_ok=True) + + installutils.cleanup(config, software, version) + + scripts = scriptutils.find_scripts(software, version) + for script in scripts: + scriptutils.run_script(script, software, version, wd=dir_install) + + print(dir_install) + + +def uninstall_software(software: str, version: str | None = None) -> None: + if not software.strip(): + print("no software provided") + exit(1) + + installed_dirs = installutils.find_installed_software(software=software, version=version) + for installed in installed_dirs: + installutils.uninstall_software(installed) + print(installed) + + +def list_installed_software(software: str | None = None, version: str | None = None) -> None: + installed = installutils.find_installed_software(software=software, version=version) + print("\n".join(installed)) + + +# ============================================================================== +# parser +# ============================================================================== +def build_parser(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + + # ========================================================================== + # setup + # ========================================================================== + help = "set up package content" + parser_setup = subparsers.add_parser("setup", help=help) + parser_setup.set_defaults(func=setup) + + # ========================================================================== + # add + # ========================================================================== + help = "add a software configuration" + parser_add = subparsers.add_parser("add", help=help) + parser_add.set_defaults(func=add_software) + + # required arguments + req_args = parser_add.add_argument_group("required arguments") + + help = "name of the software" + req_args.add_argument("-s", "--software", help=help) + + help = "download url template" + req_args.add_argument("-u", "--url", help=help) + + # ========================================================================== + # remove + # ========================================================================== + help = "remove a software configuration" + parser_remove = subparsers.add_parser("remove", help=help) + parser_remove.set_defaults(func=remove_software) + + # required arguments + req_args = parser_remove.add_argument_group("required arguments") + req_args.add_argument("-s", "--software", help="name of the software") + + # ========================================================================== + # avail + # ========================================================================== + help = "list available software to download" + parser_avail = subparsers.add_parser("avail", help=help) + parser_avail.set_defaults(func=list_available_software) + + # ========================================================================== + # download + # ========================================================================== + help = "download software" + parser_download = subparsers.add_parser("download", help=help) + parser_download.set_defaults(func=download_software) + + # required arguments + req_args = parser_download.add_argument_group("required arguments") + + help = "name of the software" + req_args.add_argument("-s", "--software", help=help) + + help = "version of the software" + req_args.add_argument("-v", "--version", help=help) + + # ========================================================================== + # install + # ========================================================================== + help = "install software" + parser_install = subparsers.add_parser("install", help=help) + parser_install.set_defaults(func=install_software) + + # required arguments + req_args = parser_install.add_argument_group("required arguments") + + help = "name of the software" + req_args.add_argument("-s", "--software", help=help) + + help = "version of the software" + req_args.add_argument("-v", "--version", help=help) + + # ========================================================================== + # uninstall + # ========================================================================== + help = "uninstall software" + parser_uninstall = subparsers.add_parser("uninstall", help=help) + parser_uninstall.set_defaults(func=uninstall_software) + + # required arguments + req_args = parser_uninstall.add_argument_group("required arguments") + + help = "name of the software" + req_args.add_argument("-s", "--software", help=help) + + # optional arguments + opt_args = parser_uninstall.add_argument_group("optional arguments") + + help = "version of the software" + opt_args.add_argument("-v", "--version", help=help, required=False) + + # ========================================================================== + # list + # ========================================================================== + help = "list installed software" + parser_list = subparsers.add_parser("list", help=help) + parser_list.set_defaults(func=list_installed_software) + + # optional arguments + opt_args = parser_list.add_argument_group("optional arguments") + + help = "name of the software" + opt_args.add_argument("-s", "--software", help=help, required=False) + + help = "version of the software" + opt_args.add_argument("-v", "--version", help=help, required=False) + + # ========================================================================== + # parser settings + # ========================================================================== + parser.description = "JSON config based software packager." + + return parser + + +def run(args: list[str] | None = None) -> None: + parser = build_parser() + namespace = parser.parse_args(args) + kwargs = vars(namespace) + + try: + func = kwargs.pop("func") + except KeyError: + print("packagerbuddy: error: missing action, see -h/--help") + exit(2) + + func(**kwargs) + exit(0) diff --git a/packagerbuddy/configutils.py b/packagerbuddy/configutils.py new file mode 100644 index 0000000..7546bbc --- /dev/null +++ b/packagerbuddy/configutils.py @@ -0,0 +1,29 @@ +# stdlib +import json + +# package +from packagerbuddy import settings + + +def load() -> dict[str, str]: + with open(settings.FILE_CONFIG, "r") as fp: + return json.load(fp) + + +def dump(config: dict[str, str]) -> None: + with open(settings.FILE_CONFIG, "w") as fp: + json.dump(config, fp, indent=True, sort_keys=True) + + +def is_software_configured(config: dict, software: str) -> bool: + return software in config + + +def add_software(config: dict[str, str], software: str, url: str) -> None: + config[software] = url + dump(config) + + +def remove_software(config: dict[str, str], software: str) -> None: + config.pop(software) + dump(config) diff --git a/packagerbuddy/downloadutils.py b/packagerbuddy/downloadutils.py new file mode 100644 index 0000000..923d73e --- /dev/null +++ b/packagerbuddy/downloadutils.py @@ -0,0 +1,33 @@ +# stdlib +import glob +import os +from urllib.request import urlopen + +# package +from packagerbuddy import pathutils, settings + + +def build_archive_path(software: str, version: str, url: str) -> str: + _, ext = pathutils.split_ext(url) + basename = "-".join([software, version]) + ext + dir_archive = os.path.join(settings.DIR_DOWNLOAD, basename) + return dir_archive + + +def find_archive(software: str, version: str) -> str | None: + result = glob.glob(f"{software}-{version}*", root_dir=settings.DIR_DOWNLOAD) + if result: + return os.path.join(settings.DIR_DOWNLOAD, result[0]) + return None + + +def download(software: str, version: str, config: dict[str, str]) -> str: + template = config[software] + url = template.format(version=version) + archive = build_archive_path(software, version, url) + + result = urlopen(url) + with open(archive, "wb+") as fp: + fp.write(result.read()) + + return archive diff --git a/packagerbuddy/installutils.py b/packagerbuddy/installutils.py new file mode 100644 index 0000000..9234dd9 --- /dev/null +++ b/packagerbuddy/installutils.py @@ -0,0 +1,86 @@ +# stdlib +import glob +import os +import shutil +import tarfile +import zipfile +from typing import Callable + +# package +from packagerbuddy import pathutils, settings + + +def build_temporary_install_path(software: str, version: str) -> str: + basename = f"tmp-{software}-{version}" + path = os.path.join(settings.DIR_INSTALL, basename) + return path + + +def build_install_path(software: str, version: str) -> str: + path = build_temporary_install_path(software, version) + path = path.replace("tmp-", "") + return path + + +def is_software_installed(software: str, version: str) -> bool: + path = build_install_path(software, version) + exists = os.path.exists(path) + return exists + + +def get_archive_name(software: str, version: str, config: dict[str, str]) -> str: + template = config[software] + url = template.format(version=version) + basename = os.path.basename(url) + _, ext = pathutils.split_ext(basename) + archive_name = basename.replace(ext, "") + return archive_name + + +def unzip(archive: str, target: str) -> None: + with zipfile.ZipFile(archive, "r") as fp: + fp.extractall(target) + + +def untar(archive: str, target: str) -> None: + read_mode = "r" + if archive.endswith(".tgz") or archive.endswith(".tar.gz"): + read_mode += ":gz" + elif archive.endswith("tar.bz2"): + read_mode += ":bz2" + + with tarfile.open(archive, read_mode) as tar: + tar.extractall(path=target) + + +def unarchive(archive: str, target: str) -> None: + _, ext = pathutils.split_ext(archive) + map_extension_func: dict[str, Callable] = { + ".zip": unzip, + ".tar.gz": untar, + } + func = map_extension_func[ext] + func(archive, target) + + +def cleanup(config: dict[str, str], software: str, version: str) -> None: + dir_temp = build_temporary_install_path(software, version) + dir_install = build_install_path(software, version) + archive_name = get_archive_name(software, version, config) + contents = os.listdir(dir_temp) + if len(contents) == 1 and contents[0] == archive_name: + shutil.copytree(os.path.join(dir_temp, archive_name), dir_install, dirs_exist_ok=True) + shutil.rmtree(dir_temp) + else: + os.rename(dir_temp, dir_install) + + +def find_installed_software(software: str | None = None, version: str | None = None) -> list[str]: + glob_path = build_install_path(software or "*", version or "*") + result = glob.glob(glob_path) + result.sort() + return result + + +def uninstall_software(dir_install: str) -> None: + shutil.rmtree(dir_install) diff --git a/packagerbuddy/pathutils.py b/packagerbuddy/pathutils.py new file mode 100644 index 0000000..68e7a6e --- /dev/null +++ b/packagerbuddy/pathutils.py @@ -0,0 +1,10 @@ +def split_ext(path: str) -> tuple[str, str]: + ext: str = "" + supported_extensions = [".tar.gz", ".tar.bz", ".tar", ".zip"] + for supported_extension in supported_extensions: + if path.endswith(supported_extension): + ext = supported_extension + break + + root = path.replace(ext, "") + return root, ext diff --git a/packagerbuddy/scriptutils.py b/packagerbuddy/scriptutils.py new file mode 100644 index 0000000..c4e2524 --- /dev/null +++ b/packagerbuddy/scriptutils.py @@ -0,0 +1,37 @@ +# stdlib +import glob +import os +import subprocess + +# package +from packagerbuddy import settings + + +def find_scripts(software: str, version: str) -> list[str]: + scripts: list[str] = [] + + version_agnostic = os.path.join(settings.DIR_SCRIPTS, software) + if os.path.exists(version_agnostic): + scripts.append(version_agnostic) + + results = glob.glob(f"{software}-{version}*", root_dir=settings.DIR_SCRIPTS) + for result in results: + path = os.path.join(settings.DIR_SCRIPTS, result) + scripts.append(path) + + scripts.sort() + return scripts + + +def run_script(script: str, software: str, version: str, wd: str | None = None) -> tuple[str, str]: + args = [script, software, version] + cmd = " ".join(args) + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + cwd=wd, + ) + stdout, stderr = process.communicate() + return stdout, stderr diff --git a/packagerbuddy/settings.py b/packagerbuddy/settings.py new file mode 100644 index 0000000..4483cde --- /dev/null +++ b/packagerbuddy/settings.py @@ -0,0 +1,15 @@ +# stdlib +import os + + +def normalize_path(path: str) -> str: + return os.path.abspath(os.path.expanduser(path)) + + +DIR_PACKAGE = normalize_path("~/.packagerbuddy") +DIR_CONFIG = os.path.join(DIR_PACKAGE, "config") +DIR_DOWNLOAD = normalize_path(os.getenv("PB_DOWNLOAD", os.path.join(DIR_PACKAGE, "downloaded"))) +DIR_INSTALL = normalize_path(os.getenv("PB_INSTALL", os.path.join(DIR_PACKAGE, "installed"))) +DIR_SCRIPTS = normalize_path(os.getenv("PB_SCRIPTS", os.path.join(DIR_PACKAGE, "scripts"))) +FILE_CONFIG = normalize_path(os.getenv("PB_CONFIG", os.path.join(DIR_CONFIG, "software.json"))) +EXTENSIONS: set[str] = {".tgz", ".tar", ".tar.gz", ".tar.bz2"} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a00549 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "packagerbuddy" +version = "2.0.0" +authors = [ + { name="Cedric Duriau", email="duriau.cedric@live.be" }, +] +readme = "README.md" +license-files = { paths = ["LICENSE.md"] } +requires-python = ">=3.8" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "ruff>=0.0.260", + "black>=23.3.0", + "pip-tools>=6.12.0", + "pytest>=7.3.1", + "pytest-cov>=4.0.0", +] + +[project.scripts] +packagerbuddy = "packagerbuddy.cli:run" + +[tool.black] +line-length = 120 +extend-exclude = ''' +/( + .env +)/ +''' + +[tool.ruff] +ignore = ["E501"] +select = ["E", "F", "I", "W"] +line-length = 120 +fixable = ["I"] +exclude = [".env"] + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.coverage.paths] +source = ["packagerbuddy"] + +[tool.coverage.run] +branch = true +relative_files = true + +[tool.coverage.report] +show_missing = true diff --git a/python/packagerbuddy/__init__.py b/python/packagerbuddy/__init__.py deleted file mode 100644 index 9c73af2..0000000 --- a/python/packagerbuddy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.3.1" diff --git a/python/packagerbuddy/packagerbuddy.py b/python/packagerbuddy/packagerbuddy.py deleted file mode 100644 index fbcf19c..0000000 --- a/python/packagerbuddy/packagerbuddy.py +++ /dev/null @@ -1,616 +0,0 @@ -# stdlib modules -from __future__ import absolute_import -import os -import json -import shutil -import tarfile -import subprocess - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - - -# ============================================================================= -# private -# ============================================================================= -def _normalize_path(path): - """ - Returns an absolute version of a path. - - :param path: path to normalize - :type path: str - - :rtype: str - """ - return os.path.abspath(os.path.expanduser(path)) - - -def _get_filename_from_request(request): - """ - Gets the filename from an url request. - - :param request: url request to get filename from - :type request: urllib.requests.Request or urllib2.Request - - :rtype: str - """ - try: - headers = request.headers - content = headers["content-disposition"] - filename_str = content.split("filename=")[1] - return filename_str.strip("\"") - except (KeyError, AttributeError): - return os.path.basename(request.url) - - -def _download(url, directory): - """ - Downloads the content of an URL to a location. - - :param url: url to from - :type url: str - - :param directory: directory to download to - :type directory: str - - :return: full path of downloaded archive - :rtype: str - """ - # get request from url - request = urlopen(url) - - # get path to download - archive_name = _get_filename_from_request(request) - archive_path = os.path.join(directory, archive_name) - - # download - with open(archive_path, "wb+") as fp: - fp.write(request.read()) - - return archive_path - - -def _build_archive_name(software, version, extension): - """ - Builds the name of an archive file for a software release. - - :param software: software to build archive file name for - :type software: str - - :param version: release of software to build archive file name for - :type version: str - - :param extension: extension of the archive file - :type extension: str - - :rtype: str - """ - return "{}-{}{}".format(software, version, extension) - - -def _get_tar_read_mode(tar_file): - """ - Get read mode for tar file. - - :param tar_file: path of tar file to get read mode for - :type tar_file: str - - :rtype: str - """ - # default non-compressed tar - read_mode = "r" - - # add suffix based on compression extension - if tar_file.endswith(".tgz") or tar_file.endswith(".tar.gz"): - read_mode += ":gz" - elif tar_file.endswith("tar.bz2"): - read_mode += ":bz2" - - return read_mode - - -def _untar(archive): - """ - Unpacks a tarfile. - - Contents of the tarfile will be extracted right now to the archive itself. - - :param archive: full path of tarfile to unpack - :type archive: str - - :return: the path of the extracted content - :rtype: str - """ - # get tar read mode - read_mode = _get_tar_read_mode(archive) - - # https://docs.python.org/2.7/library/tarfile.html#tarfile.open - directory = os.path.dirname(archive) - with tarfile.open(archive, read_mode) as tar: - extract_dir = directory - archive_subdir = tar.getnames()[0] - - # if archive does not contain single subdir, extract into one named - # after the basename of the archive - if archive_subdir == ".": - basename, _ext = _split_ext(os.path.basename(archive)) - archive_subdir = basename - extract_dir = os.path.join(directory, archive_subdir) - - # extract - tar.extractall(path=extract_dir) - path = os.path.join(directory, archive_subdir) - return path - - -def _build_download_url(template, version): - """ - Builds the software release download url. - - :param template: url template - :type template: str - - :param version: software release - :type version: str - """ - return template.format(version=version) - - -def _get_archive(software, version): - """ - Gets the downloaded source archive for a software version. - - :param software: software to get the downloaded source archive for - :type software: str - - :param version: software release - :type version: str - """ - download_dir = get_download_location() - archives = os.listdir(download_dir) - prefix = "{}-{}.".format(software, version) - - for archive in archives: - if archive.startswith(prefix): - return os.path.join(download_dir, archive) - - return None - - -def _split_ext(path): - """ - Splits a path from its extension. - - :param path: path to split - :type path: str - - :return: path excluding extension and extension - :rtype: str, str - """ - # dump extra header data - if "&" in path: - path = path.split("&")[0] - - # assume non .tar extensions do not have any suffix/compression - ext = None - - if ".tar" not in path: - path_noext, ext = os.path.splitext(path) - else: - for i in {".tar.gz", ".tar.bz2", ".tar"}: - if path.endswith(i): - path_noext, ext = path.rstrip(i), i - break - - if not ext: - msg = "could not retrieve extension from path: {}" - raise ValueError(msg.format(path)) - - return path_noext, ext - - -# ============================================================================= -# public -# ============================================================================= -def get_config_location(): - """ - Returns the full path of the software config. - - :rtype: str - """ - default = "~/.packagerbuddy/config/software.json" - path_config = os.getenv("PB_CONFIG", default) - return _normalize_path(path_config) - - -def get_download_location(): - """ - Returns the location the software will be downloaded to. - - :rtype: str - """ - dir_download = os.getenv("PB_DOWNLOAD", "~/.packagerbuddy/source/") - return _normalize_path(dir_download) - - -def get_install_location(): - """ - Returns the location the software will be installed in. - - :rtype: str - """ - dir_install = os.getenv("PB_INSTALL", "~/.packagerbuddy/installed/") - return _normalize_path(dir_install) - - -def get_scripts_location(): - """ - Returns the location of the post install scripts. - - :rtype: str - """ - dir_scripts = os.getenv("PB_SCRIPTS", "~/.packagerbuddy/scripts/") - return _normalize_path(dir_scripts) - - -def get_config(): - """ - Returns the software config. - - :rtype: dict - """ - with open(get_config_location(), "r") as fp: - return json.load(fp) - - -def install(software, version, force=False): - """ - Installs a specific release of a software. - - :param software: software to install - :type software: str - - :param version: release of software to install - :type version: str - - :param force: set True to force the install procedure again - :type force: bool - """ - if is_software_installed(software, version) and not force: - print("{} v{} is already installed".format(software, version)) - return - - # get config - config = get_config() - - # validate config for software and version - validate_config(config, software, version) - - # download - print("downloading ...") - download_dir = get_download_location() - url = config[software].format(version=version) - - archive_path = _get_archive(software, version) - if archive_path is None: - # download - source = _download(url, download_dir) - - # rename - try: - _, extension = _split_ext(url) - except ValueError: - _, extension = _split_ext(source) - validate_extension(extension) - - archive_name = _build_archive_name(software, version, extension) - archive_path = os.path.join(download_dir, archive_name) - - if os.path.basename(source) != archive_name: - os.rename(source, archive_path) - else: - extension = _split_ext(archive_path)[1] - archive_name = os.path.basename(archive_path) - - # extract - print("extracting ...") - target_name = archive_name.replace(extension, "").rstrip(".") - target_path = os.path.join(download_dir, target_name) - if not os.path.exists(target_path): - # extract - unpacked_dir = _untar(archive_path) - - # rename - if not os.path.exists(target_path): - os.rename(unpacked_dir, target_path) - - # install / move - print("installing ...") - install_dir = get_install_location() - - install_path = os.path.join(install_dir, os.path.basename(target_path)) - if not os.path.exists(install_path): - shutil.move(target_path, install_path) - - # create .pbsoftware file - cache_file = os.path.join(install_path, ".pbsoftware") - open(cache_file, "w+").close() - - # run post install script - script = get_script(software) - if script: - run_script(script, software, version, wd=install_path) - - -def is_software_installed(software, version): - """ - Returns whether a sofware release is already installed. - - :param software: software to check if it is already installed - :type software: str - - :param version: release of the software to check if it is already installed - :type version: str - - :rtype: bool - """ - target_name = "-".join([software, version]) - install_dir = get_install_location() - cache_file = os.path.join(install_dir, target_name, ".pbsoftware") - return os.path.exists(cache_file) - - -def get_installed_software(): - """ - Gets the paths of the installed software releases. - - :rtype: list[str] - """ - install_dir = get_install_location() - software = [] - for dname in os.listdir(install_dir): - path = os.path.join(install_dir, dname) - pb_package_file = os.path.join(path, ".pbsoftware") - if not os.path.islink(path) and os.path.exists(pb_package_file): - software.append(path) - software.sort() - return software - - -def get_suported_extensions(): - """ - Returns the supported software archive extensions. - - :rtype: set[str] - """ - return {".tgz", ".tar", ".tar.gz", ".tar.bz2"} - - -def validate_config(config, software, version): - """ - Validates a software config. - - :param config: software config to validate - :type config: dict - - :param software: software to validate in config - :type config: str - - :param config: release of software to validate in config - :type config: str - - :raises KeyError: if software is not in config - :raises ValueError: if url is empty - :raises ValueError: if url has no version placeholder - :raises ValueError: if url is invalid - :raises ValueError: if extension is not supported - """ - # is software in config - if software not in config: - raise KeyError("software {!r} has no configuration".format(software)) - - # is url empty - url = config[software] - if not url: - raise ValueError("url for software {!r} is empty".format(software)) - - # does url have version placeholder - validate_template_url(url) - - # is url valid - url = _build_download_url(url, version) - try: - result = urlopen(url) - except Exception as e: - raise ValueError("invalid url {!r} for software {!r} " - "({})".format(url, software, str(e))) - - # is extension valid - try: - _path, ext = _split_ext(url) - except ValueError: - _path, ext = _split_ext(result.url) - - validate_extension(ext) - - -def uninstall(software, version=None, dry_run=False): - """ - Uninstalls one or all versions of a software. - - :param software: name of the softare to uninstall - :type software: str - - :param version: release of the softare to uninstall, if not given, all - releases will be uninstalled - :type version: str - - :param dry_run: set True to skip the actual file system content - :type dry_run: bool - """ - if dry_run: - print("!DRY-RUN MODE!") - - # get installed software paths - installed = get_installed_software() - - # group paths by name for easy lookup - paths_by_name = {os.path.basename(p): p for p in installed} - - # group names by version for easy lookup - name_by_version = {n.split("-")[-1]: n for n in paths_by_name - if n.startswith(software)} - - # get how many versions of software to uninstall - to_uninstall = name_by_version.keys() - if version and version in name_by_version: - to_uninstall = [version] - - for v in to_uninstall: - name = name_by_version[v] - path = paths_by_name[name] - - print("uninstalling {} ...".format(name)) - if not dry_run: - shutil.rmtree(path) - - -def validate_template_url(url): - """ - Validates a download url template. - - :param url: url to validate - :type url: str - - :raises ValueError: if no version placeholder is present in url - """ - format_key = r"{version}" - if format_key not in url: - msg = "no format key {!r} found in url {!r}" - raise ValueError(msg.format(format_key, url)) - - -def validate_software(software): - """ - Validates a software name. - - :param software: software to validate - :type software: str - - :raises ValueError: if software name is empty - :raises ValueError: if software name is only consists of whitespace(s) - """ - # empty - if not software: - raise ValueError("software cannot be empty") - - # only whitespace(s) - if software.strip() == "": - raise ValueError("software cannot be whitespace only") - - -def add_software(software, url): - """ - Adds a software configuration. - - :param software: name of the software to add - :type software: str - - :param url: download url template of the software - :type url: str - """ - validate_software(software) - - config = get_config() - if software in config: - print("software {!r} already added".format(software)) - return - - validate_template_url(url) - config[software] = url - - # write out changes - with open(get_config_location(), "w") as fp: - json.dump(config, fp) - - -def remove_software(software): - """ - Removes a software configuration. - - :param software: software to remove - """ - config = get_config() - - try: - config.pop(software) - except KeyError: - print("software {!r} is not present in config".format(software)) - return - - # write out changes - with open(get_config_location(), "w") as fp: - json.dump(config, fp) - - -def validate_extension(extension): - """ - Validates an extension. - - :param extension: extension to validate - :type extension: str - - :raises ValueError: if extension is not supported - """ - valid_exts = get_suported_extensions() - if extension not in valid_exts: - msg = "invalid extension {!r}, valid extensions are: {}" - raise ValueError(msg.format(extension, ", ".join(valid_exts))) - - -def get_script(software): - """ - Gets the path of the post install script of a software. - - :rtype: str - """ - dir_scripts = get_scripts_location() - scripts = os.listdir(dir_scripts) - - for script in scripts: - if script == software: - return os.path.join(dir_scripts, script) - - return None - - -def run_script(script, software, version, wd=None): - """ - Run a post install script for a specific software. - - :param script: post install script - :type script: str - - :param software: software to run post install script for - :type software: str - - :param version: version of the software to run post install script for - :type version: str - - :param wd: working directory path to run the script from - :type wd: str - """ - cmd = [script, software, version] - process = subprocess.Popen(" ".join(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - cwd=wd) - print("running post install script ...") - stdout, stderr = process.communicate() - if stdout: - print(stdout) - if stderr: - print(stderr) diff --git a/runcheck b/runcheck deleted file mode 100755 index 2ddbce7..0000000 --- a/runcheck +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env sh - -flake8 --ignore=E501 --radon-max-cc 10 ./python diff --git a/runtests b/runtests deleted file mode 100755 index 12d9659..0000000 --- a/runtests +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env sh - -pytest -v --cov-report term-missing --cov=packagerbuddy ./tests diff --git a/setup.py b/setup.py deleted file mode 100644 index 0f253c3..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -# stdlib modules -import os -import sys -from setuptools import setup -from setuptools import find_packages - -# tool modules -f = os.path.abspath(__file__) -package_dir = os.path.join(os.path.dirname(f), "python") -sys.path.insert(0, package_dir) -from packagerbuddy import __version__ # noqa - -requirements_dev = ["flake8", "flake8_polyfill", "radon", "pytest", "pytest-cov", "coverage"] - - -setup(name="packagerbuddy", - version=__version__, - description="JSON config based software packager.", - license="GPLv3", - author="Cédric Duriau", - author_email="duriau.cedric@live.be", - url="https://github.com/cedricduriau/packagerbuddy", - packages=find_packages(where="python"), - package_dir={"": "python"}, - scripts=["bin/packagerbuddy"], - extras_require={"dev": requirements_dev}, - data_files=[(os.path.expanduser("~/.packagerbuddy/source"), []), - (os.path.expanduser("~/.packagerbuddy/installed"), []), - (os.path.expanduser("~/.packagerbuddy/config"), []), - (os.path.expanduser("~/.packagerbuddy/scripts"), [])]) diff --git a/tests/conftest.py b/tests/conftest.py index 2d7627b..eaa9fd5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,48 +1,83 @@ # stdlib +import json import os +import tempfile -try: - from urllib.request import build_opener, install_opener -except ImportError: - from urllib2 import build_opener, install_opener +# third party +import pytest -# tool modules -from tests.mock_urllib import MockHTTPHandler +# package +from packagerbuddy import settings -# third party modules -import pytest + +# ============================================================================== +# fixtures +# ============================================================================== +@pytest.fixture +def fix_test_data() -> str: + path = os.path.join(os.path.dirname(__file__), "data") + return path @pytest.fixture -def patch_PB_CONFIG(monkeypatch): - """Monkey patches the PB_CONFIG environment variable.""" - directory = os.path.dirname(__file__) - os.environ["PB_CONFIG"] = os.path.join(directory, "test_config", "software.json") +def fix_dir_config(fix_test_data: str) -> str: + path = os.path.join(fix_test_data, "config") + return path + + +@pytest.fixture +def fix_file_config(fix_dir_config: str) -> str: + path = os.path.join(fix_dir_config, "software.json") + return path + + +@pytest.fixture +def fix_dir_downloaded(fix_test_data: str): + path = os.path.join(fix_test_data, "downloaded") + return path + + +@pytest.fixture +def fix_dir_installed(fix_test_data: str): + path = os.path.join(fix_test_data, "installed") + return path + + +@pytest.fixture +def fix_dir_scripts(fix_test_data: str): + path = os.path.join(fix_test_data, "scripts") + return path + + +@pytest.fixture +def fix_file_config_tmp(monkeypatch: pytest.MonkeyPatch) -> None: + _, tmp_config = tempfile.mkstemp(suffix=".json", prefix="software") + monkeypatch.setattr(settings, "FILE_CONFIG", tmp_config) + + with open(tmp_config, "w") as fp: + json.dump({}, fp) + + return tmp_config +# ============================================================================== +# patches +# ============================================================================== @pytest.fixture -def patch_PB_DOWNLOAD(monkeypatch): - """Monkey patches the PB_DOWNLOAD environment variable.""" - directory = os.path.dirname(__file__) - os.environ["PB_DOWNLOAD"] = os.path.join(directory, "test_source") +def mock_settings_file_config(fix_file_config: str, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "FILE_CONFIG", fix_file_config) @pytest.fixture -def patch_PB_INSTALL(monkeypatch): - """Monkey patches the PB_INSTALL environment variable.""" - directory = os.path.dirname(__file__) - os.environ["PB_INSTALL"] = os.path.join(directory, "test_install") +def mock_settings_dir_download(fix_dir_downloaded: str, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "DIR_DOWNLOAD", fix_dir_downloaded) @pytest.fixture -def patch_PB_SCRIPTS(monkeypatch): - """Monkey patches the PB_SCRIPTS environment variable.""" - directory = os.path.dirname(__file__) - os.environ["PB_SCRIPTS"] = os.path.join(directory, "test_scripts") +def mock_settings_dir_install(fix_dir_installed: str, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "DIR_INSTALL", fix_dir_installed) @pytest.fixture -def patch_url_handler(monkeypatch): - """Monkey patches the urllib http handler to return a custom class.""" - my_opener = build_opener(MockHTTPHandler) - install_opener(my_opener) +def mock_settings_dir_scripts(fix_dir_scripts: str, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(settings, "DIR_SCRIPTS", fix_dir_scripts) diff --git a/tests/data/config/software.json b/tests/data/config/software.json new file mode 100644 index 0000000..a647713 --- /dev/null +++ b/tests/data/config/software.json @@ -0,0 +1,4 @@ +{ + "foo": "https://example.com/{version}/foo-{version}.zip", + "bar": "https://example.com/{version}/bar-{version}.tar.gz" +} \ No newline at end of file diff --git a/tests/data/downloaded/bar-0.1.0.tar.gz b/tests/data/downloaded/bar-0.1.0.tar.gz new file mode 100644 index 0000000..3c5ae96 Binary files /dev/null and b/tests/data/downloaded/bar-0.1.0.tar.gz differ diff --git a/tests/data/downloaded/bar-0.2.0.tar.gz b/tests/data/downloaded/bar-0.2.0.tar.gz new file mode 100644 index 0000000..0c7c6c2 Binary files /dev/null and b/tests/data/downloaded/bar-0.2.0.tar.gz differ diff --git a/tests/data/downloaded/foo-0.1.0.zip b/tests/data/downloaded/foo-0.1.0.zip new file mode 100644 index 0000000..cb8d986 Binary files /dev/null and b/tests/data/downloaded/foo-0.1.0.zip differ diff --git a/tests/data/downloaded/foo-0.2.0.zip b/tests/data/downloaded/foo-0.2.0.zip new file mode 100644 index 0000000..17d2897 Binary files /dev/null and b/tests/data/downloaded/foo-0.2.0.zip differ diff --git a/tests/test_install/valid-1.0.0/.pbsoftware b/tests/data/installed/bar-0.1.0/bar.txt similarity index 100% rename from tests/test_install/valid-1.0.0/.pbsoftware rename to tests/data/installed/bar-0.1.0/bar.txt diff --git a/tests/test_install/valid-2.0.0/.pbsoftware b/tests/data/installed/foo-0.1.0/foo.txt similarity index 100% rename from tests/test_install/valid-2.0.0/.pbsoftware rename to tests/data/installed/foo-0.1.0/foo.txt diff --git a/tests/test_source/valid-1.0.0.tar.gz b/tests/data/scripts/foo similarity index 100% rename from tests/test_source/valid-1.0.0.tar.gz rename to tests/data/scripts/foo diff --git a/tests/data/scripts/foo-0.1.0 b/tests/data/scripts/foo-0.1.0 new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock_urllib.py b/tests/mock_urllib.py deleted file mode 100644 index 9d62368..0000000 --- a/tests/mock_urllib.py +++ /dev/null @@ -1,33 +0,0 @@ -# stdlib modules -try: - from urllib.response import addinfourl - from urllib.error import HTTPError - from urllib.request import HTTPHandler - from io import StringIO -except ImportError: - from urllib2 import addinfourl, HTTPError, HTTPHandler - from StringIO import StringIO - - -def mock_response(req): - url = req.get_full_url() - - if url.startswith("http://valid"): - resp = addinfourl(StringIO("valid"), "valid", url) - resp.code = 200 - resp.msg = "OK" - resp.headers = {"content-disposition": "filename=valid.tar"} - return resp - elif url.startswith("http://filename"): - resp = addinfourl(StringIO("filename"), "filename", url) - resp.code = 200 - resp.msg = "OK" - resp.headers = {} - return resp - elif url.startswith("http://invalid"): - raise HTTPError(url, 404, "invalid", "", StringIO()) - - -class MockHTTPHandler(HTTPHandler): - def http_open(self, req): - return mock_response(req) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..c9279e5 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,220 @@ +# stdlib +import os +import shutil + +# third party +import pytest + +# package +from packagerbuddy import cli, configutils, downloadutils + + +def test_run(): + with pytest.raises(SystemExit) as exc: + cli.run(["-h"]) + + assert exc.value.code == 0 + + +@pytest.mark.parametrize("args", [([]), (["-!"])]) +def test_run_no_action(args: list[str]): + with pytest.raises(SystemExit) as exc: + cli.run(args) + + assert exc.value.code == 2 + + +@pytest.mark.parametrize("exists", [(True), (False)]) +def test_setup(exists: bool, monkeypatch: pytest.MonkeyPatch): + def mock_os_path_exists(p: str) -> bool: + return exists + + def mock_os_makedirs(p: str): + return + + monkeypatch.setattr(os, "makedirs", mock_os_makedirs) + monkeypatch.setattr(os.path, "exists", mock_os_path_exists) + + with pytest.raises(SystemExit) as exc: + cli.run(["setup"]) + + assert exc.value.code == 0 + + +def test_list_available_software( + mock_settings_file_config: None, + capsys, + monkeypatch: pytest.MonkeyPatch, +): + with pytest.raises(SystemExit) as exc: + cli.run(["avail"]) + + assert exc.value.code == 0 + out, _err = capsys.readouterr() + assert out == "\n".join(["bar", "foo"]) + "\n" + + +@pytest.mark.parametrize( + ["software", "url", "exit_code", "error"], + [ + (" ", "", 1, "no software provided"), + ("foo", " ", 1, "no url provided"), + ("foo", "0.1.0", 1, "software already configured"), + ("xyz", "0.1.0", 1, r"no {version} format string found in url"), + ("xyz", r"https://example.com/{version}/xyz-{version}.zip", 0, ""), + ], +) +def test_add_software( + software: str, + url: str, + exit_code: int, + error: str, + capsys, + mock_settings_file_config: None, + monkeypatch: pytest.MonkeyPatch, +): + def mock_configutils_dump(config: dict): + return + + monkeypatch.setattr(configutils, "dump", mock_configutils_dump) + + with pytest.raises(SystemExit) as exc: + cli.run(["add", "-s", software, "-u", url]) + + assert exc.value.code == exit_code + out, _err = capsys.readouterr() + + if exit_code != 0: + assert out == error + "\n" + + +@pytest.mark.parametrize( + ["software", "exit_code", "error"], + [ + (" ", 1, "no software provided"), + ("xyz", 1, "software not found"), + ("foo", 0, ""), + ], +) +def test_remove_software( + software: str, + exit_code: bool, + error: str, + capsys, + mock_settings_file_config: None, + monkeypatch: pytest.MonkeyPatch, +): + def mock_dump(config: dict) -> None: + return + + monkeypatch.setattr(configutils, "dump", mock_dump) + + with pytest.raises(SystemExit) as exc: + cli.run(["remove", "-s", software]) + + assert exc.value.code == exit_code + out, _err = capsys.readouterr() + + if exit_code != 0: + assert out == error + "\n" + + +@pytest.mark.parametrize( + ["software", "version", "exit_code", "error"], + [ + (" ", "0.1.0", 1, "no software provided"), + ("xyz", "0.1.0", 1, "software not found"), + ("foo", "0.1.0", 0, ""), + ("foo", "0.3.0", 0, ""), + ], +) +def test_download_software( + software: str, + version: str, + exit_code: int, + error: str, + capsys, + fix_dir_downloaded: str, + mock_settings_file_config: None, + mock_settings_dir_download: None, + monkeypatch: pytest.MonkeyPatch, +): + def mock_downloadutils_download(software: str, version: str, config: dict | None = None) -> str: + path = os.path.join(fix_dir_downloaded, f"{software}-{version}.zip") + return path + + monkeypatch.setattr(downloadutils, "download", mock_downloadutils_download) + + with pytest.raises(SystemExit) as exc: + cli.run(["download", "-s", software, "-v", version]) + + assert exc.value.code == exit_code + out, _err = capsys.readouterr() + + if exit_code == 0: + assert out == mock_downloadutils_download(software, version) + "\n" + else: + assert out == error + "\n" + + +@pytest.mark.parametrize( + ["software", "version", "exit_code", "error"], + [ + (" ", "0.1.0", 1, "no software provided"), + ("xyz", "0.1.0", 1, "software not found"), + ("foo", "0.1.0", 0, ""), + ("foo", "0.2.0", 0, ""), + ("bar", "0.2.0", 0, ""), + ], +) +def test_install_software( + software: str, + version: str, + exit_code: int, + error: str, + capsys, + fix_dir_installed: str, + mock_settings_file_config: None, + mock_settings_dir_download: None, + mock_settings_dir_install: None, +): + path = os.path.join(fix_dir_installed, f"{software}-{version}") + cleanup = not os.path.exists(path) + + with pytest.raises(SystemExit) as exc: + cli.run(["install", "-s", software, "-v", version]) + + assert exc.value.code == exit_code + out, _err = capsys.readouterr() + + if exit_code == 0: + assert out == path + "\n" + if cleanup: + shutil.rmtree(path) + else: + assert out == error + "\n" + + +@pytest.mark.parametrize( + ["software", "version", "count"], + [ + ("", "", 2), + ("foo", "", 1), + ("bar", "", 1), + ("foo", "0.1.0", 1), + ], +) +def test_list_installed_software( + software: str, + version: str, + count: int, + capsys, + mock_settings_dir_install: None, +): + with pytest.raises(SystemExit) as exc: + cli.run(["list", "-s", software, "-v", version]) + + assert exc.value.code == 0 + out, _err = capsys.readouterr() + + assert len(out.rstrip("\n").split("\n")) == count diff --git a/tests/test_config/software.json b/tests/test_config/software.json deleted file mode 100644 index f91da03..0000000 --- a/tests/test_config/software.json +++ /dev/null @@ -1 +0,0 @@ -{"valid": "http://valid.com/{version}.tar"} \ No newline at end of file diff --git a/tests/test_configutils.py b/tests/test_configutils.py new file mode 100644 index 0000000..f9070ed --- /dev/null +++ b/tests/test_configutils.py @@ -0,0 +1,55 @@ +# stdlib +import json + +# third party +import pytest + +# package +from packagerbuddy import configutils + + +def test_load(mock_settings_file_config: None) -> None: + config = configutils.load() + assert config == { + "foo": r"https://example.com/{version}/foo-{version}.zip", + "bar": r"https://example.com/{version}/bar-{version}.tar.gz", + } + + +def test_dump(fix_file_config_tmp: str) -> None: + tmp_data = {"bar": r"https://example.com/{version}/bar-{version}.zip"} + configutils.dump(tmp_data) + + with open(fix_file_config_tmp, "r") as fp: + config = json.load(fp) + assert config == tmp_data + + +@pytest.mark.parametrize( + ["software", "configured"], + [ + ("foo", True), + ("xyz", False), + ], +) +def test_is_software_configured( + software: str, + configured: bool, + mock_settings_file_config: None, +) -> None: + config = configutils.load() + assert configutils.is_software_configured(config, software) is configured + + +def test_add(fix_file_config_tmp: str) -> None: + config = configutils.load() + configutils.add_software(config, "bar", r"https://example.com/{version}/bar-{version}.zip") + assert config["bar"] == r"https://example.com/{version}/bar-{version}.zip" + + +def test_remove(fix_file_config_tmp: str) -> None: + config = configutils.load() + configutils.add_software(config, "bar", r"https://example.com/{version}/bar-{version}.zip") + assert "bar" in config + configutils.remove_software(config, "bar") + assert "bar" not in config diff --git a/tests/test_downloadutils.py b/tests/test_downloadutils.py new file mode 100644 index 0000000..4c42767 --- /dev/null +++ b/tests/test_downloadutils.py @@ -0,0 +1,25 @@ +# stdlib +import os + +# third party +import pytest + +# package +from packagerbuddy import downloadutils + + +def test_build_archive_path(fix_dir_downloaded: str, mock_settings_dir_download: None) -> None: + path = downloadutils.build_archive_path("foo", "bar", r"https://example.com/{version}/foo-bar.zip") + assert path == os.path.join(fix_dir_downloaded, "foo-bar.zip") + + +@pytest.mark.parametrize( + ["software", "version", "found"], + [ + ("foo", "0.1.0", True), + ("foo", "0.3.0", False), + ], +) +def test_find_archive(software: str, version: str, found: bool, mock_settings_dir_download: None): + archive = downloadutils.find_archive(software, version) + assert bool(archive) is found diff --git a/tests/test_installutils.py b/tests/test_installutils.py new file mode 100644 index 0000000..5030789 --- /dev/null +++ b/tests/test_installutils.py @@ -0,0 +1,49 @@ +# stdlib +import os + +# third party +import pytest + +# package +from packagerbuddy import installutils + + +def test_build_temporary_install_path(fix_dir_installed: str, mock_settings_dir_install: None): + path = installutils.build_temporary_install_path("foo", "bar") + expected = os.path.join(fix_dir_installed, "tmp-foo-bar") + assert path == expected + + +def test_build_install_path(fix_dir_installed: str, mock_settings_dir_install: None): + path = installutils.build_install_path("foo", "bar") + expected = os.path.join(fix_dir_installed, "foo-bar") + assert path == expected + + +def test_get_archive_name(): + software = "foo" + version = "bar" + config = {"foo": "https://example.com/{version}/software.zip"} + name = installutils.get_archive_name(software, version, config) + assert name == "software" + + +@pytest.mark.parametrize( + ["software", "version", "expected"], + [ + (None, None, 2), + ("foo", None, 1), + ("bar", None, 1), + ("bar", "0.1.0", 1), + ("foo", "0.1.0", 1), + ("foo", "0.3.0", 0), + ], +) +def test_find_installed_software( + software: str | None, + version: str | None, + expected: int, + mock_settings_dir_install: None, +): + result = installutils.find_installed_software(software, version) + assert len(result) == expected diff --git a/tests/test_packagerbuddy/test_packagerbuddy.py b/tests/test_packagerbuddy/test_packagerbuddy.py deleted file mode 100644 index aceeff7..0000000 --- a/tests/test_packagerbuddy/test_packagerbuddy.py +++ /dev/null @@ -1,259 +0,0 @@ -# stlib modules -from __future__ import absolute_import -import os - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -# tool modules -from packagerbuddy import packagerbuddy - -# third party modules -import pytest - - -def test_get_filename_from_request(patch_url_handler): - """Test getting the filename of an url from a request object.""" - # url with no filename in name, but in request content headers - request = urlopen("http://valid.com") - filename = packagerbuddy._get_filename_from_request(request) - assert filename == "valid.tar" - - # url with filename in name, not in request content headers - request = urlopen("http://filename.tar") - filename = packagerbuddy._get_filename_from_request(request) - assert filename == "filename.tar" - - -def test_normalize_path(): - """Test normalizing paths.""" - # ensure tilde is being resolved - assert packagerbuddy._normalize_path("~/") == os.environ["HOME"] - - # ensure same level relative is being resolved - assert packagerbuddy._normalize_path("/tmp/./test.txt") == "/tmp/test.txt" - - # ensure single level up relative is being resolved - assert packagerbuddy._normalize_path("/tmp/dir/../test.txt") == "/tmp/test.txt" - - -def test_download(): - pass - - -def test_build_archive_name(): - """Test building the archive name of a specific software release.""" - assert packagerbuddy._build_archive_name("software", "version", ".ext") == "software-version.ext" - - -def test_get_tar_read_mode(): - """Test getting the tar file read modes.""" - assert packagerbuddy._get_tar_read_mode("/tmp/test.tar") == "r" - assert packagerbuddy._get_tar_read_mode("/tmp/test.tar.gz") == "r:gz" - assert packagerbuddy._get_tar_read_mode("/tmp/test.tar.bz2") == "r:bz2" - - -def test_untar(): - pass - - -def test_build_download_url(): - """Test building a download url.""" - packagerbuddy._build_download_url("http://valid.com/{version}", "1.0.0") == "http://valid.com/1.0.0" - - -def test_get_archive(patch_PB_DOWNLOAD): - download_dir = os.environ["PB_DOWNLOAD"] - assert packagerbuddy._get_archive("invalid", "1.0.0") is None - archive = packagerbuddy._get_archive("valid", "1.0.0") - assert archive == os.path.join(download_dir, "valid-1.0.0.tar.gz") - - -def test_split_ext(): - """Test splitting the extension of paths with supported extensions.""" - assert packagerbuddy._split_ext("/tmp/foo.tar") == ("/tmp/foo", ".tar") - assert packagerbuddy._split_ext("/tmp/foo.tar.gz") == ("/tmp/foo", ".tar.gz") - assert packagerbuddy._split_ext("/tmp/foo.tar.gz&response-content-type=application") == ("/tmp/foo", ".tar.gz") - assert packagerbuddy._split_ext("/tmp/foo/1.2.3.tar") == ("/tmp/foo/1.2.3", ".tar") - assert packagerbuddy._split_ext("/tmp/foo/1.2.3.tar.gz") == ("/tmp/foo/1.2.3", ".tar.gz") - - -def test_split_ext_fail(): - """Test splitting the extension of paths with unsupported extensions.""" - # no extension - with pytest.raises(ValueError): - packagerbuddy._split_ext("/tmp/foo/test") - - -def test_get_config_location(patch_PB_CONFIG): - """Test getting the software configs location.""" - assert packagerbuddy.get_config_location() == os.environ["PB_CONFIG"] - - -def test_get_download_location(patch_PB_DOWNLOAD): - """Test getting the software download location.""" - current_dir = os.path.dirname(__file__) - expected = os.path.abspath(os.path.join(current_dir, "..", "test_source")) - assert packagerbuddy.get_download_location() == expected - - -def test_get_install_location(patch_PB_INSTALL): - """Test getting the software install location.""" - current_dir = os.path.dirname(__file__) - expected = os.path.abspath(os.path.join(current_dir, "..", "test_install")) - assert packagerbuddy.get_install_location() == expected - - -def test_get_scripts_location(patch_PB_SCRIPTS): - """Test getting the post install scripts location.""" - current_dir = os.path.dirname(__file__) - expected = os.path.abspath(os.path.join(current_dir, "..", "test_scripts")) - assert packagerbuddy.get_scripts_location() == expected - - -def test_get_config(patch_PB_CONFIG): - """Test getting the config for a valid software.""" - config = {"valid": "http://valid.com/{version}.tar"} - assert packagerbuddy.get_config() == config - - -def test_install(): - pass - - -def test_is_software_installed(patch_PB_INSTALL): - """Test checking whether a software is installed or not.""" - assert packagerbuddy.is_software_installed("valid", "2.0.0") is True - assert packagerbuddy.is_software_installed("valid", "1.0.0") is True - assert packagerbuddy.is_software_installed("valid", "0.0.0") is False - - -def test_get_installed_software(patch_PB_INSTALL): - """Test getting the installed software releases.""" - install_dir = os.environ["PB_INSTALL"] - assert packagerbuddy.get_installed_software() == [os.path.join(install_dir, "valid-1.0.0"), - os.path.join(install_dir, "valid-2.0.0")] - - -def test_get_suported_extensions(): - """Test getting the supported software archive extensions.""" - assert packagerbuddy.get_suported_extensions() == set([".tar", ".tar.gz", ".tar.bz2", ".tgz"]) - - -def test_validate_config(patch_url_handler): - """Test validating a valid software config.""" - config = {"valid": "http://valid.com/{version}.tar"} - packagerbuddy.validate_config(config, "valid", "1.0.0") - - -def test_validate_config_fail(patch_url_handler): - """Test validating invalid software configs.""" - version = "1.0.0" - - # missing key url - with pytest.raises(KeyError): - packagerbuddy.validate_config({"foo": None}, "valid", version) - - # no url value - config = {"valid": None} - with pytest.raises(ValueError): - packagerbuddy.validate_config(config, "valid", version) - - # url without version placeholder format - config = {"invalid": "http://invalid.com.tar"} - with pytest.raises(ValueError): - packagerbuddy.validate_config(config, "invalid", version) - - # invalid url - config = {"valid": "http://invalid.com/{version}.tar"} - with pytest.raises(ValueError): - packagerbuddy.validate_config(config, "valid", version) - - # invalid extension, unsupported - config = {"valid": "http://valid.com/{version}.FOO"} - with pytest.raises(ValueError): - packagerbuddy.validate_config(config, "valid", version) - - -def test_uninstall(): - pass - - -def test_validate_template_url(): - """Test validating an valid download template url.""" - packagerbuddy.validate_template_url("http://test.com/{version}") - - -def test_validate_template_url_fail(): - """Test validating an invalid download template url.""" - with pytest.raises(ValueError): - packagerbuddy.validate_template_url("http://test.com/") - - -def test_validate_software(): - """Test validating a valid software name.""" - packagerbuddy.validate_software("test") - - -def test_validate_software_fail(): - """Test validating an invalid software name.""" - # empty - with pytest.raises(ValueError): - packagerbuddy.validate_software("") - - # whitespaces - with pytest.raises(ValueError): - packagerbuddy.validate_software(" ") - - -def test_add_software(patch_PB_CONFIG): - """Test adding a software configuration.""" - config = packagerbuddy.get_config() - assert "test" not in config - - # add twice - packagerbuddy.add_software("test", "http://test.com/{version}") - packagerbuddy.add_software("test", "http://test.com/{version}") - - config = packagerbuddy.get_config() - assert "test" in config - assert config["test"] == "http://test.com/{version}" - packagerbuddy.remove_software("test") - - -def test_remove_software(patch_PB_CONFIG): - """Test removing a software configuration.""" - packagerbuddy.add_software("test", "http://test.com/{version}") - config = packagerbuddy.get_config() - assert "test" in config - - # remove twice - packagerbuddy.remove_software("test") - packagerbuddy.remove_software("test") - - config = packagerbuddy.get_config() - assert "test" not in config - - -def test_validate_extension(): - """Test validating a valid extension.""" - packagerbuddy.validate_extension(".tar") - packagerbuddy.validate_extension(".tar.gz") - - -def test_validate_extension_fail(): - """Test validating an invalid extension.""" - with pytest.raises(ValueError): - packagerbuddy.validate_extension("") - - with pytest.raises(ValueError): - packagerbuddy.validate_extension(".foo") - - -def test_get_script(patch_PB_SCRIPTS): - """Test getting the post install scripts of software packages.""" - assert packagerbuddy.get_script("invalid") is None - scripts_dir = os.environ["PB_SCRIPTS"] - assert packagerbuddy.get_script("valid") == os.path.join(scripts_dir, "valid") diff --git a/tests/test_pathutils.py b/tests/test_pathutils.py new file mode 100644 index 0000000..147bbc7 --- /dev/null +++ b/tests/test_pathutils.py @@ -0,0 +1,21 @@ +# third party +import pytest + +# package +from packagerbuddy import pathutils + + +@pytest.mark.parametrize( + ["path", "expected_root", "expected_ext"], + [ + ("/root/dir/file.txt", "/root/dir/file.txt", ""), + ("/root/dir/file.zip", "/root/dir/file", ".zip"), + ("/root/dir/file.tar", "/root/dir/file", ".tar"), + ("/root/dir/file.tar.gz", "/root/dir/file", ".tar.gz"), + ("/root/dir/file.dot1.dot2.tar.gz", "/root/dir/file.dot1.dot2", ".tar.gz"), + ], +) +def test_split_ext(path: str, expected_root: str, expected_ext: str) -> None: + root, ext = pathutils.split_ext(path) + assert root == expected_root + assert ext == expected_ext diff --git a/tests/test_scripts/valid b/tests/test_scripts/valid deleted file mode 100644 index 93495dd..0000000 --- a/tests/test_scripts/valid +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh - -echo $1 -echo $2 diff --git a/tests/test_scriptutils.py b/tests/test_scriptutils.py new file mode 100644 index 0000000..838729b --- /dev/null +++ b/tests/test_scriptutils.py @@ -0,0 +1,16 @@ +# stdlib +import os + +# package +from packagerbuddy import scriptutils + + +def test_find_scripts( + mock_settings_dir_scripts: None, + fix_dir_scripts: str, +) -> None: + scripts = scriptutils.find_scripts("foo", "0.1.0") + assert scripts == [ + os.path.join(fix_dir_scripts, "foo"), + os.path.join(fix_dir_scripts, "foo-0.1.0"), + ]