11"""Validate example files: syntax, method names, and basic consistency."""
22
33import ast
4- import re
5- import importlib
6- import inspect
74from pathlib import Path
85
96import pytest
107
118LIB_DIR = Path (__file__ ).parent .parent / "lib"
129
13- # Modules that exist only in MicroPython — skip import resolution for these.
14- MICROPYTHON_MODULES = {
15- "machine" , "micropython" , "utime" , "ustruct" , "ubinascii" ,
16- "framebuf" , "gc" , "sys" , "os" , "struct" , "time" , "math" ,
17- "neopixel" , "network" , "bluetooth" , "ubluetooth" ,
18- }
19-
2010
2111def _discover_examples ():
2212 """Yield (driver_dir, example_path) for every example file."""
@@ -29,7 +19,7 @@ def _discover_examples():
2919
3020
3121def _discover_driver_methods (driver_dir ):
32- """Return the set of public method names from a driver's device.py ."""
22+ """Return the set of all method names from a driver's Python files ."""
3323 # Find the module directory (may differ from driver dir name)
3424 module_dirs = [
3525 d for d in driver_dir .iterdir ()
@@ -38,18 +28,26 @@ def _discover_driver_methods(driver_dir):
3828 if not module_dirs :
3929 return set ()
4030
41- device_py = module_dirs [0 ] / "device.py"
42- try :
43- tree = ast .parse (device_py .read_text (encoding = "utf-8" ))
44- except SyntaxError :
45- return set ()
31+ # Prefer module dir matching driver name, else first alphabetically.
32+ driver_module = driver_dir .name .replace ("-" , "_" )
33+ preferred = next ((d for d in module_dirs if d .name == driver_module ), None )
34+ module_dir = preferred or sorted (module_dirs , key = lambda d : d .name )[0 ]
4635
4736 methods = set ()
48- for node in ast .walk (tree ):
49- if isinstance (node , ast .ClassDef ):
50- for item in node .body :
51- if isinstance (item , (ast .FunctionDef , ast .AsyncFunctionDef )):
52- methods .add (item .name )
37+ # Scan all .py files in the module (device.py, pin.py, etc.)
38+ for py_file in module_dir .glob ("*.py" ):
39+ if py_file .name == "__init__.py" or py_file .name == "const.py" :
40+ continue
41+ try :
42+ tree = ast .parse (py_file .read_text (encoding = "utf-8" ))
43+ except SyntaxError as e :
44+ pytest .fail (f"Syntax error in driver { py_file } : { e } " )
45+
46+ for node in ast .walk (tree ):
47+ if isinstance (node , ast .ClassDef ):
48+ for item in node .body :
49+ if isinstance (item , (ast .FunctionDef , ast .AsyncFunctionDef )):
50+ methods .add (item .name )
5351 return methods
5452
5553
@@ -104,29 +102,19 @@ def test_method_calls_exist_in_driver(self, driver_dir, example_path):
104102
105103 # Only check methods that look like driver calls:
106104 # exclude Python builtins and common MicroPython methods
107- builtins_and_common = {
108- "print" , "format" , "range" , "len" , "int" , "float" , "str" ,
109- "bytes" , "bytearray" , "list" , "tuple" , "dict" , "set" ,
110- "abs" , "min" , "max" , "round" , "hex" , "type" , "isinstance" ,
111- "append" , "extend" , "join" , "split" , "strip" , "encode" ,
112- "decode" , "replace" , "startswith" , "endswith" , "find" ,
113- "upper" , "lower" , "keys" , "values" , "items" , "get" , "pop" ,
114- "update" , "write" , "read" , "close" , "open" , "sleep" ,
105+ # Methods that are NOT from the driver but may appear on
106+ # driver variables due to MicroPython patterns or inheritance.
107+ # Keep this list minimal — only names that cause false positives.
108+ non_driver_methods = {
115109 "sleep_ms" , "sleep_us" , "ticks_ms" , "ticks_us" , "ticks_diff" ,
116- "init" , "value" , "on" , "off" , "irq" , "const" ,
117- "I2C" , "SPI" , "Pin" , "ADC" , "PWM" , "UART" , "Timer" ,
118- "fill" , "text" , "show" , "pixel" , "line" , "rect" ,
119- "fill_rect" , "scroll" , "blit" , "hline" , "vline" ,
120- "framebuf" , "FrameBuffer" ,
121110 "collect" , "mem_free" , "mem_alloc" ,
122- "scan" , "readfrom_mem" , "writeto_mem" , "readfrom" ,
123- "writeto" , "readfrom_mem_into" ,
124- # Common method names shared across drivers
125- "toggle" , "mode" , "pull" , "freq" ,
126111 }
127112
128113 # Find the driver class name to identify which variable holds it
129- tree = ast .parse (source )
114+ try :
115+ tree = ast .parse (source )
116+ except SyntaxError :
117+ return # Covered by test_syntax_valid
130118 driver_imports = set ()
131119 for node in ast .walk (tree ):
132120 if isinstance (node , ast .ImportFrom ):
@@ -169,7 +157,7 @@ def test_method_calls_exist_in_driver(self, driver_dir, example_path):
169157 obj = node .func .value
170158 if isinstance (obj , ast .Name ) and obj .id in driver_vars :
171159 method = node .func .attr
172- if method not in driver_methods and method not in builtins_and_common :
160+ if method not in driver_methods and method not in non_driver_methods :
173161 missing .append ((node .lineno , method ))
174162
175163 if missing :
0 commit comments