Skip to content

Commit 576b303

Browse files
committed
refactor(docs): migrate argument parsing to Click and enhance command execution in docs_build_examples.py
1 parent 06debb5 commit 576b303

File tree

3 files changed

+107
-136
lines changed

3 files changed

+107
-136
lines changed

.github/scripts/docs_build_examples.py

Lines changed: 100 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
ci.yml files and builds them for specified targets.
1212
"""
1313

14-
import argparse
15-
from argparse import RawDescriptionHelpFormatter
14+
import click
1615
from esp_docs.esp_extensions.docs_embed.tool.wokwi_tool import DiagramSync
1716
import os
1817
import shutil
@@ -70,41 +69,23 @@
7069
SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh"
7170

7271

73-
def run_sketch_utils(subcommand, *args, check=True, capture_output=False, text=True):
74-
"""Execute sketch_utils.sh with a controlled subcommand.
75-
76-
Security:
77-
* Uses a fixed script path (SKETCH_UTILS)
78-
* Only allows known subcommands
79-
* Always calls subprocess.run with shell=False and a list of arguments
80-
"""
81-
allowed_subcommands = {"check_requirements", "install_libs", "build"}
82-
if subcommand not in allowed_subcommands:
83-
raise ValueError(f"Unsupported sketch_utils.sh subcommand: {subcommand}")
84-
85-
cmd = [str(SKETCH_UTILS), subcommand]
86-
for arg in args:
87-
if not isinstance(arg, (str, os.PathLike)):
88-
raise ValueError(f"Invalid argument type for sketch_utils.sh: {type(arg)!r}")
89-
cmd.append(str(arg))
90-
72+
def run_cmd(cmd, check=True, capture_output=False, text=True):
73+
"""Execute a shell command with error handling."""
9174
try:
9275
return subprocess.run(
93-
cmd,
94-
check=check,
95-
capture_output=capture_output,
96-
text=text,
97-
shell=False, # explicit for static analyzers
76+
cmd, check=check, capture_output=capture_output, text=text
9877
)
9978
except subprocess.CalledProcessError as e:
79+
# CalledProcessError is raised only when check=True and the command exits non-zero
10080
print(f"ERROR: Command failed: {' '.join(cmd)}")
10181
print(f"Exit code: {e.returncode}")
102-
if e.stdout:
82+
if hasattr(e, "stdout") and e.stdout:
10383
print("--- stdout ---")
10484
print(e.stdout)
105-
if e.stderr:
85+
if hasattr(e, "stderr") and e.stderr:
10686
print("--- stderr ---")
10787
print(e.stderr)
88+
# Exit the whole script with the same return code to mimic shell behavior
10889
sys.exit(e.returncode)
10990
except FileNotFoundError:
11091
print(f"ERROR: Command not found: {cmd[0]}")
@@ -121,103 +102,43 @@ def check_requirements(sketch_dir, sdkconfig_path):
121102
Returns:
122103
bool: True if requirements are met, False otherwise
123104
"""
105+
cmd = [str(SKETCH_UTILS), "check_requirements", sketch_dir, str(sdkconfig_path)]
124106
try:
125-
res = run_sketch_utils(
126-
"check_requirements",
127-
sketch_dir,
128-
str(sdkconfig_path),
129-
check=False,
130-
capture_output=True,
131-
)
107+
res = run_cmd(cmd, check=False, capture_output=True)
132108
return res.returncode == 0
133109
except Exception:
134110
return False
135111

136112

137113
def install_libs(*args):
138114
"""Install Arduino libraries using sketch_utils.sh"""
139-
return run_sketch_utils("install_libs", *args, check=False)
115+
cmd = [str(SKETCH_UTILS), "install_libs"] + list(args)
116+
return run_cmd(cmd, check=False)
140117

141118

