Skip to content

Commit cf7a557

Browse files
committed
docs: Improve validations: add validate_readme --help, fix magic numbers, move to scripts directory:- Moves validate_readme.py to /scripts
- Replaces magic numbers with variables - Adds support for --help and --list flags- Updates contributing doc
1 parent 18e9f7d commit cf7a557

File tree

2 files changed

+172
-33
lines changed

2 files changed

+172
-33
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ Before syncing, ensure your README:
520520
- ✅ Includes proper emoji metadata (`` triggers, `🏷️` tags, `🔧` compatibility)
521521
- ✅ Has all required sections with proper formatting
522522
- ✅ Contains working examples with expected output
523-
- ✅ Passes validation (`python validate_readme.py`)
523+
- ✅ Passes validation (`python scripts/validate_readme.py`)
524524

525525
#### What Gets Transformed
526526

validate_readme.py renamed to scripts/validate_readme.py

Lines changed: 171 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,24 @@
66

77
import sys
88
import re
9+
import argparse
910
from pathlib import Path
1011
from typing import List, Tuple
1112

13+
# Validation thresholds and limits
14+
MINIMUM_BASH_EXAMPLES = 2
15+
MINIMUM_EXPECTED_OUTPUT_SECTIONS = 1
16+
MINIMUM_TROUBLESHOOTING_ISSUES = 2
17+
SUMMARY_SEPARATOR_LENGTH = 60
18+
19+
# Section search offsets
20+
SECTION_SEARCH_OFFSET = 1
21+
SECTION_NOT_FOUND = -1
22+
23+
# Exit codes
24+
EXIT_SUCCESS = 0
25+
EXIT_ERROR = 1
26+
1227
# Required sections in order
1328
REQUIRED_SECTIONS = [
1429
("# ", "Title with plugin name"),
@@ -86,11 +101,11 @@ def validate_parameter_tables(content: str) -> List[str]:
86101
# Check for required parameters section
87102
if '### Required parameters' in content:
88103
section_start = content.index('### Required parameters')
89-
section_end = content.find('\n###', section_start + 1)
90-
if section_end == -1:
91-
section_end = content.find('\n##', section_start + 1)
104+
section_end = content.find('\n###', section_start + SECTION_SEARCH_OFFSET)
105+
if section_end == SECTION_NOT_FOUND:
106+
section_end = content.find('\n##', section_start + SECTION_SEARCH_OFFSET)
92107

93-
section_content = content[section_start:section_end] if section_end != -1 else content[section_start:]
108+
section_content = content[section_start:section_end] if section_end != SECTION_NOT_FOUND else content[section_start:]
94109

95110
if 'required' not in section_content.lower():
96111
errors.append("Required parameters section should indicate which parameters are required")
@@ -103,8 +118,8 @@ def validate_examples(content: str) -> List[str]:
103118

104119
# Check for bash code examples
105120
bash_examples = re.findall(r'```bash(.*?)```', content, re.DOTALL)
106-
if len(bash_examples) < 2:
107-
errors.append(f"Should have at least 2 bash code examples (found {len(bash_examples)})")
121+
if len(bash_examples) < MINIMUM_BASH_EXAMPLES:
122+
errors.append(f"Should have at least {MINIMUM_BASH_EXAMPLES} bash code examples (found {len(bash_examples)})")
108123

109124
# Check for influxdb3 commands in examples
110125
has_create_trigger = any('influxdb3 create trigger' in ex for ex in bash_examples)
@@ -120,8 +135,8 @@ def validate_examples(content: str) -> List[str]:
120135

121136
# Check for expected output
122137
expected_output_count = content.count('### Expected output') + content.count('**Expected output')
123-
if expected_output_count < 1:
124-
errors.append("Should include at least one 'Expected output' section in examples")
138+
if expected_output_count < MINIMUM_EXPECTED_OUTPUT_SECTIONS:
139+
errors.append(f"Should include at least {MINIMUM_EXPECTED_OUTPUT_SECTIONS} 'Expected output' section in examples")
125140

126141
return errors
127142

@@ -153,8 +168,8 @@ def validate_troubleshooting(content: str) -> List[str]:
153168

154169
if '## Troubleshooting' in content:
155170
section_start = content.index('## Troubleshooting')
156-
section_end = content.find('\n##', section_start + 1)
157-
section_content = content[section_start:section_end] if section_end != -1 else content[section_start:]
171+
section_end = content.find('\n##', section_start + SECTION_SEARCH_OFFSET)
172+
section_content = content[section_start:section_end] if section_end != SECTION_NOT_FOUND else content[section_start:]
158173

159174
# Check for common subsections
160175
if '### Common issues' not in section_content:
@@ -164,8 +179,8 @@ def validate_troubleshooting(content: str) -> List[str]:
164179
issue_count = section_content.count('#### Issue:') + section_content.count('**Issue:')
165180
solution_count = section_content.count('**Solution:') + section_content.count('Solution:')
166181

167-
if issue_count < 2:
168-
errors.append("Troubleshooting should include at least 2 documented issues")
182+
if issue_count < MINIMUM_TROUBLESHOOTING_ISSUES:
183+
errors.append(f"Troubleshooting should include at least {MINIMUM_TROUBLESHOOTING_ISSUES} documented issues")
169184
if issue_count > solution_count:
170185
errors.append("Each troubleshooting issue should have a corresponding solution")
171186

@@ -177,8 +192,8 @@ def validate_code_overview(content: str) -> List[str]:
177192

178193
if '## Code overview' in content:
179194
section_start = content.index('## Code overview')
180-
section_end = content.find('\n##', section_start + 1)
181-
section_content = content[section_start:section_end] if section_end != -1 else content[section_start:]
195+
section_end = content.find('\n##', section_start + SECTION_SEARCH_OFFSET)
196+
section_content = content[section_start:section_end] if section_end != SECTION_NOT_FOUND else content[section_start:]
182197

183198
# Check for required subsections
184199
if '### Files' not in section_content:
@@ -249,22 +264,123 @@ def validate_readme(readme_path: Path) -> Tuple[List[str], List[str]]:
249264

250265
return errors, warnings
251266

267+
def parse_arguments():
268+
"""Parse command line arguments."""
269+
parser = argparse.ArgumentParser(
270+
description='Validates plugin README files against the standard template.',
271+
formatter_class=argparse.RawDescriptionHelpFormatter,
272+
epilog='''
273+
Examples:
274+
python scripts/validate_readme.py # Validate all plugins
275+
python scripts/validate_readme.py --plugins basic_transformation,downsampler
276+
python scripts/validate_readme.py --list # List available plugins
277+
python scripts/validate_readme.py --quiet # Show only errors
278+
279+
Validation Rules:
280+
- Checks for required sections in correct order
281+
- Validates emoji metadata format
282+
- Ensures parameter tables are properly formatted
283+
- Verifies code examples include required commands
284+
- Validates troubleshooting content structure
285+
'''
286+
)
287+
288+
parser.add_argument(
289+
'--plugins',
290+
type=str,
291+
help='Comma-separated list of specific plugins to validate (e.g., "basic_transformation,downsampler")'
292+
)
293+
294+
parser.add_argument(
295+
'--list',
296+
action='store_true',
297+
help='List all available plugins and exit'
298+
)
299+
300+
parser.add_argument(
301+
'--quiet',
302+
action='store_true',
303+
help='Show only errors, suppress warnings and success messages'
304+
)
305+
306+
parser.add_argument(
307+
'--errors-only',
308+
action='store_true',
309+
help='Exit with success code even if warnings are found (only fail on errors)'
310+
)
311+
312+
return parser.parse_args()
313+
314+
def list_available_plugins():
315+
"""List all available plugins and exit."""
316+
influxdata_dir = Path('influxdata')
317+
318+
if not influxdata_dir.exists():
319+
print("❌ Error: 'influxdata' directory not found. Run this script from the influxdb3_plugins root directory.")
320+
sys.exit(EXIT_ERROR)
321+
322+
readme_files = list(influxdata_dir.glob('*/README.md'))
323+
324+
if not readme_files:
325+
print("❌ No plugins found in influxdata/ subdirectories")
326+
sys.exit(EXIT_ERROR)
327+
328+
print(f"Available plugins ({len(readme_files)} found):")
329+
for readme_path in sorted(readme_files):
330+
plugin_name = readme_path.parent.name
331+
print(f" - {plugin_name}")
332+
333+
sys.exit(EXIT_SUCCESS)
334+
335+
def filter_plugins_by_name(readme_files: List[Path], plugin_names: str) -> List[Path]:
336+
"""Filter README files by specified plugin names."""
337+
requested_plugins = [name.strip() for name in plugin_names.split(',')]
338+
filtered_files = []
339+
340+
for readme_path in readme_files:
341+
plugin_name = readme_path.parent.name
342+
if plugin_name in requested_plugins:
343+
filtered_files.append(readme_path)
344+
requested_plugins.remove(plugin_name)
345+
346+
# Report any plugins that weren't found
347+
if requested_plugins:
348+
print(f"⚠️ Warning: The following plugins were not found: {', '.join(requested_plugins)}")
349+
available_plugins = [f.parent.name for f in readme_files]
350+
print(f"Available plugins: {', '.join(sorted(available_plugins))}")
351+
352+
return filtered_files
353+
252354
def main():
253355
"""Main validation function."""
356+
args = parse_arguments()
357+
358+
# Handle list option
359+
if args.list:
360+
list_available_plugins()
361+
254362
# Find all plugin READMEs
255363
influxdata_dir = Path('influxdata')
256364

257365
if not influxdata_dir.exists():
258366
print("❌ Error: 'influxdata' directory not found. Run this script from the influxdb3_plugins root directory.")
259-
sys.exit(1)
367+
sys.exit(EXIT_ERROR)
260368

261369
readme_files = list(influxdata_dir.glob('*/README.md'))
262370

263371
if not readme_files:
264372
print("❌ No README files found in influxdata/ subdirectories")
265-
sys.exit(1)
373+
sys.exit(EXIT_ERROR)
266374

267-
print(f"Validating {len(readme_files)} plugin README files...\n")
375+
# Filter by specific plugins if requested
376+
if args.plugins:
377+
readme_files = filter_plugins_by_name(readme_files, args.plugins)
378+
if not readme_files:
379+
print("❌ No matching plugins found")
380+
sys.exit(EXIT_ERROR)
381+
382+
if not args.quiet:
383+
print(f"Validating {len(readme_files)} plugin README files...\n")
268384

269385
all_valid = True
270386
error_count = 0
@@ -278,24 +394,47 @@ def main():
278394
error_count += len(errors)
279395
warning_count += len(warnings)
280396

281-
print(format_validation_result(readme_path, errors, warnings))
397+
result = format_validation_result(readme_path, errors, warnings)
398+
399+
# Apply quiet mode filtering
400+
if args.quiet:
401+
# Only show files with errors in quiet mode
402+
if errors:
403+
print(result)
404+
else:
405+
print(result)
282406

283407
# Print summary
284-
print("\n" + "=" * 60)
285-
print("VALIDATION SUMMARY")
286-
print("=" * 60)
287-
print(f"Total files validated: {len(readme_files)}")
288-
print(f"Errors found: {error_count}")
289-
print(f"Warnings found: {warning_count}")
290-
291-
if all_valid:
292-
print("\n✅ All README files are valid!")
293-
sys.exit(0)
408+
if not args.quiet:
409+
print("\n" + "=" * SUMMARY_SEPARATOR_LENGTH)
410+
print("VALIDATION SUMMARY")
411+
print("=" * SUMMARY_SEPARATOR_LENGTH)
412+
print(f"Total files validated: {len(readme_files)}")
413+
print(f"Errors found: {error_count}")
414+
print(f"Warnings found: {warning_count}")
415+
416+
# Determine exit status
417+
has_errors = error_count > 0
418+
has_warnings = warning_count > 0
419+
420+
if not has_errors and not has_warnings:
421+
if not args.quiet:
422+
print("\n✅ All README files are valid!")
423+
sys.exit(EXIT_SUCCESS)
424+
elif not has_errors and has_warnings:
425+
if not args.quiet:
426+
print(f"\n⚠️ Validation completed with {warning_count} warning(s) but no errors")
427+
# If --errors-only is specified, exit successfully even with warnings
428+
exit_code = EXIT_SUCCESS if args.errors_only else EXIT_ERROR
429+
sys.exit(exit_code)
294430
else:
295-
print(f"\n❌ Validation failed with {error_count} error(s)")
296-
print("\nPlease fix the errors above and ensure all READMEs follow the template.")
297-
print("See README_TEMPLATE.md for the correct structure.")
298-
sys.exit(1)
431+
if not args.quiet:
432+
print(f"\n❌ Validation failed with {error_count} error(s)")
433+
if has_warnings:
434+
print(f"Also found {warning_count} warning(s)")
435+
print("\nPlease fix the errors above and ensure all READMEs follow the template.")
436+
print("See README_TEMPLATE.md for the correct structure.")
437+
sys.exit(EXIT_ERROR)
299438

300439
if __name__ == "__main__":
301440
main()

0 commit comments

Comments
 (0)