Skip to content

bsutils module

Contains various utilities programs:

  • printargs: a decorator that reports the arguments of the function it decorates
  • bs_name_func: returns the name of the current function or its callers
  • bs_error_abort: reports on error and aborts execution
  • final_s: whether a word should have a final 's'
  • bs_switch: an improved switch statement
  • find_first: returns the index of and the first item in an iterable that satisfies a condition
  • print_stars: prints a title within lines of stars
  • file_print_stars: does the same, to a file
  • fstring_integer_with_significant_digits: rounds an integer and returns an f-string
  • mkdir_if_needed: creates a directory if it does not exist
  • bscomb: a combination \({n \choose k}\) operator
  • bslog, bsxlogx, bsexp: \(C^2\) extensions of \(\log(x), x\log x, \exp(x)\), and their first two derivatives
  • bs_projection_point: projects a point on a line in the plane.
Note

if the math looks strange in the documentation, just reload the page.

bs_error_abort(msg='error, aborting')

report error and abort

Parameters:

Name Type Description Default
msg str

a message

'error, aborting'

Returns:

Type Description
None

prints the message and exits with code 1

Source code in bs_python_utils/bsutils.py
67
68
69
70
71
72
73
74
75
76
77
78
def bs_error_abort(msg: str = "error, aborting") -> None:
    """
    report error and abort

    Args:
        msg: a message

    Returns:
        prints the message and exits with code 1
    """
    print_stars(f"{bs_name_func(3)}: {msg}")
    sys.exit(1)

bs_name_func(back=2)

get the name of the current function, or further back in stack

Parameters:

Name Type Description Default
back int

2 is current function, 3 the function that called it etc

2

Returns:

Type Description
str

the name of the function

Source code in bs_python_utils/bsutils.py
52
53
54
55
56
57
58
59
60
61
62
63
64
def bs_name_func(back: int = 2) -> str:
    """
    get the name of the current function, or further back in stack

    Args:
        back: 2 is current function, 3 the function that called it etc

    Returns:
        the name of the function
    """
    stack = traceback.extract_stack()
    *_, func_name, _ = stack[-back]
    return cast(str, func_name)

bs_projection_point(x, y, a, b, c)

projection of point (x,y) on line ax+by+c=0

Parameters:

Name Type Description Default
x float

y: coordinates

required
a float

b: c: line parameters (as in ax+by+c=0)

required

Returns:

Name Type Description
x_proj float

y_proj: coordinates of projection point

dist float

distance of point from line

Source code in bs_python_utils/bsutils.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
def bs_projection_point(
    x: float, y: float, a: float, b: float, c: float
) -> tuple[float, float, float]:
    """
    projection of point (x,y) on line ax+by+c=0

    Args:
        x: y: coordinates
        a: b: c: line parameters (as in ax+by+c=0)

    Returns:
        x_proj: y_proj: coordinates of projection point
        dist: distance of point from line
    """
    a2b2 = a * a + b * b
    denom = sqrt(a2b2)
    value = a * x + b * y + c
    x_proj = x - a * value / a2b2
    y_proj = y - b * value / a2b2
    dist = abs(value) / denom
    return x_proj, y_proj, dist

bs_switch(match, dico, strict=True, default='no match')

a switch statement that allows for partial matches if strict is False

Parameters:

Name Type Description Default
match str

what we look for in the keys

required
dico dict

a dictionary with string keys

required
strict bool

if False, we accept a partial match

True
default Any

what we return if no match is found

'no match'

Returns:

Type Description
Any

the value for the match, or default

Examples:

