Passed
Push — main ( 15d22f...4e3485 )
by Douglas
01:39
created

  A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 4
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
import logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import math
3
from datetime import date, datetime, timedelta
4
from typing import SupportsFloat
5
6
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
7
from pint import Quantity, UnitRegistry
0 ignored issues
show
introduced by
Unable to import 'pint'
Loading history...
8
from pint.errors import PintTypeError
0 ignored issues
show
introduced by
Unable to import 'pint.errors'
Loading history...
9
10
from pocketutils.core.exceptions import OutOfRangeError, StringPatternError
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
11
from pocketutils.tools.string_tools import StringTools
12
13
logger = logging.getLogger("pocketutils")
14
_UNIT_REG = UnitRegistry()
15
16
17
class UnitTools:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
18
    @classmethod
19
    def format_approx_big_number(cls, n: int) -> str:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "n" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
introduced by
Missing function or method docstring
Loading history...
20
        for k, v in {1e15: "", 1e12: "T", 1e9: "B", 1e6: "M", 1e3: "k"}.items():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
21
            if n >= k:
22
                return str(n // k) + v
23
        return str(n)
24
25
    @classmethod
26
    def approx_time_wrt(
0 ignored issues
show
best-practice introduced by
Too many return statements (7/6)
Loading history...
27
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
28
        now: date | datetime,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
29
        then: date | datetime,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
30
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
31
        skip_today: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
32
        sig: int = 3,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
33
    ) -> str:
34
        """
35
        Describes ``then`` with higher resolution for smaller differences to ``now``.
36
37
        Examples:
38
            - ``approx_time_wrt(date(2021, 1, 12), date(1996, 10, 1))  # "1996"``
39
            - ``approx_time_wrt(date(2021, 1, 12), date(2021, 10, 1))  # "2021-01-12"``
40
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 55))  # "2021-01-12 11:55"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (105/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
41
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 30, 222222))  # "2021-01-12 00:00:30"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (122/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
42
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 2, 222222))  # "2021-01-12 00:00:02.222"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (125/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
43
            - ``approx_time_wrt(date(2021, 10, 1), datetime(2021, 10, 1, 11, 0, 0, 2, 22))  # "2021-01-12 00:00:02.000022"``
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (124/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
44
        """
45
        delta = now - then if now > then else then - now
46
        tot_days = (delta.days) + (delta.seconds / 86400) + (delta.microseconds / 86400 / 10**6)
47
        tot_secs = tot_days * 86400
48
        _today = "" if skip_today and then.date() == now.date() else "%Y-%m-%d "
49
        if tot_days > sig * 365.24219:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
50
            return str(then.year)
51
        elif tot_days > sig * 30.437:
52
            return then.strftime("%Y-%m")
53
        elif tot_days > sig:
54
            return then.strftime("%Y-%m-%d")
55
        elif tot_secs > sig * 60:
56
            return then.strftime(_today + "%H:%M")
57
        elif tot_secs > sig:
58
            return then.strftime(_today + "%H:%M:%S")
59
        elif tot_secs > sig / 1000:
60
            return then.strftime(_today + "%H:%M:%S") + "." + str(round(then.microsecond / 1000))
61
        else:
62
            return then.strftime(_today + "%H:%M:%S.%f")
63
64
    @classmethod
65
    def delta_time_to_str(cls, delta_sec: float | timedelta, *, space: str = "") -> str:
66
        """
67
        Returns a pretty string from a difference in time in seconds.
68
        Rounds hours and minutes to 2 decimal places, and seconds to 1.
69
        Ex: delta_time_to_str(313) == 5.22min
70
            delta_sec: The time in seconds
71
            space: Space char between digits and units;
72
                   good choices are empty, ASCII space, Chars.narrownbsp, Chars.thinspace,
73
                   and Chars.nbsp.
74
75
        Returns:
76
            A string with units 'hr', 'min', or 's'
77
        """
78
        if isinstance(delta_sec, timedelta):
79
            delta_sec = delta_sec.total_seconds()
80
        if abs(delta_sec) > 60 * 60 * 3:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
81
            return (
82
                StringTools.strip_empty_decimal(str(round(delta_sec / 60 / 60, 2))) + space + "hr"
83
            )
84
        elif abs(delta_sec) > 180:
85
            return StringTools.strip_empty_decimal(str(round(delta_sec / 60, 2))) + space + "min"
86
        else:
87
            return StringTools.strip_empty_decimal(str(round(delta_sec, 1))) + space + "s"
88
89
    @classmethod
90
    def ms_to_minsec(cls, ms: int, space: str = "") -> str:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "ms" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
91
        """
92
        Converts a number of milliseconds to one of the following formats.
93
        Will be one of these:
94
            - 10ms         if < 1 sec
95
            - 10:15        if < 1 hour
96
            - 10:15:33     if < 1 day
97
            - 5d:10:15:33  if > 1 day
98
        Prepends a minus sign (−) if negative.
99
100
        Args:
101
            ms: The milliseconds
102
            space: Space char between digits and 'ms' (if used);
103
                   good choices are empty, ASCII space, Chars.narrownbsp,
104
                   Chars.thinspace, and Chars.nbsp.
105
106
        Returns:
107
            A string of one of the formats above
108
        """
109
        ms = abs(int(ms))
110
        seconds = int((ms / 1000) % 60)
111
        minutes = int((ms / (1000 * 60)) % 60)
112
        hours = int((ms / (1000 * 60 * 60)) % 24)
113
        days = int(ms / (1000 * 60 * 60 * 24))
114
        z_hr = str(hours).zfill(2)
115
        z_min = str(minutes).zfill(2)
116
        z_sec = str(seconds).zfill(2)
117
        sgn = "−" if ms < 0 else ""
118
        if ms < 1000:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
119
            return f"{sgn}{ms}{space}ms"
120
        elif days > 1:
121
            return f"{days}d:{z_hr}:{z_min}:{z_sec}"
122
        elif hours > 1:
123
            return f"{sgn}{z_hr}:{z_min}:{z_sec}"
124
        else:
125
            return f"{sgn}{z_min}:{z_sec}"
126
127
    @classmethod
128
    def round_to_sigfigs(cls, num: SupportsFloat, sig_figs: int | None) -> float:
129
        """
130
        Round to specified number of sigfigs.
131
132
        Args:
133
            num: A Python or Numpy float or something that supports __float__
134
            sig_figs: The number of significant figures, non-negative
135
136
        Returns:
137
            A Python integer
138
        """
139
        if sig_figs is None:
140
            return float(num)
141
        if sig_figs < 0:
142
            raise OutOfRangeError(f"sig_figs {sig_figs} is negative", minimum=0)
143
        num = float(num)
144
        if num != 0:
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
145
            digits = -int(math.floor(math.log10(abs(num))) - (sig_figs - 1))
146
            return round(num, digits)
147
        else:
148
            return 0  # can't take the log of 0
149
150
    @classmethod
151
    def format_micromolar(
152
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
153
        micromolar: float,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
154
        n_sigfigs: int | None = 5,
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
155
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
156
        adjust_units: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
157
        use_sigfigs: bool = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
158
        space: str = "",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
159
    ) -> str:
160
        """
161
        Returns a concentration with units, with the units scaled as needed.
162
        Can handle millimolar, micromolar, nanomolar, and picomolar.
163
164
        Args:
165
            micromolar: Value
166
            n_sigfigs: For rounding; no rounding if None
167
            adjust_units: If False, will always use micromolar
168
            use_sigfigs: If True, rounds to a number of significant figures; otherwise round to decimal places
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (110/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
169
            space: Space char between digits and units;
170
                   good choices are empty, ASCII space,
171
                   :attr:`pocketutils.core.chars.Chars.narrownbsp`,
172
                   :attr:`pocketutils.core.chars.Chars.thinspace`,
173
                   and :attr:`pocketutils.core.chars.Chars.nbsp`.
174
175
        Returns:
176
            The concentration with a suffix of µM, mM, nM, or mM
177
        """
178
        d = micromolar
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
179
        m = abs(d)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "m" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
180
        unit = "µM"
181
        if adjust_units:
182
            if m < 1e-6:
183
                d *= 1e9
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
184
                unit = "fM"
185
            elif m < 1e-3:
186
                d *= 1e6
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
187
                unit = "pM"
188
            elif m < 1:
189
                d *= 1e3
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
190
                unit = "nM"
191
            elif m >= 1e6:
192
                d /= 1e6
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
193
                unit = "M"
194
            elif m >= 1e3:
195
                d /= 1e3
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
196
                unit = "mM"
197
        if n_sigfigs is None:
198
            pass
199
        elif use_sigfigs:
200
            d = cls.round_to_sigfigs(d, n_sigfigs)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
201
        else:
202
            d = round(d, n_sigfigs)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
203
        if round(d) == d and str(d).endswith(".0"):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
204
            return str(d)[:-2] + space + unit
205
        else:
206
            return str(d) + space + unit
207
208
    @classmethod
209
    def split_species_micromolar(cls, text: str) -> tuple[str, float | None]:
0 ignored issues
show
introduced by
Value 'tuple' is unsubscriptable
Loading history...
210
        """
211
        Splits a name into a chemical/concentration pair, falling back with the full name.
212
        Ex: "abc 3.5uM" → (abc, 3.5)
213
        Ex: "abc 3.5 µM" → (abc, 3.5)
214
        Ex: "abc (3.5mM)" → (abc, 3500.0)
215
        Ex: "abc 3.5mM" → (abc, None)
216
        Ex: "3.5mM" → (3.5mM, None)  # an edge case: don't pass in only units
217
        Uses a moderately strict pattern for the drug and dose:
218
            - The dose must terminate the string, except for end parenthesis or whitespace.
219
            - The drug and dose must be separated by at least one non-alphanumeric, non-dot, non-hyphen character.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (114/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
220
            - Units must follow the digits, separated by at most whitespace, and are case-sensitive.
221
        """
222
        # lazy ops in the first group and in the non-(alphanumeric/dot/dash) separator between the drug and dose
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (112/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
223
        pat = regex.compile(
224
            r"^\s*(.*?)(?:[^A-Za-z0-9.\-]+?[\s(\[{]*(\d+(?:.\d*)?)\s*([mµunpf]M)\s*[)\]}]*)?\s*$",
225
            flags=regex.V1,
226
        )
227
        match = pat.fullmatch(text)
228
        if match is None:
229
            raise StringPatternError(f"Text {text} couldn't be parsed", value=text, pattern=pat)
230
        if match.group(2) is None:
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
231
            return text.strip(), None
232
        else:
233
            drug = match.group(1).strip("([{)]}")
234
            dose = UnitTools.concentration_to_micromolar(float(match.group(2)), match.group(3))
235
            return drug, dose
236
237
    @classmethod
238
    def extract_micromolar(cls, text: str) -> float | None:
239
        """
240
        Returns what looks like a concentration with units. Accepts one of: mM, µM, uM, nM, pM.
241
        Searches pretty flexibly.
242
        If no matches are found, returns None.
243
        If multiple matches are found, warns and returns None.
244
        """
245
        # we need to make sure mM ex isn't part of a larger name
246
        pat1 = regex.compile(r"(\d+(?:.\d*)?)\s*([mµunpf]M)\s*[)\]}]*", flags=regex.V1)
247
248
        def find(pat):
249
            return {
250
                UnitTools.concentration_to_micromolar(float(match.group(1)), match.group(2))
251
                for match in pat.finditer(text)
252
                if match is not None
253
            }
254
255
        matches = find(pat1)
256
        if len(matches) == 1:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
257
            return next(iter(matches))
258
        elif len(matches) > 1:
259
            logger.warning(f"Found {len(matches)} potential doses: {matches} . Returning None.")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
260
        return None
261
262
    @classmethod
263
    def concentration_to_micromolar(cls, digits: SupportsFloat, units: str) -> float:
264
        """
265
        Converts a concentration with units to micromolar.
266
267
        Args:
268
            digits: Float or float-compatible value
269
            units: Units that ``digits`` are in
270
271
        Example:
272
            .. code-block::
273
274
                concentration_to_micromolar(53, 'nM')  # returns 0.053
275
276
        See Also:
277
            :meth:`extract_micromolar`
278
        """
279
        return (
280
            float(digits)
281
            * {
282
                "M": 1e6,
283
                "mM": 1e3,
284
                "µM": 1,
285
                "uM": 1,
286
                "nM": 1e-3,
287
                "pM": 1e-6,
288
                "fM": 1e-9,
289
            }[units]
290
        )
291
292
    @classmethod
293
    def canonicalize_quantity(cls, s: str, dimensionality: str) -> Quantity:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "s" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
294
        """
295
        Returns a quantity in reduced units from a magnitude with units.
296
297
        Args:
298
            s: The string to parse; e.g. ``"1 m/s^2"``.
299
               Unit names and symbols permitted, and spaces may be omitted.
300
            dimensionality: The resulting Quantity is checked against this;
301
                            e.g. ``"[length]/[meter]^2"``
302
303
        Returns:
304
            a pint ``Quantity``
305
306
        Raise:
307
            PintTypeError: If the dimensionality is inconsistent
308
        """
309
        q = _UNIT_REG.Quantity(s).to_reduced_units()
0 ignored issues
show
Coding Style Naming introduced by
Variable name "q" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
310
        if not q.is_compatible_with(dimensionality):
311
            raise PintTypeError(f"{s} not of dimensionality {dimensionality}")
312
        return q
313
314
315
__all__ = ["UnitTools"]
316