Coverage for src / tracekit / extensibility / templates.py: 97%
83 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Plugin template generation for creating new TraceKit plugins.
3This module provides tools for generating plugin skeletons with all necessary
4boilerplate code, tests, and documentation.
5"""
7from __future__ import annotations
9import textwrap
10from dataclasses import dataclass
11from typing import TYPE_CHECKING, Literal
13if TYPE_CHECKING:
14 from pathlib import Path
16# Plugin type definitions
17PluginType = Literal["analyzer", "loader", "exporter", "decoder"]
20@dataclass
21class PluginTemplate:
22 """Configuration for plugin template generation.
24 Attributes:
25 name: Plugin name (e.g., 'my_custom_decoder').
26 plugin_type: Type of plugin ('analyzer', 'loader', 'exporter', 'decoder').
27 output_dir: Directory where plugin will be generated.
28 author: Plugin author name.
29 description: Brief description of plugin functionality.
30 version: Initial plugin version (default: '0.1.0').
32 Example:
33 >>> template = PluginTemplate(
34 ... name='flexray_decoder',
35 ... plugin_type='decoder',
36 ... output_dir=Path('plugins/flexray'),
37 ... author='John Doe',
38 ... description='FlexRay protocol decoder'
39 ... )
41 References:
42 PLUG-008: Plugin Template Generator
43 """
45 name: str
46 plugin_type: PluginType
47 output_dir: Path
48 author: str = "Plugin Author"
49 description: str = "Custom TraceKit plugin"
50 version: str = "0.1.0"
53def generate_plugin_template(
54 name: str,
55 plugin_type: PluginType,
56 output_dir: Path,
57 *,
58 author: str = "Plugin Author",
59 description: str | None = None,
60 version: str = "0.1.0",
61) -> Path:
62 """Generate a plugin skeleton with all necessary boilerplate.
64 Creates a complete plugin package structure including:
65 - __init__.py with plugin metadata
66 - Main module with stub implementation
67 - tests/ directory with test stubs
68 - README.md with usage instructions
69 - pyproject.toml for packaging
71 Args:
72 name: Plugin name (will be converted to snake_case).
73 plugin_type: Type of plugin to generate.
74 output_dir: Directory where plugin will be created.
75 author: Plugin author name.
76 description: Plugin description (auto-generated if None).
77 version: Initial plugin version.
79 Returns:
80 Path to the generated plugin directory.
82 Raises:
83 ValueError: If plugin_type is invalid.
85 Example:
86 >>> from pathlib import Path
87 >>> plugin_dir = generate_plugin_template(
88 ... name='flexray_decoder',
89 ... plugin_type='decoder',
90 ... output_dir=Path('plugins/flexray'),
91 ... author='John Doe',
92 ... description='FlexRay protocol decoder'
93 ... )
94 >>> print(f"Plugin generated at {plugin_dir}")
96 Plugin Structure:
97 ```
98 plugins/flexray/
99 ├── __init__.py # Plugin metadata and entry point
100 ├── flexray_decoder.py # Main implementation
101 ├── tests/
102 │ ├── __init__.py
103 │ └── test_flexray_decoder.py
104 ├── README.md # Usage documentation
105 └── pyproject.toml # Packaging configuration
106 ```
108 References:
109 PLUG-008: Plugin Template Generator
110 """
111 # Validate plugin type
112 valid_types: set[PluginType] = {"analyzer", "loader", "exporter", "decoder"}
113 if plugin_type not in valid_types:
114 raise ValueError(
115 f"Invalid plugin_type '{plugin_type}'. Must be one of: {', '.join(valid_types)}"
116 )
118 # Generate default description if not provided
119 if description is None:
120 description = f"Custom {plugin_type} plugin for TraceKit"
122 # Create template configuration
123 template = PluginTemplate(
124 name=name,
125 plugin_type=plugin_type,
126 output_dir=output_dir,
127 author=author,
128 description=description,
129 version=version,
130 )
132 # Generate plugin directory structure
133 _generate_plugin_structure(template)
135 return output_dir
138def _generate_plugin_structure(template: PluginTemplate) -> None:
139 """Generate complete plugin directory structure.
141 Args:
142 template: Plugin template configuration.
144 Raises:
145 FileExistsError: If plugin directory already exists.
146 """
147 output_dir = template.output_dir
149 # Check if directory exists
150 if output_dir.exists():
151 raise FileExistsError(
152 f"Plugin directory already exists: {output_dir}\n"
153 f"Remove it or choose a different output_dir."
154 )
156 # Create directory structure
157 output_dir.mkdir(parents=True, exist_ok=False)
158 tests_dir = output_dir / "tests"
159 tests_dir.mkdir(exist_ok=False)
161 # Generate files
162 _write_init_py(template)
163 _write_main_module(template)
164 _write_test_init(template)
165 _write_test_module(template)
166 _write_readme(template)
167 _write_pyproject_toml(template)
170def _write_init_py(template: PluginTemplate) -> None:
171 """Write plugin __init__.py with metadata.
173 Args:
174 template: Plugin template configuration.
175 """
176 content = textwrap.dedent(f'''\
177 """{template.description}
179 This plugin integrates with TraceKit via entry points.
181 Plugin Metadata:
182 Name: {template.name}
183 Type: {template.plugin_type}
184 Version: {template.version}
185 Author: {template.author}
187 Installation:
188 pip install -e .
190 Usage:
191 import tracekit as tk
192 # Plugin auto-discovered via entry points
193 # See README.md for usage examples
195 References:
196 PLUG-008: Plugin Template Generator
197 """
198 from .{template.name} import {_get_class_name(template)}
200 __version__ = "{template.version}"
202 __all__ = [
203 "{_get_class_name(template)}",
204 ]
205 ''')
207 (template.output_dir / "__init__.py").write_text(content)
210def _write_main_module(template: PluginTemplate) -> None:
211 """Write main plugin module with stub implementation.
213 Args:
214 template: Plugin template configuration.
215 """
216 class_name = _get_class_name(template)
218 if template.plugin_type == "decoder":
219 content = _generate_decoder_stub(template, class_name)
220 elif template.plugin_type == "analyzer":
221 content = _generate_analyzer_stub(template, class_name)
222 elif template.plugin_type == "loader":
223 content = _generate_loader_stub(template, class_name)
224 elif template.plugin_type == "exporter": 224 ↛ 228line 224 didn't jump to line 228 because the condition on line 224 was always true
225 content = _generate_exporter_stub(template, class_name)
226 else:
227 # Fallback generic stub
228 content = _generate_generic_stub(template, class_name) # type: ignore[unreachable]
230 (template.output_dir / f"{template.name}.py").write_text(content)
233def _write_test_init(template: PluginTemplate) -> None:
234 """Write tests/__init__.py.
236 Args:
237 template: Plugin template configuration.
238 """
239 content = '"""Test suite for plugin."""\n'
240 (template.output_dir / "tests" / "__init__.py").write_text(content)
243def _write_test_module(template: PluginTemplate) -> None:
244 """Write test module with example tests.
246 Args:
247 template: Plugin template configuration.
248 """
249 class_name = _get_class_name(template)
251 content = textwrap.dedent(f'''\
252 """Tests for {template.name} plugin.
254 This module contains unit tests for the plugin implementation.
255 """
256 import numpy as np
257 import pytest
259 from {template.name} import {class_name}
262 def test_{template.name}_initialization():
263 """Test plugin can be instantiated."""
264 plugin = {class_name}()
265 assert plugin is not None
268 def test_{template.name}_basic_functionality():
269 """Test basic plugin functionality."""
270 plugin = {class_name}()
272 # USER: Implement test for your plugin's main functionality
273 # Example:
274 # result = plugin.process(test_data)
275 # assert result is not None
277 # Placeholder assertion - replace with actual tests
278 assert True
281 def test_{template.name}_error_handling():
282 """Test plugin handles errors gracefully."""
283 plugin = {class_name}()
285 # USER: Implement error condition tests
286 # Example:
287 # with pytest.raises(ValueError):
288 # plugin.process(invalid_data)
290 # Placeholder assertion - replace with actual tests
291 assert True
294 @pytest.mark.parametrize("param", [1, 2, 3])
295 def test_{template.name}_parametrized(param):
296 """Example parametrized test."""
297 plugin = {class_name}()
299 # USER: Implement parametrized test logic
300 assert param > 0
301 ''')
303 (template.output_dir / "tests" / f"test_{template.name}.py").write_text(content)
306def _write_readme(template: PluginTemplate) -> None:
307 """Write README.md with usage instructions.
309 Args:
310 template: Plugin template configuration.
311 """
312 class_name = _get_class_name(template)
313 entry_point_group = _get_entry_point_group(template.plugin_type)
315 content = textwrap.dedent(f"""\
316 # {class_name}
318 {template.description}
320 ## Installation
322 Install in development mode:
324 ```bash
325 cd {template.output_dir.name}
326 pip install -e .
327 ```
329 ## Usage
331 The plugin integrates automatically with TraceKit via entry points:
333 ```python
334 import tracekit as tk
336 # Plugin is automatically discovered
337 # USER: Add usage examples specific to your plugin
338 ```
340 ### Direct Usage
342 ```python
343 from {template.name} import {class_name}
345 # Create instance
346 plugin = {class_name}()
348 # USER: Add direct usage examples
349 ```
351 ## CLI Integration
353 After installation, the plugin is available in TraceKit CLI:
355 ```bash
356 # List installed plugins
357 tracekit plugin list
359 # Show plugin info
360 tracekit plugin info {template.name}
361 ```
363 ## Development
365 ### Running Tests
367 ```bash
368 pytest tests/
369 ```
371 ### Code Quality
373 ```bash
374 # Linting
375 ruff check {template.name}.py
377 # Formatting
378 ruff format {template.name}.py
380 # Type checking
381 mypy {template.name}.py
382 ```
384 ## Plugin Type: {template.plugin_type}
386 This is a **{template.plugin_type}** plugin for TraceKit.
388 ### Entry Point
390 Registered in `{entry_point_group}` entry point group.
392 ## Requirements
394 - TraceKit >= 0.1.0
395 - Python >= 3.12
397 ## License
399 MIT
401 ## Author
403 {template.author}
405 ## Version
407 {template.version}
408 """)
410 (template.output_dir / "README.md").write_text(content)
413def _write_pyproject_toml(template: PluginTemplate) -> None:
414 """Write pyproject.toml for plugin packaging.
416 Args:
417 template: Plugin template configuration.
418 """
419 entry_point_group = _get_entry_point_group(template.plugin_type)
420 class_name = _get_class_name(template)
422 content = textwrap.dedent(f'''\
423 [project]
424 name = "{template.name}"
425 version = "{template.version}"
426 description = "{template.description}"
427 readme = "README.md"
428 license = {{ text = "MIT" }}
429 requires-python = ">=3.12"
430 authors = [
431 {{ name = "{template.author}" }}
432 ]
433 keywords = ["tracekit", "plugin", "{template.plugin_type}"]
434 classifiers = [
435 "Development Status :: 3 - Alpha",
436 "Intended Audience :: Developers",
437 "License :: OSI Approved :: MIT License",
438 "Programming Language :: Python :: 3",
439 "Programming Language :: Python :: 3.12",
440 "Programming Language :: Python :: 3.13",
441 ]
443 dependencies = [
444 "tracekit>=0.1.0",
445 "numpy>=1.26.0",
446 ]
448 [project.optional-dependencies]
449 dev = [
450 "pytest>=8.3.0",
451 "pytest-cov>=6.0.0",
452 "ruff>=0.8.0",
453 "mypy>=1.13.0",
454 ]
456 # TraceKit plugin entry point
457 [project.entry-points."{entry_point_group}"]
458 {template.name} = "{template.name}:{class_name}"
460 [build-system]
461 requires = ["hatchling"]
462 build-backend = "hatchling.build"
464 [tool.pytest.ini_options]
465 testpaths = ["tests"]
466 python_files = ["test_*.py"]
467 python_classes = ["Test*"]
468 python_functions = ["test_*"]
470 [tool.ruff]
471 line-length = 88
472 target-version = "py312"
474 [tool.ruff.lint]
475 select = ["E", "F", "W", "I", "N", "UP", "YTT", "B", "A", "C4", "T10", "RUF"]
477 [tool.mypy]
478 python_version = "3.12"
479 warn_return_any = true
480 warn_unused_configs = true
481 disallow_untyped_defs = true
482 ''')
484 (template.output_dir / "pyproject.toml").write_text(content)
487def _get_class_name(template: PluginTemplate) -> str:
488 """Generate class name from plugin name.
490 Args:
491 template: Plugin template configuration.
493 Returns:
494 PascalCase class name.
496 Example:
497 >>> template = PluginTemplate('my_decoder', 'decoder', Path('.'))
498 >>> _get_class_name(template)
499 'MyDecoder'
500 """
501 # Convert snake_case to PascalCase
502 parts = template.name.split("_")
503 return "".join(word.capitalize() for word in parts)
506def _get_entry_point_group(plugin_type: PluginType) -> str:
507 """Get entry point group for plugin type.
509 Args:
510 plugin_type: Type of plugin.
512 Returns:
513 Entry point group name.
514 """
515 return f"tracekit.{plugin_type}s"
518def _generate_decoder_stub(template: PluginTemplate, class_name: str) -> str:
519 """Generate decoder plugin stub.
521 Args:
522 template: Plugin template configuration.
523 class_name: Class name for the decoder.
525 Returns:
526 Python source code for decoder stub.
527 """
528 return textwrap.dedent(f'''\
529 """{template.description}
531 This decoder implements protocol decoding for TraceKit.
533 References:
534 PLUG-008: Plugin Template Generator
535 """
536 from __future__ import annotations
538 import numpy as np
539 from numpy.typing import NDArray
542 class {class_name}:
543 """Protocol decoder implementation.
545 Attributes:
546 sample_rate: Sample rate of input signal in Hz.
548 Example:
549 >>> decoder = {class_name}(sample_rate=1_000_000)
550 >>> frames = decoder.decode(digital_signal)
552 References:
553 PLUG-008: Plugin Template Generator
554 """
555 def __init__(self, *, sample_rate: float = 1_000_000.0) -> None:
556 """Initialize decoder.
558 Args:
559 sample_rate: Sample rate in Hz.
560 """
561 self.sample_rate = sample_rate
563 def decode(
564 self,
565 signal: NDArray[np.uint8],
566 ) -> list[dict[str, object]]:
567 """Decode protocol frames from digital signal.
569 Args:
570 signal: Digital signal (0/1 values).
572 Returns:
573 List of decoded frames, each a dictionary with frame data.
575 Raises:
576 ValueError: If signal is empty or invalid.
578 Example:
579 >>> signal = np.array([0, 1, 1, 0, 1], dtype=np.uint8)
580 >>> frames = decoder.decode(signal)
581 """
582 if len(signal) == 0:
583 raise ValueError("Signal cannot be empty")
585 # USER: Implement protocol decoding logic here
586 # This stub returns an empty list - replace with actual decoding
587 frames: list[dict[str, object]] = []
589 return frames
591 def configure(self, **params: object) -> None:
592 """Configure decoder parameters.
594 Args:
595 **params: Decoder-specific parameters.
597 Example:
598 >>> decoder.configure(baudrate=115200, parity='none')
599 """
600 # USER: Implement configuration logic here
601 # Store parameters as instance attributes
602 for key, value in params.items():
603 setattr(self, key, value)
604 ''')
607def _generate_analyzer_stub(template: PluginTemplate, class_name: str) -> str:
608 """Generate analyzer plugin stub.
610 Args:
611 template: Plugin template configuration.
612 class_name: Class name for the analyzer.
614 Returns:
615 Python source code for analyzer stub.
616 """
617 return textwrap.dedent(f'''\
618 """{template.description}
620 This analyzer implements custom signal analysis for TraceKit.
622 References:
623 PLUG-008: Plugin Template Generator
624 """
625 from __future__ import annotations
627 import numpy as np
628 from numpy.typing import NDArray
631 class {class_name}:
632 """Signal analyzer implementation.
634 Example:
635 >>> analyzer = {class_name}()
636 >>> result = analyzer.analyze(signal)
638 References:
639 PLUG-008: Plugin Template Generator
640 """
641 def __init__(self) -> None:
642 """Initialize analyzer."""
643 pass
645 def analyze(
646 self,
647 signal: NDArray[np.float64],
648 *,
649 sample_rate: float = 1_000_000.0,
650 ) -> dict[str, object]:
651 """Analyze signal and extract features.
653 Args:
654 signal: Input signal array.
655 sample_rate: Sample rate in Hz.
657 Returns:
658 Dictionary containing analysis results.
660 Raises:
661 ValueError: If signal is empty or invalid.
663 Example:
664 >>> signal = np.sin(2 * np.pi * 1000 * np.linspace(0, 1, 1000))
665 >>> result = analyzer.analyze(signal, sample_rate=1000)
666 """
667 if len(signal) == 0:
668 raise ValueError("Signal cannot be empty")
670 # USER: Implement analysis logic here
671 # This stub returns placeholder results - replace with actual analysis
672 result: dict[str, object] = {{
673 "status": "not_implemented",
674 "sample_count": len(signal),
675 "sample_rate": sample_rate,
676 }}
678 return result
679 ''')
682def _generate_loader_stub(template: PluginTemplate, class_name: str) -> str:
683 """Generate loader plugin stub.
685 Args:
686 template: Plugin template configuration.
687 class_name: Class name for the loader.
689 Returns:
690 Python source code for loader stub.
691 """
692 return textwrap.dedent(f'''\
693 """{template.description}
695 This loader implements file format loading for TraceKit.
697 References:
698 PLUG-008: Plugin Template Generator
699 """
700 from __future__ import annotations
702 import numpy as np
703 from numpy.typing import NDArray
704 from pathlib import Path
707 class {class_name}:
708 """File format loader implementation.
710 Example:
711 >>> loader = {class_name}()
712 >>> data = loader.load(Path("capture.dat"))
714 References:
715 PLUG-008: Plugin Template Generator
716 """
717 def __init__(self) -> None:
718 """Initialize loader."""
719 pass
721 def load(self, file_path: Path) -> dict[str, NDArray[np.float64]]:
722 """Load data from file.
724 Args:
725 file_path: Path to file to load.
727 Returns:
728 Dictionary mapping channel names to signal arrays.
730 Raises:
731 FileNotFoundError: If file does not exist.
732 ValueError: If file format is invalid.
734 Example:
735 >>> data = loader.load(Path("capture.dat"))
736 >>> print(f"Loaded {{len(data)}} channels")
737 """
738 if not file_path.exists():
739 raise FileNotFoundError(f"File not found: {{file_path}}")
741 # USER: Implement file loading logic here
742 # This stub returns empty data - replace with actual loading
743 data: dict[str, NDArray[np.float64]] = {{}}
745 return data
747 @staticmethod
748 def can_load(file_path: Path) -> bool:
749 """Check if this loader can handle the file.
751 Args:
752 file_path: Path to file.
754 Returns:
755 True if loader can handle this file format.
757 Example:
758 >>> if loader.can_load(Path("capture.dat")):
759 ... data = loader.load(Path("capture.dat"))
760 """
761 # USER: Implement format detection here
762 # Check file extension, magic bytes, etc.
763 return file_path.suffix == ".dat"
764 ''')
767def _generate_exporter_stub(template: PluginTemplate, class_name: str) -> str:
768 """Generate exporter plugin stub.
770 Args:
771 template: Plugin template configuration.
772 class_name: Class name for the exporter.
774 Returns:
775 Python source code for exporter stub.
776 """
777 return textwrap.dedent(f'''\
778 """{template.description}
780 This exporter implements custom export format for TraceKit.
782 References:
783 PLUG-008: Plugin Template Generator
784 """
785 from __future__ import annotations
787 import numpy as np
788 from numpy.typing import NDArray
789 from pathlib import Path
792 class {class_name}:
793 """Export format implementation.
795 Example:
796 >>> exporter = {class_name}()
797 >>> exporter.export(data, Path("output.dat"))
799 References:
800 PLUG-008: Plugin Template Generator
801 """
802 def __init__(self) -> None:
803 """Initialize exporter."""
804 pass
806 def export(
807 self,
808 data: dict[str, NDArray[np.float64]],
809 output_path: Path,
810 ) -> None:
811 """Export data to file.
813 Args:
814 data: Dictionary mapping channel names to signal arrays.
815 output_path: Path where file will be written.
817 Raises:
818 ValueError: If data is invalid.
819 OSError: If file cannot be written.
821 Example:
822 >>> data = {{"ch1": np.sin(np.linspace(0, 10, 100))}}
823 >>> exporter.export(data, Path("output.dat"))
824 """
825 if not data:
826 raise ValueError("Data dictionary cannot be empty")
828 # USER: Implement export logic here
829 # Write data to output_path in your custom format
831 # Placeholder implementation - replace with actual export
832 with output_path.open("w") as f:
833 f.write("# USER: Implement export format\\n")
834 for name, values in data.items():
835 f.write(f"# Channel: {{name}}, samples: {{len(values)}}\\n")
837 @staticmethod
838 def supports_format(format_name: str) -> bool:
839 """Check if this exporter supports the format.
841 Args:
842 format_name: Name of export format.
844 Returns:
845 True if format is supported.
847 Example:
848 >>> if exporter.supports_format("custom"):
849 ... exporter.export(data, path)
850 """
851 # USER: Implement format support detection here
852 return format_name == "custom"
853 ''')
856def _generate_generic_stub(template: PluginTemplate, class_name: str) -> str:
857 """Generate generic plugin stub.
859 Args:
860 template: Plugin template configuration.
861 class_name: Class name for the plugin.
863 Returns:
864 Python source code for generic stub.
865 """
866 return textwrap.dedent(f'''\
867 """{template.description}
869 This is a generic plugin implementation for TraceKit.
871 References:
872 PLUG-008: Plugin Template Generator
873 """
874 from __future__ import annotations
877 class {class_name}:
878 """Generic plugin implementation.
880 Example:
881 >>> plugin = {class_name}()
882 >>> result = plugin.process()
884 References:
885 PLUG-008: Plugin Template Generator
886 """
887 def __init__(self) -> None:
888 """Initialize plugin."""
889 pass
891 def process(self) -> dict[str, object]:
892 """Process data or perform plugin function.
894 Returns:
895 Dictionary containing results.
897 Example:
898 >>> result = plugin.process()
899 """
900 # USER: Implement plugin logic here
901 result: dict[str, object] = {{
902 "status": "not_implemented",
903 }}
905 return result
906 ''')
909__all__ = [
910 "PluginTemplate",
911 "PluginType",
912 "generate_plugin_template",
913]