Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/faker/providers/misc/__init__.py: 18%
232 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import csv
2import hashlib
3import io
4import json
5import os
6import re
7import string
8import tarfile
9import uuid
10import zipfile
12from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
14from faker.exceptions import UnsupportedFeature
16from .. import BaseProvider
18localized = True
20csv.register_dialect("faker-csv", csv.excel, quoting=csv.QUOTE_ALL)
23class Provider(BaseProvider):
24 def boolean(self, chance_of_getting_true: int = 50) -> bool:
25 """Generate a random boolean value based on ``chance_of_getting_true``.
27 :sample: chance_of_getting_true=25
28 :sample: chance_of_getting_true=50
29 :sample: chance_of_getting_true=75
30 """
31 return self.generator.random.randint(1, 100) <= chance_of_getting_true
33 def null_boolean(self) -> Optional[bool]:
34 """Generate ``None``, ``True``, or ``False``, each with equal probability."""
36 return {
37 0: None,
38 1: True,
39 -1: False,
40 }[self.generator.random.randint(-1, 1)]
42 def binary(self, length: int = (1 * 1024 * 1024)) -> bytes:
43 """Generate a random binary blob of ``length`` bytes.
45 If this faker instance has been seeded, performance will be signficiantly reduced, to conform
46 to the seeding.
48 :sample: length=64
49 """
50 # If the generator has already been seeded, urandom can't be used
51 if self.generator._is_seeded:
52 blob = [self.generator.random.randrange(256) for _ in range(length)]
53 return bytes(blob)
55 # Generator is unseeded anyway, just use urandom
56 return os.urandom(length)
58 def md5(self, raw_output: bool = False) -> Union[bytes, str]:
59 """Generate a random MD5 hash.
61 If ``raw_output`` is ``False`` (default), a hexadecimal string representation of the MD5 hash
62 will be returned. If ``True``, a ``bytes`` object representation will be returned instead.
64 :sample: raw_output=False
65 :sample: raw_output=True
66 """
67 res: hashlib._Hash = hashlib.md5(str(self.generator.random.random()).encode())
68 if raw_output:
69 return res.digest()
70 return res.hexdigest()
72 def sha1(self, raw_output: bool = False) -> Union[bytes, str]:
73 """Generate a random SHA-1 hash.
75 If ``raw_output`` is ``False`` (default), a hexadecimal string representation of the SHA-1 hash
76 will be returned. If ``True``, a ``bytes`` object representation will be returned instead.
78 :sample: raw_output=False
79 :sample: raw_output=True
80 """
81 res: hashlib._Hash = hashlib.sha1(str(self.generator.random.random()).encode())
82 if raw_output:
83 return res.digest()
84 return res.hexdigest()
86 def sha256(self, raw_output: bool = False) -> Union[bytes, str]:
87 """Generate a random SHA-256 hash.
89 If ``raw_output`` is ``False`` (default), a hexadecimal string representation of the SHA-256 hash
90 will be returned. If ``True``, a ``bytes`` object representation will be returned instead.
92 :sample: raw_output=False
93 :sample: raw_output=True
94 """
95 res: hashlib._Hash = hashlib.sha256(str(self.generator.random.random()).encode())
96 if raw_output:
97 return res.digest()
98 return res.hexdigest()
100 def uuid4(
101 self,
102 cast_to: Optional[Union[Callable[[uuid.UUID], str], Callable[[uuid.UUID], bytes]]] = str,
103 ) -> Union[bytes, str, uuid.UUID]:
104 """Generate a random UUID4 object and cast it to another type if specified using a callable ``cast_to``.
106 By default, ``cast_to`` is set to ``str``.
108 May be called with ``cast_to=None`` to return a full-fledged ``UUID``.
110 :sample:
111 :sample: cast_to=None
112 """
113 # Based on http://stackoverflow.com/q/41186818
114 generated_uuid: uuid.UUID = uuid.UUID(int=self.generator.random.getrandbits(128), version=4)
115 if cast_to is not None:
116 return cast_to(generated_uuid)
117 return generated_uuid
119 def password(
120 self,
121 length: int = 10,
122 special_chars: bool = True,
123 digits: bool = True,
124 upper_case: bool = True,
125 lower_case: bool = True,
126 ) -> str:
127 """Generate a random password of the specified ``length``.
129 The arguments ``special_chars``, ``digits``, ``upper_case``, and ``lower_case`` control
130 what category of characters will appear in the generated password. If set to ``True``
131 (default), at least one character from the corresponding category is guaranteed to appear.
132 Special characters are characters from ``!@#$%^&*()_+``, digits are characters from
133 ``0123456789``, and uppercase and lowercase characters are characters from the ASCII set of
134 letters.
136 :sample: length=12
137 :sample: length=40, special_chars=False, upper_case=False
138 """
139 choices = ""
140 required_tokens = []
141 if special_chars: 141 ↛ 142line 141 didn't jump to line 142, because the condition on line 141 was never true
142 required_tokens.append(self.generator.random.choice("!@#$%^&*()_+"))
143 choices += "!@#$%^&*()_+"
144 if digits: 144 ↛ 147line 144 didn't jump to line 147, because the condition on line 144 was never false
145 required_tokens.append(self.generator.random.choice(string.digits))
146 choices += string.digits
147 if upper_case: 147 ↛ 150line 147 didn't jump to line 150, because the condition on line 147 was never false
148 required_tokens.append(self.generator.random.choice(string.ascii_uppercase))
149 choices += string.ascii_uppercase
150 if lower_case: 150 ↛ 154line 150 didn't jump to line 154, because the condition on line 150 was never false
151 required_tokens.append(self.generator.random.choice(string.ascii_lowercase))
152 choices += string.ascii_lowercase
154 assert len(required_tokens) <= length, "Required length is shorter than required characters"
156 # Generate a first version of the password
157 chars: str = self.random_choices(choices, length=length) # type: ignore
159 # Pick some unique locations
160 random_indexes: Set[int] = set()
161 while len(random_indexes) < len(required_tokens):
162 random_indexes.add(self.generator.random.randint(0, len(chars) - 1))
164 # Replace them with the required characters
165 for i, index in enumerate(random_indexes):
166 chars[index] = required_tokens[i] # type: ignore
168 return "".join(chars)
170 def zip(
171 self,
172 uncompressed_size: int = 65536,
173 num_files: int = 1,
174 min_file_size: int = 4096,
175 compression: Optional[str] = None,
176 ) -> bytes:
177 """Generate a bytes object containing a random valid zip archive file.
179 The number and sizes of files contained inside the resulting archive can be controlled
180 using the following arguments:
182 - ``uncompressed_size`` - the total size of files before compression, 16 KiB by default
183 - ``num_files`` - the number of files archived in resulting zip file, 1 by default
184 - ``min_file_size`` - the minimum size of each file before compression, 4 KiB by default
186 No compression is used by default, but setting ``compression`` to one of the values listed
187 below will use the corresponding compression type.
189 - ``'bzip2'`` or ``'bz2'`` for BZIP2
190 - ``'lzma'`` or ``'xz'`` for LZMA
191 - ``'deflate'``, ``'gzip'``, or ``'gz'`` for GZIP
193 :sample: uncompressed_size=256, num_files=4, min_file_size=32
194 :sample: uncompressed_size=256, num_files=32, min_file_size=4, compression='bz2'
195 """
196 if any(
197 [
198 not isinstance(num_files, int) or num_files <= 0,
199 not isinstance(min_file_size, int) or min_file_size <= 0,
200 not isinstance(uncompressed_size, int) or uncompressed_size <= 0,
201 ]
202 ):
203 raise ValueError(
204 "`num_files`, `min_file_size`, and `uncompressed_size` must be positive integers",
205 )
206 if min_file_size * num_files > uncompressed_size:
207 raise AssertionError(
208 "`uncompressed_size` is smaller than the calculated minimum required size",
209 )
210 if compression in ["bzip2", "bz2"]:
211 compression_ = zipfile.ZIP_BZIP2
212 elif compression in ["lzma", "xz"]:
213 compression_ = zipfile.ZIP_LZMA
214 elif compression in ["deflate", "gzip", "gz"]:
215 compression_ = zipfile.ZIP_DEFLATED
216 else:
217 compression_ = zipfile.ZIP_STORED
219 zip_buffer = io.BytesIO()
220 remaining_size = uncompressed_size
221 with zipfile.ZipFile(zip_buffer, mode="w", compression=compression_) as zip_handle:
222 for file_number in range(1, num_files + 1):
223 filename = self.generator.pystr() + str(file_number)
225 max_allowed_size = remaining_size - (num_files - file_number) * min_file_size
226 if file_number < num_files:
227 file_size = self.generator.random.randint(min_file_size, max_allowed_size)
228 remaining_size = remaining_size - file_size
229 else:
230 file_size = remaining_size
232 data = self.generator.binary(file_size)
233 zip_handle.writestr(filename, data)
234 return zip_buffer.getvalue()
236 def tar(
237 self,
238 uncompressed_size: int = 65536,
239 num_files: int = 1,
240 min_file_size: int = 4096,
241 compression: Optional[str] = None,
242 ) -> bytes:
243 """Generate a bytes object containing a random valid tar file.
245 The number and sizes of files contained inside the resulting archive can be controlled
246 using the following arguments:
248 - ``uncompressed_size`` - the total size of files before compression, 16 KiB by default
249 - ``num_files`` - the number of files archived in resulting zip file, 1 by default
250 - ``min_file_size`` - the minimum size of each file before compression, 4 KiB by default
252 No compression is used by default, but setting ``compression`` to one of the values listed
253 below will use the corresponding compression type.
255 - ``'bzip2'`` or ``'bz2'`` for BZIP2
256 - ``'lzma'`` or ``'xz'`` for LZMA
257 - ``'gzip'`` or ``'gz'`` for GZIP
259 :sample: uncompressed_size=256, num_files=4, min_file_size=32
260 :sample: uncompressed_size=256, num_files=32, min_file_size=4, compression='bz2'
261 """
262 if any(
263 [
264 not isinstance(num_files, int) or num_files <= 0,
265 not isinstance(min_file_size, int) or min_file_size <= 0,
266 not isinstance(uncompressed_size, int) or uncompressed_size <= 0,
267 ]
268 ):
269 raise ValueError(
270 "`num_files`, `min_file_size`, and `uncompressed_size` must be positive integers",
271 )
272 if min_file_size * num_files > uncompressed_size:
273 raise AssertionError(
274 "`uncompressed_size` is smaller than the calculated minimum required size",
275 )
276 if compression in ["gzip", "gz"]:
277 mode = "w:gz"
278 elif compression in ["bzip2", "bz2"]:
279 mode = "w:bz2"
280 elif compression in ["lzma", "xz"]:
281 mode = "w:xz"
282 else:
283 mode = "w"
285 tar_buffer = io.BytesIO()
286 remaining_size = uncompressed_size
287 with tarfile.open(mode=mode, fileobj=tar_buffer) as tar_handle:
288 for file_number in range(1, num_files + 1):
289 file_buffer = io.BytesIO()
290 filename = self.generator.pystr() + str(file_number)
292 max_allowed_size = remaining_size - (num_files - file_number) * min_file_size
293 if file_number < num_files:
294 file_size = self.generator.random.randint(min_file_size, max_allowed_size)
295 remaining_size = remaining_size - file_size
296 else:
297 file_size = remaining_size
299 tarinfo = tarfile.TarInfo(name=filename)
300 data = self.generator.binary(file_size)
301 file_buffer.write(data)
302 tarinfo.size = len(file_buffer.getvalue())
303 file_buffer.seek(0)
304 tar_handle.addfile(tarinfo, file_buffer)
305 file_buffer.close()
306 return tar_buffer.getvalue()
308 def image(
309 self,
310 size: Tuple[int, int] = (256, 256),
311 image_format: str = "png",
312 hue: Optional[Union[int, Sequence[int], str]] = None,
313 luminosity: Optional[str] = None,
314 ) -> bytes:
315 """Generate an image and draw a random polygon on it using the Python Image Library.
316 Without it installed, this provider won't be functional. Returns the bytes representing
317 the image in a given format.
319 The argument ``size`` must be a 2-tuple containing (width, height) in pixels. Defaults to 256x256.
321 The argument ``image_format`` can be any valid format to the underlying library like ``'tiff'``,
322 ``'jpeg'``, ``'pdf'`` or ``'png'`` (default). Note that some formats need present system libraries
323 prior to building the Python Image Library.
324 Refer to https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html for details.
326 The arguments ``hue`` and ``luminosity`` are the same as in the color provider and are simply forwarded to
327 it to generate both the background and the shape colors. Therefore, you can ask for a "dark blue" image, etc.
329 :sample: size=(2, 2), hue='purple', luminosity='bright', image_format='pdf'
330 :sample: size=(16, 16), hue=[90,270], image_format='ico'
331 """
332 try:
333 import PIL.Image
334 import PIL.ImageDraw
335 except ImportError:
336 raise UnsupportedFeature("`image` requires the `Pillow` python library.", "image")
338 (width, height) = size
339 image = PIL.Image.new("RGB", size, self.generator.color(hue=hue, luminosity=luminosity))
340 draw = PIL.ImageDraw.Draw(image)
341 draw.polygon(
342 [(self.random_int(0, width), self.random_int(0, height)) for _ in range(self.random_int(3, 12))],
343 fill=self.generator.color(hue=hue, luminosity=luminosity),
344 outline=self.generator.color(hue=hue, luminosity=luminosity),
345 )
346 with io.BytesIO() as fobj:
347 image.save(fobj, format=image_format)
348 fobj.seek(0)
349 return fobj.read()
351 def dsv(
352 self,
353 dialect: str = "faker-csv",
354 header: Optional[Sequence[str]] = None,
355 data_columns: Tuple[str, str] = ("{{name}}", "{{address}}"),
356 num_rows: int = 10,
357 include_row_ids: bool = False,
358 **fmtparams: Any,
359 ) -> str:
360 """Generate random delimiter-separated values.
362 This method's behavior share some similarities with ``csv.writer``. The ``dialect`` and
363 ``**fmtparams`` arguments are the same arguments expected by ``csv.writer`` to control its
364 behavior, and instead of expecting a file-like object to where output will be written, the
365 output is controlled by additional keyword arguments and is returned as a string.
367 The ``dialect`` argument defaults to ``'faker-csv'`` which is the name of a ``csv.excel``
368 subclass with full quoting enabled.
370 The ``header`` argument expects a list or a tuple of strings that will serve as the header row
371 if supplied. The ``data_columns`` argument expects a list or a tuple of string tokens, and these
372 string tokens will be passed to :meth:`pystr_format() <faker.providers.python.Provider.pystr_format>`
373 for data generation. Argument Groups are used to pass arguments to the provider methods.
374 Both ``header`` and ``data_columns`` must be of the same length.
376 Example:
377 fake.set_arguments('top_half', {'min_value': 50, 'max_value': 100})
378 fake.dsv(data_columns=('{{ name }}', '{{ pyint:top_half }}'))
380 The ``num_rows`` argument controls how many rows of data to generate, and the ``include_row_ids``
381 argument may be set to ``True`` to include a sequential row ID column.
383 :sample: dialect='excel', data_columns=('{{name}}', '{{address}}')
384 :sample: dialect='excel-tab', data_columns=('{{name}}', '{{address}}'), include_row_ids=True
385 :sample: data_columns=('{{name}}', '{{address}}'), num_rows=5, delimiter='$'
386 """
388 if not isinstance(num_rows, int) or num_rows <= 0:
389 raise ValueError("`num_rows` must be a positive integer")
390 if not isinstance(data_columns, (list, tuple)):
391 raise TypeError("`data_columns` must be a tuple or a list")
392 if header is not None:
393 if not isinstance(header, (list, tuple)):
394 raise TypeError("`header` must be a tuple or a list")
395 if len(header) != len(data_columns):
396 raise ValueError("`header` and `data_columns` must have matching lengths")
398 dsv_buffer = io.StringIO()
399 writer = csv.writer(dsv_buffer, dialect=dialect, **fmtparams)
401 if header:
402 if include_row_ids:
403 header = list(header)
404 header.insert(0, "ID")
405 writer.writerow(header)
407 for row_num in range(1, num_rows + 1):
408 row = [self.generator.pystr_format(column) for column in data_columns]
409 if include_row_ids:
410 row.insert(0, str(row_num))
412 writer.writerow(row)
414 return dsv_buffer.getvalue()
416 def csv(
417 self,
418 header: Optional[Sequence[str]] = None,
419 data_columns: Tuple[str, str] = ("{{name}}", "{{address}}"),
420 num_rows: int = 10,
421 include_row_ids: bool = False,
422 ) -> str:
423 """Generate random comma-separated values.
425 For more information on the different arguments of this method, please refer to
426 :meth:`dsv() <faker.providers.misc.Provider.dsv>` which is used under the hood.
428 :sample: data_columns=('{{name}}', '{{address}}'), num_rows=10, include_row_ids=False
429 :sample: header=('Name', 'Address', 'Favorite Color'),
430 data_columns=('{{name}}', '{{address}}', '{{safe_color_name}}'),
431 num_rows=10, include_row_ids=True
432 """
433 return self.dsv(
434 header=header,
435 data_columns=data_columns,
436 num_rows=num_rows,
437 include_row_ids=include_row_ids,
438 delimiter=",",
439 )
441 def tsv(
442 self,
443 header: Optional[Sequence[str]] = None,
444 data_columns: Tuple[str, str] = ("{{name}}", "{{address}}"),
445 num_rows: int = 10,
446 include_row_ids: bool = False,
447 ) -> str:
448 """Generate random tab-separated values.
450 For more information on the different arguments of this method, please refer to
451 :meth:`dsv() <faker.providers.misc.Provider.dsv>` which is used under the hood.
453 :sample: data_columns=('{{name}}', '{{address}}'), num_rows=10, include_row_ids=False
454 :sample: header=('Name', 'Address', 'Favorite Color'),
455 data_columns=('{{name}}', '{{address}}', '{{safe_color_name}}'),
456 num_rows=10, include_row_ids=True
457 """
458 return self.dsv(
459 header=header,
460 data_columns=data_columns,
461 num_rows=num_rows,
462 include_row_ids=include_row_ids,
463 delimiter="\t",
464 )
466 def psv(
467 self,
468 header: Optional[Sequence[str]] = None,
469 data_columns: Tuple[str, str] = ("{{name}}", "{{address}}"),
470 num_rows: int = 10,
471 include_row_ids: bool = False,
472 ) -> str:
473 """Generate random pipe-separated values.
475 For more information on the different arguments of this method, please refer to
476 :meth:`dsv() <faker.providers.misc.Provider.dsv>` which is used under the hood.
478 :sample: data_columns=('{{name}}', '{{address}}'), num_rows=10, include_row_ids=False
479 :sample: header=('Name', 'Address', 'Favorite Color'),
480 data_columns=('{{name}}', '{{address}}', '{{safe_color_name}}'),
481 num_rows=10, include_row_ids=True
482 """
483 return self.dsv(
484 header=header,
485 data_columns=data_columns,
486 num_rows=num_rows,
487 include_row_ids=include_row_ids,
488 delimiter="|",
489 )
491 def json(self, data_columns: Optional[List] = None, num_rows: int = 10, indent: Optional[int] = None) -> str:
492 """
493 Generate random JSON structure values.
495 Using a dictionary or list of records that is passed as ``data_columns``,
496 define the structure that is used to build JSON structures. For complex
497 data structures it is recommended to use the dictionary format.
499 Data Column Dictionary format:
500 {'key name': 'definition'}
502 The definition can be 'provider', 'provider:argument_group', tokenized
503 'string {{ provider:argument_group }}' that is passed to the python
504 provider method pystr_format() for generation, or a fixed '@word'.
505 Using Lists, Tuples, and Dicts as a definition for structure.
507 Example:
508 fake.set_arguments('top_half', {'min_value': 50, 'max_value': 100})
509 fake.json(data_columns={'Name': 'name', 'Score': 'pyint:top_half'})
511 Data Column List format:
512 [('key name', 'definition', {'arguments'})]
514 With the list format the definition can be a list of records, to create
515 a list within the structure data. For literal entries within the list,
516 set the 'field_name' to None.
518 :param data_columns: specification for the data structure
519 :type data_columns: dict
520 :param num_rows: number of rows the returned
521 :type num_rows: int
522 :param indent: number of spaces to indent the fields
523 :type indent: int
524 :return: Serialized JSON data
525 :rtype: str
527 :sample: data_columns={'Spec': '@1.0.1', 'ID': 'pyint',
528 'Details': {'Name': 'name', 'Address': 'address'}}, num_rows=2
529 :sample: data_columns={'Candidates': ['name', 'name', 'name']},
530 num_rows=1
531 :sample: data_columns=[('Name', 'name'), ('Points', 'pyint',
532 {'min_value': 50, 'max_value': 100})], num_rows=1
533 """
534 default_data_columns = {
535 "name": "{{name}}",
536 "residency": "{{address}}",
537 }
538 data_columns: Union[List, Dict] = data_columns if data_columns else default_data_columns
540 def process_list_structure(data: Sequence[Any]) -> Any:
541 entry: Dict[str, Any] = {}
543 for name, definition, *arguments in data:
544 kwargs = arguments[0] if arguments else {}
546 if not isinstance(kwargs, dict):
547 raise TypeError("Invalid arguments type. Must be a dictionary")
549 if name is None:
550 return self._value_format_selection(definition, **kwargs)
552 if isinstance(definition, tuple):
553 entry[name] = process_list_structure(definition)
554 elif isinstance(definition, (list, set)):
555 entry[name] = [process_list_structure([item]) for item in definition]
556 else:
557 entry[name] = self._value_format_selection(definition, **kwargs)
558 return entry
560 def process_dict_structure(data: Union[int, float, bool, Dict[str, Any]]) -> Any:
561 entry: Dict[str, Any] = {}
563 if isinstance(data, str):
564 return self._value_format_selection(data)
566 if isinstance(data, dict):
567 for name, definition in data.items():
568 if isinstance(definition, (tuple, list, set)):
569 entry[name] = [process_dict_structure(item) for item in definition]
570 elif isinstance(definition, (dict, int, float, bool)):
571 entry[name] = process_dict_structure(definition)
572 else:
573 entry[name] = self._value_format_selection(definition)
574 return entry
576 return data
578 def create_json_structure(data_columns: Union[Dict, List]) -> dict:
579 if isinstance(data_columns, dict):
580 return process_dict_structure(data_columns)
582 if isinstance(data_columns, list):
583 return process_list_structure(data_columns)
585 raise TypeError("Invalid data_columns type. Must be a dictionary or list")
587 if num_rows == 1:
588 return json.dumps(create_json_structure(data_columns), indent=indent)
590 data = [create_json_structure(data_columns) for _ in range(num_rows)]
591 return json.dumps(data, indent=indent)
593 def fixed_width(self, data_columns: Optional[list] = None, num_rows: int = 10, align: str = "left") -> str:
594 """
595 Generate random fixed width values.
597 Using a list of tuple records that is passed as ``data_columns``, that
598 defines the structure that will be generated. Arguments within the
599 record are provider specific, and should be a dictionary that will be
600 passed to the provider method.
602 Data Column List format
603 [('field width', 'definition', {'arguments'})]
605 The definition can be 'provider', 'provider:argument_group', tokenized
606 'string {{ provider:argument_group }}' that is passed to the python
607 provider method pystr_format() for generation, or a fixed '@word'.
608 Using Lists, Tuples, and Dicts as a definition for structure.
610 Argument Groups can be used to pass arguments to the provider methods,
611 but will override the arguments supplied in the tuple record.
613 Example:
614 fake.set_arguments('top_half', {'min_value': 50, 'max_value': 100})
615 fake.fixed_width(data_columns=[(20, 'name'), (3, 'pyint:top_half')])
617 :param data_columns: specification for the data structure
618 :type data_columns: list
619 :param num_rows: number of rows the generator will yield
620 :type num_rows: int
621 :param align: positioning of the value. (left, middle, right)
622 :type align: str
623 :return: Serialized Fixed Width data
624 :rtype: str
626 :sample: data_columns=[(20, 'name'), (3, 'pyint', {'min_value': 50,
627 'max_value': 100})], align='right', num_rows=2
628 """
629 default_data_columns = [
630 (20, "name"),
631 (3, "pyint", {"max_value": 20}),
632 ]
633 data_columns = data_columns if data_columns else default_data_columns
634 align_map = {
635 "left": "<",
636 "middle": "^",
637 "right": ">",
638 }
639 data = []
641 for _ in range(num_rows):
642 row = []
644 for width, definition, *arguments in data_columns:
645 kwargs = arguments[0] if arguments else {}
647 if not isinstance(kwargs, dict):
648 raise TypeError("Invalid arguments type. Must be a dictionary")
650 result = self._value_format_selection(definition, **kwargs)
651 row.append(f'{result:{align_map.get(align, "<")}{width}}'[:width])
653 data.append("".join(row))
654 return "\n".join(data)
656 def _value_format_selection(self, definition: str, **kwargs: Any) -> Union[int, str]:
657 """
658 Formats the string in different ways depending on it's contents.
660 The return can be the '@word' itself, a '{{ token }}' passed to PyStr,
661 or a 'provider:argument_group' format field that returns potentially
662 a non-string type.
664 This ensures that Numbers, Boolean types that are generated in the
665 JSON structures in there proper type, and not just strings.
666 """
668 # Check for PyStr first as complex strings may start with @
669 if re.match(r".*\{\{.*\}\}.*", definition):
670 return self.generator.pystr_format(definition)
672 # Check for fixed @words that won't be generated
673 if re.match(r"^@.*", definition):
674 return definition.lstrip("@")
676 # Check if a argument group has been supplied
677 if re.match(r"^[a-zA-Z0-9_-]*:\w", definition):
678 definition, argument_group = definition.split(":")
679 arguments = self.generator.get_arguments(argument_group.strip())
681 return self.generator.format(definition.strip(), **arguments)
683 # Assume the string is refering to a provider
684 return self.generator.format(definition, **kwargs)