kbasic 0.1.16__tar.gz → 0.1.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kbasic
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: Keyan's basic utility functions.
5
5
  Author: Keyan Gootkin
6
6
  Author-email: Keyan Gootkin <keyangootkin@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kbasic"
3
- version = "0.1.16"
3
+ version = "0.1.18"
4
4
  description = "Keyan's basic utility functions."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -28,7 +28,7 @@ def texfraction(num) -> str:
28
28
  # >-|===|> Classes <|===|-<
29
29
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
30
30
  class Tex(str):
31
- def __init__(self, x):
31
+ def __init__(self, x: str) -> None:
32
32
  x = x.strip(' $')
33
33
  if x[-1]=='\n': x = x[:-1]
34
34
  if x[-2:]=='.0': x = x[:-2]
@@ -42,5 +42,5 @@ class Tex(str):
42
42
  self.string = fr"{l2t(x)}"
43
43
  self.wrap = "$"+self.string+"$"
44
44
 
45
- def __repr__(self): return self.string.strip("$")
46
- def __str__(self): return self.string
45
+ def __repr__(self) -> str: return self.string.strip("$")
46
+ def __str__(self) -> str: return self.string
@@ -1,6 +1,7 @@
1
1
  from kbasic.environment import *
2
2
  from kbasic.audio import *
3
3
  from kbasic.array import *
4
+ from kbasic.strings import *
4
5
  from kbasic.bar import *
5
6
  from kbasic.shell import *
6
7
  from kbasic.user_input import *
@@ -1,9 +1,12 @@
1
1
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
2
2
  # >-|===|> Imports <|===|-<
3
3
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
4
+ from kbasic.typing import Number, Iterable
4
5
  from typing import Callable
5
- from numpy import ndarray, argmin, any, abs, where, nanmean, nanmin, nanmax, sqrt,\
6
- linspace, nanstd, array, isnan, arange, mgrid, r_, c_, zeros
6
+ from numpy import ndarray, argmin, any, all, absolute, hypot, logspace, log10, \
7
+ where, nanmean, nanmin, nanmax, sqrt, linspace, nanstd, array, \
8
+ isnan, isfinite, arange, mgrid, r_, c_, zeros
9
+ from numpy.fft import fftshift, fft2
7
10
  from scipy.interpolate import RegularGridInterpolator
8
11
 
9
12
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
@@ -15,13 +18,42 @@ from scipy.interpolate import RegularGridInterpolator
15
18
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
16
19
  # >-|===|> Functions <|===|-<
17
20
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
18
- def tile(arr: ndarray) -> ndarray:
19
- """take an image and create a 3x3 grid of that image"""
20
- return r_[c_[arr, arr, arr], c_[arr, arr, arr], c_[arr, arr, arr]]
21
- def where_closest(arr:ndarray, x): return int(argmin(abs(arr-x)))
22
- def where_between(arr:ndarray, low, high): return where((arr>=low)&(arr<=high))
21
+ def tile(arr: Iterable) -> ndarray:
22
+ """take an array and create a 3x3 grid of copies of that array
23
+
24
+ Args:
25
+ arr (Iterable): the 2D array you want a tiled version
26
+
27
+ Returns:
28
+ ndarray: a 3x3 grid of coppies of arr
29
+ """
30
+ x = array(arr)
31
+ return r_[c_[x, x, x], c_[x, x, x], c_[x, x, x]]
32
+ def where_closest(arr:ndarray, value: Number):
33
+ """find the index where arr is closest to the value of x
34
+
35
+ Args:
36
+ arr (ndarray): the array to search for the index in
37
+ value (Number): the value you want to get close to
38
+
39
+ Returns:
40
+ _type_: _description_
41
+ """
42
+ return int(argmin(absolute(arr-value)))
43
+ def where_between(arr:ndarray, low: Number, high: Number):
44
+ """Find the range of indicies where arr is between low and high
45
+
46
+ Args:
47
+ arr (ndarray): the array to search through
48
+ low (Number): the lower edge of the range (inclusive)
49
+ high (Number): the upper edge of the range (inclusive)
50
+
51
+ Returns:
52
+ ndarray: an array which is True at indices where low<=arr<=high, and False elsewhere
53
+ """
54
+ return where((arr>=low)&(arr<=high))
23
55
  def bin_this(
24
- x, y,
56
+ x: Iterable, y: Iterable,
25
57
  n_bins: int = 50,
26
58
  func: Callable = nanmean
27
59
  ) -> tuple[ndarray]:
