# Module `paroxython.goodies`

A grab bag of various generic functions and factories.

Expand source code Browse GitHub

    import sys
    from collections import defaultdict
    from math import log2
    from textwrap import wrap
    from typing import Callable, Dict, Tuple, Union
    from unicodedata import normalize

    import regex  # type: ignore

    from .user_types import Span

    # ANSI color codes
    OK = "\033[92m"
    WARNING = "\033[1m\033[38;5;166m"
    FAIL = "\033[1m\033[91m"
    RESET = "\033[0m"

    def title_to_slug_factory() -> Callable:
        slug_counts: Dict[str, int] = defaultdict(int)
        cache: Dict[str, str] = {}

        def title_to_slug(title, deduplicate=False):
            title = title.strip()
            if title not in cache:
                slug = normalize("NFD", title.lower()).encode("ASCII", "ignore").decode("ASCII")
                slug = slug.replace(" ", "-")
                slug = regex.sub(r"[^\w-]", "", slug)
                cache[title] = slug
            if deduplicate:
                slug = cache[title]
                slug_counts[slug] += 1
                slug = f"{slug}-{slug_counts[slug] - 1}"
                slug_counts[slug] += 1
                slug = slug.rstrip("-0")
                return slug
            else:
                return cache[title]

        return title_to_slug

    def add_line_numbers(source: str) -> str:
        if not source:
            return ""
        return "\n".join(f"{n: <4}{line}" for (n, line) in enumerate(source.split("\n"), 1))

    def enumeration_to_txt_factory(
        width: int = 20,
        default_string: str = "",
        sep: str = "<br>",
        template: str = "<details><summary>{summary}</summary>{details}</details>",
        initial_indent: int = 3,  # take into account the details marker
    ) -> Callable[[str], str]:
        def enumeration_to_txt(s: str) -> str:
            if not s:
                return default_string
            if len(s) <= width:
                return s
            lines = wrap(s, width, initial_indent=" " * initial_indent)
            summary = lines[0][initial_indent:]
            details = sep.join(lines[1:])
            return template.format(summary=summary, details=details)

        return enumeration_to_txt

    def cost_bucket(cost: int) -> str:
        if cost == 0:
            return "0"
        if cost < 0.25:
            return "in ]0, 0.25["
        if cost < 0.5:
            return "in [0.25, 0.5["
        if cost < 1:
            return "in [0.5, 1["
        upper = 2 ** int(log2(cost))
        lower = 2 ** int(log2(cost) + 1)
        return f"in [{upper}, {lower}["

    def couple_to_string(couple: Union[Span, Tuple[int, int]]) -> str:
        return f"{couple[0]}" + ("" if couple[0] == couple[1] else f"-{couple[1]}")

    def print_success(message: str):
        print(f"{OK}{message}{RESET}")

    def print_warning(message: str):
        print(f"{WARNING}Warning: {message}{RESET}", file=sys.stderr)

    def print_fail(message: str):
        print(f"{FAIL}Error: {message}{RESET}", file=sys.stderr)
        raise ValueError(message)

    def print_exit(message: str):
        sys.exit(f"{FAIL}Error: {message}{RESET}")

## Functions

`def title_to_slug_factory() ‑> Callable`

  Return a function mapping a string to the appropriate slug, optionally disambiguated.

  ## Returns

  `Callable[[str, Optional[bool]]]`
    A function taking a string and an optional boolean `deduplicate` (default: `False`). If `True`, when two or more strings produce the same slug, a numeric suffix is added to the second one (`"-1"`), the third one (`"-2"`), and so on.

  ## Examples

  See `test_goodies.py`.

  Expand source code Browse GitHub

      def title_to_slug_factory() -> Callable:
          slug_counts: Dict[str, int] = defaultdict(int)
          cache: Dict[str, str] = {}

          def title_to_slug(title, deduplicate=False):
              title = title.strip()
              if title not in cache:
                  slug = normalize("NFD", title.lower()).encode("ASCII", "ignore").decode("ASCII")
                  slug = slug.replace(" ", "-")
                  slug = regex.sub(r"[^\w-]", "", slug)
                  cache[title] = slug
              if deduplicate:
                  slug = cache[title]
                  slug_counts[slug] += 1
                  slug = f"{slug}-{slug_counts[slug] - 1}"
                  slug_counts[slug] += 1
                  slug = slug.rstrip("-0")
                  return slug
              else:
                  return cache[title]

          return title_to_slug

`def add_line_numbers(source: str) ‑> str`

  Return a numbered version of the given source code. Result readable up to 999 lines.

  Expand source code Browse GitHub

      def add_line_numbers(source: str) -> str:
          if not source:
              return ""
          return "\n".join(f"{n: <4}{line}" for (n, line) in enumerate(source.split("\n"), 1))

