feat(mutable_string): add input validation for insert and delete methods; introduce unit tests for MutableString class

This commit is contained in:
ZhymabekRoman 2025-01-02 21:09:43 +05:00
parent f0658ca167
commit 189c4d851b
4 changed files with 236 additions and 86 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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