From 0ee720c5ef945cb2127d6f8d91e15ed10a8f9f1c Mon Sep 17 00:00:00 2001 From: phg Date: Wed, 30 Jul 2025 00:00:53 +0200 Subject: [PATCH] Add management header to hosts files and enhance serialization formatting; update tests to reflect changes. --- memory-bank/progress.md | 2 + src/hosts/core/parser.py | 133 ++++++++++++++++++++++++++++++++++++++- tests/test_parser.py | 34 ++++++++-- 3 files changed, 163 insertions(+), 6 deletions(-) diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 0e99b81..09a41af 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -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 diff --git a/src/hosts/core/parser.py b/src/hosts/core/parser.py index 169a058..5407a84 100644 --- a/src/hosts/core/parser.py +++ b/src/hosts/core/parser.py @@ -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. diff --git a/tests/test_parser.py b/tests/test_parser.py index 0b61425..24acea7 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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)