File: _spec_helpers.py

package info (click to toggle)
python-pint 0.25.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,940 kB
  • sloc: python: 20,478; makefile: 148
file content (147 lines) | stat: -rw-r--r-- 4,446 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
pint.delegates.formatter._spec_helpers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Convenient functions to deal with format specifications.

:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

import functools
import re
import warnings
from collections.abc import Callable
from typing import Any

FORMATTER = Callable[
    [
        Any,
    ],
    str,
]

# Extract just the type from the specification mini-language: see
# http://docs.python.org/2/library/string.html#format-specification-mini-language
# We also add uS for uncertainties.
_BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS")

REGISTERED_FORMATTERS: dict[str, Any] = {}


def parse_spec(spec: str) -> str:
    """Parse and return spec.

    If an unknown item is found, raise a ValueError.

    This function still needs work:
    - what happens if two distinct values are found?

    """

    result = ""
    for ch in reversed(spec):
        if ch == "~" or ch in _BASIC_TYPES:
            continue
        elif ch in list(REGISTERED_FORMATTERS.keys()) + ["~"]:
            if result:
                raise ValueError("expected ':' after format specifier")
            else:
                result = ch
        elif ch.isalpha():
            raise ValueError("Unknown conversion specified " + ch)
        else:
            break
    return result


def extract_custom_flags(spec: str) -> str:
    """Return custom flags present in a format specification

    (i.e those not part of Python's formatting mini language)
    """

    if not spec:
        return ""

    # sort by length, with longer items first
    known_flags = sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True)

    flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")")
    custom_flags = flag_re.findall(spec)

    return "".join(custom_flags)


def remove_custom_flags(spec: str) -> str:
    """Remove custom flags present in a format specification

    (i.e those not part of Python's formatting mini language)
    """

    for flag in sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) + ["~"]:
        if flag:
            spec = spec.replace(flag, "")
    return spec


##########
# This weird way of defining split format
# is the only reasonable way I foudn to use
# lru_cache in a function that might emit warning
# and do it every time.
# TODO: simplify it when there are no warnings.


@functools.lru_cache
def _split_format(
    spec: str, default: str, separate_format_defaults: bool = True
) -> tuple[str, str, list[str]]:
    """Split format specification into magnitude and unit format."""
    mspec = remove_custom_flags(spec)
    uspec = extract_custom_flags(spec)

    default_mspec = remove_custom_flags(default)
    default_uspec = extract_custom_flags(default)

    warns = []
    if separate_format_defaults in (False, None):
        # should we warn always or only if there was no explicit choice?
        # Given that we want to eventually remove the flag again, I'd say yes?
        if spec and separate_format_defaults is None:
            if not uspec and default_uspec:
                warns.append(
                    "The given format spec does not contain a unit formatter."
                    " Falling back to the builtin defaults, but in the future"
                    " the unit formatter specified in the `default_format`"
                    " attribute will be used instead."
                )
            if not mspec and default_mspec:
                warns.append(
                    "The given format spec does not contain a magnitude formatter."
                    " Falling back to the builtin defaults, but in the future"
                    " the magnitude formatter specified in the `default_format`"
                    " attribute will be used instead."
                )
        elif not spec:
            mspec, uspec = default_mspec, default_uspec
    else:
        mspec = mspec or default_mspec
        uspec = uspec or default_uspec

    return mspec, uspec, warns


def split_format(
    spec: str, default: str, separate_format_defaults: bool = True
) -> tuple[str, str]:
    """Split format specification into magnitude and unit format."""

    mspec, uspec, warns = _split_format(spec, default, separate_format_defaults)

    for warn_msg in warns:
        warnings.warn(warn_msg, DeprecationWarning)

    return mspec, uspec