mirror of
https://codeberg.org/Freedium-cfd/web.git
synced 2026-03-11 09:04:37 +00:00
feat(mutable_string): add input validation for insert and delete methods; introduce unit tests for MutableString class
This commit is contained in:
parent
f0658ca167
commit
189c4d851b
4 changed files with 236 additions and 86 deletions
|
|
@ -32,6 +32,8 @@ class MutableString:
|
|||
return self.string.encode(encoding, "surrogatepass")
|
||||
|
||||
def insert(self, key: int, value: str) -> "MutableString":
|
||||
if key < 0 or key > len(self._string_list):
|
||||
raise IndexError("string index out of range")
|
||||
self._string_list.insert(key, value)
|
||||
self._mark_dirty()
|
||||
return self
|
||||
|
|
@ -50,4 +52,7 @@ class MutableString:
|
|||
return self.string
|
||||
|
||||
def delete(self, start: int, length: int) -> None:
|
||||
if start < 0 or start >= len(self._string_list):
|
||||
raise IndexError("string index out of range")
|
||||
del self._string_list[start : start + length]
|
||||
self._mark_dirty()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import pytest
|
||||
|
||||
from freedium_library.utils.utils.mutable_string import MutableString
|
||||
|
||||
|
||||
def test_init():
|
||||
# Test basic string initialization
|
||||
ms = MutableString("hello")
|
||||
assert str(ms) == "hello"
|
||||
|
||||
# Test initialization from another MutableString
|
||||
ms2 = MutableString(ms)
|
||||
assert str(ms2) == "hello"
|
||||
|
||||
# Test empty string
|
||||
ms3 = MutableString("")
|
||||
assert str(ms3) == ""
|
||||
|
||||
|
||||
def test_string_property():
|
||||
ms = MutableString("test")
|
||||
ms._string_list[0] = "b"
|
||||
ms._mark_dirty()
|
||||
assert ms.string == "best"
|
||||
|
||||
|
||||
def test_len():
|
||||
ms = MutableString("hello")
|
||||
assert len(ms) == 5
|
||||
assert len(MutableString("")) == 0
|
||||
|
||||
|
||||
def test_pop():
|
||||
ms = MutableString("hello")
|
||||
ms.pop(0)
|
||||
assert str(ms) == "ello"
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
ms.pop(10)
|
||||
|
||||
|
||||
def test_encode():
|
||||
ms = MutableString("hello")
|
||||
assert ms.encode("utf-8") == b"hello"
|
||||
|
||||
# Test unicode
|
||||
ms = MutableString("hello 🌍")
|
||||
assert ms.encode("utf-8").decode("utf-8") == "hello 🌍"
|
||||
|
||||
|
||||
def test_insert():
|
||||
ms = MutableString("hello")
|
||||
ms.insert(0, "x")
|
||||
assert str(ms) == "xhello"
|
||||
|
||||
ms.insert(len(ms), "y")
|
||||
assert str(ms) == "xhelloy"
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
ms.insert(100, "z")
|
||||
|
||||
|
||||
def test_setitem():
|
||||
ms = MutableString("hello")
|
||||
ms[0] = "j"
|
||||
assert str(ms) == "jello"
|
||||
|
||||
# Test slice
|
||||
ms[1:4] = "a"
|
||||
assert str(ms) == "jao"
|
||||
|
||||
|
||||
def test_getitem():
|
||||
ms = MutableString("hello")
|
||||
assert ms[0] == "h"
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
_ = ms[10]
|
||||
|
||||
|
||||
def test_delete():
|
||||
ms = MutableString("hello")
|
||||
ms.delete(1, 2)
|
||||
assert str(ms) == "hlo"
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
ms.delete(10, 1)
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from functools import cached_property
|
||||
from typing import List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
|
@ -16,19 +15,25 @@ class UTFEncoding(Enum):
|
|||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return {
|
||||
logger.trace(f"Getting encoding name for {self}")
|
||||
name = {
|
||||
UTFEncoding.UTF8: "utf-8",
|
||||
UTFEncoding.UTF16: "utf-16-le",
|
||||
UTFEncoding.UTF32: "utf-32-le",
|
||||
}[self]
|
||||
logger.trace(f"Encoding name resolved to {name}")
|
||||
return name
|
||||
|
||||
@property
|
||||
def unit_size(self) -> int:
|
||||
return {
|
||||
logger.trace(f"Getting unit size for encoding {self}")
|
||||
size = {
|
||||
UTFEncoding.UTF8: 1,
|
||||
UTFEncoding.UTF16: 2,
|
||||
UTFEncoding.UTF32: 4,
|
||||
}[self]
|
||||
logger.trace(f"Unit size resolved to {size}")
|
||||
return size
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -41,16 +46,28 @@ class CharacterMapping:
|
|||
original_char_length: int
|
||||
char_length: int
|
||||
|
||||
def __post_init__(self):
|
||||
logger.trace(f"Validating CharacterMapping for char '{self.char}'")
|
||||
if self.original_pos < 0 or self.current_pos < 0:
|
||||
logger.error(
|
||||
f"Invalid negative position: original_pos={self.original_pos}, current_pos={self.current_pos}"
|
||||
)
|
||||
raise ValueError("Positions cannot be negative")
|
||||
if self.char_length < 1:
|
||||
logger.error(f"Invalid character length: {self.char_length}")
|
||||
raise ValueError("Character length must be positive")
|
||||
logger.trace("CharacterMapping validation successful")
|
||||
|
||||
def shift_positions(self, offset: int, encoded_offset: int) -> None:
|
||||
logger.trace(
|
||||
f"Shifting positions: offset={offset}, encoded_offset={encoded_offset}"
|
||||
f"Shifting positions for char '{self.char}': offset={offset}, encoded_offset={encoded_offset}"
|
||||
)
|
||||
logger.trace(
|
||||
logger.debug(
|
||||
f"Before shift: current_pos={self.current_pos}, encoded_pos={self.encoded_pos}"
|
||||
)
|
||||
self.current_pos += offset
|
||||
self.encoded_pos += encoded_offset
|
||||
logger.trace(
|
||||
logger.debug(
|
||||
f"After shift: current_pos={self.current_pos}, encoded_pos={self.encoded_pos}"
|
||||
)
|
||||
|
||||
|
|
@ -68,8 +85,10 @@ class PositionTracker:
|
|||
|
||||
def __init__(self):
|
||||
"""Initialize empty position tracker."""
|
||||
logger.debug("Initializing new PositionTracker")
|
||||
self._position_mappings = []
|
||||
self._original_positions = []
|
||||
logger.trace("PositionTracker initialized with empty mappings")
|
||||
|
||||
def add(self, mapping: CharacterMapping, insert_idx: Optional[int] = None) -> None:
|
||||
"""Add a new character mapping.
|
||||
|
|
@ -78,10 +97,16 @@ class PositionTracker:
|
|||
mapping: CharacterMapping object to add
|
||||
insert_idx: Optional index to insert at specific position
|
||||
"""
|
||||
logger.debug(
|
||||
f"Adding new mapping for char '{mapping.char}' at position {mapping.current_pos}"
|
||||
)
|
||||
if insert_idx is None:
|
||||
logger.trace("Appending mapping to end of list")
|
||||
self._position_mappings.append(mapping)
|
||||
else:
|
||||
logger.trace(f"Inserting mapping at index {insert_idx}")
|
||||
self._position_mappings.insert(insert_idx, mapping)
|
||||
logger.debug(f"Current mapping count: {len(self._position_mappings)}")
|
||||
|
||||
def get(self) -> List[CharacterMapping]:
|
||||
"""Get copy of all character mappings.
|
||||
|
|
@ -89,6 +114,8 @@ class PositionTracker:
|
|||
Returns:
|
||||
List of CharacterMapping objects
|
||||
"""
|
||||
logger.trace("Getting copy of all character mappings")
|
||||
logger.debug(f"Returning {len(self._position_mappings)} mappings")
|
||||
return self._position_mappings.copy()
|
||||
|
||||
def clear(self, start: int, length: int) -> int:
|
||||
|
|
@ -101,13 +128,20 @@ class PositionTracker:
|
|||
Returns:
|
||||
Number of mappings that were cleared
|
||||
"""
|
||||
logger.debug(f"Clearing mappings from position {start} to {start + length}")
|
||||
original_count = len(self._position_mappings)
|
||||
logger.trace(f"Original mapping count: {original_count}")
|
||||
|
||||
self._position_mappings = [
|
||||
mapping
|
||||
for mapping in self._position_mappings
|
||||
if not (start <= mapping.original_pos < start + length)
|
||||
]
|
||||
return original_count - len(self._position_mappings)
|
||||
|
||||
cleared_count = original_count - len(self._position_mappings)
|
||||
logger.debug(f"Cleared {cleared_count} mappings")
|
||||
logger.trace(f"Remaining mappings: {len(self._position_mappings)}")
|
||||
return cleared_count
|
||||
|
||||
def update(self, start: int, length: int, encoded_length: int) -> None:
|
||||
"""Update positions of mappings after a modification.
|
||||
|
|
@ -117,87 +151,79 @@ class PositionTracker:
|
|||
length: Length of original content modified
|
||||
encoded_length: Length of new encoded content
|
||||
"""
|
||||
logger.debug(
|
||||
f"Updating mappings: start={start}, length={length}, encoded_length={encoded_length}"
|
||||
)
|
||||
for mapping in self._position_mappings:
|
||||
if mapping.original_pos >= start + length:
|
||||
logger.trace(f"Updating mapping for char '{mapping.char}'")
|
||||
logger.trace(
|
||||
f"Before update: original_pos={mapping.original_pos}, encoded_pos={mapping.encoded_pos}"
|
||||
)
|
||||
mapping.original_pos -= length
|
||||
mapping.encoded_pos -= encoded_length
|
||||
logger.trace(
|
||||
f"After update: original_pos={mapping.original_pos}, encoded_pos={mapping.encoded_pos}"
|
||||
)
|
||||
|
||||
|
||||
class UTFHandler(ABC):
|
||||
__slots__ = ("_string", "_encoding", "_position_tracker")
|
||||
|
||||
def __init__(self, string: str, encoding: UTFEncoding):
|
||||
logger.debug(
|
||||
f"Initializing UTFHandler with string: '{string}' and encoding: {encoding}"
|
||||
)
|
||||
logger.trace(f"String length: {len(string)}")
|
||||
logger.trace(f"Encoding: {encoding}")
|
||||
|
||||
logger.info(f"Initializing UTFHandler with encoding {encoding}")
|
||||
logger.debug(f"Input string length: {len(string)}")
|
||||
self._string = MutableString(string)
|
||||
self._encoding = encoding
|
||||
self._position_tracker = PositionTracker()
|
||||
|
||||
logger.trace("Calling _initialize_mappings()")
|
||||
logger.debug("Initializing character mappings")
|
||||
self._initialize_mappings()
|
||||
|
||||
@cached_property
|
||||
def _string_len(self) -> int:
|
||||
length = len(self._string)
|
||||
logger.trace(f"Calculating string length: {length}")
|
||||
return length
|
||||
|
||||
@cached_property
|
||||
def _encoded_len(self) -> int:
|
||||
logger.trace("Calculating encoded length")
|
||||
encoded_bytes = len(self._string.encode(self._encoding.name))
|
||||
logger.trace(f"Encoded bytes: {encoded_bytes}")
|
||||
encoded_len = encoded_bytes // self._encoding.unit_size
|
||||
logger.trace(f"Encoded length: {encoded_len}")
|
||||
return encoded_len
|
||||
logger.info("UTFHandler initialization complete")
|
||||
|
||||
def _initialize_mappings(self) -> None:
|
||||
logger.debug("Starting character mappings initialization")
|
||||
logger.debug("Beginning mapping initialization")
|
||||
current_pos = 0
|
||||
encoded_pos = 0
|
||||
|
||||
for i in range(len(self._string)):
|
||||
char = self._string[i]
|
||||
char_len = len(char.encode(self._encoding.name)) // self._encoding.unit_size
|
||||
for i, char in enumerate(self._string):
|
||||
logger.trace(f"Processing character '{char}' at position {i}")
|
||||
encoded_bytes = char.encode(self._encoding.name)
|
||||
char_len = len(encoded_bytes) // self._encoding.unit_size
|
||||
logger.trace(f"Encoded length: {char_len} units")
|
||||
|
||||
if char_len > 1:
|
||||
logger.debug(f"Found multi-byte character '{char}' at position {i}")
|
||||
mapping = CharacterMapping(
|
||||
char=char,
|
||||
original_pos=i,
|
||||
current_pos=current_pos,
|
||||
encoded_pos=encoded_pos,
|
||||
char_length=char_len,
|
||||
original_char_length=len(char),
|
||||
original_encoded_pos=encoded_pos,
|
||||
char=char,
|
||||
)
|
||||
logger.trace(f"Created mapping: {mapping}")
|
||||
self._position_tracker.add(mapping)
|
||||
|
||||
current_pos += 1
|
||||
encoded_pos += char_len
|
||||
logger.trace(
|
||||
f"Updated positions: current={current_pos}, encoded={encoded_pos}"
|
||||
)
|
||||
|
||||
logger.debug("Mapping initialization complete")
|
||||
|
||||
def get_encoded_position(self, original_pos: int) -> int:
|
||||
logger.debug(f"Converting original position {original_pos} to encoded position")
|
||||
encoded_pos = original_pos
|
||||
|
||||
for mapping in self._position_tracker.get():
|
||||
if mapping.original_pos < original_pos:
|
||||
encoded_pos += mapping.char_length - 1
|
||||
logger.trace(
|
||||
f"Adjusting for mapping at {mapping.original_pos}, new encoded_pos: {encoded_pos}"
|
||||
)
|
||||
elif mapping.original_pos == original_pos:
|
||||
encoded_pos = mapping.encoded_pos
|
||||
logger.debug(f"Direct mapping found, encoded_pos set to {encoded_pos}")
|
||||
break
|
||||
return mapping.encoded_pos
|
||||
else:
|
||||
break
|
||||
|
||||
logger.debug(f"Final encoded position: {encoded_pos}")
|
||||
return encoded_pos
|
||||
|
||||
def get_original_position(self, encoded_pos: int) -> int:
|
||||
|
|
@ -205,9 +231,14 @@ class UTFHandler(ABC):
|
|||
original_pos = encoded_pos
|
||||
|
||||
for mapping in self._position_tracker.get():
|
||||
logger.trace(
|
||||
f"Checking mapping for char '{mapping.char}' at encoded position {mapping.encoded_pos}"
|
||||
)
|
||||
if mapping.encoded_pos < encoded_pos:
|
||||
logger.trace(
|
||||
f"Adjusting for multi-byte character: -{mapping.char_length + 1}"
|
||||
)
|
||||
original_pos -= mapping.char_length + 1
|
||||
logger.trace(f"Adjusting original position: {original_pos}")
|
||||
else:
|
||||
break
|
||||
|
||||
|
|
@ -225,33 +256,35 @@ class UTFHandler(ABC):
|
|||
)
|
||||
logger.debug(f"Encoded insertion length: {insert_encoded_len}")
|
||||
|
||||
# Find the original position that corresponds to the encoded position
|
||||
original_position = self.get_original_position(encoded_position)
|
||||
logger.debug(f"Corresponding original position: {original_position}")
|
||||
|
||||
# Shift existing mappings that are after the insertion point
|
||||
logger.debug("Updating existing mappings")
|
||||
for mapping in self._position_tracker.get():
|
||||
if mapping.encoded_pos >= encoded_position:
|
||||
logger.trace(f"Shifting mapping for char '{mapping.char}'")
|
||||
mapping.shift_positions(len(string_to_insert), insert_encoded_len)
|
||||
|
||||
# Add new mappings for the inserted string
|
||||
logger.debug("Adding mappings for inserted string")
|
||||
self._add_new_mappings(string_to_insert, original_position, encoded_position)
|
||||
|
||||
# Insert the string at the correct position
|
||||
logger.debug(f"Inserting string at position {original_position}")
|
||||
self._string.insert(original_position, string_to_insert)
|
||||
logger.debug(f"Insert complete. New string: '{self._string}'")
|
||||
logger.info(f"Insert complete. New string: '{self._string}'")
|
||||
|
||||
def _add_new_mappings(
|
||||
self, string_to_insert: str, original_position: int, encoded_position: int
|
||||
) -> None:
|
||||
logger.debug("Adding new mappings for inserted string")
|
||||
logger.debug(f"Adding new mappings for string '{string_to_insert}'")
|
||||
current_pos = original_position
|
||||
encoded_pos = encoded_position
|
||||
|
||||
for char in string_to_insert:
|
||||
logger.trace(f"Processing character '{char}'")
|
||||
encoded_char_len = (
|
||||
len(char.encode(self._encoding.name)) // self._encoding.unit_size
|
||||
)
|
||||
logger.trace(f"Processing char '{char}' with length {encoded_char_len}")
|
||||
logger.trace(f"Encoded character length: {encoded_char_len}")
|
||||
|
||||
if encoded_char_len > 1:
|
||||
logger.debug(f"Found multi-byte character '{char}'")
|
||||
|
|
@ -265,7 +298,6 @@ class UTFHandler(ABC):
|
|||
char=char,
|
||||
)
|
||||
|
||||
# Find the correct position to insert the new mapping
|
||||
insert_idx = next(
|
||||
(
|
||||
i
|
||||
|
|
@ -275,59 +307,85 @@ class UTFHandler(ABC):
|
|||
len(self._position_tracker.get()),
|
||||
)
|
||||
|
||||
logger.trace(f"Inserting new mapping at index {insert_idx}")
|
||||
logger.debug(f"Inserting new mapping at index {insert_idx}")
|
||||
self._position_tracker.add(new_mapping, insert_idx)
|
||||
|
||||
current_pos += 1
|
||||
encoded_pos += encoded_char_len
|
||||
logger.trace(
|
||||
f"Updated positions: current={current_pos}, encoded={encoded_pos}"
|
||||
)
|
||||
|
||||
logger.debug(f"New position mappings: {self._position_tracker.get()}")
|
||||
logger.debug("Finished adding new mappings")
|
||||
|
||||
def delete(self, start: int, length: int) -> None:
|
||||
logger.info(f"Deleting {length} characters starting at position {start}")
|
||||
logger.debug(f"Entering delete method with start={start}, length={length}")
|
||||
logger.info(
|
||||
f"Initiating deletion of {length} characters starting at position {start}"
|
||||
)
|
||||
|
||||
logger.trace("Calculating encoded positions for deletion")
|
||||
start_encoded = self.get_encoded_position(start)
|
||||
end_encoded = self.get_encoded_position(start + length)
|
||||
logger.debug(f"Calculated start encoded position: {start_encoded}")
|
||||
|
||||
end_pos = start + length
|
||||
logger.trace(f"Calculated end position: {end_pos}")
|
||||
|
||||
end_encoded = self.get_encoded_position(end_pos)
|
||||
logger.debug(f"Calculated end encoded position: {end_encoded}")
|
||||
|
||||
encoded_length = end_encoded - start_encoded
|
||||
logger.debug(
|
||||
f"Encoded deletion range: {start_encoded} to {end_encoded} (length: {encoded_length})"
|
||||
)
|
||||
|
||||
original_mapping_count = len(self._position_tracker.get())
|
||||
self._position_tracker.clear(start, length)
|
||||
logger.debug(
|
||||
f"Removed {original_mapping_count - len(self._position_tracker.get())} mappings"
|
||||
)
|
||||
logger.info(f"Calculated encoded length to delete: {encoded_length}")
|
||||
|
||||
logger.trace("Updating remaining position mappings")
|
||||
for mapping in self._position_tracker.get():
|
||||
if mapping.original_pos >= start + length:
|
||||
old_pos = mapping.original_pos
|
||||
old_encoded = mapping.encoded_pos
|
||||
mapping.original_pos -= length
|
||||
mapping.encoded_pos -= encoded_length
|
||||
logger.trace(
|
||||
f"Updated mapping: {old_pos}->{mapping.original_pos}, {old_encoded}->{mapping.encoded_pos}"
|
||||
if mapping.original_pos >= end_pos:
|
||||
logger.debug(
|
||||
f"Adjusting mapping for character at position {mapping.original_pos}"
|
||||
)
|
||||
mapping.original_pos -= length
|
||||
mapping.current_pos -= length
|
||||
mapping.encoded_pos -= encoded_length
|
||||
mapping.original_encoded_pos -= encoded_length
|
||||
logger.trace(f"Updated mapping: {mapping}")
|
||||
|
||||
logger.debug("Performing deletion on underlying string")
|
||||
self._string.delete(start, length)
|
||||
logger.debug(f"Delete complete. New string: '{self._string}'")
|
||||
logger.info(
|
||||
f"Delete operation complete. New string length: {len(self._string)}"
|
||||
)
|
||||
logger.trace(f"Updated string contents: '{self._string}'")
|
||||
|
||||
def get_string_slice(self, start: int, end: int) -> List[str]:
|
||||
logger.trace(f"Getting string slice from {start} to {end}")
|
||||
return list(self._string[start:end])
|
||||
logger.debug(f"Getting string slice from {start} to {end}")
|
||||
result = list(self._string[start:end])
|
||||
logger.trace(f"Slice result: {result}")
|
||||
return result
|
||||
|
||||
def __getitem__(self, key: int) -> str:
|
||||
logger.trace(f"Getting character at index {key}")
|
||||
return "".join(self._string[key])
|
||||
|
||||
@property
|
||||
def encoded_length(self) -> int:
|
||||
logger.trace("Retrieving encoded length")
|
||||
return self._encoded_len
|
||||
logger.debug(f"Getting character at index {key}")
|
||||
result = "".join(self._string[key])
|
||||
logger.trace(f"Retrieved character: '{result}'")
|
||||
return result
|
||||
|
||||
def __str__(self) -> str:
|
||||
logger.trace("Converting to string representation")
|
||||
return str(self._string)
|
||||
logger.debug("Converting to string representation")
|
||||
result = str(self._string)
|
||||
logger.trace(f"String representation: '{result}'")
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
logger.trace("Getting detailed string representation")
|
||||
return f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})"
|
||||
logger.debug("Getting detailed string representation")
|
||||
result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})"
|
||||
logger.trace(f"Detailed representation: {result}")
|
||||
return result
|
||||
logger.debug("Converting to string representation")
|
||||
result = str(self._string)
|
||||
logger.trace(f"String representation: '{result}'")
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
logger.debug("Getting detailed string representation")
|
||||
result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})"
|
||||
logger.trace(f"Detailed representation: {result}")
|
||||
return result
|
||||
|
|
|
|||
Loading…
Reference in a new issue