`def enumeration_to_txt_factory(width: int = 20,
default_string: str = '',
sep: str = '<br>',
template: str = '<details><summary>{summary}</summary>{details}</details>',
initial_indent: int = 3
) ‑> Callable[[str], str]`

  Return a function formatting an enumeration on a given column width.

  This function is used by `recommend_programs.get_markdown()` to limit the width of the last column of the Markdown tables, as in the following example:

  Cost | Taxon | Location
  --- | --- | ---
  0.9375 | `type/number/integer/literal` | 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3

  The enumeration, given as a string, is wrapped onto one or several lines of at most `width` characters. The first line and the remaining ones are formatted separately according to the given `template`.

  ## Args

  `width` :`int`, optional
    Column width. Defaults to `20`.
  `default_string` :`str`, optional
    String to return when the enumeration is empty. Defaults to `""`.
  `sep` :`str`, optional
    Separator between wrapped lines. Defaults to `"<br>"`.
  `template` :`str`, optional
    A formatting string on the first line (referred as `summary`) and the remaining ones (referred as `details`). Defaults to `"<details><summary>{summary}</summary>{details}</details>"`.
  `initial_indent` :`str`, optional
    Approximate space-width of the detail marker (e.g., `"▶︎ "`). Defaults to `3`.

  ## Returns

  `Callable[[str], str]`
    Function returning a wrapped version of a given string.

  ## Example

      >>> enumeration_to_txt = enumeration_to_txt_factory(7)  # create the function
      >>> enumeration_to_txt("1, 2, 3, 4, 5-6, 7, 8, 9")      # use it
      "<details><summary>1,</summary>2, 3,<br>4, 5-6,<br>7, 8, 9</details>"

  Expand source code Browse GitHub

      def enumeration_to_txt_factory(
          width: int = 20,
          default_string: str = "",
          sep: str = "<br>",
          template: str = "<details><summary>{summary}</summary>{details}</details>",
          initial_indent: int = 3,  # take into account the details marker
      ) -> Callable[[str], str]:
          def enumeration_to_txt(s: str) -> str:
              if not s:
                  return default_string
              if len(s) <= width:
                  return s
              lines = wrap(s, width, initial_indent=" " * initial_indent)
              summary = lines[0][initial_indent:]
              details = sep.join(lines[1:])
              return template.format(summary=summary, details=details)

          return enumeration_to_txt

`def cost_bucket(cost: int) ‑> str`

  Return an interval including a given positive number.

  The intervals are predefined as [0, 0], ]0, 0.25[, [2^{i-2}, 2^{i-1}[, \dots
  for all natural numbers i
  . The result is returned as a string ready to be included in the recommendation report.

  ## Examples

      >>> cost_bucket(0)
      "0"
      >>> cost_bucket(0.75)
      "in [0.5, 1["

  Expand source code Browse GitHub

      def cost_bucket(cost: int) -> str:
          if cost == 0:
              return "0"
          if cost < 0.25:
              return "in ]0, 0.25["
          if cost < 0.5:
              return "in [0.25, 0.5["
          if cost < 1:
              return "in [0.5, 1["
          upper = 2 ** int(log2(cost))
          lower = 2 ** int(log2(cost) + 1)
          return f"in [{upper}, {lower}["

`def couple_to_string(couple: Union[Span, Tuple[int, int]]
) ‑> str`

  Return a deduplicated string representation of the given couple or span.

  ## Examples

      >>> couple_to_string((12, 15))
      "12-15"
      >>> couple_to_string((12, 12))
      "12"
      >>> couple_to_string(Span(12, 15))
      "12-15"

  Expand source code Browse GitHub

      def couple_to_string(couple: Union[Span, Tuple[int, int]]) -> str:
          return f"{couple[0]}" + ("" if couple[0] == couple[1] else f"-{couple[1]}")

`def print_success(message: str)`

  Print the given message of success.

  Expand source code Browse GitHub

      def print_success(message: str):
          print(f"{OK}{message}{RESET}")

`def print_warning(message: str)`

  Print the given message on `sys.stderr`.

  Expand source code Browse GitHub

      def print_warning(message: str):
          print(f"{WARNING}Warning: {message}{RESET}", file=sys.stderr)

`def print_fail(message: str)`

  Print the given message on `sys.stderr` and raise a ValueError.

  Expand source code Browse GitHub

      def print_fail(message: str):
          print(f"{FAIL}Error: {message}{RESET}", file=sys.stderr)
          raise ValueError(message)

`def print_exit(message: str)`

  Print the given message on `sys.stderr` and exit.

  Expand source code Browse GitHub

      def print_exit(message: str):
          sys.exit(f"{FAIL}Error: {message}{RESET}")