X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=string_utils.py;h=9f67207cdf11af0772a6f3872a17fa5a1be67306;hb=865825894beeedd47d26dd092d40bfee582f5475;hp=45607f3448d85f1d61a9e614502cf6c87bfd6fdc;hpb=f2c7c1a131d8846c15613125b6ed999724f0ab2f;p=python_utils.git diff --git a/string_utils.py b/string_utils.py index 45607f3..9f67207 100644 --- a/string_utils.py +++ b/string_utils.py @@ -1,5 +1,31 @@ #!/usr/bin/env python3 +"""The MIT License (MIT) + +Copyright (c) 2016-2020 Davide Zanotti +Modifications Copyright (c) 2021-2022 Scott Gasch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This class is based on: https://github.com/daveoncode/python-string-utils. +""" + import base64 import contextlib import datetime @@ -11,9 +37,19 @@ import numbers import random import re import string -from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, +) import unicodedata from uuid import uuid4 +import warnings import list_utils @@ -31,7 +67,7 @@ URLS_RAW_STRING = ( r"([a-z-]+://)" # scheme r"([a-z_\d-]+:[a-z_\d-]+@)?" # user:password r"(www\.)?" # www. - r"((?]*/?>)(.*?())?||)", @@ -123,9 +153,7 @@ HTML_TAG_ONLY_RE = re.compile( SPACES_RE = re.compile(r"\s") -NO_LETTERS_OR_NUMBERS_RE = re.compile( - r"[^\w\d]+|_+", re.IGNORECASE | re.UNICODE -) +NO_LETTERS_OR_NUMBERS_RE = re.compile(r"[^\w\d]+|_+", re.IGNORECASE | re.UNICODE) MARGIN_RE = re.compile(r"^[^\S\r\n]+") @@ -253,10 +281,10 @@ def is_integer_number(in_str: str) -> bool: False """ return ( - (is_number(in_str) and "." not in in_str) or - is_hexidecimal_integer_number(in_str) or - is_octal_integer_number(in_str) or - is_binary_integer_number(in_str) + (is_number(in_str) and "." not in in_str) + or is_hexidecimal_integer_number(in_str) + or is_octal_integer_number(in_str) + or is_binary_integer_number(in_str) ) @@ -380,12 +408,7 @@ def strip_escape_sequences(in_str: str) -> str: return in_str -def add_thousands_separator( - in_str: str, - *, - separator_char = ',', - places = 3 -) -> str: +def add_thousands_separator(in_str: str, *, separator_char=',', places=3) -> str: """ Add thousands separator to a numeric string. Also handles numbers. @@ -405,20 +428,17 @@ def add_thousands_separator( in_str = f'{in_str}' if is_number(in_str): return _add_thousands_separator( - in_str, - separator_char = separator_char, - places = places + in_str, separator_char=separator_char, places=places ) raise ValueError(in_str) -def _add_thousands_separator(in_str: str, *, separator_char = ',', places = 3) -> str: +def _add_thousands_separator(in_str: str, *, separator_char=',', places=3) -> str: decimal_part = "" if '.' in in_str: (in_str, decimal_part) = in_str.split('.') tmp = [iter(in_str[::-1])] * places - ret = separator_char.join( - "".join(x) for x in zip_longest(*tmp, fillvalue=""))[::-1] + ret = separator_char.join("".join(x) for x in zip_longest(*tmp, fillvalue=""))[::-1] if len(decimal_part) > 0: ret += '.' ret += decimal_part @@ -459,11 +479,7 @@ def is_email(in_str: Any) -> bool: >>> is_email('@gmail.com') False """ - if ( - not is_full_string(in_str) - or len(in_str) > 320 - or in_str.startswith(".") - ): + if not is_full_string(in_str) or len(in_str) > 320 or in_str.startswith("."): return False try: @@ -473,12 +489,7 @@ def is_email(in_str: Any) -> bool: # head's size must be <= 64, tail <= 255, head must not start # with a dot or contain multiple consecutive dots. - if ( - len(head) > 64 - or len(tail) > 255 - or head.endswith(".") - or (".." in head) - ): + if len(head) > 64 or len(tail) > 255 or head.endswith(".") or (".." in head): return False # removes escaped spaces, so that later on the test regex will @@ -506,6 +517,7 @@ def suffix_string_to_number(in_str: str) -> Optional[int]: >>> suffix_string_to_number('13.1Gb') 14066017894 """ + def suffix_capitalize(s: str) -> str: if len(s) == 1: return s.upper() @@ -594,9 +606,7 @@ def is_camel_case(in_str: Any) -> bool: - it contains both lowercase and uppercase letters - it does not start with a number """ - return ( - is_full_string(in_str) and CAMEL_CASE_TEST_RE.match(in_str) is not None - ) + return is_full_string(in_str) and CAMEL_CASE_TEST_RE.match(in_str) is not None def is_snake_case(in_str: Any, *, separator: str = "_") -> bool: @@ -621,14 +631,10 @@ def is_snake_case(in_str: Any, *, separator: str = "_") -> bool: """ if is_full_string(in_str): re_map = {"_": SNAKE_CASE_TEST_RE, "-": SNAKE_CASE_TEST_DASH_RE} - re_template = ( - r"([a-z]+\d*{sign}[a-z\d{sign}]*|{sign}+[a-z\d]+[a-z\d{sign}]*)" - ) + re_template = r"([a-z]+\d*{sign}[a-z\d{sign}]*|{sign}+[a-z\d]+[a-z\d{sign}]*)" r = re_map.get( separator, - re.compile( - re_template.format(sign=re.escape(separator)), re.IGNORECASE - ), + re.compile(re_template.format(sign=re.escape(separator)), re.IGNORECASE), ) return r.match(in_str) is not None return False @@ -917,9 +923,7 @@ def camel_case_to_snake_case(in_str, *, separator="_"): raise ValueError(in_str) if not is_camel_case(in_str): return in_str - return CAMEL_CASE_REPLACE_RE.sub( - lambda m: m.group(1) + separator, in_str - ).lower() + return CAMEL_CASE_REPLACE_RE.sub(lambda m: m.group(1) + separator, in_str).lower() def snake_case_to_camel_case( @@ -1092,12 +1096,14 @@ def to_date(in_str: str) -> Optional[datetime.date]: Parses a date string. See DateParser docs for details. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() d.parse(in_str) return d.get_date() except dp.ParseException: - logger.warning(f'Unable to parse date {in_str}.') + msg = f'Unable to parse date {in_str}.' + logger.warning(msg) return None @@ -1106,12 +1112,14 @@ def valid_date(in_str: str) -> bool: True if the string represents a valid date. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() _ = d.parse(in_str) return True except dp.ParseException: - logger.warning(f'Unable to parse date {in_str}.') + msg = f'Unable to parse date {in_str}.' + logger.warning(msg) return False @@ -1120,13 +1128,15 @@ def to_datetime(in_str: str) -> Optional[datetime.datetime]: Parses a datetime string. See DateParser docs for more info. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() dt = d.parse(in_str) if type(dt) == datetime.datetime: return dt except ValueError: - logger.warning(f'Unable to parse datetime {in_str}.') + msg = f'Unable to parse datetime {in_str}.' + logger.warning(msg) return None @@ -1137,7 +1147,8 @@ def valid_datetime(in_str: str) -> bool: _ = to_datetime(in_str) if _ is not None: return True - logger.warning(f'Unable to parse datetime {in_str}.') + msg = f'Unable to parse datetime {in_str}.' + logger.warning(msg) return False @@ -1155,7 +1166,7 @@ def squeeze(in_str: str, character_to_squeeze: str = ' ') -> str: return re.sub( r'(' + re.escape(character_to_squeeze) + r')+', character_to_squeeze, - in_str + in_str, ) @@ -1227,6 +1238,7 @@ class SprintfStdout(object): 'test\n' """ + def __init__(self) -> None: self.destination = io.StringIO() self.recorder = None @@ -1326,9 +1338,7 @@ def trigrams(txt: str): def shuffle_columns_into_list( - input_lines: Iterable[str], - column_specs: Iterable[Iterable[int]], - delim='' + input_lines: Iterable[str], column_specs: Iterable[Iterable[int]], delim='' ) -> Iterable[str]: """Helper to shuffle / parse columnar data and return the results as a list. The column_specs argument is an iterable collection of @@ -1358,9 +1368,9 @@ def shuffle_columns_into_list( def shuffle_columns_into_dict( - input_lines: Iterable[str], - column_specs: Iterable[Tuple[str, Iterable[int]]], - delim='' + input_lines: Iterable[str], + column_specs: Iterable[Tuple[str, Iterable[int]]], + delim='', ) -> Dict[str, str]: """Helper to shuffle / parse columnar data and return the results as a dict. @@ -1466,13 +1476,16 @@ def chunk(txt: str, chunk_size): """ if len(txt) % chunk_size != 0: - logger.warning( - f'String to chunk\'s length ({len(txt)} is not an even multiple of chunk_size ({chunk_size})') + msg = f'String to chunk\'s length ({len(txt)} is not an even multiple of chunk_size ({chunk_size})' + logger.warning(msg) + warnings.warn(msg, stacklevel=2) for x in range(0, len(txt), chunk_size): - yield txt[x:x+chunk_size] + yield txt[x : x + chunk_size] -def to_bitstring(txt: str, *, delimiter='', encoding='utf-8', errors='surrogatepass') -> str: +def to_bitstring( + txt: str, *, delimiter='', encoding='utf-8', errors='surrogatepass' +) -> str: """Encode txt and then chop it into bytes. Note: only bitstrings with delimiter='' are interpretable by from_bitstring. @@ -1487,12 +1500,7 @@ def to_bitstring(txt: str, *, delimiter='', encoding='utf-8', errors='surrogatep """ etxt = to_ascii(txt) - bits = bin( - int.from_bytes( - etxt, - 'big' - ) - ) + bits = bin(int.from_bytes(etxt, 'big')) bits = bits[2:] return delimiter.join(chunk(bits.zfill(8 * ((len(bits) + 7) // 8)), 8)) @@ -1568,4 +1576,5 @@ def replace_all(in_str: str, replace_set: str, replacement: str) -> str: if __name__ == '__main__': import doctest + doctest.testmod()