#!/usr/bin/env python3.14# -*- coding: utf-8 -*-""" Utilities for linting code. """from__future__importannotations__all__=['lint','lazy_import_fix_code']fromsubprocessimportDEVNULL,runfromtypingimportCallablefromsolver.configimportExitCodes,configfromsolver.core.problemsimportProblemfromsolver.shellimportconsole,registerfromsolver.utils.path_utilsimportcanonical_path,iterdir_recursive@register(requires='contributor',quietable=True)deflint(problem:Problem,auto_fix:bool=False)->int:"""Lint the problem's solution files, optionally auto-fixing them. Checks the current problem's solution files for style and quality issues (flake8, plus the configured checks). Reports any findings and reflects them in the exit code. Args: problem: [problem] The problem whose solution files to lint. auto_fix: Fix what can be fixed in place — autoflake (unused imports and variables), autopep8 (style) and isort (import order) — then re-check. Defaults to False, which only reports. """console.print(f'[accent]checking[/accent] {problem}')if_linter_check(problem):returnExitCodes.EXIT_OKifauto_fix:console.print(f'\n[accent]auto-fixing [/accent] {problem}')if_auto_fix(problem):console.print(f'\n[accent]rechecking[/accent] {problem}')returnExitCodes.EXIT_OKif_linter_check(problem)elseExitCodes.EXIT_ERRORreturnExitCodes.EXIT_ERRORdef_linter_check(problem:Problem)->bool:"""Run the configured linter over *problem*'s solution dir; return True if it passes. Reports pass/fail to the console and, when the console is quiet, suppresses the subprocess's own output too. """# When the shared console is quiet, send the check subprocess to /dev/null too —# it writes straight to the terminal (inherited fds), so console.quiet alone won't hush it.pipe=DEVNULLifconsole.quietelseNonelinter_check=run(f'{config.scripts.linter}{canonical_path(problem.solution_dir)}',shell=True,cwd=config.root_dir,stdout=pipe,stderr=pipe)iflinter_check.returncode!=0:console.print(f'[error]check failed:[/error] {problem} rc={linter_check.returncode}')returnFalseconsole.print(f'[primary]check passed[/primary] {problem}\n')returnTruedeflazy_import_fix_code()->Callable[[str],str]|None:"""Return a source-fixing callable (autoflake → autopep8 → isort), or None. The `dev`-group dependencies are imported on demand; if they are missing, an install hint is printed and None is returned. """try:importautoflakeimportautopep8importisortexceptImportErrorasexc:console.print(f'[error]auto-fix needs the [accent]dev[/accent] dependency group 'f'({exc.name} is not installed) — run [accent]pip install -e ".\\[dev]"[/accent].[/error]')returnNonedeffix_code(source:str)->str:code=autoflake.fix_code(source,remove_all_unused_imports=True,remove_duplicate_keys=True)code=autopep8.fix_code(code,options={'max_line_length':config.max_line_length})code=isort.code(code,profile='black',line_length=config.max_line_length)returncodereturnfix_codedef_auto_fix(problem:Problem)->bool:"""Auto-fix the problem's Python files with autoflake + autopep8 + isort, then re-check. Dev-group deps, imported on demand so the shell starts without the `dev` group installed. """if(fix_code:=lazy_import_fix_code())isNone:returnFalsepy_files=[fforfiniterdir_recursive(problem.solution_dir,rt='path')iff.is_file()andf.suffix=='.py']fixed=Falseforpy_fileinsorted(py_files):source=py_file.read_text()code=fix_code(source)ifcode!=source:py_file.write_text(code)console.print(f'[primary]auto-fixed[/primary] {py_file.name}')fixed=Trueifnotfixed:console.print(f'[warning]no auto-fixable changes for[/warning] {problem}')returnfixed