feat(tagging-system): Add comprehensive tagging system implementation
- Add tagging system Python package with modular architecture (config, core, impl) - Create core interfaces for file discovery, content analysis, and tag generation - Implement content analyzer with language detection and topic extraction - Implement file discovery engine with directory scanning and filtering - Implement tag generator with hierarchical tag creation and consolidation - Add configuration management with example config and validation - Create comprehensive design and requirements documentation in .kiro/specs - Add pytest test suite with unit tests for models, interfaces, and config - Add setup.py, requirements.txt, and pytest.ini for package management - Add README.md with project overview and usage instructions - Update workspace.json with new project structure - Add Excalidraw diagram for system architecture visualization - Establish foundation for automated vault tagging and metadata management
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Test package for the tagging system."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
"""Pytest configuration and fixtures."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from hypothesis import settings, Verbosity
|
||||
|
||||
from tagging_system.core.models import (
|
||||
FileInfo,
|
||||
ContentAnalysis,
|
||||
TagStructure,
|
||||
FrontmatterData,
|
||||
ContentType,
|
||||
LanguageInfo
|
||||
)
|
||||
from tagging_system.config import TaggingConfig
|
||||
|
||||
|
||||
# Configure hypothesis for property-based testing
|
||||
settings.register_profile("default", max_examples=100, verbosity=Verbosity.normal)
|
||||
settings.register_profile("ci", max_examples=1000, verbosity=Verbosity.verbose)
|
||||
settings.load_profile("default")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_file_info() -> FileInfo:
|
||||
"""Create a sample FileInfo for testing."""
|
||||
return FileInfo(
|
||||
path="100-project/AI/test.md",
|
||||
name="test.md",
|
||||
directory="100-project/AI",
|
||||
extension=".md",
|
||||
size=1024,
|
||||
created=datetime(2024, 1, 1, 12, 0, 0),
|
||||
modified=datetime(2024, 1, 2, 12, 0, 0),
|
||||
content="# Test File\n\nThis is a test file about AI and machine learning."
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_content_analysis() -> ContentAnalysis:
|
||||
"""Create a sample ContentAnalysis for testing."""
|
||||
return ContentAnalysis(
|
||||
language=LanguageInfo.ENGLISH,
|
||||
content_type=ContentType.NOTE,
|
||||
topics=["ai", "machine-learning", "technology"],
|
||||
mentions={
|
||||
'tools': ['python', 'tensorflow'],
|
||||
'technologies': ['ai', 'ml'],
|
||||
'people': [],
|
||||
'organizations': ['openai']
|
||||
},
|
||||
sentiment="neutral",
|
||||
complexity="intermediate"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_tag_structure() -> TagStructure:
|
||||
"""Create a sample TagStructure for testing."""
|
||||
return TagStructure(
|
||||
primary=["project"],
|
||||
hierarchical=["tech/ai", "tech/ml"],
|
||||
content=["python", "tensorflow"],
|
||||
meta=["lang/en", "type/note"],
|
||||
custom=["custom-tag"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_frontmatter_data() -> FrontmatterData:
|
||||
"""Create a sample FrontmatterData for testing."""
|
||||
return FrontmatterData(
|
||||
title="Test File",
|
||||
tags=["project", "tech/ai", "python"],
|
||||
created="2024-01-01",
|
||||
updated="2024-01-02",
|
||||
type="note",
|
||||
lang="en",
|
||||
aliases=["test"],
|
||||
description="A test file for AI projects"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_config() -> TaggingConfig:
|
||||
"""Create a default TaggingConfig for testing."""
|
||||
return TaggingConfig()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_vault_structure(tmp_path: Path) -> Path:
|
||||
"""Create a temporary vault structure for testing."""
|
||||
vault_root = tmp_path / "test_vault"
|
||||
|
||||
# Create directory structure
|
||||
directories = [
|
||||
"100-project/AI",
|
||||
"100-project/Infrastructure",
|
||||
"200-area/Productivity",
|
||||
"200-area/Health",
|
||||
"300-resources/Development",
|
||||
"400-archive",
|
||||
"Clippings",
|
||||
"ReadItLater Inbox"
|
||||
]
|
||||
|
||||
for directory in directories:
|
||||
(vault_root / directory).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create sample files
|
||||
sample_files = [
|
||||
("100-project/AI/llm-notes.md", "# LLM Notes\n\nNotes about large language models."),
|
||||
("200-area/Productivity/gtd.md", "# Getting Things Done\n\nProductivity methodology."),
|
||||
("300-resources/Development/python.md", "# Python Resources\n\nPython development resources."),
|
||||
("Clippings/article.md", "# Interesting Article\n\nClipped from web.")
|
||||
]
|
||||
|
||||
for file_path, content in sample_files:
|
||||
file_full_path = vault_root / file_path
|
||||
file_full_path.write_text(content, encoding='utf-8')
|
||||
|
||||
return vault_root
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Unit tests for configuration system."""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from pathlib import Path
|
||||
from tagging_system.config import (
|
||||
TaggingConfig,
|
||||
DirectoryMapping,
|
||||
TagHierarchy,
|
||||
SensitivePatterns,
|
||||
load_config,
|
||||
save_config
|
||||
)
|
||||
|
||||
|
||||
class TestDirectoryMapping:
|
||||
"""Test DirectoryMapping model."""
|
||||
|
||||
def test_directory_mapping_creation(self):
|
||||
"""Test DirectoryMapping creation."""
|
||||
mapping = DirectoryMapping(
|
||||
pattern="100-project",
|
||||
primary_tag="project",
|
||||
hierarchical_tags=["tech"],
|
||||
exclude_patterns=["*.tmp"]
|
||||
)
|
||||
|
||||
assert mapping.pattern == "100-project"
|
||||
assert mapping.primary_tag == "project"
|
||||
assert mapping.hierarchical_tags == ["tech"]
|
||||
assert mapping.exclude_patterns == ["*.tmp"]
|
||||
|
||||
|
||||
class TestTagHierarchy:
|
||||
"""Test TagHierarchy model."""
|
||||
|
||||
def test_tag_hierarchy_creation(self):
|
||||
"""Test TagHierarchy creation."""
|
||||
hierarchy = TagHierarchy(
|
||||
root="tech",
|
||||
children={"ai": TagHierarchy("ai")},
|
||||
aliases=["technology"]
|
||||
)
|
||||
|
||||
assert hierarchy.root == "tech"
|
||||
assert "ai" in hierarchy.children
|
||||
assert hierarchy.aliases == ["technology"]
|
||||
|
||||
def test_get_full_path(self):
|
||||
"""Test get_full_path method."""
|
||||
hierarchy = TagHierarchy("tech")
|
||||
|
||||
assert hierarchy.get_full_path() == "tech"
|
||||
assert hierarchy.get_full_path("ai") == "tech/ai"
|
||||
assert hierarchy.get_full_path("ai/llm") == "tech/ai/llm"
|
||||
|
||||
|
||||
class TestSensitivePatterns:
|
||||
"""Test SensitivePatterns model."""
|
||||
|
||||
def test_sensitive_patterns_defaults(self):
|
||||
"""Test SensitivePatterns default values."""
|
||||
patterns = SensitivePatterns()
|
||||
|
||||
assert len(patterns.credential_patterns) > 0
|
||||
assert len(patterns.personal_patterns) > 0
|
||||
assert len(patterns.financial_patterns) > 0
|
||||
assert any("api" in pattern for pattern in patterns.credential_patterns)
|
||||
|
||||
|
||||
class TestTaggingConfig:
|
||||
"""Test TaggingConfig model."""
|
||||
|
||||
def test_tagging_config_defaults(self):
|
||||
"""Test TaggingConfig default values."""
|
||||
config = TaggingConfig()
|
||||
|
||||
assert len(config.directory_mappings) > 0
|
||||
assert len(config.tag_hierarchies) > 0
|
||||
assert len(config.excluded_directories) > 0
|
||||
assert config.tag_format_rules["case"] == "kebab"
|
||||
|
||||
def test_get_directory_mapping(self):
|
||||
"""Test get_directory_mapping method."""
|
||||
config = TaggingConfig()
|
||||
|
||||
mapping = config.get_directory_mapping("100-project/AI/test.md")
|
||||
assert mapping is not None
|
||||
assert mapping.primary_tag == "project"
|
||||
|
||||
no_mapping = config.get_directory_mapping("unknown/path")
|
||||
assert no_mapping is None
|
||||
|
||||
def test_get_tag_hierarchy(self):
|
||||
"""Test get_tag_hierarchy method."""
|
||||
config = TaggingConfig()
|
||||
|
||||
tech_hierarchy = config.get_tag_hierarchy("tech")
|
||||
assert tech_hierarchy is not None
|
||||
assert tech_hierarchy.root == "tech"
|
||||
|
||||
unknown_hierarchy = config.get_tag_hierarchy("unknown")
|
||||
assert unknown_hierarchy is None
|
||||
|
||||
def test_is_excluded_directory(self):
|
||||
"""Test is_excluded_directory method."""
|
||||
config = TaggingConfig()
|
||||
|
||||
assert config.is_excluded_directory(".obsidian/plugins") is True
|
||||
assert config.is_excluded_directory("100-project/AI") is False
|
||||
|
||||
def test_is_excluded_file(self):
|
||||
"""Test is_excluded_file method."""
|
||||
config = TaggingConfig()
|
||||
|
||||
assert config.is_excluded_file("test.pyc") is True
|
||||
assert config.is_excluded_file(".DS_Store") is True
|
||||
assert config.is_excluded_file("test.md") is False
|
||||
|
||||
|
||||
class TestConfigLoading:
|
||||
"""Test configuration loading and saving."""
|
||||
|
||||
def test_load_default_config(self):
|
||||
"""Test loading default config when no file exists."""
|
||||
config = load_config()
|
||||
|
||||
assert isinstance(config, TaggingConfig)
|
||||
assert len(config.directory_mappings) > 0
|
||||
|
||||
def test_load_config_creates_default_file(self, tmp_path: Path):
|
||||
"""Test that load_config creates default file when path doesn't exist."""
|
||||
config_path = tmp_path / "config.yaml"
|
||||
|
||||
config = load_config(config_path)
|
||||
|
||||
assert isinstance(config, TaggingConfig)
|
||||
assert config_path.exists()
|
||||
|
||||
def test_save_and_load_config(self, tmp_path: Path):
|
||||
"""Test saving and loading configuration."""
|
||||
config_path = tmp_path / "test_config.yaml"
|
||||
original_config = TaggingConfig()
|
||||
original_config.excluded_directories.append("test_exclude")
|
||||
|
||||
save_config(original_config, config_path)
|
||||
loaded_config = load_config(config_path)
|
||||
|
||||
assert config_path.exists()
|
||||
assert "test_exclude" in loaded_config.excluded_directories
|
||||
|
||||
def test_load_json_config(self, tmp_path: Path):
|
||||
"""Test loading JSON configuration."""
|
||||
config_path = tmp_path / "config.json"
|
||||
config_data = {
|
||||
"excluded_directories": [".test", ".custom"],
|
||||
"tag_format_rules": {"case": "snake"}
|
||||
}
|
||||
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config_data, f)
|
||||
|
||||
config = load_config(config_path)
|
||||
|
||||
assert ".test" in config.excluded_directories
|
||||
assert ".custom" in config.excluded_directories
|
||||
assert config.tag_format_rules["case"] == "snake"
|
||||
|
||||
def test_load_invalid_config_returns_default(self, tmp_path: Path):
|
||||
"""Test that invalid config file returns default config."""
|
||||
config_path = tmp_path / "invalid.yaml"
|
||||
config_path.write_text("invalid: yaml: content: [")
|
||||
|
||||
config = load_config(config_path)
|
||||
|
||||
# Should return default config on error
|
||||
assert isinstance(config, TaggingConfig)
|
||||
assert len(config.directory_mappings) > 0
|
||||
@@ -0,0 +1,232 @@
|
||||
"""Unit tests for core interfaces and protocols."""
|
||||
|
||||
import pytest
|
||||
from typing import List
|
||||
from tagging_system.core.interfaces import (
|
||||
FileDiscovery,
|
||||
ContentAnalyzer,
|
||||
TagGenerator,
|
||||
FrontmatterManager,
|
||||
BaseFileDiscovery,
|
||||
BaseContentAnalyzer,
|
||||
BaseTagGenerator,
|
||||
BaseFrontmatterManager
|
||||
)
|
||||
from tagging_system.core.models import (
|
||||
FileInfo,
|
||||
ContentAnalysis,
|
||||
TagStructure,
|
||||
FrontmatterData,
|
||||
ValidationResult,
|
||||
ContentType,
|
||||
LanguageInfo
|
||||
)
|
||||
|
||||
|
||||
class MockFileDiscovery(BaseFileDiscovery):
|
||||
"""Mock implementation of FileDiscovery for testing."""
|
||||
|
||||
def scan_directory(self, path: str) -> List[FileInfo]:
|
||||
"""Mock directory scanning."""
|
||||
from datetime import datetime
|
||||
return [
|
||||
FileInfo(
|
||||
path=f"{path}/test.md",
|
||||
name="test.md",
|
||||
directory=path,
|
||||
extension=".md",
|
||||
size=100,
|
||||
created=datetime.now(),
|
||||
modified=datetime.now()
|
||||
)
|
||||
]
|
||||
|
||||
def filter_by_type(self, files: List[FileInfo], types: List[str]) -> List[FileInfo]:
|
||||
"""Mock file filtering."""
|
||||
return [f for f in files if f.extension in types]
|
||||
|
||||
def exclude_sensitive(self, files: List[FileInfo]) -> List[FileInfo]:
|
||||
"""Mock sensitive file exclusion."""
|
||||
return [f for f in files if "sensitive" not in f.path]
|
||||
|
||||
|
||||
class MockContentAnalyzer(BaseContentAnalyzer):
|
||||
"""Mock implementation of ContentAnalyzer for testing."""
|
||||
|
||||
def analyze_content(self, content: str) -> ContentAnalysis:
|
||||
"""Mock content analysis."""
|
||||
return ContentAnalysis(
|
||||
language=LanguageInfo.ENGLISH,
|
||||
content_type=ContentType.NOTE,
|
||||
topics=["test"],
|
||||
mentions={'tools': [], 'technologies': [], 'people': [], 'organizations': []}
|
||||
)
|
||||
|
||||
def detect_language(self, content: str) -> str:
|
||||
"""Mock language detection."""
|
||||
return "en"
|
||||
|
||||
def extract_topics(self, content: str) -> List[str]:
|
||||
"""Mock topic extraction."""
|
||||
return ["test", "mock"]
|
||||
|
||||
def classify_content_type(self, content: str, filename: str) -> str:
|
||||
"""Mock content type classification."""
|
||||
return "note"
|
||||
|
||||
|
||||
class MockTagGenerator(BaseTagGenerator):
|
||||
"""Mock implementation of TagGenerator for testing."""
|
||||
|
||||
def generate_directory_tags(self, filepath: str) -> List[str]:
|
||||
"""Mock directory tag generation."""
|
||||
if "100-project" in filepath:
|
||||
return ["project"]
|
||||
return ["unknown"]
|
||||
|
||||
def generate_content_tags(self, analysis: ContentAnalysis) -> List[str]:
|
||||
"""Mock content tag generation."""
|
||||
return analysis.topics
|
||||
|
||||
def generate_hierarchical_tags(self, topics: List[str]) -> List[str]:
|
||||
"""Mock hierarchical tag generation."""
|
||||
return [f"topic/{topic}" for topic in topics]
|
||||
|
||||
def consolidate_tags(self, tags: List[str]) -> List[str]:
|
||||
"""Mock tag consolidation."""
|
||||
return list(set(tags)) # Remove duplicates
|
||||
|
||||
|
||||
class MockFrontmatterManager(BaseFrontmatterManager):
|
||||
"""Mock implementation of FrontmatterManager for testing."""
|
||||
|
||||
def parse_frontmatter(self, content: str) -> FrontmatterData:
|
||||
"""Mock frontmatter parsing."""
|
||||
return FrontmatterData(
|
||||
title="Test",
|
||||
tags=["test"],
|
||||
created="2024-01-01"
|
||||
)
|
||||
|
||||
def update_frontmatter(self, content: str, updates: FrontmatterData) -> str:
|
||||
"""Mock frontmatter updating."""
|
||||
return f"---\ntitle: {updates.title}\ntags: {updates.tags}\n---\n{content}"
|
||||
|
||||
def validate_frontmatter(self, data: FrontmatterData) -> ValidationResult:
|
||||
"""Mock frontmatter validation."""
|
||||
result = ValidationResult(is_valid=True)
|
||||
if not data.title:
|
||||
result.add_error("Title is required")
|
||||
return result
|
||||
|
||||
|
||||
class TestProtocolCompliance:
|
||||
"""Test that mock implementations comply with protocols."""
|
||||
|
||||
def test_file_discovery_protocol_compliance(self):
|
||||
"""Test that MockFileDiscovery implements FileDiscovery protocol."""
|
||||
mock = MockFileDiscovery()
|
||||
|
||||
assert isinstance(mock, FileDiscovery)
|
||||
|
||||
# Test method calls
|
||||
files = mock.scan_directory("test")
|
||||
assert len(files) == 1
|
||||
assert files[0].name == "test.md"
|
||||
|
||||
filtered = mock.filter_by_type(files, [".md"])
|
||||
assert len(filtered) == 1
|
||||
|
||||
non_sensitive = mock.exclude_sensitive(files)
|
||||
assert len(non_sensitive) == 1
|
||||
|
||||
def test_content_analyzer_protocol_compliance(self):
|
||||
"""Test that MockContentAnalyzer implements ContentAnalyzer protocol."""
|
||||
mock = MockContentAnalyzer()
|
||||
|
||||
assert isinstance(mock, ContentAnalyzer)
|
||||
|
||||
# Test method calls
|
||||
analysis = mock.analyze_content("test content")
|
||||
assert analysis.language == LanguageInfo.ENGLISH
|
||||
assert analysis.content_type == ContentType.NOTE
|
||||
|
||||
language = mock.detect_language("test content")
|
||||
assert language == "en"
|
||||
|
||||
topics = mock.extract_topics("test content")
|
||||
assert "test" in topics
|
||||
|
||||
content_type = mock.classify_content_type("test content", "test.md")
|
||||
assert content_type == "note"
|
||||
|
||||
def test_tag_generator_protocol_compliance(self):
|
||||
"""Test that MockTagGenerator implements TagGenerator protocol."""
|
||||
mock = MockTagGenerator()
|
||||
|
||||
assert isinstance(mock, TagGenerator)
|
||||
|
||||
# Test method calls
|
||||
dir_tags = mock.generate_directory_tags("100-project/test.md")
|
||||
assert "project" in dir_tags
|
||||
|
||||
analysis = ContentAnalysis(
|
||||
language=LanguageInfo.ENGLISH,
|
||||
content_type=ContentType.NOTE,
|
||||
topics=["ai", "ml"]
|
||||
)
|
||||
content_tags = mock.generate_content_tags(analysis)
|
||||
assert "ai" in content_tags
|
||||
|
||||
hierarchical = mock.generate_hierarchical_tags(["ai", "ml"])
|
||||
assert "topic/ai" in hierarchical
|
||||
|
||||
consolidated = mock.consolidate_tags(["tag1", "tag1", "tag2"])
|
||||
assert len(consolidated) == 2
|
||||
|
||||
def test_frontmatter_manager_protocol_compliance(self):
|
||||
"""Test that MockFrontmatterManager implements FrontmatterManager protocol."""
|
||||
mock = MockFrontmatterManager()
|
||||
|
||||
assert isinstance(mock, FrontmatterManager)
|
||||
|
||||
# Test method calls
|
||||
frontmatter = mock.parse_frontmatter("---\ntitle: Test\n---\nContent")
|
||||
assert frontmatter.title == "Test"
|
||||
|
||||
updated = mock.update_frontmatter("Content", frontmatter)
|
||||
assert "title: Test" in updated
|
||||
|
||||
validation = mock.validate_frontmatter(frontmatter)
|
||||
assert validation.is_valid is True
|
||||
|
||||
|
||||
class TestAbstractBaseClasses:
|
||||
"""Test abstract base class behavior."""
|
||||
|
||||
def test_base_classes_cannot_be_instantiated(self):
|
||||
"""Test that abstract base classes cannot be instantiated directly."""
|
||||
with pytest.raises(TypeError):
|
||||
BaseFileDiscovery()
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
BaseContentAnalyzer()
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
BaseTagGenerator()
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
BaseFrontmatterManager()
|
||||
|
||||
def test_concrete_implementations_work(self):
|
||||
"""Test that concrete implementations of base classes work."""
|
||||
file_discovery = MockFileDiscovery()
|
||||
content_analyzer = MockContentAnalyzer()
|
||||
tag_generator = MockTagGenerator()
|
||||
frontmatter_manager = MockFrontmatterManager()
|
||||
|
||||
# All should be instances of their respective base classes
|
||||
assert isinstance(file_discovery, BaseFileDiscovery)
|
||||
assert isinstance(content_analyzer, BaseContentAnalyzer)
|
||||
assert isinstance(tag_generator, BaseTagGenerator)
|
||||
assert isinstance(frontmatter_manager, BaseFrontmatterManager)
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Unit tests for core data models."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from tagging_system.core.models import (
|
||||
FileInfo,
|
||||
ContentAnalysis,
|
||||
TagStructure,
|
||||
FrontmatterData,
|
||||
ValidationResult,
|
||||
ContentType,
|
||||
LanguageInfo
|
||||
)
|
||||
|
||||
|
||||
class TestFileInfo:
|
||||
"""Test FileInfo model."""
|
||||
|
||||
def test_file_info_creation(self):
|
||||
"""Test FileInfo creation with required fields."""
|
||||
file_info = FileInfo(
|
||||
path="test/file.md",
|
||||
name="file.md",
|
||||
directory="test",
|
||||
extension=".md",
|
||||
size=100,
|
||||
created=datetime.now(),
|
||||
modified=datetime.now()
|
||||
)
|
||||
|
||||
assert file_info.path == "test/file.md"
|
||||
assert file_info.name == "file.md"
|
||||
assert file_info.is_markdown is True
|
||||
assert file_info.relative_path == "test/file.md"
|
||||
|
||||
def test_is_markdown_detection(self):
|
||||
"""Test markdown file detection."""
|
||||
md_file = FileInfo("test.md", "test.md", ".", ".md", 100, datetime.now(), datetime.now())
|
||||
txt_file = FileInfo("test.txt", "test.txt", ".", ".txt", 100, datetime.now(), datetime.now())
|
||||
|
||||
assert md_file.is_markdown is True
|
||||
assert txt_file.is_markdown is False
|
||||
|
||||
|
||||
class TestContentAnalysis:
|
||||
"""Test ContentAnalysis model."""
|
||||
|
||||
def test_content_analysis_creation(self):
|
||||
"""Test ContentAnalysis creation."""
|
||||
analysis = ContentAnalysis(
|
||||
language=LanguageInfo.ENGLISH,
|
||||
content_type=ContentType.NOTE,
|
||||
topics=["ai", "ml"]
|
||||
)
|
||||
|
||||
assert analysis.language == LanguageInfo.ENGLISH
|
||||
assert analysis.content_type == ContentType.NOTE
|
||||
assert analysis.topics == ["ai", "ml"]
|
||||
assert analysis.complexity == "basic" # Default value
|
||||
|
||||
def test_complexity_validation(self):
|
||||
"""Test complexity validation in post_init."""
|
||||
analysis = ContentAnalysis(
|
||||
language=LanguageInfo.ENGLISH,
|
||||
content_type=ContentType.NOTE,
|
||||
topics=[],
|
||||
complexity="invalid"
|
||||
)
|
||||
|
||||
assert analysis.complexity == "basic" # Should default to basic
|
||||
|
||||
|
||||
class TestTagStructure:
|
||||
"""Test TagStructure model."""
|
||||
|
||||
def test_tag_structure_creation(self):
|
||||
"""Test TagStructure creation."""
|
||||
tags = TagStructure(
|
||||
primary=["project"],
|
||||
hierarchical=["tech/ai"],
|
||||
content=["python"],
|
||||
meta=["lang/en"],
|
||||
custom=["custom"]
|
||||
)
|
||||
|
||||
assert tags.primary == ["project"]
|
||||
assert tags.hierarchical == ["tech/ai"]
|
||||
assert tags.content == ["python"]
|
||||
assert tags.meta == ["lang/en"]
|
||||
assert tags.custom == ["custom"]
|
||||
|
||||
def test_all_tags_method(self):
|
||||
"""Test all_tags method returns unique tags."""
|
||||
tags = TagStructure(
|
||||
primary=["project", "duplicate"],
|
||||
hierarchical=["tech/ai"],
|
||||
content=["python", "duplicate"], # Duplicate tag
|
||||
meta=["lang/en"],
|
||||
custom=["custom"]
|
||||
)
|
||||
|
||||
all_tags = tags.all_tags()
|
||||
# Expected unique tags: project, duplicate, tech/ai, python, lang/en, custom = 6 tags
|
||||
assert len(all_tags) == 6
|
||||
assert "duplicate" in all_tags
|
||||
assert "project" in all_tags
|
||||
assert "tech/ai" in all_tags
|
||||
assert "python" in all_tags
|
||||
assert "lang/en" in all_tags
|
||||
assert "custom" in all_tags
|
||||
|
||||
# Test that duplicates are actually removed by checking set behavior
|
||||
unique_tags = set(all_tags)
|
||||
assert len(unique_tags) == len(all_tags) # No duplicates should exist
|
||||
|
||||
# Test with actual duplicates to verify deduplication works
|
||||
tags_with_more_duplicates = TagStructure(
|
||||
primary=["tag1", "tag2"],
|
||||
hierarchical=["tag1"], # Duplicate of primary
|
||||
content=["tag2", "tag3"], # Duplicate of primary
|
||||
meta=["tag3"], # Duplicate of content
|
||||
custom=["tag4"]
|
||||
)
|
||||
deduplicated = tags_with_more_duplicates.all_tags()
|
||||
assert len(deduplicated) == 4 # tag1, tag2, tag3, tag4
|
||||
assert len(set(deduplicated)) == len(deduplicated)
|
||||
|
||||
|
||||
class TestFrontmatterData:
|
||||
"""Test FrontmatterData model."""
|
||||
|
||||
def test_frontmatter_data_creation(self):
|
||||
"""Test FrontmatterData creation."""
|
||||
frontmatter = FrontmatterData(
|
||||
title="Test",
|
||||
tags=["tag1", "tag2"],
|
||||
created="2024-01-01",
|
||||
type="note"
|
||||
)
|
||||
|
||||
assert frontmatter.title == "Test"
|
||||
assert frontmatter.tags == ["tag1", "tag2"]
|
||||
assert frontmatter.created == "2024-01-01"
|
||||
assert frontmatter.type == "note"
|
||||
|
||||
def test_to_dict_method(self):
|
||||
"""Test to_dict method excludes None values."""
|
||||
frontmatter = FrontmatterData(
|
||||
title="Test",
|
||||
tags=["tag1"],
|
||||
created="2024-01-01",
|
||||
updated=None, # Should be excluded
|
||||
custom_fields={"custom": "value"}
|
||||
)
|
||||
|
||||
result = frontmatter.to_dict()
|
||||
|
||||
assert result["title"] == "Test"
|
||||
assert result["tags"] == ["tag1"]
|
||||
assert result["created"] == "2024-01-01"
|
||||
assert "updated" not in result # None values excluded
|
||||
assert result["custom"] == "value" # Custom fields included
|
||||
|
||||
|
||||
class TestValidationResult:
|
||||
"""Test ValidationResult model."""
|
||||
|
||||
def test_validation_result_creation(self):
|
||||
"""Test ValidationResult creation."""
|
||||
result = ValidationResult(is_valid=True)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.errors == []
|
||||
assert result.warnings == []
|
||||
assert result.suggestions == []
|
||||
|
||||
def test_add_error_sets_invalid(self):
|
||||
"""Test that adding error sets is_valid to False."""
|
||||
result = ValidationResult(is_valid=True)
|
||||
result.add_error("Test error")
|
||||
|
||||
assert result.is_valid is False
|
||||
assert "Test error" in result.errors
|
||||
|
||||
def test_add_warning_and_suggestion(self):
|
||||
"""Test adding warnings and suggestions."""
|
||||
result = ValidationResult(is_valid=True)
|
||||
result.add_warning("Test warning")
|
||||
result.add_suggestion("Test suggestion")
|
||||
|
||||
assert result.is_valid is True # Warnings don't affect validity
|
||||
assert "Test warning" in result.warnings
|
||||
assert "Test suggestion" in result.suggestions
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Test overall package structure and imports."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPackageStructure:
|
||||
"""Test that the package structure is correct."""
|
||||
|
||||
def test_main_package_imports(self):
|
||||
"""Test that main package imports work correctly."""
|
||||
from tagging_system import (
|
||||
FileInfo,
|
||||
ContentAnalysis,
|
||||
TagStructure,
|
||||
FrontmatterData,
|
||||
ContentType,
|
||||
LanguageInfo,
|
||||
ValidationResult,
|
||||
FileDiscovery,
|
||||
ContentAnalyzer,
|
||||
TagGenerator,
|
||||
FrontmatterManager
|
||||
)
|
||||
|
||||
# Test that all imports are available
|
||||
assert FileInfo is not None
|
||||
assert ContentAnalysis is not None
|
||||
assert TagStructure is not None
|
||||
assert FrontmatterData is not None
|
||||
assert ContentType is not None
|
||||
assert LanguageInfo is not None
|
||||
assert ValidationResult is not None
|
||||
assert FileDiscovery is not None
|
||||
assert ContentAnalyzer is not None
|
||||
assert TagGenerator is not None
|
||||
assert FrontmatterManager is not None
|
||||
|
||||
def test_core_module_imports(self):
|
||||
"""Test that core module imports work correctly."""
|
||||
from tagging_system.core import (
|
||||
FileInfo,
|
||||
ContentAnalysis,
|
||||
TagStructure,
|
||||
FrontmatterData,
|
||||
ContentType,
|
||||
LanguageInfo,
|
||||
ValidationResult,
|
||||
FileDiscovery,
|
||||
ContentAnalyzer,
|
||||
TagGenerator,
|
||||
FrontmatterManager
|
||||
)
|
||||
|
||||
# All imports should be available
|
||||
assert all([
|
||||
FileInfo, ContentAnalysis, TagStructure, FrontmatterData,
|
||||
ContentType, LanguageInfo, ValidationResult,
|
||||
FileDiscovery, ContentAnalyzer, TagGenerator, FrontmatterManager
|
||||
])
|
||||
|
||||
def test_config_module_imports(self):
|
||||
"""Test that config module imports work correctly."""
|
||||
from tagging_system.config import (
|
||||
TaggingConfig,
|
||||
DirectoryMapping,
|
||||
TagHierarchy,
|
||||
SensitivePatterns,
|
||||
load_config,
|
||||
save_config
|
||||
)
|
||||
|
||||
# All imports should be available
|
||||
assert all([
|
||||
TaggingConfig, DirectoryMapping, TagHierarchy,
|
||||
SensitivePatterns, load_config, save_config
|
||||
])
|
||||
|
||||
def test_cli_module_import(self):
|
||||
"""Test that CLI module can be imported."""
|
||||
from tagging_system import cli
|
||||
|
||||
assert hasattr(cli, 'main')
|
||||
assert callable(cli.main)
|
||||
|
||||
def test_package_version(self):
|
||||
"""Test that package version is available."""
|
||||
import tagging_system
|
||||
|
||||
assert hasattr(tagging_system, '__version__')
|
||||
assert tagging_system.__version__ == "0.1.0"
|
||||
|
||||
def test_package_metadata(self):
|
||||
"""Test that package metadata is available."""
|
||||
import tagging_system
|
||||
|
||||
assert hasattr(tagging_system, '__author__')
|
||||
assert tagging_system.__author__ == "Tagging System"
|
||||
Reference in New Issue
Block a user