Skip to content

Commit 04e47b9

Browse files
authored
feat(class-generator): update schema and add fix tests (#2505)
1 parent 8f80932 commit 04e47b9

10 files changed

Lines changed: 2574 additions & 296 deletions

File tree

class_generator/cli.py

Lines changed: 136 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import fnmatch
44
import shutil
55
import sys
6-
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
76
from datetime import datetime
87
from pathlib import Path
98
from typing import Any
109

10+
import logging
11+
1112
import cloup
1213
from cloup.constraints import If, IsSet, accept_none, require_one
1314
from simple_logger.logger import get_logger
@@ -18,6 +19,7 @@
1819
from class_generator.core.generator import class_generator
1920
from class_generator.core.schema import update_kind_schema, ClusterVersionError
2021
from class_generator.tests.test_generation import generate_class_generator_tests
22+
from class_generator.utils import execute_parallel_tasks
2123
from ocp_resources.utils.utils import convert_camel_case_to_snake_case
2224

2325
LOGGER = get_logger(name=__name__)
@@ -56,7 +58,7 @@ def handle_schema_update(update_schema: bool, generate_missing: bool) -> bool:
5658
try:
5759
update_kind_schema()
5860
except (RuntimeError, IOError, ClusterVersionError) as e:
59-
LOGGER.error(f"Failed to update schema: {e}")
61+
LOGGER.exception(f"Failed to update schema: {e}")
6062
sys.exit(1)
6163

6264
# If only updating schema (not generating), exit
@@ -131,7 +133,7 @@ def handle_missing_resources_generation(
131133
if not dry_run:
132134
LOGGER.info(f"Generated {kind_to_generate}")
133135
except Exception as e:
134-
LOGGER.error(f"Failed to generate {kind_to_generate}: {e}")
136+
LOGGER.exception(f"Failed to generate {kind_to_generate}: {e}")
135137

136138

137139
def create_backup_if_needed(target_file: Path, backup_dir: Path | None) -> None:
@@ -163,7 +165,7 @@ def handle_regenerate_all(
163165
regenerate_all: bool,
164166
backup: bool,
165167
dry_run: bool,
166-
filter: str | None,
168+
filter_pattern: str | None,
167169
) -> bool:
168170
"""
169171
Handle regeneration of all generated resources.
@@ -172,7 +174,7 @@ def handle_regenerate_all(
172174
regenerate_all: Whether to regenerate all resources
173175
backup: Whether to create backups
174176
dry_run: Whether this is a dry run
175-
filter: Optional filter pattern for resource names
177+
filter_pattern: Optional filter pattern for resource names
176178
177179
Returns:
178180
True if regeneration was performed and main should exit, False to continue
@@ -197,13 +199,13 @@ def handle_regenerate_all(
197199
LOGGER.info(f"Found {len(discovered)} generated resources")
198200

199201
# Filter resources if pattern provided
200-
if filter:
202+
if filter_pattern:
201203
filtered = []
202204
for resource in discovered:
203-
if fnmatch.fnmatch(resource["kind"], filter):
205+
if fnmatch.fnmatch(resource["kind"], filter_pattern):
204206
filtered.append(resource)
205207
discovered = filtered
206-
LOGGER.info(f"Filtered to {len(discovered)} resources matching '{filter}'")
208+
LOGGER.info(f"Filtered to {len(discovered)} resources matching '{filter_pattern}'")
207209

208210
# Regenerate each resource
209211
success_count = 0
@@ -245,23 +247,34 @@ def regenerate_single_resource(resource: dict[str, Any]) -> tuple[str, bool, str
245247
LOGGER.warning(f"Skipped {resource_kind}: Not found in schema mapping")
246248
return resource_kind, False, "Not found in schema mapping"
247249
except Exception as e:
248-
LOGGER.error(f"Failed to regenerate {resource_kind}: {e}")
250+
LOGGER.exception(f"Failed to regenerate {resource_kind}: {e}")
249251
return resource_kind, False, str(e)
250252

253+
# Process results from parallel execution
254+
def process_regeneration_result(resource: dict[str, Any], result: tuple[str, bool, str | None]) -> None:
255+
nonlocal success_count, error_count
256+
resource_kind, success, error = result
257+
if success:
258+
success_count += 1
259+
else:
260+
error_count += 1
261+
262+
# Handle executor-level exceptions that bypass result processing
263+
def handle_regeneration_error(resource: dict[str, Any], exc: Exception) -> None:
264+
nonlocal error_count
265+
resource_kind = resource.get("kind", "unknown")
266+
LOGGER.exception(f"Executor-level failure for {resource_kind}: {exc}")
267+
error_count += 1
268+
251269
# Process resources in parallel
252-
with ThreadPoolExecutor(max_workers=10) as executor:
253-
# Submit all tasks
254-
regeneration_futures = {
255-
executor.submit(regenerate_single_resource, resource): resource for resource in discovered
256-
}
257-
258-
# Process results as they complete
259-
for future in as_completed(regeneration_futures):
260-
resource_kind, success, error = future.result()
261-
if success:
262-
success_count += 1
263-
else:
264-
error_count += 1
270+
execute_parallel_tasks(
271+
tasks=discovered,
272+
task_func=regenerate_single_resource,
273+
max_workers=10,
274+
task_name="regeneration",
275+
result_processor=process_regeneration_result,
276+
error_handler=handle_regeneration_error,
277+
)
265278

266279
# Print summary
267280
if not dry_run:
@@ -330,15 +343,24 @@ def handle_normal_kind_generation(
330343
add_tests=add_tests,
331344
)
332345
except Exception as e:
333-
LOGGER.error(f"Failed to generate {kind}: {e}")
346+
LOGGER.exception(f"Failed to generate {kind}: {e}")
334347
sys.exit(1)
335348

336349
if backup_dir and not dry_run:
337350
LOGGER.info(f"Backup files stored in: {backup_dir}")
338351
else:
339-
# Multiple kinds - run in parallel
340-
def generate_with_backup(kind_to_generate: str) -> list[str]:
341-
"""Generate a single kind with optional backup."""
352+
# Multiple kinds - run in parallel with result tracking
353+
success_count = 0
354+
error_count = 0
355+
failed_kinds = []
356+
357+
def generate_with_backup(kind_to_generate: str) -> tuple[str, bool, str | None]:
358+
"""
359+
Generate a single kind with optional backup.
360+
361+
Returns:
362+
Tuple of (kind, success, error_message)
363+
"""
342364
if overwrite and backup_dir:
343365
# Determine the output file path for this kind
344366
formatted_kind = convert_camel_case_to_snake_case(name=kind_to_generate)
@@ -348,28 +370,69 @@ def generate_with_backup(kind_to_generate: str) -> list[str]:
348370
create_backup_if_needed(target_file=target_file, backup_dir=backup_dir)
349371

350372
try:
351-
return class_generator(
373+
result = class_generator(
352374
kind=kind_to_generate,
353375
overwrite=overwrite,
354376
dry_run=dry_run,
355377
output_file=output_file,
356378
add_tests=add_tests,
379+
called_from_cli=False, # Don't prompt for missing resources during batch generation
357380
)
381+
# Check if generation was successful (empty list means failure)
382+
if result:
383+
if not dry_run:
384+
LOGGER.info(f"Successfully generated {kind_to_generate}")
385+
return kind_to_generate, True, None
386+
else:
387+
LOGGER.warning(f"Skipped {kind_to_generate}: Not found in schema mapping")
388+
return kind_to_generate, False, "Not found in schema mapping"
358389
except Exception as e:
359-
LOGGER.error(f"Failed to generate {kind_to_generate}: {e}")
360-
return []
390+
LOGGER.exception(f"Failed to generate {kind_to_generate}: {e}")
391+
return kind_to_generate, False, str(e)
361392

362-
futures: list[Future] = []
363-
with ThreadPoolExecutor(max_workers=10) as executor:
364-
for _kind in kind_list:
365-
futures.append(executor.submit(generate_with_backup, _kind))
393+
# Process results from parallel execution
394+
def process_generation_result(kind_to_generate: str, result: tuple[str, bool, str | None]) -> None:
395+
nonlocal success_count, error_count, failed_kinds
396+
kind_name, success, error = result
397+
if success:
398+
success_count += 1
399+
else:
400+
error_count += 1
401+
failed_kinds.append({"kind": kind_name, "error": error})
402+
403+
# Handle executor-level exceptions that bypass result processing
404+
def handle_generation_error(kind_to_generate: str, exc: Exception) -> None:
405+
nonlocal error_count, failed_kinds
406+
LOGGER.exception(f"Executor-level failure for {kind_to_generate}: {exc}")
407+
error_count += 1
408+
failed_kinds.append({"kind": kind_to_generate, "error": str(exc)})
409+
410+
# Generate all kinds in parallel
411+
execute_parallel_tasks(
412+
tasks=kind_list,
413+
task_func=generate_with_backup,
414+
max_workers=10,
415+
task_name="generation",
416+
result_processor=process_generation_result,
417+
error_handler=handle_generation_error,
418+
)
419+
420+
# Print summary and handle failures
421+
if not dry_run:
422+
LOGGER.info(f"\nGeneration complete: {success_count} succeeded, {error_count} failed")
423+
if backup_dir:
424+
LOGGER.info(f"Backup files stored in: {backup_dir}")
425+
else:
426+
LOGGER.info(f"\nDry run complete: would generate {len(kind_list)} kinds")
366427

367-
# Wait for all tasks to complete
368-
for _ in as_completed(futures):
369-
pass
428+
# Log detailed failure information
429+
if failed_kinds:
430+
LOGGER.error(f"\nFailed to generate {len(failed_kinds)} kind(s):")
431+
for failure in failed_kinds:
432+
LOGGER.error(f" - {failure['kind']}: {failure['error']}")
370433

371-
if backup_dir and not dry_run:
372-
LOGGER.info(f"Backup files stored in: {backup_dir}")
434+
# Exit with non-zero status if any failures occurred
435+
sys.exit(1)
373436

374437

375438
def handle_test_generation(add_tests: bool) -> None:
@@ -474,6 +537,13 @@ def handle_test_generation(add_tests: bool) -> None:
474537
type=cloup.STRING,
475538
default=None,
476539
)
540+
@cloup.option(
541+
"-v",
542+
"--verbose",
543+
help="Enable verbose output with debug logs",
544+
is_flag=True,
545+
show_default=True,
546+
)
477547
@cloup.constraint(
478548
If(IsSet("update_schema") & ~IsSet("generate_missing"), then=accept_none),
479549
[
@@ -509,8 +579,35 @@ def main(
509579
filter: str | None,
510580
json_output: bool,
511581
update_schema: bool,
582+
verbose: bool,
512583
) -> None:
513584
"""Generate Python module for K8S resource."""
585+
# Configure logging based on verbose flag
586+
if verbose:
587+
# Set debug level for all class_generator modules
588+
for logger_name in [
589+
"class_generator.core.schema",
590+
"class_generator.core.generator",
591+
"class_generator.core.coverage",
592+
"class_generator.core.discovery",
593+
"class_generator.cli",
594+
"class_generator.utils",
595+
"ocp_resources",
596+
]:
597+
logger = logging.getLogger(logger_name)
598+
logger.setLevel(logging.DEBUG)
599+
# Prevent propagation to avoid duplicate messages
600+
logger.propagate = False
601+
# Also set all handlers to DEBUG to ensure debug logs surface
602+
for handler in logger.handlers:
603+
handler.setLevel(logging.DEBUG)
604+
605+
# Set root logger to DEBUG to cover all configured handlers
606+
root_logger = logging.getLogger()
607+
root_logger.setLevel(logging.DEBUG)
608+
for handler in root_logger.handlers:
609+
handler.setLevel(logging.DEBUG)
610+
514611
# Validate input parameters
515612
validate_actions(
516613
kind=kind,
@@ -542,7 +639,7 @@ def main(
542639
)
543640

544641
# Handle regenerate-all operation
545-
if handle_regenerate_all(regenerate_all=regenerate_all, backup=backup, dry_run=dry_run, filter=filter):
642+
if handle_regenerate_all(regenerate_all=regenerate_all, backup=backup, dry_run=dry_run, filter_pattern=filter):
546643
return
547644

548645
# Exit if we only did discovery/report/generation

class_generator/core/discovery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Discovery functions for finding cluster resources and generated files."""
22

3-
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
43
from pathlib import Path
54
from typing import Any
5+
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
66

77
from kubernetes.dynamic import DynamicClient
88
from simple_logger.logger import get_logger

0 commit comments

Comments
 (0)