summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/faker/sphinx/docstring.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.11/site-packages/faker/sphinx/docstring.py')
-rw-r--r--venv/lib/python3.11/site-packages/faker/sphinx/docstring.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/faker/sphinx/docstring.py b/venv/lib/python3.11/site-packages/faker/sphinx/docstring.py
new file mode 100644
index 0000000..813dbf4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/faker/sphinx/docstring.py
@@ -0,0 +1,224 @@
+# coding=utf-8
+import inspect
+import logging
+import re
+
+from collections import namedtuple
+from typing import Pattern
+
+from faker import Faker
+from faker.config import AVAILABLE_LOCALES, DEFAULT_LOCALE
+from faker.sphinx.validator import SampleCodeValidator
+
+logger = logging.getLogger(__name__)
+_fake = Faker(AVAILABLE_LOCALES)
+_base_provider_method_pattern: Pattern = re.compile(r"^faker\.providers\.BaseProvider\.(?P<method>\w+)$")
+_standard_provider_method_pattern: Pattern = re.compile(r"^faker\.providers\.\w+\.Provider\.(?P<method>\w+)$")
+_locale_provider_method_pattern: Pattern = re.compile(
+ r"^faker\.providers\.\w+" r"\.(?P<locale>[a-z]{2,3}_[A-Z]{2})" r"\.Provider" r"\.(?P<method>\w+)$",
+)
+_sample_line_pattern: Pattern = re.compile(
+ r"^:sample" r"(?: size=(?P<size>[1-9][0-9]*))?" r"(?: seed=(?P<seed>[0-9]+))?" r":" r"(?: ?(?P<kwargs>.*))?$",
+)
+_command_template = "generator.{method}({kwargs})"
+_sample_output_template = (
+ ">>> Faker.seed({seed})\n"
+ ">>> for _ in range({size}):\n"
+ "... fake.{method}({kwargs})\n"
+ "...\n"
+ "{results}\n\n"
+)
+
+DEFAULT_SAMPLE_SIZE = 5
+DEFAULT_SEED = 0
+Sample = namedtuple("Sample", ["size", "seed", "kwargs"])
+
+
+class ProviderMethodDocstring:
+ """
+ Class that preprocesses provider method docstrings to generate sample usage and output
+
+ Notes on how samples are generated:
+ - If the docstring belongs to a standard provider method, sample usage and output will be
+ generated using a `Faker` object in the `DEFAULT_LOCALE`.
+ - If the docstring belongs to a localized provider method, the correct locale will be used.
+ - If the docstring does not belong to any provider method, docstring preprocessing will be skipped.
+ - Docstring lines will be parsed for potential sample sections, and the generation details of each
+ sample section will internally be represented as a ``Sample`` namedtuple.
+ - Each ``Sample`` will have info on the keyword arguments to pass to the provider method, how many
+ times the provider method will be called, and the initial seed value to ``Faker.seed()``.
+ """
+
+ def __init__(self, app, what, name, obj, options, lines):
+ self._line_iter = iter(lines)
+ self._parsed_lines = []
+ self._samples = []
+ self._skipped = True
+ self._log_prefix = f"{inspect.getfile(obj)}:docstring of {name}: WARNING:"
+
+ if what != "method":
+ return
+
+ base_provider_method_match = _base_provider_method_pattern.match(name)
+ locale_provider_method_match = _locale_provider_method_pattern.match(name)
+ standard_provider_method_match = _standard_provider_method_pattern.match(name)
+ if base_provider_method_match:
+ groupdict = base_provider_method_match.groupdict()
+ self._method = groupdict["method"]
+ self._locale = DEFAULT_LOCALE
+ elif standard_provider_method_match:
+ groupdict = standard_provider_method_match.groupdict()
+ self._method = groupdict["method"]
+ self._locale = DEFAULT_LOCALE
+ elif locale_provider_method_match:
+ groupdict = locale_provider_method_match.groupdict()
+ self._method = groupdict["method"]
+ self._locale = groupdict["locale"]
+ else:
+ return
+
+ self._skipped = False
+ self._parse()
+ self._generate_samples()
+
+ def _log_warning(self, warning):
+ logger.warning(f"{self._log_prefix} {warning}")
+
+ def _parse(self):
+ while True:
+ try:
+ line = next(self._line_iter)
+ except StopIteration:
+ break
+ else:
+ self._parse_section(line)
+
+ def _parse_section(self, section):
+ # No-op if section does not look like the start of a sample section
+ if not section.startswith(":sample"):
+ self._parsed_lines.append(section)
+ return
+
+ try:
+ next_line = next(self._line_iter)
+ except StopIteration:
+ # No more lines left to consume, so save current sample section
+ self._process_sample_section(section)
+ return
+
+ # Next line is the start of a new sample section, so process
+ # current sample section, and start parsing the new section
+ if next_line.startswith(":sample"):
+ self._process_sample_section(section)
+ self._parse_section(next_line)
+
+ # Next line is an empty line indicating the end of
+ # current sample section, so process current section
+ elif next_line == "":
+ self._process_sample_section(section)
+
+ # Section is assumed to be multiline, so continue
+ # adding lines to current sample section
+ else:
+ section = section + next_line
+ self._parse_section(section)
+
+ def _process_sample_section(self, section):
+ match = _sample_line_pattern.match(section)
+
+ # Discard sample section if malformed
+ if not match:
+ msg = f"The section `{section}` is malformed and will be discarded."
+ self._log_warning(msg)
+ return
+
+ # Set sample generation defaults and do some beautification if necessary
+ groupdict = match.groupdict()
+ size = groupdict.get("size")
+ seed = groupdict.get("seed")
+ kwargs = groupdict.get("kwargs")
+ size = max(int(size), DEFAULT_SAMPLE_SIZE) if size else DEFAULT_SAMPLE_SIZE
+ seed = int(seed) if seed else DEFAULT_SEED
+ kwargs = self._beautify_kwargs(kwargs) if kwargs else ""
+
+ # Store sample generation details
+ sample = Sample(size, seed, kwargs)
+ self._samples.append(sample)
+
+ def _beautify_kwargs(self, kwargs):
+ def _repl_whitespace(match):
+ quoted = match.group(1) or match.group(2)
+ return quoted if quoted else ""
+
+ def _repl_comma(match):
+ quoted = match.group(1) or match.group(2)
+ return quoted if quoted else ", "
+
+ # First, remove all whitespaces and tabs not within quotes
+ result = re.sub(r'("[^"]*")|(\'[^\']*\')|[ \t]+', _repl_whitespace, kwargs)
+
+ # Next, insert a whitespace after each comma not within quotes
+ result = re.sub(r'("[^"]*")|(\'[^\']*\')|,', _repl_comma, result)
+
+ # Then return the result with all leading and trailing whitespaces stripped
+ return result.strip()
+
+ def _stringify_result(self, value):
+ return repr(value)
+
+ def _generate_eval_scope(self):
+ from collections import OrderedDict # noqa: F401 Do not remove! The eval command needs this reference.
+
+ return {
+ "generator": _fake[self._locale],
+ "OrderedDict": OrderedDict,
+ }
+
+ def _inject_default_sample_section(self):
+ default_sample = Sample(DEFAULT_SAMPLE_SIZE, DEFAULT_SEED, "")
+ self._samples.append(default_sample)
+
+ def _generate_samples(self):
+ if not self._samples:
+ self._inject_default_sample_section()
+
+ output = ""
+ eval_scope = self._generate_eval_scope()
+ for sample in self._samples:
+ command = _command_template.format(method=self._method, kwargs=sample.kwargs)
+ validator = SampleCodeValidator(command)
+ if validator.errors:
+ msg = (
+ f"Invalid code elements detected. Sample generation will be "
+ f"skipped for method `{self._method}` with arguments `{sample.kwargs}`."
+ )
+ self._log_warning(msg)
+ continue
+
+ try:
+ Faker.seed(sample.seed)
+ results = "\n".join([self._stringify_result(eval(command, eval_scope)) for _ in range(sample.size)])
+ except Exception:
+ msg = f"Sample generation failed for method `{self._method}` with arguments `{sample.kwargs}`."
+ self._log_warning(msg)
+ continue
+ else:
+ output += _sample_output_template.format(
+ seed=sample.seed,
+ method=self._method,
+ kwargs=sample.kwargs,
+ size=sample.size,
+ results=results,
+ )
+
+ if output:
+ output = ":examples:\n\n" + output
+ self._parsed_lines.extend(output.split("\n"))
+
+ @property
+ def skipped(self):
+ return self._skipped
+
+ @property
+ def lines(self):
+ return self._parsed_lines