From 9906d12384aa057a2257438b415db4c0c4a67a0e Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 20 Jul 2023 18:07:27 +0200 Subject: [PATCH 01/11] clan-cli: initialize python project from template clan-cli: remove unnecessary unit test file clan-cli: fix shell.nix too stateful clan-cli: remove conftest.py clan-cli: fix flake-module.nix --- pkgs/clan-cli/.envrc | 1 + pkgs/clan-cli/default.nix | 72 ++++++++++++++++++++++++++++----- pkgs/clan-cli/flake-module.nix | 10 +++++ pkgs/clan-cli/pyproject.toml | 61 ++++++++++++++++++++++++++++ pkgs/clan-cli/shell.nix | 47 +++++++++++++++++++++ pkgs/clan-cli/tests/test_cli.py | 16 ++++++++ 6 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 pkgs/clan-cli/.envrc create mode 100644 pkgs/clan-cli/flake-module.nix create mode 100644 pkgs/clan-cli/pyproject.toml create mode 100644 pkgs/clan-cli/shell.nix create mode 100644 pkgs/clan-cli/tests/test_cli.py diff --git a/pkgs/clan-cli/.envrc b/pkgs/clan-cli/.envrc new file mode 100644 index 000000000..1d953f4bd --- /dev/null +++ b/pkgs/clan-cli/.envrc @@ -0,0 +1 @@ +use nix diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 72214801d..59d2fa46d 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -1,10 +1,62 @@ -{ symlinkJoin -, writers -}: -symlinkJoin { - name = "clan"; - paths = [ - (writers.writePython3Bin "clan" { } ./clan.py) - (writers.writePython3Bin "clan-admin" { flakeIgnore = [ "E501" ]; } ./clan-admin.py) - ]; -} +{ + pkgs ? import {}, + + lib ? pkgs.lib, + python3 ? pkgs.python3, + ruff ? pkgs.ruff, + runCommand ? pkgs.runCommand, +}: let + pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); + name = pyproject.project.name; + + src = lib.cleanSource ./.; + + dependencies = lib.attrValues { + # inherit (python3.pkgs) + # some-package + # ; + }; + + devDependencies = lib.attrValues { + inherit (pkgs) ruff; + inherit (python3.pkgs) + black + mypy + pytest + pytest-cov + setuptools + wheel + ; + }; + + package = python3.pkgs.buildPythonPackage { + inherit name src; + format = "pyproject"; + nativeBuildInputs = [ + python3.pkgs.setuptools + ]; + propagatedBuildInputs = + dependencies + ++ [ ]; + passthru.tests = { inherit check; }; + passthru.devDependencies = devDependencies; + }; + + checkPython = python3.withPackages (ps: devDependencies); + + check = runCommand "${name}-check" { } '' + cp -r ${src} ./src + chmod +w -R ./src + cd src + export PYTHONPATH=. + echo -e "\x1b[32m## run ruff\x1b[0m" + ${ruff}/bin/ruff check . + echo -e "\x1b[32m## run mypy\x1b[0m" + ${checkPython}/bin/mypy . + echo -e "\x1b[32m## run pytest\x1b[0m" + ${checkPython}/bin/pytest + touch $out + ''; + +in +package diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix new file mode 100644 index 000000000..1e85e6079 --- /dev/null +++ b/pkgs/clan-cli/flake-module.nix @@ -0,0 +1,10 @@ +{ + perSystem = {pkgs, ...}: let + pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); + name = pyproject.project.name; + package = pkgs.callPackage ./default.nix {}; + in { + # packages.${name} = package; + checks.python-template = package.tests.check; + }; +} diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml new file mode 100644 index 000000000..cbf9dc67c --- /dev/null +++ b/pkgs/clan-cli/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["clan.py"] + +[project] +name = "clan" +description = "cLAN CLI tool" +dynamic = ["version"] +scripts = {clan = "clan:my_cli"} + +[tool.pytest.ini_options] +addopts = "--cov . --cov-report term --cov-fail-under=100 --no-cov-on-fail" + +[tool.mypy] +python_version = "3.10" +warn_redundant_casts = true +disallow_untyped_calls = true +disallow_untyped_defs = true +no_implicit_optional = true +exclude = [ + "tests" +] + +[[tool.mypy.overrides]] +module = "setuptools.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "pytest.*" +ignore_missing_imports = true + +[tool.ruff] +line-length = 88 + +select = ["E", "F", "I"] +ignore = [ "E501" ] + +[tool.black] +line-length = 88 +target-version = ['py310'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling +)/ +''' diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix new file mode 100644 index 000000000..2b7b0748c --- /dev/null +++ b/pkgs/clan-cli/shell.nix @@ -0,0 +1,47 @@ +{ pkgs ? import { } +, system ? builtins.currentSystem +, +}: +let + lib = pkgs.lib; + python3 = pkgs.python3; + package = import ./default.nix { + inherit lib python3; + }; + pythonWithDeps = python3.withPackages ( + ps: + package.propagatedBuildInputs + ++ package.devDependencies + ++ [ + ps.pip + ] + ); + checkScript = pkgs.writeScriptBin "check" '' + nix build -f . tests.check -L "$@" + ''; + devShell = pkgs.mkShell { + packages = [ + pkgs.ruff + pythonWithDeps + ]; + # sets up an editable install and add enty points to $PATH + shellHook = '' + tmp_path=$(realpath ./.pythonenv) + repo_root=$(realpath .) + rm -rf $tmp_path + mkdir -p "$tmp_path/${pythonWithDeps.sitePackages}" + + ${pythonWithDeps.interpreter} -m pip install \ + --quiet \ + --disable-pip-version-check \ + --no-index \ + --no-build-isolation \ + --prefix "$tmp_path" \ + --editable $repo_root + + export PATH="$tmp_path/bin:${checkScript}/bin:$PATH" + export PYTHONPATH="$repo_root:$tmp_path/${pythonWithDeps.sitePackages}" + ''; + }; +in +devShell diff --git a/pkgs/clan-cli/tests/test_cli.py b/pkgs/clan-cli/tests/test_cli.py new file mode 100644 index 000000000..5311f2bda --- /dev/null +++ b/pkgs/clan-cli/tests/test_cli.py @@ -0,0 +1,16 @@ +import sys + +import my_tool + + +def test_no_args(capsys): + my_tool.my_cli() + captured = capsys.readouterr() + assert captured.out.startswith("usage:") + + +def test_version(capsys, monkeypatch): + monkeypatch.setattr(sys, "argv", ["", "--version"]) + my_tool.my_cli() + captured = capsys.readouterr() + assert captured.out.startswith("Version:") From b34141283f41d317445febfcb184dcea9714f03d Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 20 Jul 2023 19:18:28 +0200 Subject: [PATCH 02/11] clan-cli: fix mypy --- pkgs/clan-cli/clan-admin.py | 12 ++++++------ pkgs/clan-cli/clan.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkgs/clan-cli/clan-admin.py b/pkgs/clan-cli/clan-admin.py index 9ec288982..cafd59742 100755 --- a/pkgs/clan-cli/clan-admin.py +++ b/pkgs/clan-cli/clan-admin.py @@ -4,7 +4,7 @@ import os import subprocess -def create(args): +def create(args: argparse.Namespace) -> None: os.makedirs(args.folder, exist_ok=True) # TODO create clan template in flake subprocess.Popen( @@ -18,7 +18,7 @@ def create(args): ) -def edit(args): +def edit(args: argparse.Namespace) -> None: # TODO add some cli options to change certain options without relying on a text editor clan_flake = f"{args.folder}/flake.nix" if os.path.isfile(clan_flake): @@ -34,7 +34,7 @@ def edit(args): ) -def rebuild(args): +def rebuild(args: argparse.Namespace) -> None: # TODO get clients from zerotier cli? if args.host: print(f"would redeploy {args.host} from clan {args.folder}") @@ -42,7 +42,7 @@ def rebuild(args): print(f"would redeploy all hosts from clan {args.folder}") -def destroy(args): +def destroy(args: argparse.Namespace) -> None: # TODO get clan folder & hosts from somwhere (maybe ~/.config/clan/$name /) # send some kind of kill signal, then remove the folder if args.yes: @@ -53,14 +53,14 @@ def destroy(args): ) -def backup(args): +def backup(args: argparse.Namespace) -> None: if args.host: print(f"would backup {args.host} from clan {args.folder}") else: print(f"would backup all hosts from clan {args.folder}") -def git(args): +def git(args: argparse.Namespace) -> None: subprocess.Popen( [ "git", diff --git a/pkgs/clan-cli/clan.py b/pkgs/clan-cli/clan.py index 8528c2b0b..2310d8b70 100755 --- a/pkgs/clan-cli/clan.py +++ b/pkgs/clan-cli/clan.py @@ -1,9 +1,9 @@ # !/usr/bin/env python3 -import sys import subprocess +import sys -def showhelp(): +def showhelp() -> None: print(''' usage: clan admin ... From 95df33e2bc1fe52445a36f012c4dea5f85bcc332 Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 20 Jul 2023 19:28:48 +0200 Subject: [PATCH 03/11] clan-cli: rename clan-admin.py -> clan_admin.py --- pkgs/clan-cli/{clan-admin.py => clan_admin.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkgs/clan-cli/{clan-admin.py => clan_admin.py} (100%) diff --git a/pkgs/clan-cli/clan-admin.py b/pkgs/clan-cli/clan_admin.py similarity index 100% rename from pkgs/clan-cli/clan-admin.py rename to pkgs/clan-cli/clan_admin.py From 1e5cce99a9ad91df1bb0b2d91368718853a7f639 Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 21 Jul 2023 01:00:06 +0200 Subject: [PATCH 04/11] clan-cli: manage sub-commands via python argparse --- pkgs/clan-cli/clan.py | 29 ++++-------- pkgs/clan-cli/clan_admin.py | 90 +++++++++++++++++++----------------- pkgs/clan-cli/pyproject.toml | 2 +- 3 files changed, 58 insertions(+), 63 deletions(-) diff --git a/pkgs/clan-cli/clan.py b/pkgs/clan-cli/clan.py index 2310d8b70..8fcdf0c71 100755 --- a/pkgs/clan-cli/clan.py +++ b/pkgs/clan-cli/clan.py @@ -1,24 +1,15 @@ # !/usr/bin/env python3 -import subprocess -import sys +import argparse + +import clan_admin -def showhelp() -> None: - print(''' - usage: - clan admin ... - clan join ... - clan delete ... - ''') +# this will be the entrypoint under /bin/clan (see pyproject.toml config) +def clan() -> None: + parser = argparse.ArgumentParser(description="cLAN tool") + subparsers = parser.add_subparsers() + # init clan admin + parser_admin = subparsers.add_parser("admin") + clan_admin.make_parser(parser_admin) -try: - cmd = f'clan-{sys.argv[1]}' -except: # noqa - showhelp() - -try: - subprocess.Popen([cmd] + sys.argv[2:]) -except FileNotFoundError: - print(f'command {cmd} not found') - exit(2) diff --git a/pkgs/clan-cli/clan_admin.py b/pkgs/clan-cli/clan_admin.py index cafd59742..537aa2758 100755 --- a/pkgs/clan-cli/clan_admin.py +++ b/pkgs/clan-cli/clan_admin.py @@ -69,54 +69,58 @@ def git(args: argparse.Namespace) -> None: ] + args.git_args ) +# takes a (sub)parser and configures it +def make_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-f", + "--folder", + help="the folder where the clan is defined, default to the current folder", + default=os.environ["PWD"], + ) + subparser = parser.add_subparsers( + title="command", + description="the command to run", + help="the command to run", + required=True, + ) -parser = argparse.ArgumentParser(description="clan-admin") -parser.add_argument( - "-f", - "--folder", - help="the folder where the clan is defined, default to the current folder", - default=os.environ["PWD"], -) -subparser = parser.add_subparsers( - title="command", - description="the command to run", - help="the command to run", - required=True, -) + parser_create = subparser.add_parser("create", help="create a new clan") + parser_create.set_defaults(func=create) -parser_create = subparser.add_parser("create", help="create a new clan") -parser_create.set_defaults(func=create) + parser_edit = subparser.add_parser("edit", help="edit a clan") + parser_edit.set_defaults(func=edit) -parser_edit = subparser.add_parser("edit", help="edit a clan") -parser_edit.set_defaults(func=edit) + parser_rebuild = subparser.add_parser( + "rebuild", help="build configuration of a clan and push it to the target" + ) + parser_rebuild.add_argument( + "--host", help="specify single host to rebuild", default=None + ) + parser_rebuild.set_defaults(func=rebuild) -parser_rebuild = subparser.add_parser( - "rebuild", help="build configuration of a clan and push it to the target" -) -parser_rebuild.add_argument( - "--host", help="specify single host to rebuild", default=None -) -parser_rebuild.set_defaults(func=rebuild) + parser_destroy = subparser.add_parser( + "destroy", help="destroy a clan, including all the machines" + ) + parser_destroy.add_argument( + "--yes", help="specify single host to rebuild", action="store_true" + ) + parser_destroy.set_defaults(func=destroy) -parser_destroy = subparser.add_parser( - "destroy", help="destroy a clan, including all the machines" -) -parser_destroy.add_argument( - "--yes", help="specify single host to rebuild", action="store_true" -) -parser_destroy.set_defaults(func=destroy) + parser_backup = subparser.add_parser( + "backup", help="backup all the state of all machines in a clan or just a single one" + ) + parser_backup.add_argument( + "--host", help="specify single host to rebuild", default=None + ) + parser_backup.set_defaults(func=backup) -parser_backup = subparser.add_parser( - "backup", help="backup all the state of all machines in a clan or just a single one" -) -parser_backup.add_argument( - "--host", help="specify single host to rebuild", default=None -) -parser_backup.set_defaults(func=backup) + parser_git = subparser.add_parser("git", help="control the clan repo via git") + parser_git.add_argument("git_args", nargs="*") + parser_git.set_defaults(func=git) -parser_git = subparser.add_parser("git", help="control the clan repo via git") -parser_git.add_argument("git_args", nargs="*") -parser_git.set_defaults(func=git) -args = parser.parse_args() -args.func(args) +# entry point if this file is executed directly +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="clan-admin") + args = parser.parse_args() + args.func(args) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index cbf9dc67c..f395f23d0 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -9,7 +9,7 @@ include = ["clan.py"] name = "clan" description = "cLAN CLI tool" dynamic = ["version"] -scripts = {clan = "clan:my_cli"} +scripts = {clan = "clan:clan"} [tool.pytest.ini_options] addopts = "--cov . --cov-report term --cov-fail-under=100 --no-cov-on-fail" From af7e4d7bb93c78d49aef6e0cb613d3c3806288ec Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 21 Jul 2023 01:08:11 +0200 Subject: [PATCH 05/11] clan-cli: shell completion for bash, zsh, fish --- pkgs/clan-cli/clan.py | 4 ++++ pkgs/clan-cli/default.nix | 32 ++++++++++++++++++++------------ pkgs/clan-cli/pyproject.toml | 4 ++++ pkgs/clan-cli/shell.nix | 9 +++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/pkgs/clan-cli/clan.py b/pkgs/clan-cli/clan.py index 8fcdf0c71..7c34a51a1 100755 --- a/pkgs/clan-cli/clan.py +++ b/pkgs/clan-cli/clan.py @@ -1,6 +1,8 @@ # !/usr/bin/env python3 import argparse +import argcomplete + import clan_admin @@ -12,4 +14,6 @@ def clan() -> None: # init clan admin parser_admin = subparsers.add_parser("admin") clan_admin.make_parser(parser_admin) + argcomplete.autocomplete(parser) + parser.parse_args() diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 59d2fa46d..db65e551e 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -1,20 +1,21 @@ -{ - pkgs ? import {}, - - lib ? pkgs.lib, - python3 ? pkgs.python3, - ruff ? pkgs.ruff, - runCommand ? pkgs.runCommand, -}: let +{ pkgs ? import { } +, lib ? pkgs.lib +, python3 ? pkgs.python3 +, ruff ? pkgs.ruff +, runCommand ? pkgs.runCommand +, installShellFiles ? pkgs.installShellFiles +, +}: +let pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); name = pyproject.project.name; src = lib.cleanSource ./.; dependencies = lib.attrValues { - # inherit (python3.pkgs) - # some-package - # ; + inherit (python3.pkgs) + argcomplete + ; }; devDependencies = lib.attrValues { @@ -34,15 +35,22 @@ format = "pyproject"; nativeBuildInputs = [ python3.pkgs.setuptools + installShellFiles ]; propagatedBuildInputs = dependencies ++ [ ]; passthru.tests = { inherit check; }; passthru.devDependencies = devDependencies; + postInstall = '' + installShellCompletion --bash --name clan \ + <(${python3.pkgs.argcomplete}/bin/register-python-argcomplete --shell bash clan) + installShellCompletion --fish --name clan.fish \ + <(${python3.pkgs.argcomplete}/bin/register-python-argcomplete --shell fish clan) + ''; }; - checkPython = python3.withPackages (ps: devDependencies); + checkPython = python3.withPackages (ps: devDependencies ++ dependencies); check = runCommand "${name}-check" { } '' cp -r ${src} ./src diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index f395f23d0..33d3eb4f8 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -32,6 +32,10 @@ ignore_missing_imports = true module = "pytest.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "argcomplete.*" +ignore_missing_imports = true + [tool.ruff] line-length = 88 diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 2b7b0748c..9df8cbe8c 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -41,6 +41,15 @@ let export PATH="$tmp_path/bin:${checkScript}/bin:$PATH" export PYTHONPATH="$repo_root:$tmp_path/${pythonWithDeps.sitePackages}" + + export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" + mkdir -p \ + $tmp_path/share/fish/vendor_completions.d \ + $tmp_path/share/bash-completion/completions \ + $tmp_path/share/zsh/site-functions + register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish + register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan ''; }; in From a0701bf988e53d2f369ee51dd58a326a6b33eb37 Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 21 Jul 2023 01:08:35 +0200 Subject: [PATCH 06/11] clan-cli: expose via flake packages --- flake.nix | 1 + pkgs/clan-cli/flake-module.nix | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/flake.nix b/flake.nix index 77741213f..1a18e4b16 100644 --- a/flake.nix +++ b/flake.nix @@ -22,6 +22,7 @@ ./flake-parts/packages.nix ./flake-parts/formatting.nix ./templates/python-project/flake-module.nix + ./pkgs/clan-cli/flake-module.nix ]; flake = { nixosConfigurations.installer = lib.nixosSystem { diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index 1e85e6079..6426e4811 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -1,10 +1,12 @@ { - perSystem = {pkgs, ...}: let - pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); - name = pyproject.project.name; - package = pkgs.callPackage ./default.nix {}; - in { - # packages.${name} = package; - checks.python-template = package.tests.check; - }; + perSystem = { pkgs, ... }: + let + pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); + name = pyproject.project.name; + package = pkgs.callPackage ./default.nix { }; + in + { + packages.${name} = package; + checks.${name} = package.tests.check; + }; } From 4c4ff01b647c508b51f65258de89612d5765f2f7 Mon Sep 17 00:00:00 2001 From: DavHau Date: Fri, 21 Jul 2023 02:01:39 +0200 Subject: [PATCH 07/11] clan-cli: write some unit tests --- pkgs/clan-cli/clan.py | 4 +++- pkgs/clan-cli/clan_admin.py | 20 ++++++++++++-------- pkgs/clan-cli/default.nix | 1 + pkgs/clan-cli/tests/test_clan_admin.py | 17 +++++++++++++++++ pkgs/clan-cli/tests/test_cli.py | 15 +++++++++------ 5 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 pkgs/clan-cli/tests/test_clan_admin.py diff --git a/pkgs/clan-cli/clan.py b/pkgs/clan-cli/clan.py index 7c34a51a1..5995e1a78 100755 --- a/pkgs/clan-cli/clan.py +++ b/pkgs/clan-cli/clan.py @@ -1,5 +1,6 @@ # !/usr/bin/env python3 import argparse +import sys import argcomplete @@ -16,4 +17,5 @@ def clan() -> None: clan_admin.make_parser(parser_admin) argcomplete.autocomplete(parser) parser.parse_args() - + if len(sys.argv) == 1: + parser.print_help() diff --git a/pkgs/clan-cli/clan_admin.py b/pkgs/clan-cli/clan_admin.py index 537aa2758..bf230ec15 100755 --- a/pkgs/clan-cli/clan_admin.py +++ b/pkgs/clan-cli/clan_admin.py @@ -2,6 +2,7 @@ import argparse import os import subprocess +import sys def create(args: argparse.Namespace) -> None: @@ -18,7 +19,7 @@ def create(args: argparse.Namespace) -> None: ) -def edit(args: argparse.Namespace) -> None: +def edit(args: argparse.Namespace) -> None: # pragma: no cover # TODO add some cli options to change certain options without relying on a text editor clan_flake = f"{args.folder}/flake.nix" if os.path.isfile(clan_flake): @@ -30,11 +31,11 @@ def edit(args: argparse.Namespace) -> None: ) else: print( - f"{args.folder} has no flake.nix, so it does not seem to be the clan root folder" + f"{args.folder} has no flake.nix, so it does not seem to be the clan root folder", ) -def rebuild(args: argparse.Namespace) -> None: +def rebuild(args: argparse.Namespace) -> None: # pragma: no cover # TODO get clients from zerotier cli? if args.host: print(f"would redeploy {args.host} from clan {args.folder}") @@ -42,7 +43,7 @@ def rebuild(args: argparse.Namespace) -> None: print(f"would redeploy all hosts from clan {args.folder}") -def destroy(args: argparse.Namespace) -> None: +def destroy(args: argparse.Namespace) -> None: # pragma: no cover # TODO get clan folder & hosts from somwhere (maybe ~/.config/clan/$name /) # send some kind of kill signal, then remove the folder if args.yes: @@ -53,14 +54,14 @@ def destroy(args: argparse.Namespace) -> None: ) -def backup(args: argparse.Namespace) -> None: +def backup(args: argparse.Namespace) -> None: # pragma: no cover if args.host: print(f"would backup {args.host} from clan {args.folder}") else: print(f"would backup all hosts from clan {args.folder}") -def git(args: argparse.Namespace) -> None: +def git(args: argparse.Namespace) -> None: # pragma: no cover subprocess.Popen( [ "git", @@ -119,8 +120,11 @@ def make_parser(parser: argparse.ArgumentParser) -> None: parser_git.set_defaults(func=git) -# entry point if this file is executed directly -if __name__ == "__main__": +def clan_admin() -> None: # pragma: no cover parser = argparse.ArgumentParser(description="clan-admin") args = parser.parse_args() args.func(args) + +# entry point if this file is executed directly +if __name__ == "__main__": # pragma: no cover + clan_admin() diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index db65e551e..9c9cd1963 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -25,6 +25,7 @@ let mypy pytest pytest-cov + pytest-subprocess setuptools wheel ; diff --git a/pkgs/clan-cli/tests/test_clan_admin.py b/pkgs/clan-cli/tests/test_clan_admin.py new file mode 100644 index 000000000..4440242b1 --- /dev/null +++ b/pkgs/clan-cli/tests/test_clan_admin.py @@ -0,0 +1,17 @@ +import argparse + +import clan_admin + + +def test_make_parser(): + parser = argparse.ArgumentParser() + clan_admin.make_parser(parser) + +# using fp fixture from pytest-subprocess +def test_create(fp): + cmd = ["nix", "flake", "init", "-t", fp.any()] + fp.register(cmd) + args = argparse.Namespace(folder="./my-clan") + clan_admin.create(args) + assert fp.call_count(cmd) == 1 + diff --git a/pkgs/clan-cli/tests/test_cli.py b/pkgs/clan-cli/tests/test_cli.py index 5311f2bda..94d94cce4 100644 --- a/pkgs/clan-cli/tests/test_cli.py +++ b/pkgs/clan-cli/tests/test_cli.py @@ -1,16 +1,19 @@ import sys -import my_tool +import pytest + +import clan def test_no_args(capsys): - my_tool.my_cli() + clan.clan() captured = capsys.readouterr() assert captured.out.startswith("usage:") -def test_version(capsys, monkeypatch): - monkeypatch.setattr(sys, "argv", ["", "--version"]) - my_tool.my_cli() +def test_help(capsys, monkeypatch): + monkeypatch.setattr(sys, "argv", ["", "--help"]) + with pytest.raises(SystemExit): + clan.clan() captured = capsys.readouterr() - assert captured.out.startswith("Version:") + assert captured.out.startswith("usage:") From e4fbb08f91d927a943d3d011cddaadabbb081339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 21 Jul 2023 11:11:36 +0200 Subject: [PATCH 08/11] make argcomplete optional In case someone ever messes up the PYTHONPATH this will gracefull degrade --- pkgs/clan-cli/clan.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-cli/clan.py b/pkgs/clan-cli/clan.py index 5995e1a78..45261fb40 100755 --- a/pkgs/clan-cli/clan.py +++ b/pkgs/clan-cli/clan.py @@ -2,7 +2,11 @@ import argparse import sys -import argcomplete +has_argcomplete = True +try: + import argcomplete +except ImportError: + has_argcomplete = False import clan_admin @@ -15,7 +19,8 @@ def clan() -> None: # init clan admin parser_admin = subparsers.add_parser("admin") clan_admin.make_parser(parser_admin) - argcomplete.autocomplete(parser) + if has_argcomplete: + argcomplete.autocomplete(parser) parser.parse_args() if len(sys.argv) == 1: parser.print_help() From 88295ce77d3501c6417ef8f6b469099fa3ce384d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 21 Jul 2023 11:21:09 +0200 Subject: [PATCH 09/11] treefmt: add ruff + black --- flake-parts/formatting.nix | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flake-parts/formatting.nix b/flake-parts/formatting.nix index a1b21597c..7d2e6b90f 100644 --- a/flake-parts/formatting.nix +++ b/flake-parts/formatting.nix @@ -1,4 +1,4 @@ -{ self +{ lib , inputs , ... }: { @@ -11,5 +11,17 @@ treefmt.flakeFormatter = true; treefmt.programs.nixpkgs-fmt.enable = true; treefmt.programs.shellcheck.enable = true; + treefmt.settings.formatter.python = { + command = "sh"; + options = [ + "-eucx" + '' + ${lib.getExe pkgs.ruff} --fix "$@" + ${lib.getExe pkgs.black} "$@" + '' + "--" # this argument is ignored by bash + ]; + includes = [ "*.py" ]; + }; }; } From 22849366d84d82d67e9ffec616c2c2fde1351793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 21 Jul 2023 11:22:02 +0200 Subject: [PATCH 10/11] apply ruff autoformatting --- pkgs/clan-cli/clan_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/clan-cli/clan_admin.py b/pkgs/clan-cli/clan_admin.py index bf230ec15..7b216fcfb 100755 --- a/pkgs/clan-cli/clan_admin.py +++ b/pkgs/clan-cli/clan_admin.py @@ -2,7 +2,6 @@ import argparse import os import subprocess -import sys def create(args: argparse.Namespace) -> None: From 3ab90e67857d4e11af974c67fb54a07786d962dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 21 Jul 2023 11:23:10 +0200 Subject: [PATCH 11/11] add deadnix to formatter --- flake-parts/formatting.nix | 15 ++++++++++++++- flake-parts/pre-commit.nix | 2 +- flake.nix | 1 - installer.nix | 3 +-- pkgs/clan-cli/default.nix | 2 +- pkgs/clan-cli/shell.nix | 1 - templates/python-project/default.nix | 2 +- templates/python-project/flake-module.nix | 2 -- templates/python-project/shell.nix | 1 - 9 files changed, 18 insertions(+), 11 deletions(-) diff --git a/flake-parts/formatting.nix b/flake-parts/formatting.nix index 7d2e6b90f..15cbff2c3 100644 --- a/flake-parts/formatting.nix +++ b/flake-parts/formatting.nix @@ -9,8 +9,21 @@ treefmt.projectRootFile = "flake.nix"; treefmt.flakeCheck = true; treefmt.flakeFormatter = true; - treefmt.programs.nixpkgs-fmt.enable = true; treefmt.programs.shellcheck.enable = true; + treefmt.settings.formatter.nix = { + command = "sh"; + options = [ + "-eucx" + '' + # First deadnix + ${lib.getExe pkgs.deadnix} --edit "$@" + # Then nixpkgs-fmt + ${lib.getExe pkgs.nixpkgs-fmt} "$@" + '' + "--" # this argument is ignored by bash + ]; + includes = [ "*.nix" ]; + }; treefmt.settings.formatter.python = { command = "sh"; options = [ diff --git a/flake-parts/pre-commit.nix b/flake-parts/pre-commit.nix index d44afc889..fd579e9fe 100644 --- a/flake-parts/pre-commit.nix +++ b/flake-parts/pre-commit.nix @@ -1,4 +1,4 @@ -{ self, ... }: { +{ ... }: { pre-commit.settings.hooks.alejandra.enable = true; pre-commit.settings.hooks.shellcheck.enable = true; } diff --git a/flake.nix b/flake.nix index 1a18e4b16..be2d9834d 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,6 @@ outputs = inputs @ { flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } ({ lib , config - , self , ... }: { systems = lib.systems.flakeExposed; diff --git a/installer.nix b/installer.nix index 8da5d50fb..2daef898f 100644 --- a/installer.nix +++ b/installer.nix @@ -1,5 +1,4 @@ -{ config -, lib +{ lib , pkgs , ... }: { diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 9c9cd1963..74e88d5b1 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -51,7 +51,7 @@ let ''; }; - checkPython = python3.withPackages (ps: devDependencies ++ dependencies); + checkPython = python3.withPackages (_ps: devDependencies ++ dependencies); check = runCommand "${name}-check" { } '' cp -r ${src} ./src diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 9df8cbe8c..79cb983bb 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -1,5 +1,4 @@ { pkgs ? import { } -, system ? builtins.currentSystem , }: let diff --git a/templates/python-project/default.nix b/templates/python-project/default.nix index 30dae1b79..18fa4c6f8 100644 --- a/templates/python-project/default.nix +++ b/templates/python-project/default.nix @@ -42,7 +42,7 @@ let passthru.devDependencies = devDependencies; }; - checkPython = python3.withPackages (ps: devDependencies ++ dependencies); + checkPython = python3.withPackages (_ps: devDependencies ++ dependencies); check = runCommand "${name}-check" { } '' cp -r ${src} ./src diff --git a/templates/python-project/flake-module.nix b/templates/python-project/flake-module.nix index 74218c6de..cfd5c552f 100644 --- a/templates/python-project/flake-module.nix +++ b/templates/python-project/flake-module.nix @@ -1,8 +1,6 @@ { perSystem = { pkgs, ... }: let - pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); - name = pyproject.project.name; package = pkgs.callPackage ./default.nix { }; in { diff --git a/templates/python-project/shell.nix b/templates/python-project/shell.nix index 2b7b0748c..861fe5efd 100644 --- a/templates/python-project/shell.nix +++ b/templates/python-project/shell.nix @@ -1,5 +1,4 @@ { pkgs ? import { } -, system ? builtins.currentSystem , }: let