more well-defined CLI with custom mutable context

This commit is contained in:
Jörn-Michael Miehe 2021-10-20 10:54:41 +02:00
parent 217a5fa75b
commit 195fbd24fe
6 changed files with 73 additions and 40 deletions

View file

@ -1,20 +1,45 @@
import os
from pathlib import Path
import attr
import click
from ..config import Config
class KiwiCLI(click.MultiCommand):
"""Command Line Interface spread over multiple files in this directory"""
def list_commands(self, ctx):
result = []
for filename in os.listdir(os.path.abspath(os.path.dirname(__file__))):
if filename.startswith("cmd_") and filename.endswith(".py"):
result.append(filename[4:-3])
result.sort()
return result
"""list all the commands defined by cmd_*.py files in this directory"""
return (
filename[4:-3]
for filename in os.listdir(os.path.abspath(os.path.dirname(__file__)))
if filename.startswith("cmd_") and filename.endswith(".py")
)
def get_command(self, ctx, name):
"""import and return a specific command"""
try:
mod = __import__(f"kiwi_scp.commands.cmd_{name}", None, None, ["cmd"])
except ImportError:
return
return mod.cmd
@attr.s
class KiwiCTX:
"""this class is used as the commands' shared context"""
instance: Path = attr.ib(factory=lambda: Path('.'))
@property
def config(self) -> Config:
"""shorthand: get the current configuration"""
return Config.from_instance(self.instance)
pass_kiwi_ctx = click.make_pass_decorator(KiwiCTX, ensure=True)

View file

@ -1,21 +1,20 @@
import click
from ..config import Config
from .cli import KiwiCTX, pass_kiwi_ctx
@click.command(
"init",
short_help="Initializes a repo."
short_help="Initializes kiwi-scp"
)
@click.argument(
"path",
required=False,
type=click.Path(resolve_path=True)
)
@click.pass_context
def cmd(ctx, path):
"""Initializes a repository."""
kiwi: Config = ctx.obj["cfg"]
click.echo("Hello init")
click.echo(kiwi.kiwi_yml)
@pass_kiwi_ctx
def cmd(ctx: KiwiCTX, path):
"""Initialize or reconfigure a kiwi-scp instance"""
click.echo(f"Hello init, kiwi version {ctx.config.version}")
pass

View file

@ -1,3 +1,4 @@
import functools
import re
from ipaddress import IPv4Network
from pathlib import Path
@ -6,7 +7,7 @@ from typing import Optional, Dict, List, Any
import yaml
from pydantic import BaseModel, constr, root_validator, validator
from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME
from ._constants import RE_SEMVER, RE_VARNAME, HEADER_KIWI_CONF_NAME, KIWI_CONF_NAME
# indent yaml lists
@ -138,6 +139,20 @@ class Config(BaseModel):
cidr="10.22.46.0/24",
)
@classmethod
@functools.lru_cache(maxsize=5)
def from_instance(cls, instance: Path):
"""parses an actual kiwi.yml from disk"""
try:
with open(instance.joinpath(KIWI_CONF_NAME)) as kc:
yml = yaml.safe_load(kc)
return cls.parse_obj(yml)
except FileNotFoundError:
# return the defaults if no kiwi.yml found
return cls()
@property
def kiwi_dict(self) -> Dict[str, Any]:
"""write this object as a dictionary of strings"""
@ -201,7 +216,7 @@ class Config(BaseModel):
try:
return [str(value)]
except Exception as e:
except Exception:
# undefined format
raise ValueError("Invalid Shells Format")
@ -230,7 +245,7 @@ class Config(BaseModel):
# handle single project name
result.append({"name": str(entry)})
except Exception as e:
except Exception:
# undefined format
raise ValueError("Invalid Projects Format")
@ -246,7 +261,7 @@ class Config(BaseModel):
# handle as a single project name
return [{"name": str(value)}]
except Exception as e:
except Exception:
# undefined format
raise ValueError("Invalid Projects Format")

View file

@ -1,21 +1,14 @@
import click
import yaml
from kiwi_scp.commands.cli import KiwiCLI
from kiwi_scp.config import Config
@click.command(cls=KiwiCLI)
@click.pass_context
def main(ctx):
"""A complex command line interface."""
with open("./kiwi.yml") as kc:
yml = yaml.safe_load(kc)
ctx.ensure_object(dict)
ctx.obj["cfg"] = Config(**yml)
def main():
"""main entry point for command line interface"""
click.echo("Hello main")
pass
if __name__ == "__main__":

18
poetry.lock generated
View file

@ -10,7 +10,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "attrs"
version = "21.2.0"
description = "Classes Without Boilerplate"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@ -73,7 +73,7 @@ python-versions = "*"
[[package]]
name = "filelock"
version = "3.3.0"
version = "3.3.1"
description = "A platform independent file lock."
category = "dev"
optional = false
@ -102,7 +102,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[[package]]
name = "importlib-resources"
version = "5.2.2"
version = "5.3.0"
description = "Read resources from Python packages"
category = "dev"
optional = false
@ -113,7 +113,7 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
[[package]]
name = "iniconfig"
@ -283,7 +283,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.1"
content-hash = "eb1a3ab9af78ac7898062245858dbf9e9a27e82a6484f45f14b8db6c7fe812d6"
content-hash = "872024f4d335f920d178ef6b631785f4f6908f8f9f87d970932bd54dee7fd297"
[metadata.files]
atomicwrites = [
@ -315,16 +315,16 @@ distlib = [
{file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
]
filelock = [
{file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"},
{file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"},
{file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"},
{file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"},
]
importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
importlib-resources = [
{file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"},
{file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"},
{file = "importlib_resources-5.3.0-py3-none-any.whl", hash = "sha256:7a65eb0d8ee98eedab76e6deb51195c67f8e575959f6356a6e15fd7e1148f2a3"},
{file = "importlib_resources-5.3.0.tar.gz", hash = "sha256:f2e58e721b505a79abe67f5868d99f8886aec8594c962c7490d0c22925f518da"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},

View file

@ -6,14 +6,15 @@ authors = ["ldericher <40151420+ldericher@users.noreply.github.com>"]
[tool.poetry.dependencies]
python = "^3.6.1"
PyYAML = "^5.4.1"
pydantic = "^1.8.2"
attrs = "^21.2.0"
click = "^8.0.3"
pydantic = "^1.8.2"
PyYAML = "^5.4.1"
[tool.poetry.dev-dependencies]
virtualenv = "^20.8.1"
pytest = "^6.2.5"
toml = "^0.10.2"
virtualenv = "^20.8.1"
[tool.poetry.scripts]
kiwi = "kiwi_scp.scripts.kiwi:main"