add decorator for cli context
This commit is contained in:
@@ -5,3 +5,4 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
wheels/
|
wheels/
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
.venv
|
||||||
|
|||||||
+6
-6
@@ -9,8 +9,8 @@ Prerequisites:
|
|||||||
```shell
|
```shell
|
||||||
# Arch Linux
|
# Arch Linux
|
||||||
pacman -S uv just
|
pacman -S uv just
|
||||||
# optinal
|
# optional
|
||||||
pacman -S just-lsp yamlfmt shellcheck
|
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):
|
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
|
||||||
|
|
||||||
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.
|
A script is available to run the format check.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
just format-check
|
just format-check
|
||||||
just schema-scheck
|
just schema-check
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
@@ -45,6 +45,7 @@ just schema-scheck
|
|||||||
Linting requires [shellcheck](https://github.com/koalaman/shellcheck) to be installed separately.
|
Linting requires [shellcheck](https://github.com/koalaman/shellcheck) to be installed separately.
|
||||||
Validating `pyproject.toml` against the checked-in uv schema also requires
|
Validating `pyproject.toml` against the checked-in uv schema also requires
|
||||||
[jq](https://jqlang.org/).
|
[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.
|
A script is available to run the lint check.
|
||||||
|
|
||||||
@@ -55,9 +56,8 @@ just type-check
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
A script is available to run the documentaion generation.
|
A script is available to run the documentation generation.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
just docs
|
just docs
|
||||||
just man
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ app_name := "dbyaml"
|
|||||||
|
|
||||||
# interactive recipe selection
|
# interactive recipe selection
|
||||||
[group('info')]
|
[group('info')]
|
||||||
@select-shell:
|
@select:
|
||||||
just --choose
|
just --choose
|
||||||
|
|
||||||
prelude := '''
|
prelude := '''
|
||||||
@@ -25,13 +25,17 @@ version cmd=app_name:
|
|||||||
{{prelude}}
|
{{prelude}}
|
||||||
{{cmd}} --version
|
{{cmd}} --version
|
||||||
|
|
||||||
# run all dev tools
|
# run checkers
|
||||||
[parallel, group('dev')]
|
[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')]
|
[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
|
# setup virtual environment
|
||||||
[group('install')]
|
[group('install')]
|
||||||
@@ -97,9 +101,8 @@ ci-gen: format-fix lint-fix type-fix docs
|
|||||||
echo "running checker; shellcheck (shell)"
|
echo "running checker; shellcheck (shell)"
|
||||||
shellcheck scripts/*.sh
|
shellcheck scripts/*.sh
|
||||||
|
|
||||||
# TODO:
|
echo "running spell-checker: typos"
|
||||||
# Spell checking
|
uv run --only-group=check typos
|
||||||
# uv run --only-group=check typos
|
|
||||||
|
|
||||||
# fix typing
|
# fix typing
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
@@ -109,25 +112,29 @@ ci-gen: format-fix lint-fix type-fix docs
|
|||||||
|
|
||||||
echo "running checker; shellcheck (shell)"
|
echo "running checker; shellcheck (shell)"
|
||||||
shellcheck scripts/*.sh
|
shellcheck scripts/*.sh
|
||||||
|
# TODO: run fix
|
||||||
|
|
||||||
|
echo "running spell-checker: typos"
|
||||||
|
uv run --only-group=check typos --write-changes
|
||||||
|
|
||||||
# check project schema
|
# check project schema
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
@schema-check:
|
@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
|
uv run scripts/generate_schema.py | diff - src/dbyaml/resources/dbyaml.schema.json
|
||||||
|
|
||||||
# generate all docs
|
# generate all docs
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
docs: schema-generate man
|
docs: schema-generate docs-man
|
||||||
|
|
||||||
# generate project schema
|
# generate project schema
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
@schema-generate:
|
@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
|
uv run scripts/generate_schema.py --outfile src/dbyaml/resources/dbyaml.schema.json
|
||||||
|
|
||||||
# generate man pages
|
# generate man pages
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
@man:
|
@docs-man:
|
||||||
echo "generate man"
|
echo "generate man"
|
||||||
uv run click-man --man-version $(uv version --short) dbyaml
|
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: dict[str, Any] = {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$id": ("./src/dbyaml/resources/dbyaml.schema.json"),
|
"$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_ctx = generate_node_from_click(ctx, additional_properties)
|
||||||
schema.update(schema_ctx)
|
schema.update(schema_ctx)
|
||||||
|
|||||||
+101
-4
@@ -3,6 +3,7 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from dbyaml import COMPILED, __version__
|
from dbyaml import COMPILED, __version__
|
||||||
|
from dbyaml.decorator import uses_decorator
|
||||||
|
|
||||||
|
|
||||||
def debugger_enabled():
|
def debugger_enabled():
|
||||||
@@ -26,8 +27,87 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
else:
|
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(
|
@click.option(
|
||||||
"--config",
|
"--config",
|
||||||
type=click.Path(
|
type=click.Path(
|
||||||
@@ -41,6 +121,15 @@ else:
|
|||||||
is_eager=True,
|
is_eager=True,
|
||||||
help="Read configuration options from a configuration file.",
|
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(
|
@click.option(
|
||||||
"-q",
|
"-q",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
@@ -54,6 +143,7 @@ else:
|
|||||||
"-v",
|
"-v",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
|
show_envvar=True,
|
||||||
help=("Emit verbose messages."),
|
help=("Emit verbose messages."),
|
||||||
)
|
)
|
||||||
@click.version_option(
|
@click.version_option(
|
||||||
@@ -65,9 +155,16 @@ else:
|
|||||||
f"Python ({platform.python_implementation()}) {platform.python_version()}"
|
f"Python ({platform.python_implementation()}) {platform.python_version()}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.option("-D", "--debug", is_flag=True, show_envvar=True, hidden=True)
|
||||||
def main(ctx, config, quiet, verbose): ...
|
@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
|
@main.command
|
||||||
@click.pass_context
|
@pass_context
|
||||||
def list(ctx): ...
|
def list(ctx): ...
|
||||||
|
|||||||
+13
-1
@@ -1,6 +1,18 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
def debugger_enabled() -> bool: ...
|
def debugger_enabled() -> bool: ...
|
||||||
@click.group
|
@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: ...
|
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#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$id": "./src/dbyaml/resources/dbyaml.schema.json",
|
"$id": "./src/dbyaml/resources/dbyaml.schema.json",
|
||||||
"$comment": "tool.dbyaml table in pyproject.toml",
|
"$comment": "dbyaml table",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"format-type": {
|
||||||
|
"enum": [
|
||||||
|
"raw",
|
||||||
|
"yaml",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"description": "Format type."
|
||||||
|
},
|
||||||
"quiet": {
|
"quiet": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Stop emitting all non-critical output. Error messages will still be emitted (which can silenced by 2>/dev/null).",
|
"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.",
|
"description": "Emit verbose messages.",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"debug": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": null,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"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