142119
def build_sketch(args_list):
143120
"""Build a sketch using sketch_utils.sh"""
144-
return run_sketch_utils("build", *args_list, check=False)
145-
146-
147-
def parse_args(argv):
148-
"""Parse command line arguments"""
149-
epilog_text = (
150-
"Examples:\n"
151-
" docs_build_examples.py -c # Clean up binaries directory\n"
152-
" docs_build_examples.py --build # Build all examples (use env vars)\n"
153-
" docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths\n"
154-
" docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad (with env vars)\n\n"
155-
"Path detection:\n"
156-
" All paths can be set via environment variables or command line options.\n"
157-
" ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables are used by default.\n"
158-
" Set by running install-arduino-cli.sh first, or use -ai/-au to override\n\n"
159-
)
121+
cmd = [str(SKETCH_UTILS), "build"] + args_list
122+
return run_cmd(cmd, check=False)
160123

161-
p = argparse.ArgumentParser(
162-
description="Build examples that have ci.yml with upload-binary targets",
163-
formatter_class=RawDescriptionHelpFormatter,
164-
epilog=epilog_text,
165-
)
166-
p.add_argument(
167-
"-c",
168-
"--cleanup",
169-
dest="cleanup",
170-
action="store_true",
171-
help="Clean up docs binaries directory and exit",
172-
)
173-
p.add_argument(
174-
"-ai",
175-
"--arduino-cli-path",
176-
dest="arduino_cli_path",
177-
help="Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)",
178-
)
179-
p.add_argument(
180-
"-au",
181-
"--arduino-user-path",
182-
dest="user_path",
183-
help="Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)",
184-
)
185-
p.add_argument(
186-
"-b",
187-
"--build",
188-
dest="build",
189-
action="store_true",
190-
help="Build all examples",
191-
)
192-
p.add_argument(
193-
"-d",
194-
"--diagram",
195-
dest="generate_diagrams",
196-
action="store_true",
197-
help="Generate diagrams for prepared examples using docs-embed",
198-
)
199-
p.add_argument(
200-
"-l",
201-
"--launchpad",
202-
dest="generate_launchpad_config",
203-
action="store_true",
204-
help="Generate LaunchPad config for prepared examples",
205-
)
206-
return p.parse_args(argv)
207124

208125

209-
def validate_prerequisites(args):
210-
"""Validate that required prerequisites are available and get paths from env vars if needed."""
211126

127+
def validate_prerequisites(arduino_cli_path, user_path):
128+
"""Validate that required prerequisites are available and get paths from env vars if needed.
129+
130+
Returns:
131+
tuple: (arduino_cli_path, user_path) with values from env vars if not provided
132+
"""
212133
# Get paths from environment variables if not provided via arguments
213-
if not args.arduino_cli_path or not args.user_path:
134+
if not arduino_cli_path or not user_path:
214135
print("Getting Arduino paths from environment variables...")
215136
arduino_ide_path = os.environ.get("ARDUINO_IDE_PATH")
216137
arduino_usr_path = os.environ.get("ARDUINO_USR_PATH")
217138