@@ -50,11 +82,18 @@ def bin_this(
50
82
  return x_binned, y_binned, bin_errors
51
83
  def nan_clip(*args):
52
84
  """
53
- take a series of arrays and only return the indicies where ALL members are finite
85
+ take a series of arrays and only return the indicies where ALL members are not nan
54
86
  """
55
87
  mask = ~any([isnan(a) for a in args], axis=0)
56
88
  nanless_args = tuple([array(a)[mask] for a in args])
57
89
  return nanless_args
90
+ def only_finite(*args):
91
+ """
92
+ take a series of arrays and only return the indicies where ALL members are finite
93
+ """
94
+ mask = all([isfinite(a) for a in args], axis=0)
95
+ nanless_args = tuple([array(a)[mask] for a in args])
96
+ return nanless_args
58
97
  def interpolate2d(
59
98
  data, factor: int,
60
99
  method: str = 'linear',
@@ -81,9 +120,32 @@ def interpolate2d(
81
120
  new_grid = mgrid[:Nx:1/factor, :Ny:1/factor].T
82
121
  return RegularGridInterpolator(grid, data_repeat, method=method)(new_grid).T
83
122
 
84
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
85
- # >-|===|> Decorators <|===|-<
86
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
87
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
88
- # >-|===|> Classes <|===|-<
89
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
123
+ def kspec(image: ndarray) -> ndarray:
124
+ """take 2d fft and center k=0 at the center point"""
125
+ return absolute(fftshift(fft2(image) / (1. * image.shape[0] * image.shape[1])))**2
126
+
127
+ def kspec1d(image: ndarray, bins: int = 100, return_bin_edges: bool = False) -> tuple[ndarray, ndarray, ndarray]:
128
+ """Take a 2d image and turn it into a 1d fft
129
+
130
+ Args:
131
+ image (ndarray): the array to take the FFT of
132
+ bins (int, optional): number of k bins. Defaults to 100.
133
+ return_bin_edges (bool, optional): if true return the bin edges, not just the k values. Defaults to False.
134
+
135
+ Returns:
136
+ tuple[ndarray, ndarray, ndarray]:
137
+ """
138
+ k = kspec(image)
139
+ Ny, Nx = image.shape
140
+ kmag = hypot(*mgrid[
141
+ -Ny // 2: Ny // 2,
142
+ -Nx // 2: Nx // 2
143
+ ][::-1])
144
+ kmin = nanmin(kmag[kmag != 0])
145
+ kmax = nanmax(kmag)
146
+ kgrid = r_[[0], logspace(log10(3*kmin), log10(kmax), bins)]
147
+ kx = array([nanmean([kgrid[i], kgrid[i+1]]) for i in range(len(kgrid)-1)])
148
+ ks = array([nanmean(k[where((kgrid[i] < kmag) & (kmag < kgrid[i+1]))]) for i in range(len(kgrid)-1)])
149
+ kerr = array([nanstd(in_bin:=k[where((kgrid[i] < kmag) & (kmag < kgrid[i+1]))])/sqrt(len(in_bin)) for i in range(len(kgrid)-1)])
150
+ if not return_bin_edges: return kx, ks, kerr
151
+ return kgrid, kx, ks, kerr
@@ -1,7 +1,7 @@
1
1
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
2
2
  # >-|===|> Imports <|===|-<
3
3
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
4
- from kbasic.typing import Number
4
+ from kbasic.typing import Number, Iterable
5
5
  from kbasic.strings import green, yellow, black
6
6
  from contextlib import contextmanager
7
7
  import inspect
@@ -18,7 +18,7 @@ from tqdm import tqdm
18
18
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
19
19
  def bar(
20
20
  x: Number, total: Number,
21
- width: int = 20, border="|", block="▉", color='white'
21
+ width: int = 20, border: str = "|", block: str = "▉", color: str = 'white'
22
22
  ) -> str:
23
23
  """create a string representing a progress bar set at 100 * x / total % full.
24
24
 
@@ -56,13 +56,13 @@ def redirect_to_tqdm():
56
56
  yield
57
57
  finally:
58
58
  inspect.builtins.print = old_print
59
- def progress_bar(iterator, **kwargs):
59
+ def progress_bar(iterator: Iterable, **kwargs):
60
60
  """tqdm with print redirected to tqdm.write
61
61
  """
62
62
  with redirect_to_tqdm():
63
63
  for x in tqdm(iterator, **kwargs):
64
64
  yield x
65
- def verbose_bar(iterator, verbose, **kwargs):
65
+ def verbose_bar(iterator: Iterable, verbose: bool, **kwargs):
66
66
  """just a progress bar if verbose is true.
67
67
  """
68
68
  return progress_bar(iterator, **kwargs) if verbose else iterator
@@ -9,13 +9,12 @@ from glob import glob
9
9
  from shutil import copy, move, copytree, rmtree
10
10
  from os.path import isdir, isfile, exists, abspath
11
11
  from os import system, remove
12
- from functools import cached_property
13
12
  import tomllib
14
13
 
15
14
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
16
15
  # >-|===|> Definitions <|===|-<
17
16
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
18
- unreadable_file_types = ['gz', 'tar', 'zip']
17
+ unreadable_file_types: list[str] = ['gz', 'tar', 'zip']
19
18
 
20
19
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
21
20
  # >-|===|> Functions <|===|-<
@@ -27,7 +26,16 @@ def ensure_path(path: str) -> None:
27
26
  path (str): the path you want to exist
28
27
  """
29
28
  system(f"mkdir -p {path}")
30
- def could_be_path(path: str) -> bool: return isdir('/'.join(path.split('/')[:2]))
29
+ def could_be_path(path: str) -> bool:
30
+ """determine if this is anywhere close to a valid path
31
+
32
+ Args:
33
+ path (str): the path (?) to check
34
+
35
+ Returns:
36
+ bool: True if the first two members of the path are valid else False.
37
+ """
38
+ return isdir('/'.join(path.split('/')[:2]))
31
39
 
32
40
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
33
41
  # >-|===|> Classes <|===|-<
@@ -141,7 +149,7 @@ class Folder:
141
149
  return None
142
150
  rmtree(self.path)
143
151
 
144
- def parse(path: str | list[str]) -> Folder | File:
152
+ def parse(path: str | list[str]) -> Folder | File | list[Folder | File]:
145
153
  """take a path or list of paths and turn them into Folder or File objects as appropriate.
146
154
 
147
155
  Args:
@@ -151,7 +159,7 @@ def parse(path: str | list[str]) -> Folder | File:
151
159
  FileNotFoundError: if you can't match path
152
160
 
153
161
  Returns:
154
- Folder | File: path as a Folder/File.
162
+ Folder | File | list[Folder|File]: path(s) as a Folder/File.
155
163
  """
156
164
  match path:
157
165
  case str():
@@ -5,6 +5,7 @@ from kbasic.bar import redirect_to_tqdm
5
5
  from kbasic.audio import success
6
6
  from kbasic.environment import isAnvil
7
7
  if isAnvil: from kbasic.environment.anvil import anvil_user
8
+ from typing import Any
8
9
  from subprocess import check_output, DEVNULL
9
10
  from tqdm import tqdm
10
11
  from time import sleep
@@ -17,22 +18,38 @@ import numpy as np
17
18
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
18
19
  # >-|===|> Definitions <|===|-<
19
20
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
20
- bad = ['\x1b[31m', '\x1b[34m', '\x1b[m']
21
- _USERNAME_ = "x-kgootkin" if not isAnvil else anvil_user
21
+ bad: list[str] = ['\x1b[31m', '\x1b[34m', '\x1b[m']
22
+ _USERNAME_: str = "x-kgootkin" if not isAnvil else anvil_user
22
23
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
23
24
  # >-|===|> Functions <|===|-<
24
25
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
25
- def parse_shell_output(output):
26
- match output:
27
- case str(): return output
28
- case [x]: return x
29
- case [x, *_]:
30
- for i in range(len(output)):
31
- for b in bad:
32
- if b in output[i]:
33
- output[i] = output[i].strip(b)
34
- return output
35
- def system(cmd: str):
26
+ def parse_shell_output(output: str | list[str]) -> str | list[str]:
27
+ """take the output of a shell command and make it nice
28
+
29
+ Args:
30
+ output (str | list[str]): the output of a shell command
31
+
32
+ Returns:
33
+ str | list[str]: a cleaned version of output
34
+ """
35
+ match output:
36
+ case str(): return output
37
+ case [x]: return x
38
+ case [x, *_]:
39
+ for i in range(len(output)):
40
+ for b in bad:
41
+ if b in output[i]:
42
+ output[i] = output[i].strip(b)
43
+ return output
44
+ def system(cmd: str) -> str | list[str]:
45
+ """a version of os.system that actually can return the output
46
+
47
+ Args:
48
+ cmd (str): the command to give the terminal
49
+
50
+ Returns:
51
+ str | list[str]: the output of the command (cleaned of color tags)
52
+ """
36
53
  command = cmd.split(' ') if type(cmd)==str else cmd
37
54
  for i in range(len(command)-1):
38
55
  if command[i].startswith('"'):
@@ -43,11 +60,24 @@ def system(cmd: str):
43
60
  for j in range(start_quote+1, end_quote+1): del command[j]
44
61
  output = parse_shell_output(check_output(command, stderr=DEVNULL).decode().splitlines())
45
62
  return output
46
- def anvil(cmd: str, username=_USERNAME_):
63
+ def anvil(cmd: str, username=_USERNAME_) -> str | list[str]:
64
+ """Send a shell command to anvil and parse the output
65
+
66
+ Args:
67
+ cmd (str): the command to send to anvil
68
+ username (str, optional): your anvil username. tries to parse your anvil username by default.
69
+
70
+ Returns:
71
+ str | list[str]: the output of the command (cleaned of color tags)
72
+ """
47
73
  output = parse_shell_output(check_output(['ssh', f'{username}@anvil.rcac.purdue.edu', *cmd.split(' ')], stderr=DEVNULL).decode().splitlines())
48
74
  return output
49
- async def anvil_async(cmd: str): asyncio.to_thread(anvil, cmd)
50
- def anvil_queue(username=_USERNAME_): return anvil(f"squeue -u {username}")
75
+ async def anvil_async(cmd: str):
76
+ """Just an asynchronous version of the anvil command"""
77
+ asyncio.to_thread(anvil, cmd)
78
+ def anvil_queue(username=_USERNAME_):
79
+ """Send a squeue command to anvil to check on your runs"""
80
+ return anvil(f"squeue -u {username}")
51
81
  qs = anvil_queue
52
82
 
53
83
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
@@ -57,7 +87,7 @@ qs = anvil_queue
57
87
  # >-|===|> Classes <|===|-<
58
88
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
59
89
  class AnvilJob:
60
- def __init__(self, queue_row: str, sep="DISTINCTSEPERATOR"):
90
+ def __init__(self, queue_row: str, sep="DISTINCTSEPERATOR") -> None:
61
91
  self.sep = sep
62
92
  [
63
93
  jobid, username, account, name, nodes, cpus, time_limit, status, time
@@ -72,7 +102,7 @@ class AnvilJob:
72
102
  self.status = str(status)
73
103
  self.time = str(time)
74
104
 
75
- def __repr__(self): return "-"*30 + f"\n{self.name}: {self.status}\n\t{self.time}/{self.time_limit}"
105
+ def __repr__(self) -> str: return "-"*30 + f"\n{self.name}: {self.status}\n\t{self.time}/{self.time_limit}"
76
106
 
77
107
  # def update(self, input=True, iter=True):
78
108
  # match input, iter:
@@ -85,11 +115,13 @@ class AnvilJob:
85
115
  # self.input = dHybridRinput(anvil(f"cat /anvil/scratch/{self.username}/sims/{self.name}/input/input"))
86
116
  # case False, True:
87
117
  # self.iter = int(anvil(f"ls /anvil/scratch/{self.username}/sims/{self.name}/Output/Fields/Magnetic/Total/x/")[-1][5:-3])
88
- def get_anvil_jobs(username=_USERNAME_):
118
+ def get_anvil_jobs(username=_USERNAME_) -> list[AnvilJob]:
119
+ """Parse the squeue and return AnvilJob objects"""
89
120
  q = anvil(f"squeue -u {username}")
90
121
  if type(q)==str: return []
91
122
  return [AnvilJob(x) for x in q[1:]]
92
123
  async def get_anvil_jobs_async(username=_USERNAME_):
124
+ """Just an asynchronous version of get_anvil_jobs"""
93
125
  q = await asyncio.to_thread(anvil, f"squeue -u {username}")
94
126
  if type(q)==str: return []
95
127
  return [AnvilJob(x) for x in q[1:]]
@@ -9,7 +9,8 @@
9
9
  # >-|===|> Imports <|===|-<
10
10
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
11
11
  from numpy import ndarray, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float16, float32, float64, longdouble, complex64, complex128, clongdouble
12
-
12
+ from collections.abc import Callable, Iterable
13
+ from typing import Any
13
14
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
14
15
  # >-|===|> Types <|===|-<
15
16
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
@@ -19,20 +20,6 @@ class Number:
19
20
  float, float16, float32, float64, longdouble,
20
21
  complex, complex64, complex128, clongdouble
21
22
  ]
22
- class Iterable:
23
- types: list = [
24
- list, set, dict, tuple, ndarray
25
- ]
26
-
27
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
28
- # >-|===|> Definitions <|===|-<
29
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
30
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
31
- # >-|===|> Functions <|===|-<
32
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
33
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
34
- # >-|===|> Decorators <|===|-<
35
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
36
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
37
- # >-|===|> Classes <|===|-<
38
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
23
+
24
+ def __new__(cls):
25
+ return cls
@@ -1,15 +1,20 @@
1
1
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
2
2
  # >-|===|> Imports <|===|-<
3
3
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
4
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
5
- # >-|===|> Types <|===|-<
6
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
7
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
8
- # >-|===|> Definitions <|===|-<
9
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
4
+ from kbasic.typing import Number
5
+ from typing import Any
6
+
10
7
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
11
8
  # >-|===|> Functions <|===|-<
12
9
  # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
10
+ def parse_user_input(response: str, sep: str = ',') -> Any:
11
+ match response:
12
+ case str(x) if x.isnumeric():
13
+ return Number.apply(response)
14
+ case str(x) if ',' in x:
15
+ return tuple(parse_user_input(xi.strip(), sep=sep) for xi in response.split(sep))
16
+ case _:
17
+ return response.lower().strip()
13
18
  def yesno(prompt: str):
14
19
  """
15
20
  prompt the user to either reply yes or no
@@ -33,10 +38,6 @@ def yesno(prompt: str):
33
38
  raise ValueError("need a response with either y or n in it.")
34
39
 
35
40
  return retry_yesno()
36
-
37
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
38
- # >-|===|> Decorators <|===|-<
39
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
40
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
41
- # >-|===|> Classes <|===|-<
42
- # !==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==!==
41
+ def interactive_set_attribute(obj: Any, attr: str, default_answer: str = "") -> None:
42
+ res: Any = parse_user_input(input(f"Set a value for {repr(obj)}.{attr}:\n\t"))
43
+ setattr(obj, attr, res)
File without changes
File without changes
File without changes