>>> calc_dict = {
"plus": lambda x, y: x + y,
"minus": lambda x, y: x - y
}
>>> plus = bs_switch('plus', calc_dict, default="unintended function")
>>> minus = bs_switch('min', calc_dict, strict=False, default="unintended function")
>>> plus(6, 4)
10
>>> minus(6, 4)
2
>>> bs_switch('plu', calc_dict)
"no match"
Source code in bs_python_utils/bsutils.py
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
def bs_switch(
    match: str, dico: dict, strict: bool = True, default: Any = "no match"
) -> Any:
    """
    a switch statement that allows for partial matches if strict is False

    Args:
        match: what we look for in the keys
        dico: a dictionary with string keys
        strict: if `False`, we accept a partial match
        default: what we return if no match is found

    Returns:
        the value for the match, or `default`

    Examples:
        >>> calc_dict = {
        "plus": lambda x, y: x + y,
        "minus": lambda x, y: x - y
        }
        >>> plus = bs_switch('plus', calc_dict, default="unintended function")
        >>> minus = bs_switch('min', calc_dict, strict=False, default="unintended function")
        >>> plus(6, 4)
        10
        >>> minus(6, 4)
        2
        >>> bs_switch('plu', calc_dict)
        "no match"
    """
    if strict:
        for key in dico:
            if match == key:
                return dico.get(key)
    else:
        for key in dico:
            if match in key:
                return dico.get(key)
    return default

bscomb(n, k)

number of combinations of k among n {n \choose k}

Parameters:

Name Type Description Default
n int
required
k int

should be smaller than n

required

Returns:

Type Description
int

{n \choose k}.

Source code in bs_python_utils/bsutils.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def bscomb(n: int, k: int) -> int:
    """
    number of combinations of k among n `{n \\choose k}`

    Args:
        n:
        k: should be smaller than n

    Returns:
        `{n \\choose k}`.
    """
    if not isinstance(n, int):
        bs_error_abort(f"n should be an integer, not {n}")
    if not isinstance(k, int):
        bs_error_abort(f"k should be an integer, not {k}")
    if n < k:
        bs_error_abort(f"k={k} should not be larger than n={n}")
    return factorial(n) // (factorial(k) * factorial(n - k))

bsexp(x, bigx=50.0, lowx=-50.0, deriv=0)

C^2-extends the exponential above bigx and below lowx perhaps with derivatives

Parameters:

Name Type Description Default
x float

argument

required
bigx float

upper bound

50.0
lowx float

lower bound

-50.0
deriv int

if 1, also return first derivative; if 2, the first two derivatives

0

Returns:

Type Description
float | TwoFloats | ThreeFloats

the exponential C^2-extended above bigx and below lowx

float | TwoFloats | ThreeFloats

perhaps with derivatives

Source code in bs_python_utils/bsutils.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
def bsexp(
    x: float,
    bigx: float = 50.0,
    lowx: float = -50.0,
    deriv: int = 0,
) -> float | TwoFloats | ThreeFloats:
    """
    `C^2`-extends the exponential above `bigx` and below `lowx`
    perhaps with derivatives

    Args:
        x: argument
        bigx: upper bound
        lowx: lower bound
        deriv: if 1, also return first derivative; if 2, the first two derivatives

    Returns:
        the exponential `C^2`-extended above `bigx` and below `lowx`
        perhaps with derivatives
    """
    if deriv not in [0, 1, 2]:
        bs_error_abort(f"deriv can only be 0, 1, or 2; not {deriv}")
    if lowx < x < bigx:
        expx = exp(x)
        if deriv == 0:
            return expx
        if deriv == 1:
            return expx, expx
        return expx, expx, expx
    elif x < lowx:
        return _bsexp_extend(x, deriv, lowx)
    else:
        return _bsexp_extend(x, deriv, bigx)

bslog(x, eps=1e-30, deriv=0)

extends the logarithm below eps by taking a second-order approximation perhaps with derivatives

Parameters:

Name Type Description Default
x float

argument

required
eps float

lower bound

1e-30
deriv int

if 1, also return first derivative; if 2, the first two derivatives

0

Returns:

Type Description
float | TwoFloats | ThreeFloats

\ln(x) C^2-extended below eps, perhaps with derivatives

