add decorator for cli context
This commit is contained in:
@@ -5,3 +5,4 @@ build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
.venv
|
||||
|
||||
+6
-6
@@ -9,8 +9,8 @@ Prerequisites:
|
||||
```shell
|
||||
# Arch Linux
|
||||
pacman -S uv just
|
||||
# optinal
|
||||
pacman -S just-lsp yamlfmt shellcheck
|
||||
# optional
|
||||
pacman -S just-lsp yamlfmt shellcheck typos
|
||||
```
|
||||
|
||||
The development environment and testing requires multiple specific Python versions; they can be installed with (inside the pulled git project directory):
|
||||
@@ -31,13 +31,13 @@ just test
|
||||
|
||||
## Formatting
|
||||
|
||||
Formatting of YAML files requires [yamlfmt](https://github.com/google/yamlfmt) to be installed seperately.
|
||||
Formatting of YAML files requires [yamlfmt](https://github.com/google/yamlfmt) to be installed separately.
|
||||
|
||||
A script is available to run the format check.
|
||||
|
||||
```shell
|
||||
just format-check
|
||||
just schema-scheck
|
||||
just schema-check
|
||||
```
|
||||
|
||||
## Linting
|
||||
@@ -45,6 +45,7 @@ just schema-scheck
|
||||
Linting requires [shellcheck](https://github.com/koalaman/shellcheck) to be installed separately.
|
||||
Validating `pyproject.toml` against the checked-in uv schema also requires
|
||||
[jq](https://jqlang.org/).
|
||||
Spell checking requires [typos](https://github.com/crate-ci/typos) to be installed separately.
|
||||
|
||||
A script is available to run the lint check.
|
||||
|
||||
@@ -55,9 +56,8 @@ just type-check
|
||||
|
||||
## Documentation
|
||||
|
||||
A script is available to run the documentaion generation.
|
||||
A script is available to run the documentation generation.
|
||||
|
||||
```shell
|
||||
just docs
|
||||
just man
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@ app_name := "dbyaml"
|
||||
|
||||
# interactive recipe selection
|
||||
[group('info')]
|
||||
@select-shell:
|
||||
@select:
|
||||
just --choose
|
||||
|
||||
prelude := '''
|
||||
@@ -25,13 +25,17 @@ version cmd=app_name:
|
||||
{{prelude}}
|
||||
{{cmd}} --version
|
||||
|
||||
# run all dev tools
|
||||
# run checkers
|
||||
[parallel, group('dev')]
|
||||
ci: test format-check lint-check type-check schema-check
|
||||
ci-check: test format-check lint-check type-check schema-check
|
||||
|
||||
# run all dev tools
|
||||
# run fixes
|
||||
[parallel, group('dev')]
|
||||
ci-gen: format-fix lint-fix type-fix docs
|
||||
ci-fix: format-fix lint-fix type-fix
|
||||
|
||||
# run generators (docs)
|
||||
[parallel, group('dev')]
|
||||
ci-gen: docs
|
||||
|
||||
# setup virtual environment
|
||||
[group('install')]
|
||||
@@ -97,9 +101,8 @@ ci-gen: format-fix lint-fix type-fix docs
|
||||
echo "running checker; shellcheck (shell)"
|
||||
shellcheck scripts/*.sh
|
||||
|
||||
# TODO:
|
||||
# Spell checking
|
||||
# uv run --only-group=check typos
|
||||
echo "running spell-checker: typos"
|
||||
uv run --only-group=check typos
|
||||
|
||||
# fix typing
|
||||
[group('dev')]
|
||||
@@ -109,25 +112,29 @@ ci-gen: format-fix lint-fix type-fix docs
|
||||
|
||||
echo "running checker; shellcheck (shell)"
|
||||
shellcheck scripts/*.sh
|
||||
# TODO: run fix
|
||||
|
||||
echo "running spell-checker: typos"
|
||||
uv run --only-group=check typos --write-changes
|
||||
|
||||
# check project schema
|
||||
[group('dev')]
|
||||
@schema-check:
|
||||
echo "running schema checker (Metadata dbyaml)"
|
||||
echo "running schema checker (dbyaml config)"
|
||||
uv run scripts/generate_schema.py | diff - src/dbyaml/resources/dbyaml.schema.json
|
||||
|
||||
# generate all docs
|
||||
[group('dev')]
|
||||
docs: schema-generate man
|
||||
docs: schema-generate docs-man
|
||||
|
||||
# generate project schema
|
||||
[group('dev')]
|
||||
@schema-generate:
|
||||
echo "Python project metadata and dbyaml schema"
|
||||
echo "running schema checker (dbyaml config)"
|
||||
uv run scripts/generate_schema.py --outfile src/dbyaml/resources/dbyaml.schema.json
|
||||
|
||||
# generate man pages
|
||||
[group('dev')]
|
||||
@man:
|
||||
@docs-man:
|
||||
echo "generate man"
|
||||
uv run click-man --man-version $(uv version --short) dbyaml
|
||||
|
||||
@@ -11,7 +11,7 @@ def generate_schema_from_click(ctx, additional_properties=False):
|
||||
schema: dict[str, Any] = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": ("./src/dbyaml/resources/dbyaml.schema.json"),
|
||||
"$comment": "tool.dbyaml table in pyproject.toml",
|
||||
"$comment": "dbyaml table",
|
||||
}
|
||||
schema_ctx = generate_node_from_click(ctx, additional_properties)
|
||||
schema.update(schema_ctx)
|
||||
|
||||
+101
-4
@@ -3,6 +3,7 @@ import platform
|
||||
import sys
|
||||
|
||||
from dbyaml import COMPILED, __version__
|
||||
from dbyaml.decorator import uses_decorator
|
||||
|
||||
|
||||
def debugger_enabled():
|
||||
@@ -26,8 +27,87 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
import enum
|
||||
import json
|
||||
from functools import update_wrapper
|
||||
|
||||
@click.group
|
||||
import yaml
|
||||
|
||||
class FormatType(str, enum.Enum):
|
||||
@staticmethod
|
||||
def _generate_next_value_(name, start, count, last_values):
|
||||
return name
|
||||
|
||||
raw = enum.auto()
|
||||
yaml = enum.auto()
|
||||
json = enum.auto()
|
||||
|
||||
def pass_context(*d_args, **d_kwargs):
|
||||
"""
|
||||
uses FormatType for printouts in different formats
|
||||
|
||||
@pass_context
|
||||
@pass_context()
|
||||
@pass_context(obj)
|
||||
@pass_context(module)
|
||||
@pass_context(module.function)
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@click.pass_context
|
||||
def new_func(ctx, *args, **kwargs):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj.update(ctx.params)
|
||||
if ctx.obj["debug"]:
|
||||
click.echo(f"DEBUG {f.__name__} obj:\n{yaml.dump(ctx.obj)}", err=True)
|
||||
|
||||
def echo(obj):
|
||||
if ctx.obj["format_type"] == FormatType.json:
|
||||
res = click.echo(json.dumps(obj))
|
||||
elif ctx.obj["format_type"] == FormatType.yaml:
|
||||
res = click.echo(yaml.dump(obj))
|
||||
else:
|
||||
res = click.echo(obj)
|
||||
return res
|
||||
|
||||
ctx.echo = echo
|
||||
ret = ctx.invoke(f, ctx, *args, **kwargs)
|
||||
if ctx.obj["debug"]:
|
||||
click.echo(f"DEBUG {f.__name__} return: {ret}")
|
||||
return ret
|
||||
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
isf = False
|
||||
if (
|
||||
len(d_args)
|
||||
and callable(d_args[0])
|
||||
and d_args[0] is not None
|
||||
and uses_decorator(
|
||||
func_name=d_args[0].__name__, deco_name="pass_context", filename=__file__
|
||||
)
|
||||
):
|
||||
isf = d_args[0]
|
||||
return decorator(isf) if isf else decorator
|
||||
|
||||
@click.group(
|
||||
invoke_without_command=True,
|
||||
context_settings={
|
||||
"help_option_names": ["-h", "--help"],
|
||||
"auto_envvar_prefix": "DBYAML",
|
||||
},
|
||||
)
|
||||
@click.argument(
|
||||
"filename",
|
||||
type=click.Path(
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
allow_dash=False,
|
||||
path_type=str,
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--config",
|
||||
type=click.Path(
|
||||
@@ -41,6 +121,15 @@ else:
|
||||
is_eager=True,
|
||||
help="Read configuration options from a configuration file.",
|
||||
)
|
||||
@click.option(
|
||||
"-t",
|
||||
"--format",
|
||||
"format_type",
|
||||
help="Format type.",
|
||||
type=click.Choice(FormatType, case_sensitive=False),
|
||||
envvar="DBYAML_FORMAT", # instead of auto DBYAML_FORMAT_TYPE
|
||||
show_envvar=True,
|
||||
)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--quiet",
|
||||
@@ -54,6 +143,7 @@ else:
|
||||
"-v",
|
||||
"--verbose",
|
||||
is_flag=True,
|
||||
show_envvar=True,
|
||||
help=("Emit verbose messages."),
|
||||
)
|
||||
@click.version_option(
|
||||
@@ -65,9 +155,16 @@ else:
|
||||
f"Python ({platform.python_implementation()}) {platform.python_version()}"
|
||||
),
|
||||
)
|
||||
@click.pass_context
|
||||
def main(ctx, config, quiet, verbose): ...
|
||||
@click.option("-D", "--debug", is_flag=True, show_envvar=True, hidden=True)
|
||||
@pass_context
|
||||
def main(ctx, filename, config, format_type, quiet, verbose, debug):
|
||||
"""The uncompromising db(yaml) utility."""
|
||||
ctx.obj["DEBUG"] = debug
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo("I was invoked without subcommand")
|
||||
else:
|
||||
click.echo(f"I am about to invoke {ctx.invoked_subcommand}")
|
||||
|
||||
@main.command
|
||||
@click.pass_context
|
||||
@pass_context
|
||||
def list(ctx): ...
|
||||
|
||||
+13
-1
@@ -1,6 +1,18 @@
|
||||
import enum
|
||||
|
||||
import click
|
||||
|
||||
def debugger_enabled() -> bool: ...
|
||||
@click.group
|
||||
def main(ctx: click.Context, config: str | None, quiet: bool) -> None: ...
|
||||
def main(
|
||||
ctx: click.Context,
|
||||
filename: str,
|
||||
config: str | None,
|
||||
format_type: FormatType | None,
|
||||
quiet: bool,
|
||||
verbose: bool,
|
||||
debug: bool,
|
||||
) -> None: ...
|
||||
def list(ctx: click.Context) -> None: ...
|
||||
|
||||
class FormatType(str, enum.Enum): ...
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import ast
|
||||
|
||||
|
||||
def attribute_path(node, left_to_right=True):
|
||||
"""
|
||||
if reverse=True:
|
||||
@module.decorator() -> ["module", "decorator()"]
|
||||
"""
|
||||
stack = []
|
||||
while node:
|
||||
if isinstance(node, ast.Name):
|
||||
stack.append(node.id)
|
||||
break
|
||||
elif isinstance(node, ast.Attribute):
|
||||
stack.append(node.attr)
|
||||
node = node.value
|
||||
elif isinstance(node, ast.Call):
|
||||
# if call append () to string representation
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
stack.append(f"{node.func.attr}()")
|
||||
node = node.func.value
|
||||
elif isinstance(node.func, ast.Name):
|
||||
stack.append(f"{node.func.id}()")
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
# ASTs parses from the outside in (i.e. stack is from right to left)
|
||||
if left_to_right:
|
||||
return list(reversed(stack))
|
||||
return stack
|
||||
|
||||
|
||||
def uses_decorator(func_name="", deco_name="", filename=None):
|
||||
"""check if 1st level functions are decorated"""
|
||||
if filename is None:
|
||||
return False
|
||||
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
tree = ast.parse(file.read())
|
||||
|
||||
node = None
|
||||
for n in ast.walk(tree):
|
||||
if isinstance(n, ast.FunctionDef) and n.name == func_name:
|
||||
node = n
|
||||
break
|
||||
|
||||
if not node:
|
||||
return False
|
||||
|
||||
for d in node.decorator_list:
|
||||
if attribute_path(d)[-1] == deco_name:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,8 @@
|
||||
import ast
|
||||
|
||||
def attribute_path(
|
||||
node: ast.Attribute | ast.Call | ast.Name, left_to_right: bool = True
|
||||
) -> list: ...
|
||||
def uses_decorator(
|
||||
func_name: str = "", deco_name: str = "", filename: str | None = None
|
||||
) -> bool: ...
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "./src/dbyaml/resources/dbyaml.schema.json",
|
||||
"$comment": "tool.dbyaml table in pyproject.toml",
|
||||
"$comment": "dbyaml table",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"format-type": {
|
||||
"enum": [
|
||||
"raw",
|
||||
"yaml",
|
||||
"json"
|
||||
],
|
||||
"description": "Format type."
|
||||
},
|
||||
"quiet": {
|
||||
"type": "boolean",
|
||||
"description": "Stop emitting all non-critical output. Error messages will still be emitted (which can silenced by 2>/dev/null).",
|
||||
@@ -15,6 +23,11 @@
|
||||
"description": "Emit verbose messages.",
|
||||
"default": false
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": null,
|
||||
"default": false
|
||||
},
|
||||
"list": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import click
|
||||
import yaml
|
||||
|
||||
import dbyaml.cli
|
||||
from dbyaml.cli import pass_context, uses_decorator # ty: ignore[unresolved-import]
|
||||
|
||||
|
||||
def test_uses_decorator_cli():
|
||||
assert uses_decorator("main", "pass_context", dbyaml.cli.__file__)
|
||||
assert uses_decorator("list", "pass_context", dbyaml.cli.__file__)
|
||||
|
||||
|
||||
def test_uses_decorator_wo():
|
||||
@click.command
|
||||
@pass_context
|
||||
def cli_wo(ctx): ...
|
||||
|
||||
assert uses_decorator("cli_wo", "pass_context", __file__)
|
||||
|
||||
|
||||
def test_uses_decorator_wo_long():
|
||||
@click.command
|
||||
@dbyaml.cli.pass_context # ty: ignore[unresolved-attribute]
|
||||
def cli_wh_long(ctx): ...
|
||||
|
||||
assert dbyaml.cli.uses_decorator("cli_wh_long", "pass_context", __file__) # ty: ignore[unresolved-attribute]
|
||||
|
||||
|
||||
def test_uses_decorator_call():
|
||||
@click.command
|
||||
@pass_context()
|
||||
def cli_call(ctx): ...
|
||||
|
||||
assert uses_decorator("cli_call", "pass_context()", __file__)
|
||||
|
||||
|
||||
def test_uses_decorator_obj():
|
||||
@click.command
|
||||
@pass_context(False)
|
||||
def cli_obj(ctx): ...
|
||||
|
||||
assert uses_decorator("cli_obj", "pass_context()", __file__)
|
||||
|
||||
|
||||
def test_uses_decorator_mod():
|
||||
@click.command
|
||||
@pass_context(yaml)
|
||||
def cli_mod(ctx): ...
|
||||
|
||||
assert uses_decorator("cli_mod", "pass_context()", __file__)
|
||||
|
||||
|
||||
def test_uses_decorator_func():
|
||||
@click.command
|
||||
@pass_context(yaml.dump)
|
||||
def cli_func(ctx): ...
|
||||
|
||||
assert uses_decorator("cli_func", "pass_context()", __file__)
|
||||
@@ -0,0 +1,10 @@
|
||||
import ast
|
||||
|
||||
from dbyaml.decorator import attribute_path
|
||||
|
||||
tree = ast.parse("alpha.beta().gamma").body[0].value # ty: ignore[unresolved-attribute]
|
||||
|
||||
|
||||
def test_attribute_path():
|
||||
path = attribute_path(tree)
|
||||
assert path == ["alpha", "beta()", "gamma"]
|
||||
Reference in New Issue
Block a user