Skip to content

Commit 941518e

Browse files
authored
Merge pull request #19 from openSUSE/refactor-callback
Refactor: Introduce new callback.py file
2 parents 1c5bcf8 + 52ba2dc commit 941518e

4 files changed

Lines changed: 106 additions & 115 deletions

File tree

changelog.d/19.refactor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introduce new :file:`callback.py` file to separate :func:`validate_doctypes` function from the build command.

src/docbuild/cli/callback.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# --- Callback Function ---
2+
import click
3+
from pydantic import Field, ValidationError
4+
5+
from ..models.doctype import Doctype
6+
from ..utils.merge import merge_doctypes
7+
8+
9+
def validate_doctypes(
10+
ctx: click.Context,
11+
param: click.Parameter | None,
12+
doctypes: tuple[str, ...],
13+
) -> list[Doctype]:
14+
"""Click callback function to validate a list of doctype strings.
15+
16+
Each string must conform to the format: PRODUCT/DOCSET@LIFECYCLE/LANGS
17+
LANGS can be a single language code, a comma-separated list (no spaces),
18+
or '*' for all.
19+
Defaults and wildcards (*) are handled.
20+
21+
:param param: The click parameter that triggered this callback.
22+
:param doctypes: A tuple of doctype strings to validate.
23+
:return: A list of validated Doctype objects.
24+
:raises click.Abort: If any doctype string is invalid, the command is aborted.
25+
"""
26+
processed_data = [] # Store successfully parsed/validated data
27+
28+
if not doctypes:
29+
return []
30+
31+
# click.echo(f"Our doctypes: {doctypes=}")
32+
for doctype_str in doctypes:
33+
try:
34+
doctype = Doctype.from_str(doctype_str)
35+
click.echo(f'Got {doctype}')
36+
processed_data.append(doctype)
37+
38+
except ValidationError as err:
39+
for error in err.errors():
40+
field = error['loc'][0]
41+
# Convert to string to ensure it works as dictionary key
42+
field_name = str(field)
43+
msg = error['msg']
44+
# Make accessing of .description and .examples safe(r) if
45+
# the definition in Doctype is not present
46+
safe_field = Field(description=None, examples=None)
47+
hint = getattr(
48+
Doctype.model_fields.get(field_name, safe_field),
49+
'description',
50+
None,
51+
)
52+
examples = getattr(
53+
Doctype.model_fields.get(field_name, safe_field),
54+
'examples',
55+
None,
56+
)
57+
click.secho(
58+
f"ERROR in '{field}': {msg}",
59+
fg='red',
60+
err=True,
61+
)
62+
if hint:
63+
click.echo(f' → Hint: {hint}')
64+
if examples:
65+
click.echo(f' → Examples: {", ".join(examples)}')
66+
click.echo()
67+
raise click.Abort(err) from err
68+
69+
# --- Optional: Post-validation checks across all inputs ---
70+
# This part becomes more complex if you need to merge/check '*' against lists.
71+
# For now, we'll just validate each argument independently.
72+
# You might handle the combination logic in the main command.
73+
74+
processed_data = merge_doctypes(*processed_data)
75+
ctx.obj.doctypes = processed_data
76+
return processed_data

