Add management header to hosts files and enhance serialization formatting; update tests to reflect changes.
This commit is contained in:
		
							parent
							
								
									5a2e0d2623
								
							
						
					
					
						commit
						0ee720c5ef
					
				
					 3 changed files with 163 additions and 6 deletions
				
			
		| 
						 | 
				
			
			@ -60,6 +60,8 @@
 | 
			
		|||
- ✅ **Error handling**: Comprehensive error handling with user feedback
 | 
			
		||||
- ✅ **Keyboard shortcuts**: All edit mode shortcuts implemented and tested
 | 
			
		||||
- ✅ **Live testing**: Manual testing confirms all functionality works correctly
 | 
			
		||||
- ✅ **Human-readable formatting**: Tab-based column alignment with proper spacing
 | 
			
		||||
- ✅ **Management header**: Automatic addition of management header to hosts files
 | 
			
		||||
 | 
			
		||||
### Phase 4: Advanced Edit Features
 | 
			
		||||
- ❌ **Add new entries**: Create new host entries
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,9 +92,12 @@ class HostsParser:
 | 
			
		|||
        """
 | 
			
		||||
        lines = []
 | 
			
		||||
        
 | 
			
		||||
        # Ensure header has management line
 | 
			
		||||
        header_comments = self._ensure_management_header(hosts_file.header_comments)
 | 
			
		||||
        
 | 
			
		||||
        # Add header comments
 | 
			
		||||
        if hosts_file.header_comments:
 | 
			
		||||
            for comment in hosts_file.header_comments:
 | 
			
		||||
        if header_comments:
 | 
			
		||||
            for comment in header_comments:
 | 
			
		||||
                if comment.strip():
 | 
			
		||||
                    lines.append(f"# {comment}")
 | 
			
		||||
                else:
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +121,132 @@ class HostsParser:
 | 
			
		|||
        
 | 
			
		||||
        return "\n".join(lines) + "\n"
 | 
			
		||||
    
 | 
			
		||||
    def _ensure_management_header(self, header_comments: list) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Ensure the header contains the management line with proper formatting.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            header_comments: List of existing header comments
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of header comments with management line added if needed
 | 
			
		||||
        """
 | 
			
		||||
        management_line = "Managed by hosts - https://git.s1q.dev/phg/hosts"
 | 
			
		||||
        
 | 
			
		||||
        # Check if management line already exists
 | 
			
		||||
        for comment in header_comments:
 | 
			
		||||
            if "git.s1q.dev/phg/hosts" in comment:
 | 
			
		||||
                return header_comments
 | 
			
		||||
        
 | 
			
		||||
        # If no header exists, create default header
 | 
			
		||||
        if not header_comments:
 | 
			
		||||
            return [
 | 
			
		||||
                "#",
 | 
			
		||||
                "Host Database",
 | 
			
		||||
                "",
 | 
			
		||||
                management_line,
 | 
			
		||||
                "#"
 | 
			
		||||
            ]
 | 
			
		||||
        
 | 
			
		||||
        # Check for enclosing comment patterns
 | 
			
		||||
        enclosing_pattern = self._detect_enclosing_pattern(header_comments)
 | 
			
		||||
        
 | 
			
		||||
        if enclosing_pattern:
 | 
			
		||||
            # Insert management line within the enclosing pattern
 | 
			
		||||
            return self._insert_in_enclosing_pattern(header_comments, management_line, enclosing_pattern)
 | 
			
		||||
        else:
 | 
			
		||||
            # No enclosing pattern, append management line
 | 
			
		||||
            result = header_comments.copy()
 | 
			
		||||
            result.append(management_line)
 | 
			
		||||
            return result
 | 
			
		||||
    
 | 
			
		||||
    def _detect_enclosing_pattern(self, header_comments: list) -> dict | None:
 | 
			
		||||
        """
 | 
			
		||||
        Detect if header has enclosing comment patterns like ###, # #, etc.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            header_comments: List of header comments
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dictionary with pattern info or None if no pattern detected
 | 
			
		||||
        """
 | 
			
		||||
        if len(header_comments) < 2:
 | 
			
		||||
            return None
 | 
			
		||||
        
 | 
			
		||||
        # Look for matching patterns at start and end, ignoring management line if present
 | 
			
		||||
        first_line = header_comments[0].strip()
 | 
			
		||||
        
 | 
			
		||||
        # Find the last line that could be a closing pattern (not the management line)
 | 
			
		||||
        last_pattern_index = -1
 | 
			
		||||
        for i in range(len(header_comments) - 1, -1, -1):
 | 
			
		||||
            line = header_comments[i].strip()
 | 
			
		||||
            if "git.s1q.dev/phg/hosts" not in line:
 | 
			
		||||
                last_pattern_index = i
 | 
			
		||||
                break
 | 
			
		||||
        
 | 
			
		||||
        if last_pattern_index <= 0:
 | 
			
		||||
            return None
 | 
			
		||||
            
 | 
			
		||||
        last_line = header_comments[last_pattern_index].strip()
 | 
			
		||||
        
 | 
			
		||||
        # Check for ### pattern
 | 
			
		||||
        if first_line == "###" and last_line == "###":
 | 
			
		||||
            return {
 | 
			
		||||
                'type': 'triple_hash',
 | 
			
		||||
                'start_index': 0,
 | 
			
		||||
                'end_index': last_pattern_index,
 | 
			
		||||
                'pattern': '###'
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        # Check for # # pattern
 | 
			
		||||
        if first_line == "#" and last_line == "#":
 | 
			
		||||
            return {
 | 
			
		||||
                'type': 'single_hash',
 | 
			
		||||
                'start_index': 0,
 | 
			
		||||
                'end_index': last_pattern_index,
 | 
			
		||||
                'pattern': '#'
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        # Check for other repeating patterns (like ####, #####, etc.)
 | 
			
		||||
        if len(first_line) > 1 and first_line == last_line and all(c == '#' for c in first_line):
 | 
			
		||||
            return {
 | 
			
		||||
                'type': 'repeating_hash',
 | 
			
		||||
                'start_index': 0,
 | 
			
		||||
                'end_index': last_pattern_index,
 | 
			
		||||
                'pattern': first_line
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        return None
 | 
			
		||||
    
 | 
			
		||||
    def _insert_in_enclosing_pattern(self, header_comments: list, management_line: str, pattern_info: dict) -> list:
 | 
			
		||||
        """
 | 
			
		||||
        Insert management line within an enclosing comment pattern.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            header_comments: List of header comments
 | 
			
		||||
            management_line: Management line to insert
 | 
			
		||||
            pattern_info: Information about the enclosing pattern
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Updated list of header comments
 | 
			
		||||
        """
 | 
			
		||||
        result = header_comments.copy()
 | 
			
		||||
        
 | 
			
		||||
        # Find the best insertion point (before the closing pattern)
 | 
			
		||||
        insert_index = pattern_info['end_index']
 | 
			
		||||
        
 | 
			
		||||
        # Look for an empty line before the closing pattern to insert after it
 | 
			
		||||
        # Otherwise, insert right before the closing pattern
 | 
			
		||||
        if insert_index > 1 and header_comments[insert_index - 1].strip() == "":
 | 
			
		||||
            # Insert after the empty line, before closing pattern
 | 
			
		||||
            result.insert(insert_index, management_line)
 | 
			
		||||
        else:
 | 
			
		||||
            # Insert empty line and management line before closing pattern
 | 
			
		||||
            result.insert(insert_index, "")
 | 
			
		||||
            result.insert(insert_index + 1, management_line)
 | 
			
		||||
        
 | 
			
		||||
        return result
 | 
			
		||||
    
 | 
			
		||||
    def _calculate_column_widths(self, entries: list) -> tuple[int, int]:
 | 
			
		||||
        """
 | 
			
		||||
        Calculate the maximum width needed for IP and hostname columns.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -168,7 +168,12 @@ class TestHostsParser:
 | 
			
		|||
        parser = HostsParser()
 | 
			
		||||
        content = parser.serialize(hosts_file)
 | 
			
		||||
 | 
			
		||||
        expected = """127.0.0.1\tlocalhost
 | 
			
		||||
        expected = """# #
 | 
			
		||||
# Host Database
 | 
			
		||||
#
 | 
			
		||||
# Managed by hosts - https://git.s1q.dev/phg/hosts
 | 
			
		||||
# #
 | 
			
		||||
127.0.0.1\tlocalhost
 | 
			
		||||
192.168.1.1\trouter
 | 
			
		||||
"""
 | 
			
		||||
        assert content == expected
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +203,7 @@ class TestHostsParser:
 | 
			
		|||
        
 | 
			
		||||
        expected = """# Header comment 1
 | 
			
		||||
# Header comment 2
 | 
			
		||||
# Managed by hosts - https://git.s1q.dev/phg/hosts
 | 
			
		||||
127.0.0.1\tlocalhost\t# Loopback
 | 
			
		||||
# 10.0.0.1\ttest
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +217,13 @@ class TestHostsParser:
 | 
			
		|||
        parser = HostsParser()
 | 
			
		||||
        content = parser.serialize(hosts_file)
 | 
			
		||||
        
 | 
			
		||||
        assert content == "\n"
 | 
			
		||||
        expected = """# #
 | 
			
		||||
# Host Database
 | 
			
		||||
#
 | 
			
		||||
# Managed by hosts - https://git.s1q.dev/phg/hosts
 | 
			
		||||
# #
 | 
			
		||||
"""
 | 
			
		||||
        assert content == expected
 | 
			
		||||
    
 | 
			
		||||
    def test_write_hosts_file(self):
 | 
			
		||||
        """Test writing hosts file to disk."""
 | 
			
		||||
| 
						 | 
				
			
			@ -226,7 +238,14 @@ class TestHostsParser:
 | 
			
		|||
            # Read back and verify
 | 
			
		||||
            with open(f.name, 'r') as read_file:
 | 
			
		||||
                content = read_file.read()
 | 
			
		||||
                assert content == "127.0.0.1\tlocalhost\n"
 | 
			
		||||
                expected = """# #
 | 
			
		||||
# Host Database
 | 
			
		||||
#
 | 
			
		||||
# Managed by hosts - https://git.s1q.dev/phg/hosts
 | 
			
		||||
# #
 | 
			
		||||
127.0.0.1\tlocalhost
 | 
			
		||||
"""
 | 
			
		||||
                assert content == expected
 | 
			
		||||
        
 | 
			
		||||
        os.unlink(f.name)
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -259,7 +278,14 @@ class TestHostsParser:
 | 
			
		|||
            # Check new content
 | 
			
		||||
            with open(f.name, 'r') as new_file:
 | 
			
		||||
                new_content = new_file.read()
 | 
			
		||||
                assert new_content == "127.0.0.1\tlocalhost\n"
 | 
			
		||||
                expected = """# #
 | 
			
		||||
# Host Database
 | 
			
		||||
#
 | 
			
		||||
# Managed by hosts - https://git.s1q.dev/phg/hosts
 | 
			
		||||
# #
 | 
			
		||||
127.0.0.1\tlocalhost
 | 
			
		||||
"""
 | 
			
		||||
                assert new_content == expected
 | 
			
		||||
            
 | 
			
		||||
            # Cleanup
 | 
			
		||||
            os.unlink(backup_path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue