1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
|
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.
'''
Project-wide **string joining utilities** (i.e., callables joining passed
strings into new strings delimited by passed substring delimiters).
This private submodule is *not* intended for importation by downstream callers.
'''
# ....................{ IMPORTS }....................
from beartype._data.typing.datatyping import (
BoolTristate,
IterableStrs,
IterableTypes,
)
from collections.abc import (
Iterable as IterableABC,
Sequence as SequenceABC,
)
# ....................{ JOINERS }....................
#FIXME: Unit test the "is_double_quoted" parameter, please.
def join_delimited(
# Mandatory parameters.
strs: IterableStrs,
# Mandatory keyword-only parameters.
*,
delimiter_if_two: str,
delimiter_if_three_or_more_nonlast: str,
delimiter_if_three_or_more_last: str,
# Optional keyword-only parameters.
is_double_quoted: bool = False,
) -> str:
'''
Concatenate the passed iterable of zero or more strings delimited by the
passed delimiter (conditionally depending on both the length of this
sequence and index of each string in this sequence), yielding a
human-readable string listing arbitrarily many substrings.
Specifically, this function returns either:
* If this iterable contains no strings, the empty string.
* If this iterable contains one string, this string as is is unmodified.
* If this iterable contains two strings, these strings delimited by the
passed ``delimiter_if_two`` delimiter.
* If this iterable contains three or more strings, a string listing these
contained strings such that:
* All contained strings except the last two are suffixed by the passed
``delimiter_if_three_or_more_nonlast`` delimiter.
* The last two contained strings are delimited by the passed
``delimiter_if_three_or_more_last`` separator.
Parameters
----------
strs : Iterable[str]
Iterable of all strings to be joined.
delimiter_if_two : str
Substring separating each string contained in this iterable if this
iterable contains exactly two strings.
delimiter_if_three_or_more_nonlast : str
Substring separating each string *except* the last two contained in
this iterable if this iterable contains three or more strings.
delimiter_if_three_or_more_last : str
Substring separating each string the last two contained in this
iterable if this iterable contains three or more strings.
is_double_quoted : bool, optional
:data:`True` only if **double-quoting** (i.e., both prefixing and
suffixing by the ``"`` character) each item of this iterable. Defaults
to :data:`False`.
Returns
-------
str
Concatenation of these strings.
Examples
--------
>>> join_delimited(
... strs=('Fulgrim', 'Perturabo', 'Angron', 'Mortarion'),
... delimiter_if_two=' and ',
... delimiter_if_three_or_more_nonlast=', ',
... delimiter_if_three_or_more_last=', and ',
... )
'Fulgrim, Perturabo, Angron, and Mortarion'
'''
assert isinstance(strs, IterableABC) and not isinstance(strs, str), (
f'{repr(strs)} not non-string iterable.')
assert isinstance(delimiter_if_two, str), (
f'{repr(delimiter_if_two)} not string.')
assert isinstance(delimiter_if_three_or_more_nonlast, str), (
f'{repr(delimiter_if_three_or_more_nonlast)} not string.')
assert isinstance(delimiter_if_three_or_more_last, str), (
f'{repr(delimiter_if_three_or_more_last)} not string.')
# If this iterable is *NOT* a sequence, internally coerce this iterable
# into a sequence for subsequent indexing purposes.
if not isinstance(strs, SequenceABC):
strs = tuple(strs)
# Else, this iterable is already a sequence.
#
# In either case, this iterable is now a sequence.
# If double-quoting these strings, do so.
if is_double_quoted:
strs = tuple(f'"{text}"' for text in strs)
# Else, preserve these strings as is.
# Number of strings in this sequence.
num_strs = len(strs)
# If no strings are passed, return the empty string.
if num_strs == 0:
return ''
# If one string is passed, return this string as is.
elif num_strs == 1:
# This is clearly a string, yet mypy thinks it's Any
return strs[0] # type: ignore[no-any-return]
# If two strings are passed, return these strings delimited appropriately.
elif num_strs == 2:
return f'{strs[0]}{delimiter_if_two}{strs[1]}'
# Else, three or more strings are passed.
# All such strings except the last two, delimited appropriately.
strs_nonlast = delimiter_if_three_or_more_nonlast.join(strs[0:-2])
# The last two such strings, delimited appropriately.
strs_last = f'{strs[-2]}{delimiter_if_three_or_more_last}{strs[-1]}'
# Return these two substrings, delimited appropriately.
return f'{strs_nonlast}{delimiter_if_three_or_more_nonlast}{strs_last}'
# ....................{ JOINERS ~ conjunction }....................
#FIXME: Unit test us up, please.
def join_delimited_conjunction(strs: IterableStrs, **kwargs) -> str:
'''
Concatenate the passed iterable of zero or more strings delimited by commas
and/or the conjunction "and" (conditionally depending on both the length of
this iterable and index of each string in this iterable), yielding a
human-readable string listing arbitrarily many substrings conjunctively.
Specifically, this function returns either:
* If this iterable contains no strings, the empty string.
* If this iterable contains one string, this string as is is unmodified.
* If this iterable contains two strings, these strings delimited by the
conjunction "and".
* If this iterable contains three or more strings, a string listing these
contained strings such that:
* All contained strings except the last two are suffixed by commas.
* The last two contained strings are delimited by the conjunction "and".
Parameters
----------
strs : Iterable[str]
Iterable of all strings to be concatenated conjunctively.
All remaining keyword parameters are passed as is to the lower-level
:func:`.join_delimeted` function underlying this higher-level function.
Returns
-------
str
Conjunctive concatenation of these strings.
'''
# One of us. We accept one-liner. One of us.
return join_delimited(
strs=strs,
delimiter_if_two=' and ',
delimiter_if_three_or_more_nonlast=', ',
delimiter_if_three_or_more_last=', and ',
**kwargs
)
# ....................{ JOINERS ~ disjunction }....................
def join_delimited_disjunction(strs: IterableStrs, **kwargs) -> str:
'''
Concatenate the passed iterable of zero or more strings delimited by commas
and/or the disjunction "or" (conditionally depending on both the length of
this iterable and index of each string in this iterable), yielding a
human-readable string listing arbitrarily many substrings disjunctively.
Specifically, this function returns either:
* If this iterable contains no strings, the empty string.
* If this iterable contains one string, this string as is is unmodified.
* If this iterable contains two strings, these strings delimited by the
disjunction "or".
* If this iterable contains three or more strings, a string listing these
contained strings such that:
* All contained strings except the last two are suffixed by commas.
* The last two contained strings are delimited by the disjunction "or".
Parameters
----------
strs : Iterable[str]
Iterable of all strings to be concatenated disjunctively.
All remaining keyword parameters are passed as is to the lower-level
:func:`.join_delimeted` function underlying this higher-level function.
Returns
-------
str
Disjunctive concatenation of these strings.
'''
# He will join us... OR DIE! *cackling heard*
return join_delimited(
strs=strs,
delimiter_if_two=' or ',
delimiter_if_three_or_more_nonlast=', ',
delimiter_if_three_or_more_last=', or ',
**kwargs
)
def join_delimited_disjunction_types(
# Mandatory parameters.
types: IterableTypes,
# Optional parameters.
is_color: BoolTristate = False,
) -> str:
'''
Concatenate the human-readable classname of each class in the passed
iterable delimited by commas and/or the disjunction "or" (conditionally
depending on both the length of this iterable and index of each string in
this iterable), yielding a human-readable string listing arbitrarily many
classnames disjunctively.
Parameters
----------
types : Iterable[type]
Iterable of all classes whose human-readable classnames are to be
concatenated disjunctively.
is_color : BoolTristate, optional
Tri-state colouring boolean governing ANSI usage. See the
:attr:`beartype.BeartypeConf.is_color` attribute for further details.
Defaults to :data:`False`.
Returns
-------
str
Disjunctive concatenation of these classnames.
'''
# Avoid circular import dependencies.
from beartype._util.text.utiltextlabel import label_type
# Make it so, ensign.
return join_delimited_disjunction(
label_type(cls=cls, is_color=is_color) for cls in types)
|