Source code in bs_python_utils/bsutils.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def bslog(
    x: float, eps: float = 1e-30, deriv: int = 0
) -> float | TwoFloats | ThreeFloats:
    """
    extends the logarithm below `eps` by taking a second-order approximation
    perhaps with derivatives

    Args:
        x: argument
        eps: lower bound
        deriv: if 1, also return first derivative; if 2, the first two derivatives

    Returns:
        `\\ln(x)` `C^2`-extended below `eps`, perhaps with derivatives
    """
    if deriv not in [0, 1, 2]:
        bs_error_abort(f"deriv can only be 0, 1, or 2; not {deriv}")
    if x > eps:
        logx = log(x)
        if deriv == 0:
            return logx
        dlogx = 1.0 / x
        if deriv == 1:
            return logx, dlogx
        d2logx = -dlogx * dlogx
        return logx, dlogx, d2logx
    else:
        dx = 1.0 - x / eps
        log_smaller = log(eps) - dx - dx * dx / 2.0
        if deriv == 0:
            return log_smaller
        dlog_smaller = (1.0 + dx) / eps
        if deriv == 1:
            return log_smaller, dlog_smaller
        d2log_smaller = -1.0 / eps / eps
        return log_smaller, dlog_smaller, d2log_smaller

bsxlogx(x, eps=1e-30, deriv=0)

extends x \ln(x) below eps by making it go to zero in a C^2 extension perhaps with derivatives

Parameters:

Name Type Description Default
x float

argument

required
eps float

lower bound

1e-30
deriv int

if 1, also return first derivative; if 2, the first two derivatives

0

Returns:

Type Description
float | TwoFloats | ThreeFloats

x \ln(x) C^2-extended below eps, perhaps with derivatives

Source code in bs_python_utils/bsutils.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def bsxlogx(
    x: float, eps: float = 1e-30, deriv: int = 0
) -> float | TwoFloats | ThreeFloats:
    """
    extends `x \\ln(x)` below `eps` by making it go to zero in a `C^2` extension
    perhaps with derivatives

    Args:
        x: argument
        eps: lower bound
        deriv: if 1, also return first derivative; if 2, the first two derivatives

    Returns:
        `x \\ln(x)`  `C^2`-extended below `eps`, perhaps with derivatives
    """
    if deriv not in [0, 1, 2]:
        bs_error_abort(f"deriv can only be 0, 1, or 2; not {deriv}")
    if x > eps:
        logx = log(x)
        if deriv == 0:
            return x * logx
        if deriv == 1:
            return x * logx, 1.0 + logx
        return x * logx, 1.0 + logx, 1.0 / x
    else:
        logeps = log(eps)
        dx = x / eps
        log_smaller = x * logeps - eps / 2.0 + x * dx / 2.0
        if deriv == 0:
            return log_smaller
        if deriv == 1:
            return log_smaller, logeps + dx
        return log_smaller, logeps + dx, 1.0 / eps

file_print_stars(file_handle, title=None, n=70)

prints to a file a title within stars

Parameters:

Name Type Description Default
file_handle TextIOBase

file handle

required
title str | None

title

None
n int

length of line

70

Returns:

Type Description
None

prints a starred line to the file, or two around the title

Source code in bs_python_utils/bsutils.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def file_print_stars(
    file_handle: TextIOBase, title: str | None = None, n: int = 70
) -> None:
    """
    prints to a file a title within stars

    Args:
        file_handle: file handle
        title:  title
        n:   length of line

    Returns:
        prints a starred line to the file, or two around the title
    """
    line_stars = "*" * n
    file_handle.write("\n")
    file_handle.write(line_stars)
    file_handle.write("\n")
    if title:
        file_handle.write(title.center(n))
        file_handle.write("\n")
        file_handle.write(line_stars)
        file_handle.write("\n")
    file_handle.write("\n")

final_s(n, word)

pluralizes word if n > 1

Parameters:

Name Type Description Default
n int

how many times

required
word str

to be pluralized, maybe

required

Returns:

Type Description
str

1 word or n words

Source code in bs_python_utils/bsutils.py
81
82
83
84
85
86
87
88
89
90
91
92
93
def final_s(n: int, word: str) -> str:
    """
    pluralizes word if n > 1

    Args:
        n: how many times
        word: to be pluralized, maybe

    Returns:
        `1 word` or `n words`
    """
    suffix = "s" if n > 1 else ""
    return f"{n} {word}{suffix}"

find_first(iterable, condition=lambda x: True)

Returns the index of and the first item in the iterable that satisfies the condition.

Parameters:

Name Type Description Default
iterable Iterable

where to look

required
condition Callable

must return a boolean

lambda x: True

Returns:

Type Description
Any

If the condition is not given, returns 0 and the first item of

