Skip to content
This repository was archived by the owner on Jun 2, 2026. It is now read-only.

Commit 84a0c78

Browse files
authored
classyclick: make use of helpers to simplify code (#60)
1 parent e8dc487 commit 84a0c78

12 files changed

Lines changed: 38 additions & 281 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ lint-check:
66
uv run ruff format --diff
77
uv run ruff check
88

9+
sync:
10+
uv sync --dev --all-extras
11+
912
.venv39: export VIRTUAL_ENV=.venv39
1013
.venv39:
1114
uv sync --dev --extra cli --python 3.9 --active

defectdojo_api_generated/cli/__main__.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
"""Command-line entrypoint for defectdojo_api_generated."""
22

3-
import importlib
4-
import pkgutil
53
import sys
6-
from pathlib import Path
7-
8-
9-
def discover_commands():
10-
# str() required because of py3.10
11-
for _, module_name, _ in pkgutil.iter_modules([str(Path(__file__).parent / 'commands')]):
12-
importlib.import_module(f'{__package__}.commands.{module_name}')
134

145

156
def load_cli():
@@ -28,7 +19,6 @@ def load_cli():
2819

2920
def main():
3021
CLI = load_cli()
31-
discover_commands()
3222
CLI.click()
3323

3424

defectdojo_api_generated/cli/commands/__init__.py

Whitespace-only changes.

defectdojo_api_generated/cli/commands/apis.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434

3535

36-
class API(CLI.SubGroup, classyclick.Group):
36+
class API(CLI.SubGroup):
3737
"""Interact directly with any API/method"""
3838

3939

@@ -326,7 +326,7 @@ def _build_command_namespace(command_name: str, api_class: type, target_method:
326326
def _build_command_class(parent_class: type, namespace: dict[str, Any]) -> type:
327327
return type(
328328
'ApiCommand',
329-
(parent_class, classyclick.Command),
329+
(parent_class,),
330330
namespace,
331331
)
332332

@@ -572,7 +572,7 @@ def make_api_group(module_name: str, api_class: type) -> type:
572572
_, target_method = command_methods[0]
573573
return make_api_command(api_class, group_name, target_method, parent_class=API.Command)
574574

575-
class ApiGroup(API.SubGroup, classyclick.Group):
575+
class ApiGroup(API.SubGroup):
576576
__config__ = classyclick.Group.Config(
577577
name=group_name,
578578
help=f'methods from `{api_class.__name__}`.',

defectdojo_api_generated/cli/commands/cli.py

Lines changed: 15 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,25 @@
22

33
import logging
44
from pathlib import Path
5-
from typing import Optional
65

76
import classyclick
87
import click
9-
from platformdirs import user_config_dir
108

119
from defectdojo_api_generated.client import DefectDojo
1210

1311
from ... import __version__
1412

15-
try:
16-
# Python 3.11+
17-
import tomllib
18-
except ModuleNotFoundError:
19-
# Python < 3.11
20-
import tomli as tomllib # type: ignore
2113

22-
23-
DEFAULT_PATH = Path(user_config_dir('defectdojo-generated-api')) / 'config.toml'
24-
25-
26-
def ensure_config_file(config_path: Optional[Path]) -> Path:
27-
config_path = config_path or DEFAULT_PATH
28-
if not config_path.exists():
29-
config_path.parent.mkdir(parents=True, exist_ok=True)
30-
config_path.write_text((Path(__file__).parent.parent / 'config.example.toml').read_text())
31-
print(f'Info: No configuration file found at {config_path}, a sample config has been placed there.')
32-
33-
return config_path
34-
35-
36-
def load_config_data(config_path: Path) -> dict:
37-
with config_path.open('rb') as f:
38-
return tomllib.load(f)
39-
40-
41-
class CLI(classyclick.Group):
14+
class CLI(classyclick.helpers.ConfigFileMixin, classyclick.Group):
4215
"""DefectDojo CLI"""
4316

44-
__config__ = classyclick.Group.Config(context_settings=dict(show_default=True))
45-
46-
config: Path = classyclick.Option(help='Path to the configuration file', show_default=str(DEFAULT_PATH))
47-
env: str = classyclick.Option(
48-
'-e', help='Environment to use for the command (as many can be specified in config.toml)'
17+
__config__ = classyclick.Group.Config(
18+
context_settings=dict(show_default=True),
19+
decorators=[click.version_option(version=__version__, message='%(version)s')],
4920
)
21+
CONFIG_DEFAULT_NAME = 'defectdojo-generated-api'
22+
CONFIG_EXAMPLE_PATH = Path(__file__).parent.parent / 'config.example.toml'
23+
5024
host: str = classyclick.Option(
5125
'-h',
5226
envvar='DEFECTDOJO_HOST',
@@ -73,37 +47,9 @@ class CLI(classyclick.Group):
7347
)
7448
disable_tls: bool = classyclick.Option(help='Disable TLS verification in DefectDojo API requests')
7549
debug_http: bool = classyclick.Option(help='Log HTTP requests and responses')
76-
ctx: classyclick.Context = classyclick.Context()
7750

7851
def __call__(self):
79-
self.config = ensure_config_file(self.config)
80-
config_data = load_config_data(self.config)
81-
82-
if self.env is None:
83-
self.env = config_data.get('default_env')
84-
85-
# allow empty string to choose root environment when "default_env" is set to something else
86-
if self.env:
87-
if self.env not in config_data.get('env', {}):
88-
raise click.ClickException(f'Environment "{self.env}" not found in {self.config}')
89-
env_config = config_data['env'][self.env]
90-
config_data = merge_dicts(config_data, env_config)
91-
92-
# to late to have these options from default_map, so process them manually
93-
if self.host is None:
94-
self.host = config_data.get('host')
95-
96-
if self.token is None:
97-
self.token = config_data.get('token')
98-
99-
if self.user is None:
100-
self.user = config_data.get('user')
101-
102-
if self.password is None:
103-
self.password = config_data.get('password')
104-
105-
if self.disable_tls is None:
106-
self.disable_tls = config_data.get('disable_tls', False)
52+
self.load_config()
10753

10854
if self.debug_http:
10955
import http.client
@@ -120,32 +66,10 @@ def __call__(self):
12066
auth=None if self.token else (self.user, self.password),
12167
verify_ssl=not self.disable_tls,
12268
)
123-
self.ctx.meta['config_path'] = self.config
124-
self.ctx.meta['config_data'] = config_data
125-
self.ctx.meta['selected_env'] = self.env
126-
127-
self.ctx.default_map = config_data
128-
129-
130-
def merge_dicts(base: dict, override: dict) -> dict:
131-
"""
132-
To merge two Python dictionaries where:
133-
* nested dictionaries should merge recursively
134-
* lists and other values should be replaced entirely
135-
136-
by ChatGPT
137-
"""
138-
result = base.copy()
139-
for key, value in override.items():
140-
if key in result:
141-
if isinstance(result[key], dict) and isinstance(value, dict):
142-
result[key] = merge_dicts(result[key], value)
143-
else:
144-
result[key] = value
145-
else:
146-
result[key] = value
147-
return result
148-
149-
150-
# TODO: classyclick missing @click.version_option - https://github.com/fopina/classyclick/issues/48
151-
CLI.click = click.version_option(version=__version__, message='%(version)s')(CLI.click)
69+
70+
71+
class Config(classyclick.helpers.ConfigBaseCommand, CLI.Command):
72+
pass
73+
74+
75+
classyclick.helpers.discover_commands(__package__)

defectdojo_api_generated/cli/commands/config.py

Lines changed: 0 additions & 71 deletions
This file was deleted.

defectdojo_api_generated/cli/commands/status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .cli import CLI
99

1010

11-
class Status(CLI.Command, classyclick.Command):
11+
class Status(CLI.Command):
1212
"""Quick connectivity check"""
1313

1414
client: DefectDojo = classyclick.ContextMeta('client')

docs/hooks.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
def on_config(config, **kwargs):
2-
from defectdojo_api_generated.cli import __main__
3-
4-
__main__.discover_commands()
5-
62
# TODO: mkdocs-click does not support class nor class methods - create PR for this
3+
# so `cli.md` can use `CLI.click` directly instead of monkeypatched `_docs_cli_`
74
from defectdojo_api_generated.cli.commands import cli
5+
from defectdojo_api_generated.cli.commands.cli import CLI
86

9-
cli._docs_cli_ = cli.CLI.click
7+
cli._docs_cli_ = CLI.click
108

119
return config

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ dependencies = [
1818

1919
[project.optional-dependencies]
2020
cli = [
21-
"classyclick==0.8.0",
21+
"classyclick==1.0.0",
2222
"jmespath>=1.0.1",
23-
"platformdirs>=4.4.0",
2423
]
2524

2625
[project.scripts]

tests/integration/test_e2e_cli.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from click.testing import CliRunner
66

7-
from defectdojo_api_generated.cli import __main__
7+
from defectdojo_api_generated.cli.commands.cli import CLI
88

99
from . import E2ETestCase
1010

@@ -15,19 +15,11 @@ class Test(E2ETestCase):
1515
# Run `run_dojo.sh` manually (and stop after), set this one to True and then you can run individual tests here quickly
1616
_PARTIAL_RUN = False
1717

18-
@classmethod
19-
def setUpClass(cls):
20-
__main__.discover_commands()
21-
cls.CLI = __main__.load_cli()
22-
super().setUpClass()
23-
2418
def setUp(self):
2519
self.runner = CliRunner()
2620

2721
def _run_cli(self, *args):
28-
return self.runner.invoke(
29-
self.CLI.click, ['--config', Path(__file__).parent / 'config.integration.toml', *args]
30-
)
22+
return self.runner.invoke(CLI.click, ['--config', Path(__file__).parent / 'config.integration.toml', *args])
3123

3224
def _run_cli_api(self, *args):
3325
return self._run_cli('api', *args)

0 commit comments

Comments
 (0)