diff --git a/src/skill_seekers/cli/arguments/analyze.py b/src/skill_seekers/cli/arguments/analyze.py index 9eab0de..98e2b10 100644 --- a/src/skill_seekers/cli/arguments/analyze.py +++ b/src/skill_seekers/cli/arguments/analyze.py @@ -171,6 +171,7 @@ ANALYZE_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_analyze_arguments(parser: argparse.ArgumentParser) -> None: """Add all analyze command arguments to a parser.""" for arg_name, arg_def in ANALYZE_ARGUMENTS.items(): @@ -178,6 +179,7 @@ def add_analyze_arguments(parser: argparse.ArgumentParser) -> None: kwargs = arg_def["kwargs"] parser.add_argument(*flags, **kwargs) + def get_analyze_argument_names() -> set: """Get the set of analyze argument destination names.""" return set(ANALYZE_ARGUMENTS.keys()) diff --git a/src/skill_seekers/cli/arguments/common.py b/src/skill_seekers/cli/arguments/common.py index a580d69..2973fa5 100644 --- a/src/skill_seekers/cli/arguments/common.py +++ b/src/skill_seekers/cli/arguments/common.py @@ -96,6 +96,7 @@ RAG_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_common_arguments(parser: argparse.ArgumentParser) -> None: """Add common arguments to a parser. @@ -114,6 +115,7 @@ def add_common_arguments(parser: argparse.ArgumentParser) -> None: kwargs = arg_def["kwargs"] parser.add_argument(*flags, **kwargs) + def get_common_argument_names() -> set: """Get the set of common argument destination names. @@ -122,6 +124,7 @@ def get_common_argument_names() -> set: """ return set(COMMON_ARGUMENTS.keys()) + def add_rag_arguments(parser: argparse.ArgumentParser) -> None: """Add RAG (Retrieval-Augmented Generation) arguments to a parser. @@ -140,6 +143,7 @@ def add_rag_arguments(parser: argparse.ArgumentParser) -> None: kwargs = arg_def["kwargs"] parser.add_argument(*flags, **kwargs) + def get_rag_argument_names() -> set: """Get the set of RAG argument destination names. @@ -148,6 +152,7 @@ def get_rag_argument_names() -> set: """ return set(RAG_ARGUMENTS.keys()) + def get_argument_help(arg_name: str) -> str: """Get the help text for a common argument. diff --git a/src/skill_seekers/cli/arguments/create.py b/src/skill_seekers/cli/arguments/create.py index 9729a27..0e2ee7d 100644 --- a/src/skill_seekers/cli/arguments/create.py +++ b/src/skill_seekers/cli/arguments/create.py @@ -388,10 +388,12 @@ ADVANCED_ARGUMENTS: dict[str, dict[str, Any]] = { # HELPER FUNCTIONS # ============================================================================= + def get_universal_argument_names() -> set[str]: """Get set of universal argument names.""" return set(UNIVERSAL_ARGUMENTS.keys()) + def get_source_specific_arguments(source_type: str) -> dict[str, dict[str, Any]]: """Get source-specific arguments for a given source type. @@ -402,14 +404,15 @@ def get_source_specific_arguments(source_type: str) -> dict[str, dict[str, Any]] Dict of argument definitions """ source_args = { - 'web': WEB_ARGUMENTS, - 'github': GITHUB_ARGUMENTS, - 'local': LOCAL_ARGUMENTS, - 'pdf': PDF_ARGUMENTS, - 'config': {}, # Config files don't have extra args + "web": WEB_ARGUMENTS, + "github": GITHUB_ARGUMENTS, + "local": LOCAL_ARGUMENTS, + "pdf": PDF_ARGUMENTS, + "config": {}, # Config files don't have extra args } return source_args.get(source_type, {}) + def get_compatible_arguments(source_type: str) -> list[str]: """Get list of compatible argument names for a source type. @@ -431,7 +434,8 @@ def get_compatible_arguments(source_type: str) -> list[str]: return compatible -def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default') -> None: + +def add_create_arguments(parser: argparse.ArgumentParser, mode: str = "default") -> None: """Add create command arguments to parser. Supports multiple help modes for progressive disclosure: @@ -449,10 +453,10 @@ def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default') """ # Positional argument for source parser.add_argument( - 'source', - nargs='?', + "source", + nargs="?", type=str, - help='Source to create skill from (URL, GitHub repo, directory, PDF, or config file)' + help="Source to create skill from (URL, GitHub repo, directory, PDF, or config file)", ) # Always add universal arguments @@ -460,23 +464,23 @@ def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default') parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) # Add source-specific arguments based on mode - if mode in ['web', 'all']: + if mode in ["web", "all"]: for arg_name, arg_def in WEB_ARGUMENTS.items(): parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) - if mode in ['github', 'all']: + if mode in ["github", "all"]: for arg_name, arg_def in GITHUB_ARGUMENTS.items(): parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) - if mode in ['local', 'all']: + if mode in ["local", "all"]: for arg_name, arg_def in LOCAL_ARGUMENTS.items(): parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) - if mode in ['pdf', 'all']: + if mode in ["pdf", "all"]: for arg_name, arg_def in PDF_ARGUMENTS.items(): parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) # Add advanced arguments if requested - if mode in ['advanced', 'all']: + if mode in ["advanced", "all"]: for arg_name, arg_def in ADVANCED_ARGUMENTS.items(): parser.add_argument(*arg_def["flags"], **arg_def["kwargs"]) diff --git a/src/skill_seekers/cli/arguments/enhance.py b/src/skill_seekers/cli/arguments/enhance.py index 753c8ca..3ffca30 100644 --- a/src/skill_seekers/cli/arguments/enhance.py +++ b/src/skill_seekers/cli/arguments/enhance.py @@ -68,6 +68,7 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_enhance_arguments(parser: argparse.ArgumentParser) -> None: """Add all enhance command arguments to a parser.""" for arg_name, arg_def in ENHANCE_ARGUMENTS.items(): diff --git a/src/skill_seekers/cli/arguments/github.py b/src/skill_seekers/cli/arguments/github.py index e660f1e..a4b52e9 100644 --- a/src/skill_seekers/cli/arguments/github.py +++ b/src/skill_seekers/cli/arguments/github.py @@ -133,6 +133,7 @@ GITHUB_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_github_arguments(parser: argparse.ArgumentParser) -> None: """Add all github command arguments to a parser. @@ -153,6 +154,7 @@ def add_github_arguments(parser: argparse.ArgumentParser) -> None: kwargs = arg_def["kwargs"] parser.add_argument(*flags, **kwargs) + def get_github_argument_names() -> set: """Get the set of github argument destination names. @@ -161,6 +163,7 @@ def get_github_argument_names() -> set: """ return set(GITHUB_ARGUMENTS.keys()) + def get_github_argument_count() -> int: """Get the total number of github arguments. diff --git a/src/skill_seekers/cli/arguments/package.py b/src/skill_seekers/cli/arguments/package.py index c3c6eab..c7c9416 100644 --- a/src/skill_seekers/cli/arguments/package.py +++ b/src/skill_seekers/cli/arguments/package.py @@ -123,6 +123,7 @@ PACKAGE_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_package_arguments(parser: argparse.ArgumentParser) -> None: """Add all package command arguments to a parser.""" for arg_name, arg_def in PACKAGE_ARGUMENTS.items(): diff --git a/src/skill_seekers/cli/arguments/pdf.py b/src/skill_seekers/cli/arguments/pdf.py index 7af93fe..27493e9 100644 --- a/src/skill_seekers/cli/arguments/pdf.py +++ b/src/skill_seekers/cli/arguments/pdf.py @@ -51,6 +51,7 @@ PDF_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_pdf_arguments(parser: argparse.ArgumentParser) -> None: """Add all pdf command arguments to a parser.""" for arg_name, arg_def in PDF_ARGUMENTS.items(): diff --git a/src/skill_seekers/cli/arguments/scrape.py b/src/skill_seekers/cli/arguments/scrape.py index 9c5c976..cb83d33 100644 --- a/src/skill_seekers/cli/arguments/scrape.py +++ b/src/skill_seekers/cli/arguments/scrape.py @@ -198,6 +198,7 @@ SCRAPE_ARGUMENTS: dict[str, dict[str, Any]] = { # Merge RAG arguments from common.py SCRAPE_ARGUMENTS.update(RAG_ARGUMENTS) + def add_scrape_arguments(parser: argparse.ArgumentParser) -> None: """Add all scrape command arguments to a parser. @@ -218,6 +219,7 @@ def add_scrape_arguments(parser: argparse.ArgumentParser) -> None: kwargs = arg_def["kwargs"] parser.add_argument(*flags, **kwargs) + def get_scrape_argument_names() -> set: """Get the set of scrape argument destination names. @@ -226,6 +228,7 @@ def get_scrape_argument_names() -> set: """ return set(SCRAPE_ARGUMENTS.keys()) + def get_scrape_argument_count() -> int: """Get the total number of scrape arguments. diff --git a/src/skill_seekers/cli/arguments/unified.py b/src/skill_seekers/cli/arguments/unified.py index 7c56a60..111230c 100644 --- a/src/skill_seekers/cli/arguments/unified.py +++ b/src/skill_seekers/cli/arguments/unified.py @@ -42,6 +42,7 @@ UNIFIED_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_unified_arguments(parser: argparse.ArgumentParser) -> None: """Add all unified command arguments to a parser.""" for arg_name, arg_def in UNIFIED_ARGUMENTS.items(): diff --git a/src/skill_seekers/cli/arguments/upload.py b/src/skill_seekers/cli/arguments/upload.py index 9192199..dccae0f 100644 --- a/src/skill_seekers/cli/arguments/upload.py +++ b/src/skill_seekers/cli/arguments/upload.py @@ -98,6 +98,7 @@ UPLOAD_ARGUMENTS: dict[str, dict[str, Any]] = { }, } + def add_upload_arguments(parser: argparse.ArgumentParser) -> None: """Add all upload command arguments to a parser.""" for arg_name, arg_def in UPLOAD_ARGUMENTS.items(): diff --git a/src/skill_seekers/cli/config_extractor.py b/src/skill_seekers/cli/config_extractor.py index 9119c95..bd2b47f 100644 --- a/src/skill_seekers/cli/config_extractor.py +++ b/src/skill_seekers/cli/config_extractor.py @@ -870,7 +870,7 @@ def main(): # AI Enhancement (if requested) enhance_mode = args.ai_mode - if getattr(args, 'enhance_level', 0) > 0: + if getattr(args, "enhance_level", 0) > 0: # Auto-detect mode if enhance_level is set enhance_mode = "auto" # ConfigEnhancer will auto-detect API vs LOCAL diff --git a/src/skill_seekers/cli/create_command.py b/src/skill_seekers/cli/create_command.py index c61fc74..46b924a 100644 --- a/src/skill_seekers/cli/create_command.py +++ b/src/skill_seekers/cli/create_command.py @@ -16,6 +16,7 @@ from skill_seekers.cli.arguments.create import ( logger = logging.getLogger(__name__) + class CreateCommand: """Unified create command implementation.""" @@ -74,7 +75,7 @@ class CreateCommand: continue # Skip internal arguments - if arg_name in ['source', 'func', 'subcommand']: + if arg_name in ["source", "func", "subcommand"]: continue # Warn about incompatible argument @@ -104,10 +105,10 @@ class CreateCommand: # Check against common defaults defaults = { - 'max_issues': 100, - 'chunk_size': 512, - 'chunk_overlap': 50, - 'output': None, + "max_issues": 100, + "chunk_size": 512, + "chunk_overlap": 50, + "output": None, } if arg_name in defaults: @@ -122,15 +123,15 @@ class CreateCommand: Returns: Exit code from scraper """ - if self.source_info.type == 'web': + if self.source_info.type == "web": return self._route_web() - elif self.source_info.type == 'github': + elif self.source_info.type == "github": return self._route_github() - elif self.source_info.type == 'local': + elif self.source_info.type == "local": return self._route_local() - elif self.source_info.type == 'pdf': + elif self.source_info.type == "pdf": return self._route_pdf() - elif self.source_info.type == 'config': + elif self.source_info.type == "config": return self._route_config() else: logger.error(f"Unknown source type: {self.source_info.type}") @@ -141,10 +142,10 @@ class CreateCommand: from skill_seekers.cli import doc_scraper # Reconstruct argv for doc_scraper - argv = ['doc_scraper'] + argv = ["doc_scraper"] # Add URL - url = self.source_info.parsed['url'] + url = self.source_info.parsed["url"] argv.append(url) # Add universal arguments @@ -152,21 +153,21 @@ class CreateCommand: # Add web-specific arguments if self.args.max_pages: - argv.extend(['--max-pages', str(self.args.max_pages)]) - if getattr(self.args, 'skip_scrape', False): - argv.append('--skip-scrape') - if getattr(self.args, 'resume', False): - argv.append('--resume') - if getattr(self.args, 'fresh', False): - argv.append('--fresh') - if getattr(self.args, 'rate_limit', None): - argv.extend(['--rate-limit', str(self.args.rate_limit)]) - if getattr(self.args, 'workers', None): - argv.extend(['--workers', str(self.args.workers)]) - if getattr(self.args, 'async_mode', False): - argv.append('--async') - if getattr(self.args, 'no_rate_limit', False): - argv.append('--no-rate-limit') + argv.extend(["--max-pages", str(self.args.max_pages)]) + if getattr(self.args, "skip_scrape", False): + argv.append("--skip-scrape") + if getattr(self.args, "resume", False): + argv.append("--resume") + if getattr(self.args, "fresh", False): + argv.append("--fresh") + if getattr(self.args, "rate_limit", None): + argv.extend(["--rate-limit", str(self.args.rate_limit)]) + if getattr(self.args, "workers", None): + argv.extend(["--workers", str(self.args.workers)]) + if getattr(self.args, "async_mode", False): + argv.append("--async") + if getattr(self.args, "no_rate_limit", False): + argv.append("--no-rate-limit") # Call doc_scraper with modified argv logger.debug(f"Calling doc_scraper with argv: {argv}") @@ -182,32 +183,32 @@ class CreateCommand: from skill_seekers.cli import github_scraper # Reconstruct argv for github_scraper - argv = ['github_scraper'] + argv = ["github_scraper"] # Add repo - repo = self.source_info.parsed['repo'] - argv.extend(['--repo', repo]) + repo = self.source_info.parsed["repo"] + argv.extend(["--repo", repo]) # Add universal arguments self._add_common_args(argv) # Add GitHub-specific arguments - if getattr(self.args, 'token', None): - argv.extend(['--token', self.args.token]) - if getattr(self.args, 'profile', None): - argv.extend(['--profile', self.args.profile]) - if getattr(self.args, 'non_interactive', False): - argv.append('--non-interactive') - if getattr(self.args, 'no_issues', False): - argv.append('--no-issues') - if getattr(self.args, 'no_changelog', False): - argv.append('--no-changelog') - if getattr(self.args, 'no_releases', False): - argv.append('--no-releases') - if getattr(self.args, 'max_issues', None) and self.args.max_issues != 100: - argv.extend(['--max-issues', str(self.args.max_issues)]) - if getattr(self.args, 'scrape_only', False): - argv.append('--scrape-only') + if getattr(self.args, "token", None): + argv.extend(["--token", self.args.token]) + if getattr(self.args, "profile", None): + argv.extend(["--profile", self.args.profile]) + if getattr(self.args, "non_interactive", False): + argv.append("--non-interactive") + if getattr(self.args, "no_issues", False): + argv.append("--no-issues") + if getattr(self.args, "no_changelog", False): + argv.append("--no-changelog") + if getattr(self.args, "no_releases", False): + argv.append("--no-releases") + if getattr(self.args, "max_issues", None) and self.args.max_issues != 100: + argv.extend(["--max-issues", str(self.args.max_issues)]) + if getattr(self.args, "scrape_only", False): + argv.append("--scrape-only") # Call github_scraper with modified argv logger.debug(f"Calling github_scraper with argv: {argv}") @@ -223,30 +224,30 @@ class CreateCommand: from skill_seekers.cli import codebase_scraper # Reconstruct argv for codebase_scraper - argv = ['codebase_scraper'] + argv = ["codebase_scraper"] # Add directory - directory = self.source_info.parsed['directory'] - argv.extend(['--directory', directory]) + directory = self.source_info.parsed["directory"] + argv.extend(["--directory", directory]) # Add universal arguments self._add_common_args(argv) # Add local-specific arguments - if getattr(self.args, 'languages', None): - argv.extend(['--languages', self.args.languages]) - if getattr(self.args, 'file_patterns', None): - argv.extend(['--file-patterns', self.args.file_patterns]) - if getattr(self.args, 'skip_patterns', False): - argv.append('--skip-patterns') - if getattr(self.args, 'skip_test_examples', False): - argv.append('--skip-test-examples') - if getattr(self.args, 'skip_how_to_guides', False): - argv.append('--skip-how-to-guides') - if getattr(self.args, 'skip_config', False): - argv.append('--skip-config') - if getattr(self.args, 'skip_docs', False): - argv.append('--skip-docs') + if getattr(self.args, "languages", None): + argv.extend(["--languages", self.args.languages]) + if getattr(self.args, "file_patterns", None): + argv.extend(["--file-patterns", self.args.file_patterns]) + if getattr(self.args, "skip_patterns", False): + argv.append("--skip-patterns") + if getattr(self.args, "skip_test_examples", False): + argv.append("--skip-test-examples") + if getattr(self.args, "skip_how_to_guides", False): + argv.append("--skip-how-to-guides") + if getattr(self.args, "skip_config", False): + argv.append("--skip-config") + if getattr(self.args, "skip_docs", False): + argv.append("--skip-docs") # Call codebase_scraper with modified argv logger.debug(f"Calling codebase_scraper with argv: {argv}") @@ -262,20 +263,20 @@ class CreateCommand: from skill_seekers.cli import pdf_scraper # Reconstruct argv for pdf_scraper - argv = ['pdf_scraper'] + argv = ["pdf_scraper"] # Add PDF file - file_path = self.source_info.parsed['file_path'] - argv.extend(['--pdf', file_path]) + file_path = self.source_info.parsed["file_path"] + argv.extend(["--pdf", file_path]) # Add universal arguments self._add_common_args(argv) # Add PDF-specific arguments - if getattr(self.args, 'ocr', False): - argv.append('--ocr') - if getattr(self.args, 'pages', None): - argv.extend(['--pages', self.args.pages]) + if getattr(self.args, "ocr", False): + argv.append("--ocr") + if getattr(self.args, "pages", None): + argv.extend(["--pages", self.args.pages]) # Call pdf_scraper with modified argv logger.debug(f"Calling pdf_scraper with argv: {argv}") @@ -291,11 +292,11 @@ class CreateCommand: from skill_seekers.cli import unified_scraper # Reconstruct argv for unified_scraper - argv = ['unified_scraper'] + argv = ["unified_scraper"] # Add config file - config_path = self.source_info.parsed['config_path'] - argv.extend(['--config', config_path]) + config_path = self.source_info.parsed["config_path"] + argv.extend(["--config", config_path]) # Add universal arguments (unified scraper supports most) self._add_common_args(argv) @@ -317,53 +318,54 @@ class CreateCommand: """ # Identity arguments if self.args.name: - argv.extend(['--name', self.args.name]) - elif hasattr(self, 'source_info') and self.source_info: + argv.extend(["--name", self.args.name]) + elif hasattr(self, "source_info") and self.source_info: # Use suggested name from source detection - argv.extend(['--name', self.source_info.suggested_name]) + argv.extend(["--name", self.source_info.suggested_name]) if self.args.description: - argv.extend(['--description', self.args.description]) + argv.extend(["--description", self.args.description]) if self.args.output: - argv.extend(['--output', self.args.output]) + argv.extend(["--output", self.args.output]) # Enhancement arguments (consolidated to --enhance-level only) if self.args.enhance_level > 0: - argv.extend(['--enhance-level', str(self.args.enhance_level)]) + argv.extend(["--enhance-level", str(self.args.enhance_level)]) if self.args.api_key: - argv.extend(['--api-key', self.args.api_key]) + argv.extend(["--api-key", self.args.api_key]) # Behavior arguments if self.args.dry_run: - argv.append('--dry-run') + argv.append("--dry-run") if self.args.verbose: - argv.append('--verbose') + argv.append("--verbose") if self.args.quiet: - argv.append('--quiet') + argv.append("--quiet") # RAG arguments (NEW - universal!) - if getattr(self.args, 'chunk_for_rag', False): - argv.append('--chunk-for-rag') - if getattr(self.args, 'chunk_size', None) and self.args.chunk_size != 512: - argv.extend(['--chunk-size', str(self.args.chunk_size)]) - if getattr(self.args, 'chunk_overlap', None) and self.args.chunk_overlap != 50: - argv.extend(['--chunk-overlap', str(self.args.chunk_overlap)]) + if getattr(self.args, "chunk_for_rag", False): + argv.append("--chunk-for-rag") + if getattr(self.args, "chunk_size", None) and self.args.chunk_size != 512: + argv.extend(["--chunk-size", str(self.args.chunk_size)]) + if getattr(self.args, "chunk_overlap", None) and self.args.chunk_overlap != 50: + argv.extend(["--chunk-overlap", str(self.args.chunk_overlap)]) # Preset argument - if getattr(self.args, 'preset', None): - argv.extend(['--preset', self.args.preset]) + if getattr(self.args, "preset", None): + argv.extend(["--preset", self.args.preset]) # Config file if self.args.config: - argv.extend(['--config', self.args.config]) + argv.extend(["--config", self.args.config]) # Advanced arguments - if getattr(self.args, 'no_preserve_code_blocks', False): - argv.append('--no-preserve-code-blocks') - if getattr(self.args, 'no_preserve_paragraphs', False): - argv.append('--no-preserve-paragraphs') - if getattr(self.args, 'interactive_enhancement', False): - argv.append('--interactive-enhancement') + if getattr(self.args, "no_preserve_code_blocks", False): + argv.append("--no-preserve-code-blocks") + if getattr(self.args, "no_preserve_paragraphs", False): + argv.append("--no-preserve-paragraphs") + if getattr(self.args, "interactive_enhancement", False): + argv.append("--interactive-enhancement") + def main() -> int: """Entry point for create command. @@ -381,8 +383,8 @@ def main() -> int: return text.splitlines() parser = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill from any source (auto-detects type)', + prog="skill-seekers create", + description="Create skill from any source (auto-detects type)", formatter_class=NoWrapFormatter, epilog=textwrap.dedent("""\ Examples: @@ -416,19 +418,25 @@ Common Workflows: skill-seekers create -p quick skill-seekers create -p standard --enhance-level 2 skill-seekers create --chunk-for-rag - """) + """), ) # Add arguments in default mode (universal only) - add_create_arguments(parser, mode='default') + add_create_arguments(parser, mode="default") # Add hidden help mode flags (use underscore prefix to match CreateParser) - parser.add_argument('--help-web', action='store_true', help=argparse.SUPPRESS, dest='_help_web') - parser.add_argument('--help-github', action='store_true', help=argparse.SUPPRESS, dest='_help_github') - parser.add_argument('--help-local', action='store_true', help=argparse.SUPPRESS, dest='_help_local') - parser.add_argument('--help-pdf', action='store_true', help=argparse.SUPPRESS, dest='_help_pdf') - parser.add_argument('--help-advanced', action='store_true', help=argparse.SUPPRESS, dest='_help_advanced') - parser.add_argument('--help-all', action='store_true', help=argparse.SUPPRESS, dest='_help_all') + parser.add_argument("--help-web", action="store_true", help=argparse.SUPPRESS, dest="_help_web") + parser.add_argument( + "--help-github", action="store_true", help=argparse.SUPPRESS, dest="_help_github" + ) + parser.add_argument( + "--help-local", action="store_true", help=argparse.SUPPRESS, dest="_help_local" + ) + parser.add_argument("--help-pdf", action="store_true", help=argparse.SUPPRESS, dest="_help_pdf") + parser.add_argument( + "--help-advanced", action="store_true", help=argparse.SUPPRESS, dest="_help_advanced" + ) + parser.add_argument("--help-all", action="store_true", help=argparse.SUPPRESS, dest="_help_all") # Parse arguments args = parser.parse_args() @@ -437,67 +445,62 @@ Common Workflows: if args._help_web: # Recreate parser with web-specific arguments parser_web = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill from web documentation', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill from web documentation", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_web, mode='web') + add_create_arguments(parser_web, mode="web") parser_web.print_help() return 0 elif args._help_github: parser_github = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill from GitHub repository', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill from GitHub repository", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_github, mode='github') + add_create_arguments(parser_github, mode="github") parser_github.print_help() return 0 elif args._help_local: parser_local = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill from local codebase', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill from local codebase", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_local, mode='local') + add_create_arguments(parser_local, mode="local") parser_local.print_help() return 0 elif args._help_pdf: parser_pdf = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill from PDF file', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill from PDF file", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_pdf, mode='pdf') + add_create_arguments(parser_pdf, mode="pdf") parser_pdf.print_help() return 0 elif args._help_advanced: parser_advanced = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill - advanced options', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill - advanced options", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_advanced, mode='advanced') + add_create_arguments(parser_advanced, mode="advanced") parser_advanced.print_help() return 0 elif args._help_all: parser_all = argparse.ArgumentParser( - prog='skill-seekers create', - description='Create skill - all options', - formatter_class=argparse.RawDescriptionHelpFormatter + prog="skill-seekers create", + description="Create skill - all options", + formatter_class=argparse.RawDescriptionHelpFormatter, ) - add_create_arguments(parser_all, mode='all') + add_create_arguments(parser_all, mode="all") parser_all.print_help() return 0 # Setup logging - log_level = logging.DEBUG if args.verbose else ( - logging.WARNING if args.quiet else logging.INFO - ) - logging.basicConfig( - level=log_level, - format='%(levelname)s: %(message)s' - ) + log_level = logging.DEBUG if args.verbose else (logging.WARNING if args.quiet else logging.INFO) + logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s") # Validate source provided if not args.source: @@ -507,5 +510,6 @@ Common Workflows: command = CreateCommand(args) return command.execute() -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(main()) diff --git a/src/skill_seekers/cli/doc_scraper.py b/src/skill_seekers/cli/doc_scraper.py index 0f4db16..4637120 100755 --- a/src/skill_seekers/cli/doc_scraper.py +++ b/src/skill_seekers/cli/doc_scraper.py @@ -2231,8 +2231,9 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non import subprocess # Optional enhancement with auto-detected mode (API or LOCAL) - if getattr(args, 'enhance_level', 0) > 0: + if getattr(args, "enhance_level", 0) > 0: import os + has_api_key = bool(os.environ.get("ANTHROPIC_API_KEY") or args.api_key) mode = "API" if has_api_key else "LOCAL" @@ -2246,7 +2247,7 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non if args.api_key: enhance_cmd.extend(["--api-key", args.api_key]) - if getattr(args, 'interactive_enhancement', False): + if getattr(args, "interactive_enhancement", False): enhance_cmd.append("--interactive-enhancement") result = subprocess.run(enhance_cmd, check=True) @@ -2256,14 +2257,18 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non logger.warning("\n⚠ Enhancement failed, but skill was still built") except FileNotFoundError: logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:") - logger.info(" skill-seekers-enhance output/%s/ --enhance-level %d", config["name"], args.enhance_level) + logger.info( + " skill-seekers-enhance output/%s/ --enhance-level %d", + config["name"], + args.enhance_level, + ) # Print packaging instructions logger.info("\nšŸ“¦ Package your skill:") logger.info(" skill-seekers-package output/%s/", config["name"]) # Suggest enhancement if not done - if getattr(args, 'enhance_level', 0) == 0: + if getattr(args, "enhance_level", 0) == 0: logger.info("\nšŸ’” Optional: Enhance SKILL.md with Claude:") logger.info(" skill-seekers-enhance output/%s/ --enhance-level 2", config["name"]) logger.info(" or re-run with: --enhance-level 2 (auto-detects API vs LOCAL mode)") diff --git a/src/skill_seekers/cli/github_scraper.py b/src/skill_seekers/cli/github_scraper.py index c209852..08301bc 100644 --- a/src/skill_seekers/cli/github_scraper.py +++ b/src/skill_seekers/cli/github_scraper.py @@ -100,6 +100,7 @@ EXCLUDED_DIRS = { ".tmp", } + def extract_description_from_readme(readme_content: str, repo_name: str) -> str: """ Extract a meaningful description from README content for skill description. @@ -180,6 +181,7 @@ def extract_description_from_readme(readme_content: str, repo_name: str) -> str: project_name = repo_name.split("/")[-1] return f"Use when working with {project_name}" + class GitHubScraper: """ GitHub Repository Scraper (C1.1-C1.9) @@ -892,6 +894,7 @@ class GitHubScraper: logger.info(f"Data saved to: {self.data_file}") + class GitHubToSkillConverter: """ Convert extracted GitHub data to Claude skill format (C1.10). @@ -1347,6 +1350,7 @@ Use this skill when you need to: f.write(content) logger.info(f"Generated: {structure_path}") + def setup_argument_parser() -> argparse.ArgumentParser: """Setup and configure command-line argument parser. @@ -1374,6 +1378,7 @@ Examples: return parser + def main(): """C1.10: CLI tool entry point.""" parser = setup_argument_parser() @@ -1421,14 +1426,16 @@ def main(): skill_dir = f"output/{skill_name}" # Phase 3: Optional enhancement with auto-detected mode - if getattr(args, 'enhance_level', 0) > 0: + if getattr(args, "enhance_level", 0) > 0: import os # Auto-detect mode based on API key availability api_key = args.api_key or os.environ.get("ANTHROPIC_API_KEY") mode = "API" if api_key else "LOCAL" - logger.info(f"\nšŸ“ Enhancing SKILL.md with Claude ({mode} mode, level {args.enhance_level})...") + logger.info( + f"\nšŸ“ Enhancing SKILL.md with Claude ({mode} mode, level {args.enhance_level})..." + ) if api_key: # API-based enhancement @@ -1438,9 +1445,7 @@ def main(): enhance_skill_md(skill_dir, api_key) logger.info("āœ… API enhancement complete!") except ImportError: - logger.error( - "āŒ API enhancement not available. Install: pip install anthropic" - ) + logger.error("āŒ API enhancement not available. Install: pip install anthropic") logger.info("šŸ’” Falling back to LOCAL mode...") # Fall back to LOCAL mode from pathlib import Path @@ -1460,7 +1465,7 @@ def main(): logger.info(f"\nāœ… Success! Skill created at: {skill_dir}/") - if getattr(args, 'enhance_level', 0) == 0: + if getattr(args, "enhance_level", 0) == 0: logger.info("\nšŸ’” Optional: Enhance SKILL.md with Claude:") logger.info(f" skill-seekers enhance {skill_dir}/ --enhance-level 2") logger.info(" (auto-detects API vs LOCAL mode based on ANTHROPIC_API_KEY)") @@ -1471,5 +1476,6 @@ def main(): logger.error(f"Error: {e}") sys.exit(1) + if __name__ == "__main__": main() diff --git a/src/skill_seekers/cli/main.py b/src/skill_seekers/cli/main.py index 530a6ed..9879cd2 100644 --- a/src/skill_seekers/cli/main.py +++ b/src/skill_seekers/cli/main.py @@ -133,7 +133,7 @@ def _reconstruct_argv(command: str, args: argparse.Namespace) -> list[str]: if key.startswith("_help_"): if value: # Convert _help_web -> --help-web - help_flag = key.replace('_help_', 'help-') + help_flag = key.replace("_help_", "help-") argv.append(f"--{help_flag}") continue @@ -181,6 +181,7 @@ def main(argv: list[str] | None = None) -> int: argv = sys.argv[1:] if len(argv) >= 2 and argv[0] == "analyze" and "--preset-list" in argv: from skill_seekers.cli.codebase_scraper import main as analyze_main + original_argv = sys.argv.copy() sys.argv = ["codebase_scraper.py", "--preset-list"] try: @@ -274,8 +275,8 @@ def _handle_analyze_command(args: argparse.Namespace) -> int: sys.argv.extend(["--depth", args.depth]) # Determine enhance_level (simplified - use default or override) - enhance_level = getattr(args, 'enhance_level', 2) # Default is 2 - if getattr(args, 'quick', False): + enhance_level = getattr(args, "enhance_level", 2) # Default is 2 + if getattr(args, "quick", False): enhance_level = 0 # Quick mode disables enhancement sys.argv.extend(["--enhance-level", str(enhance_level)]) diff --git a/src/skill_seekers/cli/parsers/__init__.py b/src/skill_seekers/cli/parsers/__init__.py index c4737c3..7410341 100644 --- a/src/skill_seekers/cli/parsers/__init__.py +++ b/src/skill_seekers/cli/parsers/__init__.py @@ -52,6 +52,7 @@ PARSERS = [ QualityParser(), ] + def register_parsers(subparsers): """Register all subcommand parsers. @@ -64,6 +65,7 @@ def register_parsers(subparsers): for parser_instance in PARSERS: parser_instance.create_parser(subparsers) + def get_parser_names(): """Get list of all subcommand names. @@ -72,6 +74,7 @@ def get_parser_names(): """ return [p.name for p in PARSERS] + __all__ = [ "SubcommandParser", "PARSERS", diff --git a/src/skill_seekers/cli/parsers/analyze_parser.py b/src/skill_seekers/cli/parsers/analyze_parser.py index 897e302..24a44a3 100644 --- a/src/skill_seekers/cli/parsers/analyze_parser.py +++ b/src/skill_seekers/cli/parsers/analyze_parser.py @@ -9,6 +9,7 @@ Includes preset system support (Issue #268). from .base import SubcommandParser from skill_seekers.cli.arguments.analyze import add_analyze_arguments + class AnalyzeParser(SubcommandParser): """Parser for analyze subcommand.""" diff --git a/src/skill_seekers/cli/parsers/base.py b/src/skill_seekers/cli/parsers/base.py index 45e57ad..765cf01 100644 --- a/src/skill_seekers/cli/parsers/base.py +++ b/src/skill_seekers/cli/parsers/base.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod import argparse + class SubcommandParser(ABC): """Base class for subcommand parsers. diff --git a/src/skill_seekers/cli/parsers/config_parser.py b/src/skill_seekers/cli/parsers/config_parser.py index d46aba0..f78c36e 100644 --- a/src/skill_seekers/cli/parsers/config_parser.py +++ b/src/skill_seekers/cli/parsers/config_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class ConfigParser(SubcommandParser): """Parser for config subcommand.""" diff --git a/src/skill_seekers/cli/parsers/create_parser.py b/src/skill_seekers/cli/parsers/create_parser.py index e1f59a9..58c35a9 100644 --- a/src/skill_seekers/cli/parsers/create_parser.py +++ b/src/skill_seekers/cli/parsers/create_parser.py @@ -13,6 +13,7 @@ import argparse from .base import SubcommandParser from skill_seekers.cli.arguments.create import add_create_arguments + class CreateParser(SubcommandParser): """Parser for create subcommand with multi-mode help.""" @@ -54,45 +55,45 @@ Presets: -p quick (1-2min) | -p standard (5-10min) | -p comprehensive (20-60min) """ # Add all arguments in 'default' mode (universal only) # This keeps help text clean and focused - add_create_arguments(parser, mode='default') + add_create_arguments(parser, mode="default") # Add hidden help mode flags # These won't show in default help but can be used to get source-specific help parser.add_argument( - '--help-web', - action='store_true', - help='Show web scraping specific options', - dest='_help_web' + "--help-web", + action="store_true", + help="Show web scraping specific options", + dest="_help_web", ) parser.add_argument( - '--help-github', - action='store_true', - help='Show GitHub repository specific options', - dest='_help_github' + "--help-github", + action="store_true", + help="Show GitHub repository specific options", + dest="_help_github", ) parser.add_argument( - '--help-local', - action='store_true', - help='Show local codebase specific options', - dest='_help_local' + "--help-local", + action="store_true", + help="Show local codebase specific options", + dest="_help_local", ) parser.add_argument( - '--help-pdf', - action='store_true', - help='Show PDF extraction specific options', - dest='_help_pdf' + "--help-pdf", + action="store_true", + help="Show PDF extraction specific options", + dest="_help_pdf", ) parser.add_argument( - '--help-advanced', - action='store_true', - help='Show advanced/rare options', - dest='_help_advanced' + "--help-advanced", + action="store_true", + help="Show advanced/rare options", + dest="_help_advanced", ) parser.add_argument( - '--help-all', - action='store_true', - help='Show all available options (120+ flags)', - dest='_help_all' + "--help-all", + action="store_true", + help="Show all available options (120+ flags)", + dest="_help_all", ) def register(self, subparsers): @@ -104,16 +105,14 @@ Presets: -p quick (1-2min) | -p standard (5-10min) | -p comprehensive (20-60min) Returns: Configured ArgumentParser for this subcommand """ + # Custom formatter that preserves line breaks class NoWrapFormatter(argparse.RawDescriptionHelpFormatter): def _split_lines(self, text, width): return text.splitlines() parser = subparsers.add_parser( - self.name, - help=self.help, - description=self.description, - formatter_class=NoWrapFormatter + self.name, help=self.help, description=self.description, formatter_class=NoWrapFormatter ) self.add_arguments(parser) return parser diff --git a/src/skill_seekers/cli/parsers/enhance_parser.py b/src/skill_seekers/cli/parsers/enhance_parser.py index bd15163..138403c 100644 --- a/src/skill_seekers/cli/parsers/enhance_parser.py +++ b/src/skill_seekers/cli/parsers/enhance_parser.py @@ -7,6 +7,7 @@ consistency with the standalone enhance_skill_local module. from .base import SubcommandParser from skill_seekers.cli.arguments.enhance import add_enhance_arguments + class EnhanceParser(SubcommandParser): """Parser for enhance subcommand.""" diff --git a/src/skill_seekers/cli/parsers/enhance_status_parser.py b/src/skill_seekers/cli/parsers/enhance_status_parser.py index 119ba31..229098c 100644 --- a/src/skill_seekers/cli/parsers/enhance_status_parser.py +++ b/src/skill_seekers/cli/parsers/enhance_status_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class EnhanceStatusParser(SubcommandParser): """Parser for enhance-status subcommand.""" diff --git a/src/skill_seekers/cli/parsers/estimate_parser.py b/src/skill_seekers/cli/parsers/estimate_parser.py index e699058..5a21bdc 100644 --- a/src/skill_seekers/cli/parsers/estimate_parser.py +++ b/src/skill_seekers/cli/parsers/estimate_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class EstimateParser(SubcommandParser): """Parser for estimate subcommand.""" diff --git a/src/skill_seekers/cli/parsers/github_parser.py b/src/skill_seekers/cli/parsers/github_parser.py index e7ea2f7..051e342 100644 --- a/src/skill_seekers/cli/parsers/github_parser.py +++ b/src/skill_seekers/cli/parsers/github_parser.py @@ -7,6 +7,7 @@ consistency with the standalone github_scraper module. from .base import SubcommandParser from skill_seekers.cli.arguments.github import add_github_arguments + class GitHubParser(SubcommandParser): """Parser for github subcommand.""" diff --git a/src/skill_seekers/cli/parsers/install_agent_parser.py b/src/skill_seekers/cli/parsers/install_agent_parser.py index dcc1803..884d56e 100644 --- a/src/skill_seekers/cli/parsers/install_agent_parser.py +++ b/src/skill_seekers/cli/parsers/install_agent_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class InstallAgentParser(SubcommandParser): """Parser for install-agent subcommand.""" diff --git a/src/skill_seekers/cli/parsers/install_parser.py b/src/skill_seekers/cli/parsers/install_parser.py index 6bb06e8..3d48e6d 100644 --- a/src/skill_seekers/cli/parsers/install_parser.py +++ b/src/skill_seekers/cli/parsers/install_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class InstallParser(SubcommandParser): """Parser for install subcommand.""" diff --git a/src/skill_seekers/cli/parsers/multilang_parser.py b/src/skill_seekers/cli/parsers/multilang_parser.py index 2de4c8f..68bdb55 100644 --- a/src/skill_seekers/cli/parsers/multilang_parser.py +++ b/src/skill_seekers/cli/parsers/multilang_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class MultilangParser(SubcommandParser): """Parser for multilang subcommand.""" diff --git a/src/skill_seekers/cli/parsers/package_parser.py b/src/skill_seekers/cli/parsers/package_parser.py index 0511459..cb0658d 100644 --- a/src/skill_seekers/cli/parsers/package_parser.py +++ b/src/skill_seekers/cli/parsers/package_parser.py @@ -7,6 +7,7 @@ consistency with the standalone package_skill module. from .base import SubcommandParser from skill_seekers.cli.arguments.package import add_package_arguments + class PackageParser(SubcommandParser): """Parser for package subcommand.""" diff --git a/src/skill_seekers/cli/parsers/pdf_parser.py b/src/skill_seekers/cli/parsers/pdf_parser.py index 3c9f0d6..e285548 100644 --- a/src/skill_seekers/cli/parsers/pdf_parser.py +++ b/src/skill_seekers/cli/parsers/pdf_parser.py @@ -7,6 +7,7 @@ consistency with the standalone pdf_scraper module. from .base import SubcommandParser from skill_seekers.cli.arguments.pdf import add_pdf_arguments + class PDFParser(SubcommandParser): """Parser for pdf subcommand.""" diff --git a/src/skill_seekers/cli/parsers/quality_parser.py b/src/skill_seekers/cli/parsers/quality_parser.py index e394cba..69803fe 100644 --- a/src/skill_seekers/cli/parsers/quality_parser.py +++ b/src/skill_seekers/cli/parsers/quality_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class QualityParser(SubcommandParser): """Parser for quality subcommand.""" diff --git a/src/skill_seekers/cli/parsers/resume_parser.py b/src/skill_seekers/cli/parsers/resume_parser.py index f024d2a..9bb5d07 100644 --- a/src/skill_seekers/cli/parsers/resume_parser.py +++ b/src/skill_seekers/cli/parsers/resume_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class ResumeParser(SubcommandParser): """Parser for resume subcommand.""" diff --git a/src/skill_seekers/cli/parsers/scrape_parser.py b/src/skill_seekers/cli/parsers/scrape_parser.py index 83cbea9..7f9869d 100644 --- a/src/skill_seekers/cli/parsers/scrape_parser.py +++ b/src/skill_seekers/cli/parsers/scrape_parser.py @@ -7,6 +7,7 @@ consistency with the standalone doc_scraper module. from .base import SubcommandParser from skill_seekers.cli.arguments.scrape import add_scrape_arguments + class ScrapeParser(SubcommandParser): """Parser for scrape subcommand.""" diff --git a/src/skill_seekers/cli/parsers/stream_parser.py b/src/skill_seekers/cli/parsers/stream_parser.py index 79f7cfc..6ee513a 100644 --- a/src/skill_seekers/cli/parsers/stream_parser.py +++ b/src/skill_seekers/cli/parsers/stream_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class StreamParser(SubcommandParser): """Parser for stream subcommand.""" diff --git a/src/skill_seekers/cli/parsers/test_examples_parser.py b/src/skill_seekers/cli/parsers/test_examples_parser.py index 68df0ed..da2bde9 100644 --- a/src/skill_seekers/cli/parsers/test_examples_parser.py +++ b/src/skill_seekers/cli/parsers/test_examples_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class TestExamplesParser(SubcommandParser): """Parser for extract-test-examples subcommand.""" diff --git a/src/skill_seekers/cli/parsers/unified_parser.py b/src/skill_seekers/cli/parsers/unified_parser.py index c6ee77a..b4550c4 100644 --- a/src/skill_seekers/cli/parsers/unified_parser.py +++ b/src/skill_seekers/cli/parsers/unified_parser.py @@ -7,6 +7,7 @@ consistency with the standalone unified_scraper module. from .base import SubcommandParser from skill_seekers.cli.arguments.unified import add_unified_arguments + class UnifiedParser(SubcommandParser): """Parser for unified subcommand.""" diff --git a/src/skill_seekers/cli/parsers/update_parser.py b/src/skill_seekers/cli/parsers/update_parser.py index 46711f4..eaa321a 100644 --- a/src/skill_seekers/cli/parsers/update_parser.py +++ b/src/skill_seekers/cli/parsers/update_parser.py @@ -2,6 +2,7 @@ from .base import SubcommandParser + class UpdateParser(SubcommandParser): """Parser for update subcommand.""" diff --git a/src/skill_seekers/cli/parsers/upload_parser.py b/src/skill_seekers/cli/parsers/upload_parser.py index 4aff69c..fb0a982 100644 --- a/src/skill_seekers/cli/parsers/upload_parser.py +++ b/src/skill_seekers/cli/parsers/upload_parser.py @@ -7,6 +7,7 @@ consistency with the standalone upload_skill module. from .base import SubcommandParser from skill_seekers.cli.arguments.upload import add_upload_arguments + class UploadParser(SubcommandParser): """Parser for upload subcommand.""" diff --git a/src/skill_seekers/cli/presets/analyze_presets.py b/src/skill_seekers/cli/presets/analyze_presets.py index a2949fe..a547c1c 100644 --- a/src/skill_seekers/cli/presets/analyze_presets.py +++ b/src/skill_seekers/cli/presets/analyze_presets.py @@ -15,6 +15,7 @@ from dataclasses import dataclass, field import argparse + @dataclass(frozen=True) class AnalysisPreset: """Definition of an analysis preset. @@ -29,12 +30,14 @@ class AnalysisPreset: features: Dict of feature flags (feature_name -> enabled) estimated_time: Human-readable time estimate """ + name: str description: str depth: str features: dict[str, bool] = field(default_factory=dict) estimated_time: str = "" + # Preset definitions ANALYZE_PRESETS = { "quick": AnalysisPreset( @@ -49,9 +52,8 @@ ANALYZE_PRESETS = { "how_to_guides": False, "config_patterns": False, }, - estimated_time="1-2 minutes" + estimated_time="1-2 minutes", ), - "standard": AnalysisPreset( name="Standard", description="Balanced analysis with core features (recommended)", @@ -64,9 +66,8 @@ ANALYZE_PRESETS = { "how_to_guides": False, "config_patterns": True, }, - estimated_time="5-10 minutes" + estimated_time="5-10 minutes", ), - "comprehensive": AnalysisPreset( name="Comprehensive", description="Full analysis with all features", @@ -79,10 +80,11 @@ ANALYZE_PRESETS = { "how_to_guides": True, "config_patterns": True, }, - estimated_time="20-60 minutes" + estimated_time="20-60 minutes", ), } + def apply_analyze_preset(args: argparse.Namespace, preset_name: str) -> None: """Apply an analysis preset to the args namespace. @@ -113,6 +115,7 @@ def apply_analyze_preset(args: argparse.Namespace, preset_name: str) -> None: skip_attr = f"skip_{feature}" setattr(args, skip_attr, not enabled) + def get_preset_help_text(preset_name: str) -> str: """Get formatted help text for a preset. @@ -129,6 +132,7 @@ def get_preset_help_text(preset_name: str) -> str: f" Depth: {preset.depth}" ) + def show_preset_list() -> None: """Print the list of available presets to stdout. @@ -161,6 +165,7 @@ def show_preset_list() -> None: print(" skill-seekers analyze --directory --preset comprehensive --enhance-level 2") print() + def resolve_enhance_level(args: argparse.Namespace) -> int: """Determine the enhance level based on user arguments. @@ -186,6 +191,7 @@ def resolve_enhance_level(args: argparse.Namespace) -> int: # Default is no enhancement return 0 + def apply_preset_with_warnings(args: argparse.Namespace) -> str: """Apply preset with deprecation warnings for legacy flags. @@ -240,6 +246,7 @@ def apply_preset_with_warnings(args: argparse.Namespace) -> str: return preset_name + def print_deprecation_warning(old_flag: str, new_flag: str) -> None: """Print a deprecation warning for legacy flags. diff --git a/src/skill_seekers/cli/presets/github_presets.py b/src/skill_seekers/cli/presets/github_presets.py index 9f034aa..9f8fe78 100644 --- a/src/skill_seekers/cli/presets/github_presets.py +++ b/src/skill_seekers/cli/presets/github_presets.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field import argparse + @dataclass(frozen=True) class GitHubPreset: """Definition of a GitHub preset. @@ -23,12 +24,14 @@ class GitHubPreset: features: Dict of feature flags (feature_name -> enabled) estimated_time: Human-readable time estimate """ + name: str description: str max_issues: int features: dict[str, bool] = field(default_factory=dict) estimated_time: str = "" + # Preset definitions GITHUB_PRESETS = { "quick": GitHubPreset( @@ -40,9 +43,8 @@ GITHUB_PRESETS = { "include_changelog": True, "include_releases": False, }, - estimated_time="1-3 minutes" + estimated_time="1-3 minutes", ), - "standard": GitHubPreset( name="Standard", description="Balanced scraping with issues and releases (recommended)", @@ -52,9 +54,8 @@ GITHUB_PRESETS = { "include_changelog": True, "include_releases": True, }, - estimated_time="5-15 minutes" + estimated_time="5-15 minutes", ), - "comprehensive": GitHubPreset( name="Comprehensive", description="Comprehensive scraping with all available data", @@ -64,10 +65,11 @@ GITHUB_PRESETS = { "include_changelog": True, "include_releases": True, }, - estimated_time="20-60 minutes" + estimated_time="20-60 minutes", ), } + def apply_github_preset(args: argparse.Namespace, preset_name: str) -> None: """Apply a GitHub preset to the args namespace. @@ -90,6 +92,7 @@ def apply_github_preset(args: argparse.Namespace, preset_name: str) -> None: if not hasattr(args, skip_attr) or not getattr(args, skip_attr): setattr(args, skip_attr, not enabled) + def show_github_preset_list() -> None: """Print the list of available GitHub presets to stdout.""" print("\nAvailable GitHub Presets") diff --git a/src/skill_seekers/cli/presets/manager.py b/src/skill_seekers/cli/presets/manager.py index 15bed25..55b573c 100644 --- a/src/skill_seekers/cli/presets/manager.py +++ b/src/skill_seekers/cli/presets/manager.py @@ -6,6 +6,7 @@ between speed and comprehensiveness. from dataclasses import dataclass + @dataclass class AnalysisPreset: """Analysis preset configuration. @@ -22,6 +23,7 @@ class AnalysisPreset: estimated_time: str icon: str + # Preset definitions PRESETS = { "quick": AnalysisPreset( @@ -77,6 +79,7 @@ PRESETS = { ), } + class PresetManager: """Manages analysis presets and applies them to CLI arguments.""" @@ -164,6 +167,7 @@ class PresetManager: """ return "standard" + # Public API __all__ = [ "AnalysisPreset", diff --git a/src/skill_seekers/cli/presets/scrape_presets.py b/src/skill_seekers/cli/presets/scrape_presets.py index e81f638..a1cfc20 100644 --- a/src/skill_seekers/cli/presets/scrape_presets.py +++ b/src/skill_seekers/cli/presets/scrape_presets.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field import argparse + @dataclass(frozen=True) class ScrapePreset: """Definition of a scrape preset. @@ -25,6 +26,7 @@ class ScrapePreset: workers: Number of parallel workers estimated_time: Human-readable time estimate """ + name: str description: str rate_limit: float @@ -33,6 +35,7 @@ class ScrapePreset: workers: int = 1 estimated_time: str = "" + # Preset definitions SCRAPE_PRESETS = { "quick": ScrapePreset( @@ -45,9 +48,8 @@ SCRAPE_PRESETS = { }, async_mode=True, workers=5, - estimated_time="2-5 minutes" + estimated_time="2-5 minutes", ), - "standard": ScrapePreset( name="Standard", description="Balanced scraping with good coverage (recommended)", @@ -58,9 +60,8 @@ SCRAPE_PRESETS = { }, async_mode=True, workers=3, - estimated_time="10-30 minutes" + estimated_time="10-30 minutes", ), - "comprehensive": ScrapePreset( name="Comprehensive", description="Comprehensive scraping with all features", @@ -71,10 +72,11 @@ SCRAPE_PRESETS = { }, async_mode=True, workers=2, - estimated_time="1-3 hours" + estimated_time="1-3 hours", ), } + def apply_scrape_preset(args: argparse.Namespace, preset_name: str) -> None: """Apply a scrape preset to the args namespace. @@ -100,9 +102,12 @@ def apply_scrape_preset(args: argparse.Namespace, preset_name: str) -> None: # Apply feature flags for feature, enabled in preset.features.items(): - if feature == "rag_chunking" and (not hasattr(args, 'chunk_for_rag') or not args.chunk_for_rag): + if feature == "rag_chunking" and ( + not hasattr(args, "chunk_for_rag") or not args.chunk_for_rag + ): args.chunk_for_rag = enabled + def show_scrape_preset_list() -> None: """Print the list of available scrape presets to stdout.""" print("\nAvailable Scrape Presets") diff --git a/src/skill_seekers/cli/source_detector.py b/src/skill_seekers/cli/source_detector.py index 1d1f443..8f98408 100644 --- a/src/skill_seekers/cli/source_detector.py +++ b/src/skill_seekers/cli/source_detector.py @@ -13,6 +13,7 @@ import logging logger = logging.getLogger(__name__) + @dataclass class SourceInfo: """Information about a detected source. @@ -23,18 +24,20 @@ class SourceInfo: suggested_name: Auto-suggested name for the skill raw_input: Original user input """ + type: str parsed: dict[str, Any] suggested_name: str raw_input: str + class SourceDetector: """Detects source type from user input and extracts relevant information.""" # GitHub repo patterns - GITHUB_REPO_PATTERN = re.compile(r'^([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)$') + GITHUB_REPO_PATTERN = re.compile(r"^([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)$") GITHUB_URL_PATTERN = re.compile( - r'(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)(?:\.git)?' + r"(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)(?:\.git)?" ) @classmethod @@ -51,10 +54,10 @@ class SourceDetector: ValueError: If source type cannot be determined """ # 1. File extension detection - if source.endswith('.json'): + if source.endswith(".json"): return cls._detect_config(source) - if source.endswith('.pdf'): + if source.endswith(".pdf"): return cls._detect_pdf(source) # 2. Directory detection @@ -67,12 +70,12 @@ class SourceDetector: return github_info # 4. URL detection - if source.startswith('http://') or source.startswith('https://'): + if source.startswith("http://") or source.startswith("https://"): return cls._detect_web(source) # 5. Domain inference (add https://) - if '.' in source and not source.startswith('/'): - return cls._detect_web(f'https://{source}') + if "." in source and not source.startswith("/"): + return cls._detect_web(f"https://{source}") # 6. Error - cannot determine raise ValueError( @@ -90,10 +93,7 @@ class SourceDetector: """Detect config file source.""" name = os.path.splitext(os.path.basename(source))[0] return SourceInfo( - type='config', - parsed={'config_path': source}, - suggested_name=name, - raw_input=source + type="config", parsed={"config_path": source}, suggested_name=name, raw_input=source ) @classmethod @@ -101,10 +101,7 @@ class SourceDetector: """Detect PDF file source.""" name = os.path.splitext(os.path.basename(source))[0] return SourceInfo( - type='pdf', - parsed={'file_path': source}, - suggested_name=name, - raw_input=source + type="pdf", parsed={"file_path": source}, suggested_name=name, raw_input=source ) @classmethod @@ -115,10 +112,7 @@ class SourceDetector: name = os.path.basename(directory) return SourceInfo( - type='local', - parsed={'directory': directory}, - suggested_name=name, - raw_input=source + type="local", parsed={"directory": directory}, suggested_name=name, raw_input=source ) @classmethod @@ -135,10 +129,10 @@ class SourceDetector: if match: owner, repo = match.groups() return SourceInfo( - type='github', - parsed={'repo': f'{owner}/{repo}'}, + type="github", + parsed={"repo": f"{owner}/{repo}"}, suggested_name=repo, - raw_input=source + raw_input=source, ) # Try GitHub URL pattern @@ -146,13 +140,13 @@ class SourceDetector: if match: owner, repo = match.groups() # Clean up repo name (remove .git suffix if present) - if repo.endswith('.git'): + if repo.endswith(".git"): repo = repo[:-4] return SourceInfo( - type='github', - parsed={'repo': f'{owner}/{repo}'}, + type="github", + parsed={"repo": f"{owner}/{repo}"}, suggested_name=repo, - raw_input=source + raw_input=source, ) return None @@ -167,15 +161,10 @@ class SourceDetector: # Clean up domain for name suggestion # docs.react.dev -> react # reactjs.org -> react - name = domain.replace('www.', '').replace('docs.', '') - name = name.split('.')[0] # Take first part before TLD + name = domain.replace("www.", "").replace("docs.", "") + name = name.split(".")[0] # Take first part before TLD - return SourceInfo( - type='web', - parsed={'url': source}, - suggested_name=name, - raw_input=source - ) + return SourceInfo(type="web", parsed={"url": source}, suggested_name=name, raw_input=source) @classmethod def validate_source(cls, source_info: SourceInfo) -> None: @@ -187,22 +176,22 @@ class SourceDetector: Raises: ValueError: If source is not accessible """ - if source_info.type == 'local': - directory = source_info.parsed['directory'] + if source_info.type == "local": + directory = source_info.parsed["directory"] if not os.path.exists(directory): raise ValueError(f"Directory does not exist: {directory}") if not os.path.isdir(directory): raise ValueError(f"Path is not a directory: {directory}") - elif source_info.type == 'pdf': - file_path = source_info.parsed['file_path'] + elif source_info.type == "pdf": + file_path = source_info.parsed["file_path"] if not os.path.exists(file_path): raise ValueError(f"PDF file does not exist: {file_path}") if not os.path.isfile(file_path): raise ValueError(f"Path is not a file: {file_path}") - elif source_info.type == 'config': - config_path = source_info.parsed['config_path'] + elif source_info.type == "config": + config_path = source_info.parsed["config_path"] if not os.path.exists(config_path): raise ValueError(f"Config file does not exist: {config_path}") if not os.path.isfile(config_path): diff --git a/tests/test_cli_refactor_e2e.py b/tests/test_cli_refactor_e2e.py index e60a783..8226a59 100644 --- a/tests/test_cli_refactor_e2e.py +++ b/tests/test_cli_refactor_e2e.py @@ -13,15 +13,14 @@ import pytest import subprocess import argparse + class TestParserSync: """E2E tests for parser synchronization (Issue #285).""" def test_scrape_interactive_flag_works(self): """Test that --interactive flag (previously missing) now works.""" result = subprocess.run( - ["skill-seekers", "scrape", "--interactive", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--interactive", "--help"], capture_output=True, text=True ) assert result.returncode == 0, "Command should execute successfully" assert "--interactive" in result.stdout, "Help should show --interactive flag" @@ -30,9 +29,7 @@ class TestParserSync: def test_scrape_chunk_for_rag_flag_works(self): """Test that --chunk-for-rag flag (previously missing) now works.""" result = subprocess.run( - ["skill-seekers", "scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True ) assert "--chunk-for-rag" in result.stdout, "Help should show --chunk-for-rag flag" assert "--chunk-size" in result.stdout, "Help should show --chunk-size flag" @@ -41,9 +38,7 @@ class TestParserSync: def test_scrape_verbose_flag_works(self): """Test that --verbose flag (previously missing) now works.""" result = subprocess.run( - ["skill-seekers", "scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True ) assert "--verbose" in result.stdout, "Help should show --verbose flag" assert "-v" in result.stdout, "Help should show short form -v" @@ -51,18 +46,14 @@ class TestParserSync: def test_scrape_url_flag_works(self): """Test that --url flag (previously missing) now works.""" result = subprocess.run( - ["skill-seekers", "scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True ) assert "--url URL" in result.stdout, "Help should show --url flag" def test_github_all_flags_present(self): """Test that github command has all expected flags.""" result = subprocess.run( - ["skill-seekers", "github", "--help"], - capture_output=True, - text=True + ["skill-seekers", "github", "--help"], capture_output=True, text=True ) # Key github flags that should be present expected_flags = [ @@ -74,15 +65,14 @@ class TestParserSync: for flag in expected_flags: assert flag in result.stdout, f"Help should show {flag} flag" + class TestPresetSystem: """E2E tests for preset system (Issue #268).""" def test_analyze_preset_flag_exists(self): """Test that analyze command has --preset flag.""" result = subprocess.run( - ["skill-seekers", "analyze", "--help"], - capture_output=True, - text=True + ["skill-seekers", "analyze", "--help"], capture_output=True, text=True ) assert "--preset" in result.stdout, "Help should show --preset flag" assert "quick" in result.stdout, "Help should mention 'quick' preset" @@ -92,18 +82,14 @@ class TestPresetSystem: def test_analyze_preset_list_flag_exists(self): """Test that analyze command has --preset-list flag.""" result = subprocess.run( - ["skill-seekers", "analyze", "--help"], - capture_output=True, - text=True + ["skill-seekers", "analyze", "--help"], capture_output=True, text=True ) assert "--preset-list" in result.stdout, "Help should show --preset-list flag" def test_preset_list_shows_presets(self): """Test that --preset-list shows all available presets.""" result = subprocess.run( - ["skill-seekers", "analyze", "--preset-list"], - capture_output=True, - text=True + ["skill-seekers", "analyze", "--preset-list"], capture_output=True, text=True ) assert result.returncode == 0, "Command should execute successfully" assert "Available presets" in result.stdout, "Should show preset list header" @@ -118,7 +104,7 @@ class TestPresetSystem: result = subprocess.run( ["skill-seekers", "analyze", "--directory", ".", "--quick"], capture_output=True, - text=True + text=True, ) # Note: Deprecation warnings go to stderr output = result.stdout + result.stderr @@ -131,22 +117,19 @@ class TestPresetSystem: result = subprocess.run( ["skill-seekers", "analyze", "--directory", ".", "--comprehensive"], capture_output=True, - text=True + text=True, ) output = result.stdout + result.stderr assert "DEPRECATED" in output, "Should show deprecation warning" assert "--preset comprehensive" in output, "Should suggest alternative" + class TestBackwardCompatibility: """E2E tests for backward compatibility.""" def test_old_scrape_command_still_works(self): """Test that old scrape command invocations still work.""" - result = subprocess.run( - ["skill-seekers-scrape", "--help"], - capture_output=True, - text=True - ) + result = subprocess.run(["skill-seekers-scrape", "--help"], capture_output=True, text=True) assert result.returncode == 0, "Old command should still work" assert "documentation" in result.stdout.lower(), "Help should mention documentation" @@ -154,16 +137,12 @@ class TestBackwardCompatibility: """Test that unified CLI and standalone have identical arguments.""" # Get help from unified CLI unified_result = subprocess.run( - ["skill-seekers", "scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True ) # Get help from standalone standalone_result = subprocess.run( - ["skill-seekers-scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers-scrape", "--help"], capture_output=True, text=True ) # Both should have the same key flags @@ -180,6 +159,7 @@ class TestBackwardCompatibility: assert flag in unified_result.stdout, f"Unified should have {flag}" assert flag in standalone_result.stdout, f"Standalone should have {flag}" + class TestProgrammaticAPI: """Test that the shared argument functions work programmatically.""" @@ -221,16 +201,13 @@ class TestProgrammaticAPI: # Note: enhance_level is not part of AnalysisPreset anymore. # It's controlled separately via --enhance-level flag (default 2) + class TestIntegration: """Integration tests for the complete flow.""" def test_unified_cli_subcommands_registered(self): """Test that all subcommands are properly registered.""" - result = subprocess.run( - ["skill-seekers", "--help"], - capture_output=True, - text=True - ) + result = subprocess.run(["skill-seekers", "--help"], capture_output=True, text=True) # All major commands should be listed expected_commands = [ @@ -250,9 +227,7 @@ class TestIntegration: def test_scrape_help_detailed(self): """Test that scrape help shows all argument details.""" result = subprocess.run( - ["skill-seekers", "scrape", "--help"], - capture_output=True, - text=True + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True ) # Check for argument categories @@ -263,13 +238,14 @@ class TestIntegration: def test_analyze_help_shows_presets(self): """Test that analyze help prominently shows preset information.""" result = subprocess.run( - ["skill-seekers", "analyze", "--help"], - capture_output=True, - text=True + ["skill-seekers", "analyze", "--help"], capture_output=True, text=True ) assert "--preset" in result.stdout, "Should show --preset flag" - assert "DEFAULT" in result.stdout or "default" in result.stdout, "Should indicate default preset" + assert "DEFAULT" in result.stdout or "default" in result.stdout, ( + "Should indicate default preset" + ) + class TestE2EWorkflow: """End-to-end workflow tests.""" @@ -279,15 +255,18 @@ class TestE2EWorkflow: """Test scraping with previously missing arguments (dry run).""" result = subprocess.run( [ - "skill-seekers", "scrape", - "--url", "https://example.com", - "--interactive", "false", # Would fail if arg didn't exist + "skill-seekers", + "scrape", + "--url", + "https://example.com", + "--interactive", + "false", # Would fail if arg didn't exist "--verbose", # Would fail if arg didn't exist "--dry-run", ], capture_output=True, text=True, - timeout=10 + timeout=10, ) # Dry run should complete without errors @@ -314,5 +293,6 @@ class TestE2EWorkflow: assert "--preset" in result.stdout, "Should have --preset flag" assert "unrecognized arguments" not in result.stderr.lower() + if __name__ == "__main__": pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_create_arguments.py b/tests/test_create_arguments.py index d55160c..8b3518f 100644 --- a/tests/test_create_arguments.py +++ b/tests/test_create_arguments.py @@ -19,6 +19,7 @@ from skill_seekers.cli.arguments.create import ( add_create_arguments, ) + class TestUniversalArguments: """Test universal argument definitions.""" @@ -29,25 +30,34 @@ class TestUniversalArguments: def test_universal_argument_names(self): """Universal arguments should have expected names.""" expected_names = { - 'name', 'description', 'output', - 'enhance_level', 'api_key', # Phase 1: consolidated from enhance + enhance_local - 'dry_run', 'verbose', 'quiet', - 'chunk_for_rag', 'chunk_size', 'chunk_overlap', # Phase 2: RAG args from common.py - 'preset', 'config' + "name", + "description", + "output", + "enhance_level", + "api_key", # Phase 1: consolidated from enhance + enhance_local + "dry_run", + "verbose", + "quiet", + "chunk_for_rag", + "chunk_size", + "chunk_overlap", # Phase 2: RAG args from common.py + "preset", + "config", } assert set(UNIVERSAL_ARGUMENTS.keys()) == expected_names def test_all_universal_have_flags(self): """All universal arguments should have flags.""" for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items(): - assert 'flags' in arg_def - assert len(arg_def['flags']) > 0 + assert "flags" in arg_def + assert len(arg_def["flags"]) > 0 def test_all_universal_have_kwargs(self): """All universal arguments should have kwargs.""" for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items(): - assert 'kwargs' in arg_def - assert 'help' in arg_def['kwargs'] + assert "kwargs" in arg_def + assert "help" in arg_def["kwargs"] + class TestSourceSpecificArguments: """Test source-specific argument definitions.""" @@ -55,29 +65,29 @@ class TestSourceSpecificArguments: def test_web_arguments_exist(self): """Web-specific arguments should be defined.""" assert len(WEB_ARGUMENTS) > 0 - assert 'max_pages' in WEB_ARGUMENTS - assert 'rate_limit' in WEB_ARGUMENTS - assert 'workers' in WEB_ARGUMENTS + assert "max_pages" in WEB_ARGUMENTS + assert "rate_limit" in WEB_ARGUMENTS + assert "workers" in WEB_ARGUMENTS def test_github_arguments_exist(self): """GitHub-specific arguments should be defined.""" assert len(GITHUB_ARGUMENTS) > 0 - assert 'repo' in GITHUB_ARGUMENTS - assert 'token' in GITHUB_ARGUMENTS - assert 'max_issues' in GITHUB_ARGUMENTS + assert "repo" in GITHUB_ARGUMENTS + assert "token" in GITHUB_ARGUMENTS + assert "max_issues" in GITHUB_ARGUMENTS def test_local_arguments_exist(self): """Local-specific arguments should be defined.""" assert len(LOCAL_ARGUMENTS) > 0 - assert 'directory' in LOCAL_ARGUMENTS - assert 'languages' in LOCAL_ARGUMENTS - assert 'skip_patterns' in LOCAL_ARGUMENTS + assert "directory" in LOCAL_ARGUMENTS + assert "languages" in LOCAL_ARGUMENTS + assert "skip_patterns" in LOCAL_ARGUMENTS def test_pdf_arguments_exist(self): """PDF-specific arguments should be defined.""" assert len(PDF_ARGUMENTS) > 0 - assert 'pdf' in PDF_ARGUMENTS - assert 'ocr' in PDF_ARGUMENTS + assert "pdf" in PDF_ARGUMENTS + assert "ocr" in PDF_ARGUMENTS def test_no_duplicate_flags_across_sources(self): """Source-specific arguments should not have duplicate flags.""" @@ -86,21 +96,25 @@ class TestSourceSpecificArguments: for source_args in [WEB_ARGUMENTS, GITHUB_ARGUMENTS, LOCAL_ARGUMENTS, PDF_ARGUMENTS]: for arg_name, arg_def in source_args.items(): - flags = arg_def['flags'] + flags = arg_def["flags"] for flag in flags: # Check if this flag already exists in source-specific args - if flag not in [f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg['flags']]: + if flag not in [ + f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg["flags"] + ]: assert flag not in all_flags, f"Duplicate flag: {flag}" all_flags.add(flag) + class TestAdvancedArguments: """Test advanced/rare argument definitions.""" def test_advanced_arguments_exist(self): """Advanced arguments should be defined.""" assert len(ADVANCED_ARGUMENTS) > 0 - assert 'no_rate_limit' in ADVANCED_ARGUMENTS - assert 'interactive_enhancement' in ADVANCED_ARGUMENTS + assert "no_rate_limit" in ADVANCED_ARGUMENTS + assert "interactive_enhancement" in ADVANCED_ARGUMENTS + class TestArgumentHelpers: """Test helper functions.""" @@ -110,106 +124,108 @@ class TestArgumentHelpers: names = get_universal_argument_names() assert isinstance(names, set) assert len(names) == 13 - assert 'name' in names - assert 'enhance_level' in names # Phase 1: consolidated flag + assert "name" in names + assert "enhance_level" in names # Phase 1: consolidated flag def test_get_source_specific_web(self): """Should return web-specific arguments.""" - args = get_source_specific_arguments('web') + args = get_source_specific_arguments("web") assert args == WEB_ARGUMENTS def test_get_source_specific_github(self): """Should return github-specific arguments.""" - args = get_source_specific_arguments('github') + args = get_source_specific_arguments("github") assert args == GITHUB_ARGUMENTS def test_get_source_specific_local(self): """Should return local-specific arguments.""" - args = get_source_specific_arguments('local') + args = get_source_specific_arguments("local") assert args == LOCAL_ARGUMENTS def test_get_source_specific_pdf(self): """Should return pdf-specific arguments.""" - args = get_source_specific_arguments('pdf') + args = get_source_specific_arguments("pdf") assert args == PDF_ARGUMENTS def test_get_source_specific_config(self): """Config should return empty dict (no extra args).""" - args = get_source_specific_arguments('config') + args = get_source_specific_arguments("config") assert args == {} def test_get_source_specific_unknown(self): """Unknown source should return empty dict.""" - args = get_source_specific_arguments('unknown') + args = get_source_specific_arguments("unknown") assert args == {} + class TestCompatibleArguments: """Test compatible argument detection.""" def test_web_compatible_arguments(self): """Web source should include universal + web + advanced.""" - compatible = get_compatible_arguments('web') + compatible = get_compatible_arguments("web") # Should include universal arguments - assert 'name' in compatible - assert 'enhance_level' in compatible # Phase 1: consolidated flag + assert "name" in compatible + assert "enhance_level" in compatible # Phase 1: consolidated flag # Should include web-specific arguments - assert 'max_pages' in compatible - assert 'rate_limit' in compatible + assert "max_pages" in compatible + assert "rate_limit" in compatible # Should include advanced arguments - assert 'no_rate_limit' in compatible + assert "no_rate_limit" in compatible def test_github_compatible_arguments(self): """GitHub source should include universal + github + advanced.""" - compatible = get_compatible_arguments('github') + compatible = get_compatible_arguments("github") # Should include universal arguments - assert 'name' in compatible + assert "name" in compatible # Should include github-specific arguments - assert 'repo' in compatible - assert 'token' in compatible + assert "repo" in compatible + assert "token" in compatible # Should include advanced arguments - assert 'interactive_enhancement' in compatible + assert "interactive_enhancement" in compatible def test_local_compatible_arguments(self): """Local source should include universal + local + advanced.""" - compatible = get_compatible_arguments('local') + compatible = get_compatible_arguments("local") # Should include universal arguments - assert 'description' in compatible + assert "description" in compatible # Should include local-specific arguments - assert 'directory' in compatible - assert 'languages' in compatible + assert "directory" in compatible + assert "languages" in compatible def test_pdf_compatible_arguments(self): """PDF source should include universal + pdf + advanced.""" - compatible = get_compatible_arguments('pdf') + compatible = get_compatible_arguments("pdf") # Should include universal arguments - assert 'output' in compatible + assert "output" in compatible # Should include pdf-specific arguments - assert 'pdf' in compatible - assert 'ocr' in compatible + assert "pdf" in compatible + assert "ocr" in compatible def test_config_compatible_arguments(self): """Config source should include universal + advanced only.""" - compatible = get_compatible_arguments('config') + compatible = get_compatible_arguments("config") # Should include universal arguments - assert 'config' in compatible + assert "config" in compatible # Should include advanced arguments - assert 'no_preserve_code_blocks' in compatible + assert "no_preserve_code_blocks" in compatible # Should not include source-specific arguments - assert 'repo' not in compatible - assert 'directory' not in compatible + assert "repo" not in compatible + assert "directory" not in compatible + class TestAddCreateArguments: """Test add_create_arguments function.""" @@ -217,16 +233,17 @@ class TestAddCreateArguments: def test_default_mode_adds_universal_only(self): """Default mode should add only universal arguments + source positional.""" import argparse + parser = argparse.ArgumentParser() - add_create_arguments(parser, mode='default') + add_create_arguments(parser, mode="default") # Parse to get all arguments args = vars(parser.parse_args([])) # Should have universal arguments - assert 'name' in args - assert 'enhance_level' in args - assert 'chunk_for_rag' in args + assert "name" in args + assert "enhance_level" in args + assert "chunk_for_rag" in args # Should not have source-specific arguments (they're not added in default mode) # Note: argparse won't error on unknown args, but they won't be in namespace @@ -234,62 +251,60 @@ class TestAddCreateArguments: def test_web_mode_adds_web_arguments(self): """Web mode should add universal + web arguments.""" import argparse + parser = argparse.ArgumentParser() - add_create_arguments(parser, mode='web') + add_create_arguments(parser, mode="web") args = vars(parser.parse_args([])) # Should have universal arguments - assert 'name' in args + assert "name" in args # Should have web-specific arguments - assert 'max_pages' in args - assert 'rate_limit' in args + assert "max_pages" in args + assert "rate_limit" in args def test_all_mode_adds_all_arguments(self): """All mode should add every argument.""" import argparse + parser = argparse.ArgumentParser() - add_create_arguments(parser, mode='all') + add_create_arguments(parser, mode="all") args = vars(parser.parse_args([])) # Should have universal arguments - assert 'name' in args + assert "name" in args # Should have all source-specific arguments - assert 'max_pages' in args # web - assert 'repo' in args # github - assert 'directory' in args # local - assert 'pdf' in args # pdf + assert "max_pages" in args # web + assert "repo" in args # github + assert "directory" in args # local + assert "pdf" in args # pdf # Should have advanced arguments - assert 'no_rate_limit' in args + assert "no_rate_limit" in args def test_positional_source_argument_always_added(self): """Source positional argument should always be added.""" import argparse - for mode in ['default', 'web', 'github', 'local', 'pdf', 'all']: + + for mode in ["default", "web", "github", "local", "pdf", "all"]: parser = argparse.ArgumentParser() add_create_arguments(parser, mode=mode) # Should accept source as positional - args = parser.parse_args(['some_source']) - assert args.source == 'some_source' + args = parser.parse_args(["some_source"]) + assert args.source == "some_source" + class TestNoDuplicates: """Test that there are no duplicate arguments across tiers.""" def test_no_duplicates_between_universal_and_web(self): """Universal and web args should not overlap.""" - universal_flags = { - flag for arg in UNIVERSAL_ARGUMENTS.values() - for flag in arg['flags'] - } - web_flags = { - flag for arg in WEB_ARGUMENTS.values() - for flag in arg['flags'] - } + universal_flags = {flag for arg in UNIVERSAL_ARGUMENTS.values() for flag in arg["flags"]} + web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]} # Allow some overlap since we intentionally include common args # in multiple places, but check that they're properly defined @@ -299,10 +314,10 @@ class TestNoDuplicates: def test_no_duplicates_between_source_specific_args(self): """Different source-specific arg groups should not overlap.""" - web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg['flags']} - github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg['flags']} - local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg['flags']} - pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg['flags']} + web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]} + github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg["flags"]} + local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg["flags"]} + pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg["flags"]} # No overlap between different source types assert len(web_flags & github_flags) == 0 @@ -312,6 +327,7 @@ class TestNoDuplicates: assert len(github_flags & pdf_flags) == 0 assert len(local_flags & pdf_flags) == 0 + class TestArgumentQuality: """Test argument definition quality.""" @@ -327,8 +343,8 @@ class TestArgumentQuality: } for arg_name, arg_def in all_args.items(): - assert 'help' in arg_def['kwargs'], f"{arg_name} missing help text" - assert len(arg_def['kwargs']['help']) > 0, f"{arg_name} has empty help text" + assert "help" in arg_def["kwargs"], f"{arg_name} missing help text" + assert len(arg_def["kwargs"]["help"]) > 0, f"{arg_name} has empty help text" def test_boolean_arguments_use_store_true(self): """Boolean flags should use store_true action.""" @@ -342,13 +358,25 @@ class TestArgumentQuality: } boolean_args = [ - 'dry_run', 'verbose', 'quiet', - 'chunk_for_rag', 'skip_scrape', 'resume', 'fresh', 'async_mode', - 'no_issues', 'no_changelog', 'no_releases', 'scrape_only', - 'skip_patterns', 'skip_test_examples', 'ocr', 'no_rate_limit' + "dry_run", + "verbose", + "quiet", + "chunk_for_rag", + "skip_scrape", + "resume", + "fresh", + "async_mode", + "no_issues", + "no_changelog", + "no_releases", + "scrape_only", + "skip_patterns", + "skip_test_examples", + "ocr", + "no_rate_limit", ] for arg_name in boolean_args: if arg_name in all_args: - action = all_args[arg_name]['kwargs'].get('action') - assert action == 'store_true', f"{arg_name} should use store_true" + action = all_args[arg_name]["kwargs"].get("action") + assert action == "store_true", f"{arg_name} should use store_true" diff --git a/tests/test_create_integration_basic.py b/tests/test_create_integration_basic.py index 621cf4f..335bef0 100644 --- a/tests/test_create_integration_basic.py +++ b/tests/test_create_integration_basic.py @@ -6,21 +6,21 @@ and routes to the correct scrapers without actually scraping. import pytest + class TestCreateCommandBasic: """Basic integration tests for create command (dry-run mode).""" def test_create_command_help(self): """Test that create command help works.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'create', '--help'], - capture_output=True, - text=True + ["skill-seekers", "create", "--help"], capture_output=True, text=True ) assert result.returncode == 0 - assert 'Auto-detects source type' in result.stdout - assert 'auto-detected' in result.stdout - assert '--help-web' in result.stdout + assert "Auto-detects source type" in result.stdout + assert "auto-detected" in result.stdout + assert "--help-web" in result.stdout def test_create_detects_web_url(self): """Test that web URLs are detected and routed correctly.""" @@ -31,11 +31,12 @@ class TestCreateCommandBasic: def test_create_detects_github_repo(self): """Test that GitHub repos are detected.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'create', 'facebook/react', '--help'], + ["skill-seekers", "create", "facebook/react", "--help"], capture_output=True, text=True, - timeout=10 + timeout=10, ) # Just verify help works - actual scraping would need API token assert result.returncode in [0, 2] # 0 for success, 2 for argparse help @@ -49,10 +50,10 @@ class TestCreateCommandBasic: test_dir.mkdir() result = subprocess.run( - ['skill-seekers', 'create', str(test_dir), '--help'], + ["skill-seekers", "create", str(test_dir), "--help"], capture_output=True, text=True, - timeout=10 + timeout=10, ) # Verify help works assert result.returncode in [0, 2] @@ -66,10 +67,10 @@ class TestCreateCommandBasic: pdf_file.touch() result = subprocess.run( - ['skill-seekers', 'create', str(pdf_file), '--help'], + ["skill-seekers", "create", str(pdf_file), "--help"], capture_output=True, text=True, - timeout=10 + timeout=10, ) # Verify help works assert result.returncode in [0, 2] @@ -81,17 +82,14 @@ class TestCreateCommandBasic: # Create a minimal config file config_file = tmp_path / "test.json" - config_data = { - "name": "test", - "base_url": "https://example.com/" - } + config_data = {"name": "test", "base_url": "https://example.com/"} config_file.write_text(json.dumps(config_data)) result = subprocess.run( - ['skill-seekers', 'create', str(config_file), '--help'], + ["skill-seekers", "create", str(config_file), "--help"], capture_output=True, text=True, - timeout=10 + timeout=10, ) # Verify help works assert result.returncode in [0, 2] @@ -105,20 +103,19 @@ class TestCreateCommandBasic: def test_create_supports_universal_flags(self): """Test that universal flags are accepted.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'create', '--help'], - capture_output=True, - text=True, - timeout=10 + ["skill-seekers", "create", "--help"], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0 # Check that universal flags are present - assert '--name' in result.stdout - assert '--enhance' in result.stdout - assert '--chunk-for-rag' in result.stdout - assert '--preset' in result.stdout - assert '--dry-run' in result.stdout + assert "--name" in result.stdout + assert "--enhance" in result.stdout + assert "--chunk-for-rag" in result.stdout + assert "--preset" in result.stdout + assert "--dry-run" in result.stdout + class TestBackwardCompatibility: """Test that old commands still work.""" @@ -126,53 +123,45 @@ class TestBackwardCompatibility: def test_scrape_command_still_works(self): """Old scrape command should still function.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'scrape', '--help'], - capture_output=True, - text=True, - timeout=10 + ["skill-seekers", "scrape", "--help"], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0 - assert 'scrape' in result.stdout.lower() + assert "scrape" in result.stdout.lower() def test_github_command_still_works(self): """Old github command should still function.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'github', '--help'], - capture_output=True, - text=True, - timeout=10 + ["skill-seekers", "github", "--help"], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0 - assert 'github' in result.stdout.lower() + assert "github" in result.stdout.lower() def test_analyze_command_still_works(self): """Old analyze command should still function.""" import subprocess + result = subprocess.run( - ['skill-seekers', 'analyze', '--help'], - capture_output=True, - text=True, - timeout=10 + ["skill-seekers", "analyze", "--help"], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0 - assert 'analyze' in result.stdout.lower() + assert "analyze" in result.stdout.lower() def test_main_help_shows_all_commands(self): """Main help should show both old and new commands.""" import subprocess + result = subprocess.run( - ['skill-seekers', '--help'], - capture_output=True, - text=True, - timeout=10 + ["skill-seekers", "--help"], capture_output=True, text=True, timeout=10 ) assert result.returncode == 0 # Should show create command - assert 'create' in result.stdout + assert "create" in result.stdout # Should still show old commands - assert 'scrape' in result.stdout - assert 'github' in result.stdout - assert 'analyze' in result.stdout + assert "scrape" in result.stdout + assert "github" in result.stdout + assert "analyze" in result.stdout diff --git a/tests/test_issue_219_e2e.py b/tests/test_issue_219_e2e.py index f3dae07..856455c 100644 --- a/tests/test_issue_219_e2e.py +++ b/tests/test_issue_219_e2e.py @@ -167,7 +167,8 @@ class TestIssue219Problem2CLIFlags(unittest.TestCase): "test/test", "--name", "test", - "--enhance-level", "2", + "--enhance-level", + "2", ] with ( diff --git a/tests/test_parser_sync.py b/tests/test_parser_sync.py index 39cb3c0..c58380b 100644 --- a/tests/test_parser_sync.py +++ b/tests/test_parser_sync.py @@ -7,6 +7,7 @@ the same arguments as the standalone scraper modules. This prevents the import argparse + class TestScrapeParserSync: """Ensure scrape_parser has all arguments from doc_scraper.""" @@ -17,12 +18,12 @@ class TestScrapeParserSync: # Get source arguments from doc_scraper source_parser = setup_argument_parser() - source_count = len([a for a in source_parser._actions if a.dest != 'help']) + source_count = len([a for a in source_parser._actions if a.dest != "help"]) # Get target arguments from unified CLI parser target_parser = argparse.ArgumentParser() ScrapeParser().add_arguments(target_parser) - target_count = len([a for a in target_parser._actions if a.dest != 'help']) + target_count = len([a for a in target_parser._actions if a.dest != "help"]) assert source_count == target_count, ( f"Argument count mismatch: doc_scraper has {source_count}, " @@ -36,12 +37,12 @@ class TestScrapeParserSync: # Get source arguments from doc_scraper source_parser = setup_argument_parser() - source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'} + source_dests = {a.dest for a in source_parser._actions if a.dest != "help"} # Get target arguments from unified CLI parser target_parser = argparse.ArgumentParser() ScrapeParser().add_arguments(target_parser) - target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'} + target_dests = {a.dest for a in target_parser._actions if a.dest != "help"} # Check for missing arguments missing = source_dests - target_dests @@ -64,27 +65,28 @@ class TestScrapeParserSync: break assert subparsers_action is not None, "No subparsers found" - assert 'scrape' in subparsers_action.choices, "scrape subparser not found" + assert "scrape" in subparsers_action.choices, "scrape subparser not found" - scrape_parser = subparsers_action.choices['scrape'] - arg_dests = {a.dest for a in scrape_parser._actions if a.dest != 'help'} + scrape_parser = subparsers_action.choices["scrape"] + arg_dests = {a.dest for a in scrape_parser._actions if a.dest != "help"} # Check key arguments that were missing in Issue #285 required_args = [ - 'interactive', - 'url', - 'verbose', - 'quiet', - 'resume', - 'fresh', - 'rate_limit', - 'no_rate_limit', - 'chunk_for_rag', + "interactive", + "url", + "verbose", + "quiet", + "resume", + "fresh", + "rate_limit", + "no_rate_limit", + "chunk_for_rag", ] for arg in required_args: assert arg in arg_dests, f"Required argument '{arg}' missing from scrape parser" + class TestGitHubParserSync: """Ensure github_parser has all arguments from github_scraper.""" @@ -95,12 +97,12 @@ class TestGitHubParserSync: # Get source arguments from github_scraper source_parser = setup_argument_parser() - source_count = len([a for a in source_parser._actions if a.dest != 'help']) + source_count = len([a for a in source_parser._actions if a.dest != "help"]) # Get target arguments from unified CLI parser target_parser = argparse.ArgumentParser() GitHubParser().add_arguments(target_parser) - target_count = len([a for a in target_parser._actions if a.dest != 'help']) + target_count = len([a for a in target_parser._actions if a.dest != "help"]) assert source_count == target_count, ( f"Argument count mismatch: github_scraper has {source_count}, " @@ -114,12 +116,12 @@ class TestGitHubParserSync: # Get source arguments from github_scraper source_parser = setup_argument_parser() - source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'} + source_dests = {a.dest for a in source_parser._actions if a.dest != "help"} # Get target arguments from unified CLI parser target_parser = argparse.ArgumentParser() GitHubParser().add_arguments(target_parser) - target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'} + target_dests = {a.dest for a in target_parser._actions if a.dest != "help"} # Check for missing arguments missing = source_dests - target_dests @@ -128,6 +130,7 @@ class TestGitHubParserSync: assert not missing, f"github_parser missing arguments: {missing}" assert not extra, f"github_parser has extra arguments not in github_scraper: {extra}" + class TestUnifiedCLI: """Test the unified CLI main parser.""" @@ -154,7 +157,7 @@ class TestUnifiedCLI: assert subparsers_action is not None, "No subparsers found" # Check expected subcommands - expected_commands = ['scrape', 'github'] + expected_commands = ["scrape", "github"] for cmd in expected_commands: assert cmd in subparsers_action.choices, f"Subcommand '{cmd}' not found" @@ -166,7 +169,7 @@ class TestUnifiedCLI: # This should not raise an exception try: - parser.parse_args(['scrape', '--help']) + parser.parse_args(["scrape", "--help"]) except SystemExit as e: # --help causes SystemExit(0) which is expected assert e.code == 0 @@ -179,7 +182,7 @@ class TestUnifiedCLI: # This should not raise an exception try: - parser.parse_args(['github', '--help']) + parser.parse_args(["github", "--help"]) except SystemExit as e: # --help causes SystemExit(0) which is expected assert e.code == 0 diff --git a/tests/test_source_detector.py b/tests/test_source_detector.py index d402333..c53ceb6 100644 --- a/tests/test_source_detector.py +++ b/tests/test_source_detector.py @@ -13,47 +13,49 @@ import pytest from skill_seekers.cli.source_detector import SourceDetector, SourceInfo + class TestWebDetection: """Test web URL detection.""" def test_detect_full_https_url(self): """Full HTTPS URL should be detected as web.""" info = SourceDetector.detect("https://docs.react.dev/") - assert info.type == 'web' - assert info.parsed['url'] == "https://docs.react.dev/" - assert info.suggested_name == 'react' + assert info.type == "web" + assert info.parsed["url"] == "https://docs.react.dev/" + assert info.suggested_name == "react" def test_detect_full_http_url(self): """Full HTTP URL should be detected as web.""" info = SourceDetector.detect("http://example.com/docs") - assert info.type == 'web' - assert info.parsed['url'] == "http://example.com/docs" + assert info.type == "web" + assert info.parsed["url"] == "http://example.com/docs" def test_detect_domain_only(self): """Domain without protocol should add https:// and detect as web.""" info = SourceDetector.detect("docs.react.dev") - assert info.type == 'web' - assert info.parsed['url'] == "https://docs.react.dev" - assert info.suggested_name == 'react' + assert info.type == "web" + assert info.parsed["url"] == "https://docs.react.dev" + assert info.suggested_name == "react" def test_detect_complex_url(self): """Complex URL with path should be detected as web.""" info = SourceDetector.detect("https://docs.python.org/3/library/") - assert info.type == 'web' - assert info.parsed['url'] == "https://docs.python.org/3/library/" - assert info.suggested_name == 'python' + assert info.type == "web" + assert info.parsed["url"] == "https://docs.python.org/3/library/" + assert info.suggested_name == "python" def test_suggested_name_removes_www(self): """Should remove www. prefix from suggested name.""" info = SourceDetector.detect("https://www.example.com/") - assert info.type == 'web' - assert info.suggested_name == 'example' + assert info.type == "web" + assert info.suggested_name == "example" def test_suggested_name_removes_docs(self): """Should remove docs. prefix from suggested name.""" info = SourceDetector.detect("https://docs.vue.org/") - assert info.type == 'web' - assert info.suggested_name == 'vue' + assert info.type == "web" + assert info.suggested_name == "vue" + class TestGitHubDetection: """Test GitHub repository detection.""" @@ -61,37 +63,38 @@ class TestGitHubDetection: def test_detect_owner_repo_format(self): """owner/repo format should be detected as GitHub.""" info = SourceDetector.detect("facebook/react") - assert info.type == 'github' - assert info.parsed['repo'] == "facebook/react" - assert info.suggested_name == 'react' + assert info.type == "github" + assert info.parsed["repo"] == "facebook/react" + assert info.suggested_name == "react" def test_detect_github_https_url(self): """Full GitHub HTTPS URL should be detected.""" info = SourceDetector.detect("https://github.com/facebook/react") - assert info.type == 'github' - assert info.parsed['repo'] == "facebook/react" - assert info.suggested_name == 'react' + assert info.type == "github" + assert info.parsed["repo"] == "facebook/react" + assert info.suggested_name == "react" def test_detect_github_url_with_git_suffix(self): """GitHub URL with .git should strip suffix.""" info = SourceDetector.detect("https://github.com/facebook/react.git") - assert info.type == 'github' - assert info.parsed['repo'] == "facebook/react" - assert info.suggested_name == 'react' + assert info.type == "github" + assert info.parsed["repo"] == "facebook/react" + assert info.suggested_name == "react" def test_detect_github_url_without_protocol(self): """GitHub URL without protocol should be detected.""" info = SourceDetector.detect("github.com/vuejs/vue") - assert info.type == 'github' - assert info.parsed['repo'] == "vuejs/vue" - assert info.suggested_name == 'vue' + assert info.type == "github" + assert info.parsed["repo"] == "vuejs/vue" + assert info.suggested_name == "vue" def test_owner_repo_with_dots_and_dashes(self): """Repo names with dots and dashes should work.""" info = SourceDetector.detect("microsoft/vscode-python") - assert info.type == 'github' - assert info.parsed['repo'] == "microsoft/vscode-python" - assert info.suggested_name == 'vscode-python' + assert info.type == "github" + assert info.parsed["repo"] == "microsoft/vscode-python" + assert info.suggested_name == "vscode-python" + class TestLocalDetection: """Test local directory detection.""" @@ -107,9 +110,9 @@ class TestLocalDetection: try: os.chdir(tmp_path) info = SourceDetector.detect("./my_project") - assert info.type == 'local' - assert 'my_project' in info.parsed['directory'] - assert info.suggested_name == 'my_project' + assert info.type == "local" + assert "my_project" in info.parsed["directory"] + assert info.suggested_name == "my_project" finally: os.chdir(original_cwd) @@ -120,16 +123,17 @@ class TestLocalDetection: test_dir.mkdir() info = SourceDetector.detect(str(test_dir)) - assert info.type == 'local' - assert info.parsed['directory'] == str(test_dir.resolve()) - assert info.suggested_name == 'test_repo' + assert info.type == "local" + assert info.parsed["directory"] == str(test_dir.resolve()) + assert info.suggested_name == "test_repo" def test_detect_current_directory(self): """Current directory (.) should be detected.""" cwd = os.getcwd() info = SourceDetector.detect(".") - assert info.type == 'local' - assert info.parsed['directory'] == cwd + assert info.type == "local" + assert info.parsed["directory"] == cwd + class TestPDFDetection: """Test PDF file detection.""" @@ -137,22 +141,23 @@ class TestPDFDetection: def test_detect_pdf_extension(self): """File with .pdf extension should be detected.""" info = SourceDetector.detect("tutorial.pdf") - assert info.type == 'pdf' - assert info.parsed['file_path'] == "tutorial.pdf" - assert info.suggested_name == 'tutorial' + assert info.type == "pdf" + assert info.parsed["file_path"] == "tutorial.pdf" + assert info.suggested_name == "tutorial" def test_detect_pdf_with_path(self): """PDF file with path should be detected.""" info = SourceDetector.detect("/path/to/guide.pdf") - assert info.type == 'pdf' - assert info.parsed['file_path'] == "/path/to/guide.pdf" - assert info.suggested_name == 'guide' + assert info.type == "pdf" + assert info.parsed["file_path"] == "/path/to/guide.pdf" + assert info.suggested_name == "guide" def test_suggested_name_removes_pdf_extension(self): """Suggested name should not include .pdf extension.""" info = SourceDetector.detect("my-awesome-guide.pdf") - assert info.type == 'pdf' - assert info.suggested_name == 'my-awesome-guide' + assert info.type == "pdf" + assert info.suggested_name == "my-awesome-guide" + class TestConfigDetection: """Test config file detection.""" @@ -160,16 +165,17 @@ class TestConfigDetection: def test_detect_json_extension(self): """File with .json extension should be detected as config.""" info = SourceDetector.detect("react.json") - assert info.type == 'config' - assert info.parsed['config_path'] == "react.json" - assert info.suggested_name == 'react' + assert info.type == "config" + assert info.parsed["config_path"] == "react.json" + assert info.suggested_name == "react" def test_detect_config_with_path(self): """Config file with path should be detected.""" info = SourceDetector.detect("configs/django.json") - assert info.type == 'config' - assert info.parsed['config_path'] == "configs/django.json" - assert info.suggested_name == 'django' + assert info.type == "config" + assert info.parsed["config_path"] == "configs/django.json" + assert info.suggested_name == "django" + class TestValidation: """Test source validation.""" @@ -191,10 +197,10 @@ class TestValidation: # First try to detect it (will succeed since it looks like a path) with pytest.raises(ValueError, match="Directory does not exist"): info = SourceInfo( - type='local', - parsed={'directory': nonexistent}, - suggested_name='test', - raw_input=nonexistent + type="local", + parsed={"directory": nonexistent}, + suggested_name="test", + raw_input=nonexistent, ) SourceDetector.validate_source(info) @@ -211,10 +217,10 @@ class TestValidation: """Validation should fail for nonexistent PDF.""" with pytest.raises(ValueError, match="PDF file does not exist"): info = SourceInfo( - type='pdf', - parsed={'file_path': '/tmp/nonexistent.pdf'}, - suggested_name='test', - raw_input='/tmp/nonexistent.pdf' + type="pdf", + parsed={"file_path": "/tmp/nonexistent.pdf"}, + suggested_name="test", + raw_input="/tmp/nonexistent.pdf", ) SourceDetector.validate_source(info) @@ -231,13 +237,14 @@ class TestValidation: """Validation should fail for nonexistent config.""" with pytest.raises(ValueError, match="Config file does not exist"): info = SourceInfo( - type='config', - parsed={'config_path': '/tmp/nonexistent.json'}, - suggested_name='test', - raw_input='/tmp/nonexistent.json' + type="config", + parsed={"config_path": "/tmp/nonexistent.json"}, + suggested_name="test", + raw_input="/tmp/nonexistent.json", ) SourceDetector.validate_source(info) + class TestAmbiguousCases: """Test handling of ambiguous inputs.""" @@ -255,8 +262,8 @@ class TestAmbiguousCases: """GitHub URL should be detected as github, not web.""" # Even though this is a URL, it should be detected as GitHub info = SourceDetector.detect("https://github.com/owner/repo") - assert info.type == 'github' - assert info.parsed['repo'] == "owner/repo" + assert info.type == "github" + assert info.parsed["repo"] == "owner/repo" def test_directory_takes_precedence_over_domain(self, tmp_path): """Existing directory should be detected even if it looks like domain.""" @@ -266,7 +273,8 @@ class TestAmbiguousCases: info = SourceDetector.detect(str(dir_like_domain)) # Should detect as local directory, not web - assert info.type == 'local' + assert info.type == "local" + class TestRawInputPreservation: """Test that raw_input is preserved correctly.""" @@ -292,6 +300,7 @@ class TestRawInputPreservation: info = SourceDetector.detect(original) assert info.raw_input == original + class TestEdgeCases: """Test edge cases and corner cases.""" @@ -300,19 +309,19 @@ class TestEdgeCases: info1 = SourceDetector.detect("https://docs.react.dev/") info2 = SourceDetector.detect("https://docs.react.dev") - assert info1.type == 'web' - assert info2.type == 'web' + assert info1.type == "web" + assert info2.type == "web" def test_uppercase_in_github_repo(self): """GitHub repos with uppercase should be detected.""" info = SourceDetector.detect("Microsoft/TypeScript") - assert info.type == 'github' - assert info.parsed['repo'] == "Microsoft/TypeScript" + assert info.type == "github" + assert info.parsed["repo"] == "Microsoft/TypeScript" def test_numbers_in_repo_name(self): """GitHub repos with numbers should be detected.""" info = SourceDetector.detect("python/cpython3.11") - assert info.type == 'github' + assert info.type == "github" def test_nested_directory_path(self, tmp_path): """Nested directory paths should work.""" @@ -320,5 +329,5 @@ class TestEdgeCases: nested.mkdir(parents=True) info = SourceDetector.detect(str(nested)) - assert info.type == 'local' - assert info.suggested_name == 'c' + assert info.type == "local" + assert info.suggested_name == "c"