Any

the iterable.

Any

Raises StopIteration if no item satisfyng the condition is found.

Examples:

>>> find_first( (1,2,3), condition=lambda x: x % 2 == 0)
(1, 2)
>>> find_first(range(3, 100))
(0, 3)
>>> find_first( () )
Traceback (most recent call last):
...
StopIteration
Source code in bs_python_utils/bsutils.py
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
def find_first(iterable: Iterable, condition: Callable = lambda x: True) -> Any:
    """
    Returns the index of and the first item in the `iterable` that
    satisfies the `condition`.

    Args:
        iterable: where to look
        condition: must return a boolean

    Returns:
        If the condition is not given, returns 0 and the first item of
        the iterable.

        Raises `StopIteration` if no item satisfyng the condition is found.

    Examples:
        >>> find_first( (1,2,3), condition=lambda x: x % 2 == 0)
        (1, 2)
        >>> find_first(range(3, 100))
        (0, 3)
        >>> find_first( () )
        Traceback (most recent call last):
        ...
        StopIteration


    """
    return next((i, x) for i, x in enumerate(iterable) if condition(x))

fstring_integer_with_significant_digits(number, m)

returns an f-string with number rounded to m significant digits

Parameters:

Name Type Description Default
number int

the integer we want to round

required
m int

how many digits we keep; the rest is filled with zeroes

required
Return

a string with the rounded integer

Examples:

fstring_integer_with_significant_digits(12345, 0) ' 0' fstring_integer_with_significant_digits(12345, 3) '12,300' fstring_integer_with_significant_digits(12345, 7) '12345'

Source code in bs_python_utils/bsutils.py
 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
def fstring_integer_with_significant_digits(number: int, m: int) -> str:
    """returns an f-string with `number` rounded to `m` significant digits

    Args:
        number: the integer we want to round
        m:  how many digits we keep; the rest is filled with zeroes

    Return:
        a string with the rounded integer

    Examples:
    >>> fstring_integer_with_significant_digits(12345, 0)
    ' 0'
    >>> fstring_integer_with_significant_digits(12345, 3)
    '12,300'
    >>> fstring_integer_with_significant_digits(12345, 7)
    '12345'
    """
    if (not isinstance(number, int)) or (not isinstance(m, int)):
        bs_error_abort("Both arguments should be integers.")
    if number == 0:
        return f"{number:d}"
    else:
        digits = len(str(abs(int(number))))
        if digits <= m:
            return f"{int(number):d}"
        else:
            power = digits - m
            rounded = round(number / 10**power) * 10**power
            return f"{int(rounded): ,d}"

mkdir_if_needed(p)

create the directory if it does not exist

Parameters:

Name Type Description Default
p Path | str

a path

required

Returns:

Type Description
Path

the directory Path

Source code in bs_python_utils/bsutils.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def mkdir_if_needed(p: Path | str) -> Path:
    """
    create the directory if it does not exist

    Args:
        p: a path

    Returns:
        the directory Path

    """
    try:
        q = Path(p)
    except OSError:
        bs_error_abort(f"{p} is not a path")
    if not q.exists():
        q.mkdir(parents=True)
    return q

print_stars(title=None, n=70)

prints a title within stars

Parameters:

Name Type Description Default
title str | None

title

None
n int

number of stars on line

70

Returns:

Type Description
None

prints a starred line, or two around the title

Source code in bs_python_utils/bsutils.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def print_stars(title: str | None = None, n: int = 70) -> None:
    """
    prints a title within stars

    Args:
        title:  title
        n: number of stars on line

    Returns:
        prints a starred line, or two around the title
    """
    line_stars = "*" * n
    print()
    print(line_stars)
    if title:
        print(title.center(n))
        print(line_stars)
    print()

printargs(func)

Decorator that reports the arguments of the function

Parameters:

Name Type Description Default
func Callable

the decorated function

required
Source code in bs_python_utils/bsutils.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def printargs(func: Callable) -> Callable:
    """
    Decorator that reports the arguments of the function

    Args:
      func: the decorated function
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(
            f"Function {func.__name__} called with args = {args} and kwargs = {kwargs}"
        )
        return func(*args, **kwargs)

    return wrapper