218-
if not args.arduino_cli_path:
139+
if not arduino_cli_path:
219140
if arduino_ide_path:
220-
args.arduino_cli_path = arduino_ide_path
141+
arduino_cli_path = arduino_ide_path
221142
print(f" Arduino CLI path (ARDUINO_IDE_PATH): {arduino_ide_path}")
222143
else:
223144
print(
@@ -226,9 +147,9 @@ def validate_prerequisites(args):
226147
print("Run install-arduino-cli.sh first to set environment variables")
227148
sys.exit(1)
228149

229-
if not args.user_path:
150+
if not user_path:
230151
if arduino_usr_path:
231-
args.user_path = arduino_usr_path
152+
user_path = arduino_usr_path
232153
print(f" Arduino user path (ARDUINO_USR_PATH): {arduino_usr_path}")
233154
else:
234155
print(
@@ -238,14 +159,16 @@ def validate_prerequisites(args):
238159
sys.exit(1)
239160

240161
# Create Arduino user path if it doesn't exist
241-
user_path = Path(args.user_path)
242-
if not user_path.is_dir():
162+
user_path_obj = Path(user_path)
163+
if not user_path_obj.is_dir():
243164
try:
244-
user_path.mkdir(parents=True, exist_ok=True)
245-
print(f"Created Arduino user directory: {user_path}")
165+
user_path_obj.mkdir(parents=True, exist_ok=True)
166+
print(f"Created Arduino user directory: {user_path_obj}")
246167
except Exception as e:
247-
print(f"ERROR: Failed to create Arduino user path {user_path}: {e}")
168+
print(f"ERROR: Failed to create Arduino user path {user_path_obj}: {e}")
248169
sys.exit(1)
170+
171+
return arduino_cli_path, user_path
249172

250173

251174
def cleanup_binaries():
@@ -281,8 +204,8 @@ def cleanup_binaries():
281204
if not os.listdir(root):
282205
try:
283206
os.rmdir(root)
284-
except Exception as e:
285-
print(f"WARNING: Failed to remove empty directory {root}: {e}")
207+
except Exception:
208+
pass
286209
print("Cleanup completed")
287210

288211

@@ -305,8 +228,7 @@ def find_examples_with_upload_binary():
305228
data = yaml.safe_load(ci_yml.read_text())
306229
if "upload-binary" in data and data["upload-binary"]:
307230
res.append(str(ino))
308-
except Exception as e:
309-
print(f"WARNING: Failed to parse ci.yml for {ci_yml}: {e}")
231+
except Exception:
310232
continue
311233
return res
312234

@@ -329,14 +251,16 @@ def get_upload_binary_targets(sketch_dir):
329251
return []
330252

331253

332-
def build_example_for_target(sketch_dir, target, relative_path, args):
254+
def build_example_for_target(sketch_dir, target, relative_path, arduino_cli_path, user_path, generate_diagrams):
333255
"""Build a single example for a specific target.
334256
335257
Args:
336258
sketch_dir (Path): Path to the sketch directory
337259
target (str): Target board/configuration name
338260
relative_path (str): Relative path for output organization
339-
args (argparse.Namespace): Parsed command line arguments
261+
arduino_cli_path (str): Path to Arduino CLI installation directory
262+
user_path (str): Path to Arduino user directory
263+
generate_diagrams (bool): Whether to generate diagrams
340264
341265
Returns:
342266
bool: True if build succeeded, False otherwise
@@ -355,9 +279,9 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
355279
# Build the sketch using sketch_utils.sh build - pass args as in shell script
356280
build_args = [
357281
"-ai",
358-
args.arduino_cli_path,
282+
arduino_cli_path,
359283
"-au",
360-
args.user_path,
284+
user_path,
361285
"-s",
362286
str(sketch_dir),
363287
"-t",
@@ -372,7 +296,7 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
372296
ci_yml = Path(sketch_dir) / "ci.yml"
373297
if ci_yml.exists():
374298
shutil.copy(ci_yml, output_dir / "ci.yml")
375-
if args.generate_diagrams:
299+
if generate_diagrams:
376300
print(f"Generating diagram for {relative_path} ({target})...")
377301
try:
378302
sync = DiagramSync(output_dir)
@@ -387,7 +311,7 @@ def build_example_for_target(sketch_dir, target, relative_path, args):
387311
return True
388312

389313

390-
def build_all_examples(args):
314+
def build_all_examples(arduino_cli_path, user_path, generate_diagrams, generate_launchpad_config):
391315
"""Build all examples that have upload-binary configuration
392316
393317
Prerequisites are validated in main() before calling this function
@@ -428,7 +352,7 @@ def build_all_examples(args):
428352
continue
429353
print(f"Building {relative_path} for targets: {targets}")
430354
for target in targets:
431-
if build_example_for_target(sketch_dir, target, relative_path, args):
355+
if build_example_for_target(sketch_dir, target, relative_path, arduino_cli_path, user_path, generate_diagrams):
432356
total_built += 1
433357
else:
434358
total_failed += 1
@@ -441,7 +365,7 @@ def build_all_examples(args):
441365
if ci_yml.exists():
442366
shutil.copy(ci_yml, output_sketch_dir / "ci.yml")
443367

444-
if args.generate_launchpad_config:
368+
if generate_launchpad_config:
445369
print(f"Generating LaunchPad config for {relative_path}/{target}...")
446370
try:
447371
sync = DiagramSync(output_sketch_dir)
@@ -463,35 +387,80 @@ def build_all_examples(args):
463387
return total_failed
464388

465389

466-
def main(argv):
390+
@click.command(
391+
help="""Build examples that have ci.yml with upload-binary targets.
392+
393+
\b
394+
Examples:
395+
docs_build_examples.py -c # Clean up binaries directory
396+
docs_build_examples.py --build # Build all examples (use env vars)
397+
docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths
398+
docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad (with env vars)
399+
400+
\b
401+
Path detection:
402+
All paths can be set via environment variables or command line options.
403+
ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables are used by default.
404+
Set by running install-arduino-cli.sh first, or use -ai/-au to override
405+
""")
406+
@click.option(
407+
"-c", "--cleanup",
408+
is_flag=True,
409+
help="Clean up docs binaries directory and exit"
410+
)
411+
@click.option(
412+
"-ai", "--arduino-cli-path",
413+
default=None,
414+
help="Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)"
415+
)
416+
@click.option(
417+
"-au", "--arduino-user-path",
418+
default=None,
419+
help="Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)"
420+
)
421+
@click.option(
422+
"-b", "--build",
423+
is_flag=True,
424+
help="Build all examples"
425+
)
426+
@click.option(
427+
"-d", "--diagram",
428+
is_flag=True,
429+
help="Generate diagrams for prepared examples using docs-embed"
430+
)
431+
@click.option(
432+
"-l", "--launchpad",
433+
is_flag=True,
434+
help="Generate LaunchPad config for prepared examples"
435+
)
436+
def main(cleanup, arduino_cli_path, arduino_user_path, build, diagram, launchpad):
467437
"""Main entry point for the script"""
468-
args = parse_args(argv)
469-
470-
if args.cleanup:
438+
if cleanup:
471439
cleanup_binaries()
472440
return
473441

474-
if args.build:
442+
if build:
475443
# Validate prerequisites and auto-detect paths if needed
476-
validate_prerequisites(args)
444+
arduino_cli_path, arduino_user_path = validate_prerequisites(arduino_cli_path, arduino_user_path)
477445

478-
result = build_all_examples(args)
446+
result = build_all_examples(arduino_cli_path, arduino_user_path, diagram, launchpad)
479447
if result == 0:
480448
print("\nAll examples built successfully!")
481449
else:
482450
print("\nSome builds failed. Check the output above for details.")
483451
sys.exit(1)
484452
return
485453

486-
if args.generate_diagrams or args.generate_launchpad_config:
454+
if diagram or launchpad:
487455
print(
488456
"ERROR: --diagram and --launchpad options are only available when building examples (--build)"
489457
)
490458
sys.exit(1)
491459

492460
# If no specific action is requested, show help
493-
parse_args(["--help"])
461+
ctx = click.get_current_context()
462+
click.echo(ctx.get_help())
494463

495464

496465
if __name__ == "__main__":
497-
main(sys.argv[1:])
466+
main()

docs/en/api/i2c.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,5 @@ LiquidCrystal_I2C
521521
This example demonstrates how to use the `LiquidCrystal_I2C`_ library to control an LCD display via I2C.
522522

523523
.. wokwi-example:: libraries/Wire/examples/LiquidCrystal_I2C
524+
525+
.. _LiquidCrystal_I2C: https://github.com/johnrickman/LiquidCrystal_I2C

0 commit comments

Comments
 (0)