Move all string support functions to corelibs_strings and other modules

This commit is contained in:
Clemens Schwaighofer
2026-02-06 08:49:44 +09:00
parent 8bb4a202cd
commit b58a26f79a
9 changed files with 50 additions and 1632 deletions

View File

@@ -9,6 +9,7 @@ dependencies = [
"corelibs-csv>=1.0.0",
"corelibs-datetime>=1.0.1",
"corelibs-debug>=1.0.0",
"corelibs-double-byte-format>=1.0.0",
"corelibs-dump-data>=1.0.0",
"corelibs-encryption>=1.0.0",
"corelibs-enum-base>=1.0.0",
@@ -20,6 +21,7 @@ dependencies = [
"corelibs-requests>=1.0.0",
"corelibs-search>=1.0.0",
"corelibs-stack-trace>=1.0.0",
"corelibs-strings>=1.0.0",
"corelibs-text-colors>=1.0.0",
"corelibs-var>=1.0.0",
"cryptography>=46.0.3",

View File

@@ -2,7 +2,11 @@
Format bytes
"""
from warnings import deprecated
from corelibs_strings.string_format import format_bytes as corelibs_format_bytes
@deprecated("Use corelibs_strings.string_format.format_bytes instead")
def format_bytes(byte_value: float | int | str) -> str:
"""
Format a byte value to a human readable string
@@ -14,24 +18,9 @@ def format_bytes(byte_value: float | int | str) -> str:
str -- _description_
"""
# if string exit
if isinstance(byte_value, str):
return byte_value
# empty byte value is set to 0
if not byte_value:
byte_value = float(0)
# if not float, convert to flaot
if isinstance(byte_value, int):
byte_value = float(byte_value)
# loop through valid extensions
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
# never go into the negativ and check if it is smaller than next set
# if it is, print out return string
if abs(byte_value) < 1024.0:
return f"{byte_value:,.2f} {unit}"
# divided for the next loop check
byte_value /= 1024.0
# if it is too big, return YB
return f"{byte_value:,.2f} YB"
return corelibs_format_bytes(
byte_value=byte_value,
)
# __NED__

View File

@@ -2,225 +2,18 @@
Format double byte strings to exact length
"""
import unicodedata
from warnings import warn
from corelibs_double_byte_format.double_byte_string_format import (
DoubleByteFormatString as CorelibsDoubleByteFormatString
)
class DoubleByteFormatString:
class DoubleByteFormatString(CorelibsDoubleByteFormatString):
"""
Format a string to exact length
"""
def __init__(
self,
string: str,
cut_length: int,
format_length: int | None = None,
placeholder: str = '..',
format_string: str = '{{:<{len}}}'
):
"""
shorts a string to exact cut length and sets it to format length
after "cut_length" cut the "placeholder" will be added, so that the new cut_length is never
larget than the cut_length given (".." is counted to cut_length)
if format_length if set and outside format_length will be set
the cut_length is adjusted to format_length if the format_length is shorter
Example
"Foo bar baz" 10 charcters -> 5 cut_length -> 10 format_length
"Foo.. "
use class.get_string_short() for cut length shortend string
use class.get_string_short_formated() to get the shorted string to format length padding
creates a class that shortens and sets the format length
to use with a print format run the format needs to be pre set in
the style of {{:<{len}}} style
self.get_string_short_formated() for the "len" parameter
Args:
string (str): string to work with
cut_length (int): width to shorten to
format_length (int | None): format length. Defaults to None
placeholder (str, optional): placeholder to put after shortened string. Defaults to '..'.
format_string (str, optional): format string. Defaults to '{{:<{len}}}'
"""
# output variables
self.string_short: str = ''
self.string_width_value: int = 0
self.string_short_width: int = 0
self.format_length_value: int = 0
# internal varaibles
self.placeholder: str = placeholder
# original string
self.string: str = ''
# width to cut string to
self.cut_length: int = 0
# format length to set to
self.format_length: int = 0
# main string
self.string = str(string)
self.format_string: str = format_string
# if width is > 0 set, else set width of string (fallback)
if cut_length > 0:
self.cut_length = cut_length
elif cut_length <= 0:
self.cut_length = self.__string_width_calc(self.string)
# format length set, if not set or smaller than 0, set to width of string
self.format_length = self.cut_length
if format_length is not None and format_length > 0:
self.format_length = format_length
# check that width is not larger then length if yes, set width to length
self.cut_length = min(self.cut_length, self.format_length)
# process the string shorten and format length calculation
self.process()
def process(self):
"""
runs all the class methods to set string length, the string shortened
and the format length
"""
# call the internal ones to set the data
if self.string:
self.__string_width()
self.__shorten_string()
if self.format_length:
self.__format_length()
def get_string_short(self) -> str:
"""
get the shortend string
Returns:
str -- _description_
"""
return self.string_short
def get_string_short_formated(self, format_string: str = '{{:<{len}}}') -> str:
"""
get the formatted string
Keyword Arguments:
format_string {_type_} -- _description_ (default: {'{{:<{len}}}'})
Returns:
str -- _description_
"""
if not format_string:
format_string = self.format_string
return format_string.format(
len=self.get_format_length()
).format(
self.get_string_short()
)
def get_format_length(self) -> int:
"""
get the format length for outside length set
Returns:
int -- _description_
"""
return self.format_length_value
def get_cut_length(self) -> int:
"""
get the actual cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_cut_length(self) -> int:
"""
get the requested cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_format_length(self) -> int:
"""
get the requested format length
Returns:
int -- _description_
"""
return self.format_length
def __string_width_calc(self, string: str) -> int:
"""
does the actual string width calculation
Args:
string (str): string to calculate from
Returns:
int: stringth width
"""
return sum(1 + (unicodedata.east_asian_width(c) in "WF") for c in string)
def __string_width(self):
"""
calculates the string width based on the characters
this is an internal method and should not be called on itself
"""
# only run if string is set and is valid string
if self.string:
# calculate width. add +1 for each double byte character
self.string_width_value = self.__string_width_calc(self.string)
def __format_length(self):
"""
set the format length based on the length for the format
and the shortend string
this is an internal method and should not be called on itself
"""
if not self.string_short:
self.__shorten_string()
# get correct format length based on string
if (
self.string_short and
self.format_length > 0 and
self.string_short_width > 0
):
# length: format length wanted
# substract the width of the shortend string - the length of the shortend string
self.format_length_value = self.format_length - (self.string_short_width - len(self.string_short))
else:
# if we have nothing to shorten the length, keep the old one
self.format_length_value = self.format_length
def __shorten_string(self):
"""
shorten string down to set width
this is an internal method and should not be called on itself
"""
# set string width if not set
if not self.string_width_value:
self.__string_width()
# if the double byte string width is larger than the wanted width
if self.string_width_value > self.cut_length:
cur_len = 0
self.string_short = ''
for char in str(self.string):
# set the current length if we add the character
cur_len += 2 if unicodedata.east_asian_width(char) in "WF" else 1
# if the new length is smaller than the output length to shorten too add the char
if cur_len <= (self.cut_length - len(self.placeholder)):
self.string_short += char
self.string_short_width = cur_len
# return string with new width and placeholder
self.string_short = f"{self.string_short}{self.placeholder}"
self.string_short_width += len(self.placeholder)
else:
# if string is same saze just copy
self.string_short = self.string
warn("Use 'corelibs_double_byte_format.double_byte_string_format' instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,10 +2,11 @@
Various hash helpers for strings and things
"""
import re
import hashlib
from warnings import deprecated
from corelibs_hash.string_hash import crc32b_fix as corelibs_crc32b_fix, sha1_short as corelibs_sha1_short
@deprecated("Use corelibs_hash.string_hash.crc32b_fix instead")
def crc32b_fix(crc: str) -> str:
"""
fix a CRC32B with wrong order (from old PHP)
@@ -16,15 +17,10 @@ def crc32b_fix(crc: str) -> str:
Returns:
str -- _description_
"""
# left pad with 0 to 8 chars
crc = ("0" * (8 - len(crc))) + crc
# flip two chars (byte hex)
crc = re.sub(
r"^([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})$", r"\4\3\2\1", crc
)
return crc
return corelibs_crc32b_fix(crc)
@deprecated("Use corelibs_hash.string_hash.sha1_short instead")
def sha1_short(string: str) -> str:
"""
Return a 9 character long SHA1 part
@@ -35,6 +31,6 @@ def sha1_short(string: str) -> str:
Returns:
str -- _description_
"""
return hashlib.sha1(string.encode('utf-8')).hexdigest()[:9]
return corelibs_sha1_short(string)
# __END__

View File

@@ -2,11 +2,16 @@
String helpers
"""
import re
from decimal import Decimal, getcontext
from textwrap import shorten
from warnings import deprecated
from corelibs_strings.string_support import (
shorten_string as corelibs_shorten_string,
left_fill as corelibs_left_fill,
prepare_url_slash as corelibs_prepare_url_slash,
)
from corelibs_strings.string_format import format_number as corelibs_format_number
@deprecated("Use corelibs_strings.string_support.shorten_string instead")
def shorten_string(
string: str | int | float, length: int, hard_shorten: bool = False, placeholder: str = " [~]"
) -> str:
@@ -23,25 +28,15 @@ def shorten_string(
Returns:
str: _description_
"""
string = str(string)
# if placeholder > lenght
if len(string) > length:
if hard_shorten is True or " " not in string:
# hard shorten error
if len(placeholder) > length:
raise ValueError(f"Cannot shorten string: placeholder {placeholder} is too large for max width")
short_string = f"{string[:(length - len(placeholder))]}{placeholder}"
else:
try:
short_string = shorten(string, width=length, placeholder=placeholder)
except ValueError as e:
raise ValueError(f"Cannot shorten string: {e}") from e
else:
short_string = string
return short_string
return corelibs_shorten_string(
string=string,
length=length,
hard_shorten=hard_shorten,
placeholder=placeholder,
)
@deprecated("Use corelibs_strings.string_support.left_fill instead")
def left_fill(string: str, width: int, char: str = " ") -> str:
"""
left fill for a certain length to fill a max size
@@ -58,19 +53,14 @@ def left_fill(string: str, width: int, char: str = " ") -> str:
Returns:
str -- _description_
"""
# the width needs to be string
if width < 0:
width = len(string)
# char can only be one length long
if len(char) != 1:
char = " "
return (
"{:"
f"{char}>{width}"
"}"
).format(string)
return corelibs_left_fill(
string=string,
width=width,
char=char,
)
@deprecated("Use corelibs_strings.string_format.format_number instead")
def format_number(number: float, precision: int = 0) -> str:
"""
format numbers, current trailing zeros does not work
@@ -88,21 +78,13 @@ def format_number(number: float, precision: int = 0) -> str:
Returns:
str -- _description_
"""
if precision < 0 or precision > 100:
precision = 0
if precision > 0:
getcontext().prec = precision
# make it a string to avoid mangling
_number = Decimal(str(number))
else:
_number = number
return (
"{:,."
f"{str(precision)}"
"f}"
).format(_number)
return corelibs_format_number(
number=number,
precision=precision,
)
@deprecated("Use corelibs_strings.string_support.prepare_url_slash instead")
def prepare_url_slash(url: str) -> str:
"""
if the URL does not start with /, add slash
@@ -114,9 +96,8 @@ def prepare_url_slash(url: str) -> str:
Returns:
str -- _description_
"""
url = re.sub(r'\/+', '/', url)
if not url.startswith("/"):
url = "/" + url
return url
return corelibs_prepare_url_slash(
url=url,
)
# __END__

View File

@@ -1,164 +0,0 @@
"""
PyTest: string_handling/byte_helpers
"""
from corelibs.string_handling.byte_helpers import format_bytes
class TestFormatBytes:
"""Tests for format_bytes function"""
def test_string_input_returned_unchanged(self):
"""Test that string inputs are returned as-is"""
result = format_bytes("already formatted")
assert result == "already formatted"
def test_empty_string_returned_unchanged(self):
"""Test that empty strings are returned as-is"""
result = format_bytes("")
assert result == ""
def test_zero_int(self):
"""Test zero integer returns 0 bytes"""
result = format_bytes(0)
assert result == "0.00 B"
def test_zero_float(self):
"""Test zero float returns 0 bytes"""
result = format_bytes(0.0)
assert result == "0.00 B"
def test_none_value(self):
"""Test None is treated as 0 bytes"""
result = format_bytes(None) # type: ignore[arg-type]
assert result == "0.00 B"
def test_bytes_less_than_1kb(self):
"""Test formatting bytes less than 1KB"""
result = format_bytes(512)
assert result == "512.00 B"
def test_kilobytes(self):
"""Test formatting kilobytes"""
result = format_bytes(1024)
assert result == "1.00 KB"
def test_kilobytes_with_decimals(self):
"""Test formatting kilobytes with decimal values"""
result = format_bytes(1536) # 1.5 KB
assert result == "1.50 KB"
def test_megabytes(self):
"""Test formatting megabytes"""
result = format_bytes(1048576) # 1 MB
assert result == "1.00 MB"
def test_megabytes_with_decimals(self):
"""Test formatting megabytes with decimal values"""
result = format_bytes(2621440) # 2.5 MB
assert result == "2.50 MB"
def test_gigabytes(self):
"""Test formatting gigabytes"""
result = format_bytes(1073741824) # 1 GB
assert result == "1.00 GB"
def test_terabytes(self):
"""Test formatting terabytes"""
result = format_bytes(1099511627776) # 1 TB
assert result == "1.00 TB"
def test_petabytes(self):
"""Test formatting petabytes"""
result = format_bytes(1125899906842624) # 1 PB
assert result == "1.00 PB"
def test_exabytes(self):
"""Test formatting exabytes"""
result = format_bytes(1152921504606846976) # 1 EB
assert result == "1.00 EB"
def test_zettabytes(self):
"""Test formatting zettabytes"""
result = format_bytes(1180591620717411303424) # 1 ZB
assert result == "1.00 ZB"
def test_yottabytes(self):
"""Test formatting yottabytes"""
result = format_bytes(1208925819614629174706176) # 1 YB
assert result == "1.00 YB"
def test_negative_bytes(self):
"""Test formatting negative byte values"""
result = format_bytes(-512)
assert result == "-512.00 B"
def test_negative_kilobytes(self):
"""Test formatting negative kilobytes"""
result = format_bytes(-1024)
assert result == "-1.00 KB"
def test_negative_megabytes(self):
"""Test formatting negative megabytes"""
result = format_bytes(-1048576)
assert result == "-1.00 MB"
def test_float_input_bytes(self):
"""Test float input for bytes"""
result = format_bytes(512.5)
assert result == "512.50 B"
def test_float_input_kilobytes(self):
"""Test float input for kilobytes"""
result = format_bytes(1536.75)
assert result == "1.50 KB"
def test_large_number_formatting(self):
"""Test that large numbers use comma separators"""
result = format_bytes(10240) # 10 KB
assert result == "10.00 KB"
def test_very_large_byte_value(self):
"""Test very large byte value (beyond ZB)"""
result = format_bytes(1208925819614629174706176)
assert result == "1.00 YB"
def test_boundary_1023_bytes(self):
"""Test boundary case just below 1KB"""
result = format_bytes(1023)
assert result == "1,023.00 B"
def test_boundary_1024_bytes(self):
"""Test boundary case at exactly 1KB"""
result = format_bytes(1024)
assert result == "1.00 KB"
def test_int_converted_to_float(self):
"""Test that integer input is properly converted to float"""
result = format_bytes(2048)
assert result == "2.00 KB"
assert "." in result # Verify decimal point is present
def test_small_decimal_value(self):
"""Test small decimal byte value"""
result = format_bytes(0.5)
assert result == "0.50 B"
def test_precision_two_decimals(self):
"""Test that result always has two decimal places"""
result = format_bytes(1024)
assert result == "1.00 KB"
assert result.count('.') == 1
decimal_part = result.split('.')[1].split()[0]
assert len(decimal_part) == 2
def test_mixed_units_progression(self):
"""Test progression through multiple unit levels"""
# Start with bytes
assert "B" in format_bytes(100)
# Move to KB
assert "KB" in format_bytes(100 * 1024)
# Move to MB
assert "MB" in format_bytes(100 * 1024 * 1024)
# Move to GB
assert "GB" in format_bytes(100 * 1024 * 1024 * 1024)

View File

@@ -1,524 +0,0 @@
"""
PyTest: string_handling/double_byte_string_format
"""
import pytest
from corelibs.string_handling.double_byte_string_format import DoubleByteFormatString
class TestDoubleByteFormatStringInit:
"""Tests for DoubleByteFormatString initialization"""
def test_basic_initialization(self):
"""Test basic initialization with string and cut_length"""
formatter = DoubleByteFormatString("Hello World", 10)
assert formatter.string == "Hello World"
assert formatter.cut_length == 10
assert formatter.format_length == 10
assert formatter.placeholder == ".."
def test_initialization_with_format_length(self):
"""Test initialization with both cut_length and format_length"""
formatter = DoubleByteFormatString("Hello World", 5, 15)
assert formatter.cut_length == 5
assert formatter.format_length == 15
def test_initialization_with_custom_placeholder(self):
"""Test initialization with custom placeholder"""
formatter = DoubleByteFormatString("Hello World", 10, placeholder="...")
assert formatter.placeholder == "..."
def test_initialization_with_custom_format_string(self):
"""Test initialization with custom format string"""
formatter = DoubleByteFormatString("Hello", 10, format_string="{{:>{len}}}")
assert formatter.format_string == "{{:>{len}}}"
def test_zero_cut_length_uses_string_width(self):
"""Test that zero cut_length defaults to string width"""
formatter = DoubleByteFormatString("Hello", 0)
assert formatter.cut_length > 0
# For ASCII string, width should equal length
assert formatter.cut_length == 5
def test_negative_cut_length_uses_string_width(self):
"""Test that negative cut_length defaults to string width"""
formatter = DoubleByteFormatString("Hello", -5)
assert formatter.cut_length > 0
def test_cut_length_adjusted_to_format_length(self):
"""Test that cut_length is adjusted when larger than format_length"""
formatter = DoubleByteFormatString("Hello World", 20, 10)
assert formatter.cut_length == 10 # Should be min(20, 10)
def test_none_format_length(self):
"""Test with None format_length"""
formatter = DoubleByteFormatString("Hello", 10, None)
assert formatter.format_length == 10 # Should default to cut_length
class TestDoubleByteFormatStringWithAscii:
"""Tests for ASCII (single-byte) string handling"""
def test_ascii_no_shortening_needed(self):
"""Test ASCII string shorter than cut_length"""
formatter = DoubleByteFormatString("Hello", 10)
assert formatter.get_string_short() == "Hello"
assert formatter.string_short_width == 0 # Not set because no shortening
def test_ascii_exact_cut_length(self):
"""Test ASCII string equal to cut_length"""
formatter = DoubleByteFormatString("Hello", 5)
assert formatter.get_string_short() == "Hello"
def test_ascii_shortening_required(self):
"""Test ASCII string requiring shortening"""
formatter = DoubleByteFormatString("Hello World", 8)
result = formatter.get_string_short()
assert result == "Hello .."
assert len(result) == 8
def test_ascii_with_custom_placeholder(self):
"""Test ASCII shortening with custom placeholder"""
formatter = DoubleByteFormatString("Hello World", 8, placeholder="...")
result = formatter.get_string_short()
assert result.endswith("...")
assert len(result) == 8
def test_ascii_very_short_cut_length(self):
"""Test ASCII with very short cut_length"""
formatter = DoubleByteFormatString("Hello World", 3)
result = formatter.get_string_short()
assert result == "H.."
assert len(result) == 3
def test_ascii_format_length_calculation(self):
"""Test format_length calculation for ASCII strings"""
formatter = DoubleByteFormatString("Hello", 10, 15)
# String is not shortened, format_length should be 15
assert formatter.get_format_length() == 15
class TestDoubleByteFormatStringWithDoubleByte:
"""Tests for double-byte (Asian) character handling"""
def test_japanese_characters(self):
"""Test Japanese string handling"""
formatter = DoubleByteFormatString("こんにちは", 10)
# Each Japanese character is double-width
# "こんにちは" = 5 chars * 2 width = 10 width
assert formatter.get_string_short() == "こんにちは"
def test_japanese_shortening(self):
"""Test Japanese string requiring shortening"""
formatter = DoubleByteFormatString("こんにちは世界", 8)
# Should fit 3 double-width chars (6 width) + placeholder (2 chars)
result = formatter.get_string_short()
assert result.endswith("..")
assert len(result) <= 5 # 3 Japanese chars + 2 placeholder chars
def test_chinese_characters(self):
"""Test Chinese string handling"""
formatter = DoubleByteFormatString("你好世界", 8)
# 4 Chinese chars = 8 width, should fit exactly
assert formatter.get_string_short() == "你好世界"
def test_chinese_shortening(self):
"""Test Chinese string requiring shortening"""
formatter = DoubleByteFormatString("你好世界朋友", 8)
# Should fit 3 double-width chars (6 width) + placeholder (2 chars)
result = formatter.get_string_short()
assert result.endswith("..")
assert len(result) <= 5
def test_korean_characters(self):
"""Test Korean string handling"""
formatter = DoubleByteFormatString("안녕하세요", 10)
# Korean characters are also double-width
assert formatter.get_string_short() == "안녕하세요"
def test_mixed_ascii_japanese(self):
"""Test mixed ASCII and Japanese characters"""
formatter = DoubleByteFormatString("Hello世界", 10)
# "Hello" = 5 width, "世界" = 4 width, total = 9 width
assert formatter.get_string_short() == "Hello世界"
def test_mixed_ascii_japanese_shortening(self):
"""Test mixed string requiring shortening"""
formatter = DoubleByteFormatString("Hello世界Test", 10)
# Should shorten to fit within 10 width
result = formatter.get_string_short()
assert result.endswith("..")
# Total visual width should be <= 10
def test_fullwidth_ascii(self):
"""Test fullwidth ASCII characters"""
# Fullwidth ASCII characters (U+FF01 to U+FF5E)
formatter = DoubleByteFormatString("world", 10)
result = formatter.get_string_short()
assert result.endswith("..")
class TestDoubleByteFormatStringGetters:
"""Tests for getter methods"""
def test_get_string_short(self):
"""Test get_string_short method"""
formatter = DoubleByteFormatString("Hello World", 8)
result = formatter.get_string_short()
assert isinstance(result, str)
assert result == "Hello .."
def test_get_format_length(self):
"""Test get_format_length method"""
formatter = DoubleByteFormatString("Hello", 5, 10)
assert formatter.get_format_length() == 10
def test_get_cut_length(self):
"""Test get_cut_length method"""
formatter = DoubleByteFormatString("Hello", 8)
assert formatter.get_cut_length() == 8
def test_get_requested_cut_length(self):
"""Test get_requested_cut_length method"""
formatter = DoubleByteFormatString("Hello", 15)
assert formatter.get_requested_cut_length() == 15
def test_get_requested_format_length(self):
"""Test get_requested_format_length method"""
formatter = DoubleByteFormatString("Hello", 5, 20)
assert formatter.get_requested_format_length() == 20
def test_get_string_short_formated_default(self):
"""Test get_string_short_formated with default format"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated()
assert isinstance(result, str)
assert len(result) == 10 # Should be padded to format_length
assert result.startswith("Hello")
def test_get_string_short_formated_custom(self):
"""Test get_string_short_formated with custom format string"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated("{{:>{len}}}")
assert isinstance(result, str)
assert result.endswith("Hello") # Right-aligned
def test_get_string_short_formated_empty_format_string(self):
"""Test get_string_short_formated with empty format string falls back to default"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated("")
# Should use default format_string from initialization
assert isinstance(result, str)
class TestDoubleByteFormatStringFormatting:
"""Tests for formatted output"""
def test_format_with_padding(self):
"""Test formatted string with padding"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated()
assert len(result) == 10
assert result == "Hello " # Left-aligned with spaces
def test_format_shortened_string(self):
"""Test formatted shortened string"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
result = formatter.get_string_short_formated()
# Should be "Hello .." padded to 12
assert len(result) == 12
assert result.startswith("Hello ..")
def test_format_with_double_byte_chars(self):
"""Test formatting with double-byte characters"""
formatter = DoubleByteFormatString("日本語", 6, 10)
result = formatter.get_string_short_formated()
# "日本語" = 3 chars * 2 width = 6 width
# Format should account for visual width difference
assert isinstance(result, str)
def test_format_shortened_double_byte(self):
"""Test formatting shortened double-byte string"""
formatter = DoubleByteFormatString("こんにちは世界", 8, 12)
result = formatter.get_string_short_formated()
assert isinstance(result, str)
# Should be shortened and formatted
class TestDoubleByteFormatStringProcess:
"""Tests for process method"""
def test_process_called_on_init(self):
"""Test that process is called during initialization"""
formatter = DoubleByteFormatString("Hello World", 8)
# process() should have been called, so string_short should be set
assert formatter.string_short != ''
def test_manual_process_call(self):
"""Test calling process manually"""
formatter = DoubleByteFormatString("Hello World", 8)
# Modify internal state
formatter.string = "New String"
# Call process again
formatter.process()
# Should recalculate based on new string
assert formatter.string_short != ''
def test_process_with_empty_string(self):
"""Test process with empty string"""
formatter = DoubleByteFormatString("", 10)
formatter.process()
# Should handle empty string gracefully
assert formatter.string_short == ''
class TestDoubleByteFormatStringEdgeCases:
"""Tests for edge cases"""
def test_empty_string(self):
"""Test with empty string"""
formatter = DoubleByteFormatString("", 10)
assert formatter.get_string_short() == ""
def test_single_character(self):
"""Test with single character"""
formatter = DoubleByteFormatString("A", 5)
assert formatter.get_string_short() == "A"
def test_single_double_byte_character(self):
"""Test with single double-byte character"""
formatter = DoubleByteFormatString("", 5)
assert formatter.get_string_short() == ""
def test_placeholder_only_length(self):
"""Test when cut_length equals placeholder length"""
formatter = DoubleByteFormatString("Hello World", 2)
result = formatter.get_string_short()
assert result == ".."
def test_very_long_string(self):
"""Test with very long string"""
long_string = "A" * 1000
formatter = DoubleByteFormatString(long_string, 10)
result = formatter.get_string_short()
assert len(result) == 10
assert result.endswith("..")
def test_very_long_double_byte_string(self):
"""Test with very long double-byte string"""
long_string = "" * 500
formatter = DoubleByteFormatString(long_string, 10)
result = formatter.get_string_short()
# Should be shortened to fit 10 visual width
assert result.endswith("..")
def test_special_characters(self):
"""Test with special characters"""
formatter = DoubleByteFormatString("Hello!@#$%^&*()", 10)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_newlines_and_tabs(self):
"""Test with newlines and tabs"""
formatter = DoubleByteFormatString("Hello\nWorld\t!", 10)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_unicode_emoji(self):
"""Test with Unicode emoji"""
formatter = DoubleByteFormatString("Hello 👋 World 🌍", 15)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_non_string_input_conversion(self):
"""Test that non-string inputs are converted to string"""
formatter = DoubleByteFormatString(12345, 10) # type: ignore[arg-type]
assert formatter.string == "12345"
assert formatter.get_string_short() == "12345"
def test_none_conversion(self):
"""Test None conversion to string"""
formatter = DoubleByteFormatString(None, 10) # type: ignore[arg-type]
assert formatter.string == "None"
class TestDoubleByteFormatStringWidthCalculation:
"""Tests for width calculation accuracy"""
def test_ascii_width_calculation(self):
"""Test width calculation for ASCII"""
formatter = DoubleByteFormatString("Hello", 10)
formatter.process()
# ASCII characters should have width = length
assert formatter.string_width_value == 5
def test_japanese_width_calculation(self):
"""Test width calculation for Japanese"""
formatter = DoubleByteFormatString("こんにちは", 20)
formatter.process()
# 5 Japanese characters * 2 width each = 10
assert formatter.string_width_value == 10
def test_mixed_width_calculation(self):
"""Test width calculation for mixed characters"""
formatter = DoubleByteFormatString("Hello日本", 20)
formatter.process()
# "Hello" = 5 width, "日本" = 4 width, total = 9
assert formatter.string_width_value == 9
def test_fullwidth_latin_calculation(self):
"""Test width calculation for fullwidth Latin characters"""
# Fullwidth Latin letters
formatter = DoubleByteFormatString("", 10)
formatter.process()
# 3 fullwidth characters * 2 width each = 6
assert formatter.string_width_value == 6
# Parametrized tests
@pytest.mark.parametrize("string,cut_length,expected_short", [
("Hello", 10, "Hello"),
("Hello World", 8, "Hello .."),
("Hello World Test", 5, "Hel.."),
("", 5, ""),
("A", 5, "A"),
])
def test_ascii_shortening_parametrized(string: str, cut_length: int, expected_short: str):
"""Parametrized test for ASCII string shortening"""
formatter = DoubleByteFormatString(string, cut_length)
assert formatter.get_string_short() == expected_short
@pytest.mark.parametrize("string,cut_length,format_length,expected_format_len", [
("Hello", 5, 10, 10),
("Hello", 10, 5, 5),
("Hello World", 8, 12, 12),
])
def test_format_length_parametrized(
string: str,
cut_length: int,
format_length: int,
expected_format_len: int
):
"""Parametrized test for format length"""
formatter = DoubleByteFormatString(string, cut_length, format_length)
assert formatter.get_format_length() == expected_format_len
@pytest.mark.parametrize("string,expected_width", [
("Hello", 5),
("こんにちは", 10), # 5 Japanese chars * 2
("Hello日本", 9), # 5 + 4
("", 0),
("A", 1),
("", 2),
])
def test_width_calculation_parametrized(string: str, expected_width: int):
"""Parametrized test for width calculation"""
formatter = DoubleByteFormatString(string, 100) # Large cut_length to avoid shortening
formatter.process()
if string:
assert formatter.string_width_value == expected_width
else:
assert formatter.string_width_value == 0
@pytest.mark.parametrize("placeholder", [
"..",
"...",
"",
">>>",
"~",
])
def test_custom_placeholder_parametrized(placeholder: str):
"""Parametrized test for custom placeholders"""
formatter = DoubleByteFormatString("Hello World Test", 8, placeholder=placeholder)
result = formatter.get_string_short()
assert result.endswith(placeholder)
assert len(result) == 8
class TestDoubleByteFormatStringIntegration:
"""Integration tests for complete workflows"""
def test_complete_workflow_ascii(self):
"""Test complete workflow with ASCII string"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short == "Hello .."
assert len(formatted) == 12
assert formatted.startswith("Hello ..")
def test_complete_workflow_japanese(self):
"""Test complete workflow with Japanese string"""
formatter = DoubleByteFormatString("こんにちは世界", 8, 12)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short.endswith("..")
assert isinstance(formatted, str)
def test_complete_workflow_mixed(self):
"""Test complete workflow with mixed characters"""
formatter = DoubleByteFormatString("Hello世界World", 10, 15)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short.endswith("..")
assert isinstance(formatted, str)
def test_table_like_output(self):
"""Test creating table-like output with multiple formatters"""
items = [
("Name", "Alice", 10, 15),
("City", "Tokyo東京", 10, 15),
("Country", "Japan日本国", 10, 15),
]
results: list[str] = []
for _label, value, cut, fmt in items:
formatter = DoubleByteFormatString(value, cut, fmt)
results.append(formatter.get_string_short_formated())
# All results should be formatted strings
# Note: Due to double-byte character width adjustments,
# the actual string length may differ from format_length
assert all(isinstance(result, str) for result in results)
assert all(len(result) > 0 for result in results)
def test_reprocess_after_modification(self):
"""Test reprocessing after modifying formatter properties"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
initial = formatter.get_string_short()
# Modify and reprocess
formatter.string = "New String Test"
formatter.process()
modified = formatter.get_string_short()
assert initial != modified
assert modified.endswith("..")
class TestDoubleByteFormatStringRightAlignment:
"""Tests for right-aligned formatting"""
def test_right_aligned_format(self):
"""Test right-aligned formatting"""
formatter = DoubleByteFormatString("Hello", 5, 10, format_string="{{:>{len}}}")
result = formatter.get_string_short_formated()
assert len(result) == 10
# The format applies to the short string
assert "Hello" in result
def test_center_aligned_format(self):
"""Test center-aligned formatting"""
formatter = DoubleByteFormatString("Hello", 5, 11, format_string="{{:^{len}}}")
result = formatter.get_string_short_formated()
assert len(result) == 11
assert "Hello" in result
# __END__

View File

@@ -1,328 +0,0 @@
"""
PyTest: string_handling/hash_helpers
"""
import pytest
from corelibs.string_handling.hash_helpers import (
crc32b_fix, sha1_short
)
class TestCrc32bFix:
"""Tests for crc32b_fix function"""
def test_basic_crc_fix(self):
"""Test basic CRC32B byte order fix"""
# Example: if input is "abcdefgh", it should become "ghefcdab"
result = crc32b_fix("abcdefgh")
assert result == "ghefcdab"
def test_short_crc_padding(self):
"""Test that short CRC is left-padded with zeros"""
# Input with 6 chars should be padded to 8: "00abcdef"
# Split into pairs: "00", "ab", "cd", "ef"
# Reversed: "ef", "cd", "ab", "00"
result = crc32b_fix("abcdef")
assert result == "efcdab00"
assert len(result) == 8
def test_4_char_crc(self):
"""Test CRC with 4 characters"""
# Padded: "0000abcd"
# Pairs: "00", "00", "ab", "cd"
# Reversed: "cd", "ab", "00", "00"
result = crc32b_fix("abcd")
assert result == "cdab0000"
assert len(result) == 8
def test_2_char_crc(self):
"""Test CRC with 2 characters"""
# Padded: "000000ab"
# Pairs: "00", "00", "00", "ab"
# Reversed: "ab", "00", "00", "00"
result = crc32b_fix("ab")
assert result == "ab000000"
assert len(result) == 8
def test_1_char_crc(self):
"""Test CRC with 1 character"""
# Padded: "0000000a"
# Pairs: "00", "00", "00", "0a"
# Reversed: "0a", "00", "00", "00"
result = crc32b_fix("a")
assert result == "0a000000"
assert len(result) == 8
def test_empty_crc(self):
"""Test empty CRC string"""
result = crc32b_fix("")
assert result == "00000000"
assert len(result) == 8
def test_numeric_crc(self):
"""Test CRC with numeric characters"""
result = crc32b_fix("12345678")
assert result == "78563412"
def test_mixed_alphanumeric(self):
"""Test CRC with mixed alphanumeric characters"""
result = crc32b_fix("a1b2c3d4")
assert result == "d4c3b2a1"
def test_lowercase_letters(self):
"""Test CRC with lowercase letters"""
result = crc32b_fix("aabbccdd")
assert result == "ddccbbaa"
def test_with_numbers_and_letters(self):
"""Test CRC with numbers and letters (typical hex)"""
result = crc32b_fix("1a2b3c4d")
assert result == "4d3c2b1a"
def test_all_zeros(self):
"""Test CRC with all zeros"""
result = crc32b_fix("00000000")
assert result == "00000000"
def test_short_padding_all_numbers(self):
"""Test padding with all numbers"""
# Padded: "00123456"
# Pairs: "00", "12", "34", "56"
# Reversed: "56", "34", "12", "00"
result = crc32b_fix("123456")
assert result == "56341200"
assert len(result) == 8
def test_typical_hex_values(self):
"""Test with typical hexadecimal hash values"""
result = crc32b_fix("a1b2c3d4")
assert result == "d4c3b2a1"
def test_7_char_crc(self):
"""Test CRC with 7 characters (needs 1 zero padding)"""
# Padded: "0abcdefg"
# Pairs: "0a", "bc", "de", "fg"
# Reversed: "fg", "de", "bc", "0a"
result = crc32b_fix("abcdefg")
assert result == "fgdebc0a"
assert len(result) == 8
class TestSha1Short:
"""Tests for sha1_short function"""
def test_basic_sha1_short(self):
"""Test basic SHA1 short hash generation"""
result = sha1_short("hello")
assert len(result) == 9
assert result.isalnum() # Should be hexadecimal
def test_consistent_output(self):
"""Test that same input produces same output"""
result1 = sha1_short("test")
result2 = sha1_short("test")
assert result1 == result2
def test_different_inputs_different_outputs(self):
"""Test that different inputs produce different outputs"""
result1 = sha1_short("hello")
result2 = sha1_short("world")
assert result1 != result2
def test_empty_string(self):
"""Test SHA1 of empty string"""
result = sha1_short("")
assert len(result) == 9
# SHA1 of empty string is known: "da39a3ee5e6b4b0d3255bfef95601890afd80709"
assert result == "da39a3ee5"
def test_single_character(self):
"""Test SHA1 of single character"""
result = sha1_short("a")
assert len(result) == 9
# SHA1 of "a" is "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8"
assert result == "86f7e437f"
def test_long_string(self):
"""Test SHA1 of long string"""
long_string = "a" * 1000
result = sha1_short(long_string)
assert len(result) == 9
assert result.isalnum()
def test_special_characters(self):
"""Test SHA1 with special characters"""
result = sha1_short("hello@world!")
assert len(result) == 9
assert result.isalnum()
def test_unicode_characters(self):
"""Test SHA1 with unicode characters"""
result = sha1_short("こんにちは")
assert len(result) == 9
assert result.isalnum()
def test_numbers(self):
"""Test SHA1 with numeric string"""
result = sha1_short("12345")
assert len(result) == 9
assert result.isalnum()
def test_whitespace(self):
"""Test SHA1 with whitespace"""
result1 = sha1_short("hello world")
result2 = sha1_short("helloworld")
assert result1 != result2
assert len(result1) == 9
assert len(result2) == 9
def test_newlines_and_tabs(self):
"""Test SHA1 with newlines and tabs"""
result = sha1_short("hello\nworld\ttab")
assert len(result) == 9
assert result.isalnum()
def test_mixed_case(self):
"""Test SHA1 with mixed case (should be case sensitive)"""
result1 = sha1_short("Hello")
result2 = sha1_short("hello")
assert result1 != result2
def test_hexadecimal_output(self):
"""Test that output is valid hexadecimal"""
result = sha1_short("test")
# Should only contain 0-9 and a-f
assert all(c in "0123456789abcdef" for c in result)
def test_known_value_verification(self):
"""Test against known SHA1 values"""
# SHA1 of "hello" is "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
result = sha1_short("hello")
assert result == "aaf4c61dd"
def test_numeric_string_input(self):
"""Test with numeric string"""
result = sha1_short("123456789")
assert len(result) == 9
assert result.isalnum()
def test_emoji_input(self):
"""Test with emoji characters"""
result = sha1_short("😀🎉")
assert len(result) == 9
assert result.isalnum()
def test_multiline_string(self):
"""Test with multiline string"""
multiline = """This is
a multiline
string"""
result = sha1_short(multiline)
assert len(result) == 9
assert result.isalnum()
# Parametrized tests
@pytest.mark.parametrize("input_crc,expected", [
("abcdefgh", "ghefcdab"),
("12345678", "78563412"),
("aabbccdd", "ddccbbaa"),
("00000000", "00000000"),
("", "00000000"),
("a", "0a000000"),
("ab", "ab000000"),
("abcd", "cdab0000"),
("abcdef", "efcdab00"),
])
def test_crc32b_fix_parametrized(input_crc: str, expected: str):
"""Parametrized test for crc32b_fix"""
result = crc32b_fix(input_crc)
assert len(result) == 8
assert result == expected
@pytest.mark.parametrize("input_string,expected_length", [
("hello", 9),
("world", 9),
("", 9),
("a" * 1000, 9),
("test123", 9),
("😀", 9),
])
def test_sha1_short_parametrized_length(input_string: str, expected_length: int):
"""Parametrized test for sha1_short to verify consistent length"""
result = sha1_short(input_string)
assert len(result) == expected_length
@pytest.mark.parametrize("input_string,expected_hash", [
("", "da39a3ee5"),
("a", "86f7e437f"),
("hello", "aaf4c61dd"),
("world", "7c211433f"),
("test", "a94a8fe5c"),
])
def test_sha1_short_known_values(input_string: str, expected_hash: str):
"""Parametrized test for sha1_short with known SHA1 values"""
result = sha1_short(input_string)
assert result == expected_hash
# Edge case tests
class TestEdgeCases:
"""Test edge cases for hash helper functions"""
def test_crc32b_fix_with_max_length(self):
"""Test crc32b_fix with exactly 8 characters"""
result = crc32b_fix("ffffffff")
assert result == "ffffffff"
assert len(result) == 8
def test_sha1_short_very_long_input(self):
"""Test sha1_short with very long input"""
very_long = "x" * 10000
result = sha1_short(very_long)
assert len(result) == 9
assert result.isalnum()
def test_sha1_short_binary_like_string(self):
"""Test sha1_short with binary-like string"""
result = sha1_short("\x00\x01\x02\x03")
assert len(result) == 9
assert result.isalnum()
def test_crc32b_fix_preserves_characters(self):
"""Test that crc32b_fix only reorders, doesn't change characters"""
input_crc = "12345678"
result = crc32b_fix(input_crc)
# All characters from input should be in output (after padding)
for char in input_crc:
assert char in result or '0' in result # 0 is for padding
# Integration tests
class TestIntegration:
"""Integration tests for hash helper functions"""
def test_sha1_short_produces_valid_crc_input(self):
"""Test that sha1_short output could be used as CRC input"""
sha1_result = sha1_short("test")
# SHA1 short is 9 chars, CRC expects up to 8, so take first 8
crc_input = sha1_result[:8]
crc_result = crc32b_fix(crc_input)
assert len(crc_result) == 8
def test_multiple_sha1_short_consistency(self):
"""Test that multiple calls to sha1_short are consistent"""
results = [sha1_short("consistency_test") for _ in range(10)]
assert all(r == results[0] for r in results)
def test_crc32b_fix_reversibility_concept(self):
"""Test that applying crc32b_fix twice reverses the operation"""
original = "abcdefgh"
fixed_once = crc32b_fix(original)
fixed_twice = crc32b_fix(fixed_once)
assert fixed_twice == original
# __END__

View File

@@ -1,327 +0,0 @@
"""
PyTest: string_handling/string_helpers
"""
from textwrap import shorten
import pytest
from corelibs.string_handling.string_helpers import (
shorten_string, left_fill, format_number, prepare_url_slash
)
class TestShortenString:
"""Tests for shorten_string function"""
def test_string_shorter_than_length(self):
"""Test that strings shorter than length are returned unchanged"""
result = shorten_string("hello", 10)
assert result == "hello"
def test_string_equal_to_length(self):
"""Test that strings equal to length are returned unchanged"""
result = shorten_string("hello", 5)
assert result == "hello"
def test_hard_shorten_true(self):
"""Test hard shortening with default placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True)
assert result == "hell [~]"
def test_hard_shorten_custom_placeholder(self):
"""Test hard shortening with custom placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True, placeholder="...")
assert result == "hello..."
def test_no_spaces_auto_hard_shorten(self):
"""Test that strings without spaces automatically use hard shorten"""
result = shorten_string("helloworld", 8)
assert result == "hell [~]"
def test_soft_shorten_with_spaces(self):
"""Test soft shortening using textwrap.shorten"""
result = shorten_string("hello world test", 12)
# Should use textwrap.shorten behavior
expected = shorten("hello world test", width=12, placeholder=" [~]")
assert result == expected
def test_placeholder_too_large_hard_shorten(self):
"""Test error when placeholder is larger than allowed length"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, hard_shorten=True, placeholder=" [~]")
def test_placeholder_too_large_no_spaces(self):
"""Test error when placeholder is larger than allowed length for string without spaces"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, placeholder=" [~]")
def test_textwrap_shorten_error(self):
"""Test handling of textwrap.shorten ValueError"""
# This might be tricky to trigger, but we can mock it
with pytest.raises(ValueError, match="Cannot shorten string:"):
# Very short length that might cause textwrap.shorten to fail
shorten_string("hello world", 1, hard_shorten=False)
def test_type_conversion(self):
"""Test that inputs are converted to proper types"""
result = shorten_string(12345, 8, hard_shorten=True)
assert result == "12345"
def test_empty_string(self):
"""Test with empty string"""
result = shorten_string("", 5)
assert result == ""
def test_zero_length(self):
"""Test with zero length"""
with pytest.raises(ValueError):
shorten_string("hello", 0, hard_shorten=True)
class TestLeftFill:
"""Tests for left_fill function"""
def test_basic_left_fill(self):
"""Test basic left filling with spaces"""
result = left_fill("hello", 10)
assert result == " hello"
assert len(result) == 10
def test_custom_fill_character(self):
"""Test left filling with custom character"""
result = left_fill("hello", 10, "0")
assert result == "00000hello"
def test_string_longer_than_width(self):
"""Test when string is longer than width"""
result = left_fill("hello world", 5)
assert result == "hello world" # Should return original string
def test_string_equal_to_width(self):
"""Test when string equals width"""
result = left_fill("hello", 5)
assert result == "hello"
def test_negative_width(self):
"""Test with negative width"""
result = left_fill("hello", -5)
assert result == "hello" # Should use string length
def test_zero_width(self):
"""Test with zero width"""
result = left_fill("hello", 0)
assert result == "hello" # Should return original string
def test_invalid_fill_character(self):
"""Test with invalid fill character (not single char)"""
result = left_fill("hello", 10, "abc")
assert result == " hello" # Should default to space
def test_empty_fill_character(self):
"""Test with empty fill character"""
result = left_fill("hello", 10, "")
assert result == " hello" # Should default to space
def test_empty_string(self):
"""Test with empty string"""
result = left_fill("", 5)
assert result == " "
class TestFormatNumber:
"""Tests for format_number function"""
def test_integer_default_precision(self):
"""Test formatting integer with default precision"""
result = format_number(1234)
assert result == "1,234"
def test_float_default_precision(self):
"""Test formatting float with default precision"""
result = format_number(1234.56)
assert result == "1,235" # Should round to nearest integer
def test_with_precision(self):
"""Test formatting with specified precision"""
result = format_number(1234.5678, 2)
assert result == "1,234.57"
def test_large_number(self):
"""Test formatting large number"""
result = format_number(1234567.89, 2)
assert result == "1,234,567.89"
def test_zero(self):
"""Test formatting zero"""
result = format_number(0)
assert result == "0"
def test_negative_number(self):
"""Test formatting negative number"""
result = format_number(-1234.56, 2)
assert result == "-1,234.56"
def test_negative_precision(self):
"""Test with negative precision (should default to 0)"""
result = format_number(1234.56, -1)
assert result == "1,235"
def test_excessive_precision(self):
"""Test with precision > 100 (should default to 0)"""
result = format_number(1234.56, 101)
assert result == "1,235"
def test_precision_boundary_values(self):
"""Test precision boundary values"""
# Test precision = 0 (should work)
result = format_number(1234.56, 0)
assert result == "1,235"
# Test precision = 100 (should work)
result = format_number(1234.56, 100)
assert "1,234.56" in result # Will have many trailing zeros
def test_small_decimal(self):
"""Test formatting small decimal number"""
result = format_number(0.123456, 4)
assert result == "0.1235"
def test_very_small_number(self):
"""Test formatting very small number"""
result = format_number(0.001, 3)
assert result == "0.001"
class TestPrepareUrlSlash:
"""Tests for prepare_url_slash function"""
def test_url_without_leading_slash(self):
"""Test that URL without leading slash gets one added"""
result = prepare_url_slash("api/users")
assert result == "/api/users"
def test_url_with_leading_slash(self):
"""Test that URL with leading slash remains unchanged"""
result = prepare_url_slash("/api/users")
assert result == "/api/users"
def test_url_with_double_slashes(self):
"""Test that double slashes are reduced to single slash"""
result = prepare_url_slash("/api//users")
assert result == "/api/users"
def test_url_with_multiple_slashes(self):
"""Test that multiple consecutive slashes are reduced to single slash"""
result = prepare_url_slash("api///users////data")
assert result == "/api/users/data"
def test_url_with_leading_double_slash(self):
"""Test URL starting with double slash"""
result = prepare_url_slash("//api/users")
assert result == "/api/users"
def test_url_without_slash_and_double_slashes(self):
"""Test URL without leading slash and containing double slashes"""
result = prepare_url_slash("api//users//data")
assert result == "/api/users/data"
def test_single_slash(self):
"""Test single slash URL"""
result = prepare_url_slash("/")
assert result == "/"
def test_multiple_slashes_only(self):
"""Test URL with only multiple slashes"""
result = prepare_url_slash("///")
assert result == "/"
def test_empty_string(self):
"""Test empty string"""
result = prepare_url_slash("")
assert result == "/"
def test_url_with_query_params(self):
"""Test URL with query parameters"""
result = prepare_url_slash("/api/users?id=1")
assert result == "/api/users?id=1"
def test_url_with_double_slashes_and_query(self):
"""Test URL with double slashes and query parameters"""
result = prepare_url_slash("api//users?id=1")
assert result == "/api/users?id=1"
def test_complex_url_path(self):
"""Test complex URL path with multiple segments"""
result = prepare_url_slash("api/v1/users/123/profile")
assert result == "/api/v1/users/123/profile"
def test_complex_url_with_multiple_issues(self):
"""Test URL with both missing leading slash and multiple double slashes"""
result = prepare_url_slash("api//v1///users//123////profile")
assert result == "/api/v1/users/123/profile"
# Additional integration tests
class TestIntegration:
"""Integration tests combining functions"""
def test_format_and_fill(self):
"""Test formatting a number then left filling"""
formatted = format_number(1234.56, 2)
result = left_fill(formatted, 15)
assert result.endswith("1,234.56")
assert len(result) == 15
def test_format_and_shorten(self):
"""Test formatting a large number then shortening"""
formatted = format_number(123456789.123, 3)
result = shorten_string(formatted, 10)
assert len(result) <= 10
# Fixtures for parameterized tests
@pytest.mark.parametrize("input_str,length,expected", [
("hello", 10, "hello"),
("hello world", 5, "h [~]"),
("test", 4, "test"),
("", 5, ""),
])
def test_shorten_string_parametrized(input_str: str, length: int, expected: str):
"""Parametrized test for shorten_string"""
result = shorten_string(input_str, length, hard_shorten=True)
if expected.endswith(" [~]"):
assert result.endswith(" [~]")
assert len(result) == length
else:
assert result == expected
@pytest.mark.parametrize("number,precision,expected", [
(1000, 0, "1,000"),
(1234.56, 2, "1,234.56"),
(0, 1, "0.0"),
(-500, 0, "-500"),
])
def test_format_number_parametrized(number: float | int, precision: int, expected: str):
"""Parametrized test for format_number"""
assert format_number(number, precision) == expected
@pytest.mark.parametrize("input_url,expected", [
("api/users", "/api/users"),
("/api/users", "/api/users"),
("api//users", "/api/users"),
("/api//users", "/api/users"),
("//api/users", "/api/users"),
("api///users////data", "/api/users/data"),
("/", "/"),
("///", "/"),
("", "/"),
("api/v1/users/123", "/api/v1/users/123"),
("/api/users?id=1&name=test", "/api/users?id=1&name=test"),
("api//users//123//profile", "/api/users/123/profile"),
])
def test_prepare_url_slash_parametrized(input_url: str, expected: str):
"""Parametrized test for prepare_url_slash"""
assert prepare_url_slash(input_url) == expected
# __END__