src/docbuild/cli/cmd_build.py

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -35,84 +35,12 @@
3535
""" # noqa: D301
3636

3737
import click
38-
from pydantic import Field, ValidationError
3938

4039
from ..models.doctype import Doctype
41-
from ..utils.merge import merge_doctypes
40+
from .callback import validate_doctypes
4241
from .context import DocBuildContext
4342

4443

45-
# --- Callback Function ---
46-
def validate_doctypes(
47-
ctx: click.Context,
48-
param: click.Parameter | None,
49-
doctypes: tuple[str, ...],
50-
) -> list[Doctype]:
51-
"""Click callback function to validate a list of doctype strings.
52-
53-
Each string must conform to the format: PRODUCT/DOCSET@LIFECYCLE/LANGS
54-
LANGS can be a single language code, a comma-separated list (no spaces),
55-
or '*' for all.
56-
Defaults and wildcards (*) are handled.
57-
58-
:param param: The click parameter that triggered this callback.
59-
:param doctypes: A tuple of doctype strings to validate.
60-
:return: A list of validated Doctype objects.
61-
:raises click.Abort: If any doctype string is invalid, the command is aborted.
62-
"""
63-
processed_data = [] # Store successfully parsed/validated data
64-
65-
if not doctypes:
66-
return []
67-
68-
# click.echo(f"Our doctypes: {doctypes=}")
69-
for doctype_str in doctypes:
70-
try:
71-
doctype = Doctype.from_str(doctype_str)
72-
click.echo(f'Got {doctype}')
73-
processed_data.append(doctype)
74-
75-
except ValidationError as err:
76-
for error in err.errors():
77-
field = error['loc'][0]
78-
# Convert to string to ensure it works as dictionary key
79-
field_name = str(field)
80-
msg = error['msg']
81-
# Make accessing of .description and .examples safe(r) if
82-
# the definition in Doctype is not present
83-
safe_field = Field(description=None, examples=None)
84-
hint = getattr(
85-
Doctype.model_fields.get(field_name, safe_field),
86-
'description',
87-
None,
88-
)
89-
examples = getattr(
90-
Doctype.model_fields.get(field_name, safe_field),
91-
'examples',
92-
None,
93-
)
94-
click.secho(
95-
f"ERROR in '{field}': {msg}",
96-
fg='red',
97-
err=True,
98-
)
99-
if hint:
100-
click.echo(f' → Hint: {hint}')
101-
if examples:
102-
click.echo(f' → Examples: {", ".join(examples)}')
103-
click.echo()
104-
raise click.Abort(err) from err
105-
106-
# --- Optional: Post-validation checks across all inputs ---
107-
# This part becomes more complex if you need to merge/check '*' against lists.
108-
# For now, we'll just validate each argument independently.
109-
# You might handle the combination logic in the main command.
110-
111-
processed_data = merge_doctypes(*processed_data)
112-
ctx.obj.doctypes = processed_data
113-
return processed_data
114-
115-
11644
@click.command(
11745
help=__doc__.replace('\b\n\n', '\b\n').replace('``', ''), # type: ignore
11846
)

tests/cli/test_cmd_build.py

Lines changed: 28 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from pydantic import Field, ValidationError
66
import pytest
77

8-
import docbuild.cli.cmd_build as cmd_build
8+
from docbuild.cli import callback as callback_module
9+
from docbuild.cli.callback import validate_doctypes
910
from docbuild.cli.cmd_cli import cli
1011

1112

@@ -14,7 +15,7 @@ def test_validate_doctypes_with_empty_doctypes():
1415
ctx = Context(cmd)
1516
ctx.obj = SimpleNamespace()
1617

17-
result = cmd_build.validate_doctypes(ctx, None, tuple())
18+
result = validate_doctypes(ctx, None, tuple())
1819
assert not result
1920
# No ctx.obj.doctypes as this doesn't exist
2021

@@ -29,28 +30,13 @@ def raise_for_invalid(s: str) -> DummyDoctype:
2930
return DummyDoctype(s)
3031

3132
monkeypatch.setattr(
32-
cmd_build.Doctype,
33+
callback_module.Doctype,
3334
'from_str',
3435
staticmethod(raise_for_invalid),
3536
)
3637

3738
with pytest.raises(Abort, match='is not a valid Product'):
38-
cmd_build.validate_doctypes(ctx, None, ('wrong/1/en-us',))
39-
40-
41-
@pytest.mark.skip('Replace --role with --env-config')
42-
def test_validate_doctypes_called_from_build(context, fake_envfile, runner):
43-
result = runner.invoke(
44-
cli,
45-
['--role=production', 'build', 'sles/17/en-us'],
46-
obj=context,
47-
)
48-
49-
assert fake_envfile.mock.call_count == 1
50-
assert result.exit_code == 0
51-
# assert "Got sles/17@supported/en-us" in result.output
52-
assert context.doctypes == [DummyDoctype('sles/17/en-us')]
53-
assert context.role == 'production'
39+
validate_doctypes(ctx, None, ('wrong/1/en-us',))
5440

5541

5642
class DummyDoctype:
@@ -68,7 +54,7 @@ def __str__(self):
6854
def patch_doctype(monkeypatch):
6955
# Patch Doctype.from_str to return a DummyDoctype for testing
7056
monkeypatch.setattr(
71-
cmd_build,
57+
callback_module,
7258
'Doctype',
7359
type(
7460
'Doctype',
@@ -86,20 +72,20 @@ def patch_doctype(monkeypatch):
8672
),
8773
)
8874
# Patch merge_doctypes to just return the list for simplicity
89-
monkeypatch.setattr(cmd_build, 'merge_doctypes', lambda *args: list(args))
75+
monkeypatch.setattr(callback_module, 'merge_doctypes', lambda *args: list(args))
9076

9177

9278
def test_validate_doctypes_empty(ctx):
9379
context = ctx(SimpleNamespace())
94-
result = cmd_build.validate_doctypes(context, None, ())
80+
result = validate_doctypes(context, None, ())
9581
assert result == []
9682
assert not hasattr(context.obj, 'doctypes') or context.obj.doctypes == []
9783

9884

9985
def test_validate_doctypes_valid(ctx):
10086
context = ctx(SimpleNamespace())
10187
doctypes = ('foo/1/en-us', 'bar/2/de-de')
102-
result = cmd_build.validate_doctypes(context, None, doctypes)
88+
result = validate_doctypes(context, None, doctypes)
10389
assert result == [DummyDoctype('foo/1/en-us'), DummyDoctype('bar/2/de-de')]
10490
assert context.obj.doctypes == result
10591

@@ -113,7 +99,7 @@ def errors(self):
11399
return [{'loc': ['field'], 'msg': 'bad', 'type': 'value_error'}]
114100

115101
monkeypatch.setattr(
116-
cmd_build,
102+
callback_module,
117103
'Doctype',
118104
type(
119105
'Doctype',
@@ -133,7 +119,7 @@ def errors(self):
133119
),
134120
)
135121
with pytest.raises(click.Abort):
136-
cmd_build.validate_doctypes(context, None, ('bad/doctype',))
122+
validate_doctypes(context, None, ('bad/doctype',))
137123

138124

139125
@pytest.mark.parametrize(
@@ -151,7 +137,7 @@ def test_validate_doctypes_with_doctypes(ctx, doctypes, expected):
151137
"""Test validate_doctypes with different doctype inputs."""
152138
context = ctx(SimpleNamespace(doctypes=[]))
153139

154-
result = cmd_build.validate_doctypes(context, None, doctypes)
140+
result = validate_doctypes(context, None, doctypes)
155141
assert result == expected
156142
assert context.obj.doctypes == expected
157143

@@ -177,10 +163,10 @@ def mock_from_str(s: str):
177163
return DummyDoctype(s)
178164

179165
# Patch necessary methods
180-
monkeypatch.setattr(cmd_build, 'ValidationError', MockValidationError)
181-
monkeypatch.setattr(cmd_build.Doctype, 'from_str', staticmethod(mock_from_str))
166+
monkeypatch.setattr(callback_module, 'ValidationError', MockValidationError)
167+
monkeypatch.setattr(callback_module.Doctype, 'from_str', staticmethod(mock_from_str))
182168
monkeypatch.setattr(
183-
cmd_build.Doctype,
169+
callback_module.Doctype,
184170
'model_fields',
185171
{
186172
'product': type(
@@ -196,7 +182,7 @@ def mock_from_str(s: str):
196182

197183
# Test that the function properly aborts and formats error messages
198184
with pytest.raises(click.Abort):
199-
cmd_build.validate_doctypes(context, None, ('invalid/product/en-us',))
185+
validate_doctypes(context, None, ('invalid/product/en-us',))
200186

201187
captured = capsys.readouterr()
202188
assert "ERROR in 'product': Invalid product name" in captured.err
@@ -217,9 +203,9 @@ def mock_merge(*args):
217203
mock_merge_called_with = args
218204
return list(args)
219205

220-
monkeypatch.setattr(cmd_build, 'merge_doctypes', mock_merge)
206+
monkeypatch.setattr(callback_module, 'merge_doctypes', mock_merge)
221207

222-
cmd_build.validate_doctypes(context, None, doctypes)
208+
validate_doctypes(context, None, doctypes)
223209

224210
# Verify merge_doctypes was called with both doctypes
225211
assert len(mock_merge_called_with) == 2
@@ -233,7 +219,7 @@ def mock_merge(*args):
233219
def test_validate_doctypes_echo_outputs(ctx, capsys):
234220
"""Test the echo statements in validate_doctypes."""
235221
context = ctx(SimpleNamespace())
236-
cmd_build.validate_doctypes(context, None, ('sles/15/en-us',))
222+
validate_doctypes(context, None, ('sles/15/en-us',))
237223

238224
captured = capsys.readouterr()
239225
assert 'Got sles/15' in captured.out
@@ -254,7 +240,7 @@ def mock_validation_error(*args, **kwargs):
254240
)
255241

256242
monkeypatch.setattr(
257-
cmd_build.Doctype,
243+
callback_module.Doctype,
258244
'model_fields',
259245
{
260246
'product': Field(
@@ -264,10 +250,10 @@ def mock_validation_error(*args, **kwargs):
264250
),
265251
},
266252
)
267-
monkeypatch.setattr(cmd_build.Doctype, 'from_str', mock_validation_error)
253+
monkeypatch.setattr(callback_module.Doctype, 'from_str', mock_validation_error)
268254

269255
with pytest.raises(click.Abort, match=r'Mock validation error'):
270-
cmd_build.validate_doctypes(
256+
validate_doctypes(
271257
click.Context(click.Command('dummy')),
272258
None,
273259
('foo/bar',),
@@ -297,7 +283,7 @@ def mock_validation_error(*args, **kwargs):
297283
)
298284

299285
monkeypatch.setattr(
300-
cmd_build.Doctype,
286+
callback_module.Doctype,
301287
'model_fields',
302288
{
303289
'product': type(
@@ -310,10 +296,10 @@ def mock_validation_error(*args, **kwargs):
310296
)(),
311297
},
312298
)
313-
monkeypatch.setattr(cmd_build.Doctype, 'from_str', mock_validation_error)
299+
monkeypatch.setattr(callback_module.Doctype, 'from_str', mock_validation_error)
314300

315301
with pytest.raises(click.Abort, match=r'Mock validation error'):
316-
cmd_build.validate_doctypes(
302+
validate_doctypes(
317303
click.Context(click.Command('dummy')),
318304
None,
319305
('foo/bar',),
@@ -342,7 +328,7 @@ def mock_validation_error(*args, **kwargs):
342328
)
343329

344330
monkeypatch.setattr(
345-
cmd_build.Doctype,
331+
callback_module.Doctype,
346332
'model_fields',
347333
{
348334
'product': type(
@@ -355,10 +341,10 @@ def mock_validation_error(*args, **kwargs):
355341
)(),
356342
},
357343
)
358-
monkeypatch.setattr(cmd_build.Doctype, 'from_str', mock_validation_error)
344+
monkeypatch.setattr(callback_module.Doctype, 'from_str', mock_validation_error)
359345

360346
with pytest.raises(click.Abort, match=r'Mock validation error'):
361-
cmd_build.validate_doctypes(
347+
validate_doctypes(
362348
click.Context(click.Command('dummy')),
363349
None,
364350
('foo/bar',),

0 commit comments

Comments
 (0)