1import itertools
2import logging
3import math
4import re
5from collections import defaultdict
6
7import pytest
8
9from pint import (
10    DefinitionSyntaxError,
11    DimensionalityError,
12    UndefinedUnitError,
13    UnitRegistry,
14)
15from pint.context import Context
16from pint.testsuite import helpers
17from pint.util import UnitsContainer
18
19
20def add_ctxs(ureg):
21    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1})
22    d = Context("lc")
23    d.add_transformation(a, b, lambda ureg, x: ureg.speed_of_light / x)
24    d.add_transformation(b, a, lambda ureg, x: ureg.speed_of_light / x)
25
26    ureg.add_context(d)
27
28    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1})
29    d = Context("ab")
30    d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x)
31    d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x)
32
33    ureg.add_context(d)
34
35
36def add_arg_ctxs(ureg):
37    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1})
38    d = Context("lc")
39    d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n)
40    d.add_transformation(b, a, lambda ureg, x, n: ureg.speed_of_light / x / n)
41
42    ureg.add_context(d)
43
44    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1})
45    d = Context("ab")
46    d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x)
47    d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x)
48
49    ureg.add_context(d)
50
51
52def add_argdef_ctxs(ureg):
53    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1})
54    d = Context("lc", defaults=dict(n=1))
55    assert d.defaults == dict(n=1)
56
57    d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n)
58    d.add_transformation(b, a, lambda ureg, x, n: ureg.speed_of_light / x / n)
59
60    ureg.add_context(d)
61
62    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1})
63    d = Context("ab")
64    d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x)
65    d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x)
66
67    ureg.add_context(d)
68
69
70def add_sharedargdef_ctxs(ureg):
71    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1})
72    d = Context("lc", defaults=dict(n=1))
73    assert d.defaults == dict(n=1)
74
75    d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n)
76    d.add_transformation(b, a, lambda ureg, x, n: ureg.speed_of_light / x / n)
77
78    ureg.add_context(d)
79
80    a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1})
81    d = Context("ab", defaults=dict(n=0))
82    d.add_transformation(a, b, lambda ureg, x, n: ureg.ampere * ureg.meter * n / x)
83    d.add_transformation(b, a, lambda ureg, x, n: ureg.ampere * ureg.meter * n / x)
84
85    ureg.add_context(d)
86
87
88class TestContexts:
89    def test_known_context(self, func_registry):
90        ureg = UnitRegistry()
91        add_ctxs(ureg)
92        with ureg.context("lc"):
93            assert ureg._active_ctx
94            assert ureg._active_ctx.graph
95
96        assert not ureg._active_ctx
97        assert not ureg._active_ctx.graph
98
99        with ureg.context("lc", n=1):
100            assert ureg._active_ctx
101            assert ureg._active_ctx.graph
102
103        assert not ureg._active_ctx
104        assert not ureg._active_ctx.graph
105
106    def test_known_context_enable(self, func_registry):
107        ureg = UnitRegistry()
108        add_ctxs(ureg)
109        ureg.enable_contexts("lc")
110        assert ureg._active_ctx
111        assert ureg._active_ctx.graph
112        ureg.disable_contexts(1)
113
114        assert not ureg._active_ctx
115        assert not ureg._active_ctx.graph
116
117        ureg.enable_contexts("lc", n=1)
118        assert ureg._active_ctx
119        assert ureg._active_ctx.graph
120        ureg.disable_contexts(1)
121
122        assert not ureg._active_ctx
123        assert not ureg._active_ctx.graph
124
125    def test_graph(self, func_registry):
126        ureg = UnitRegistry()
127        add_ctxs(ureg)
128        l = UnitsContainer({"[length]": 1.0})  # noqa: E741
129        t = UnitsContainer({"[time]": -1.0})
130        c = UnitsContainer({"[current]": 1.0})
131
132        g_sp = defaultdict(set)
133        g_sp.update({l: {t}, t: {l}})
134
135        g_ab = defaultdict(set)
136        g_ab.update({l: {c}, c: {l}})
137
138        g = defaultdict(set)
139        g.update({l: {t, c}, t: {l}, c: {l}})
140
141        with ureg.context("lc"):
142            assert ureg._active_ctx.graph == g_sp
143
144        with ureg.context("lc", n=1):
145            assert ureg._active_ctx.graph == g_sp
146
147        with ureg.context("ab"):
148            assert ureg._active_ctx.graph == g_ab
149
150        with ureg.context("lc"):
151            with ureg.context("ab"):
152                assert ureg._active_ctx.graph == g
153
154        with ureg.context("ab"):
155            with ureg.context("lc"):
156                assert ureg._active_ctx.graph == g
157
158        with ureg.context("lc", "ab"):
159            assert ureg._active_ctx.graph == g
160
161        with ureg.context("ab", "lc"):
162            assert ureg._active_ctx.graph == g
163
164    def test_graph_enable(self, func_registry):
165        ureg = UnitRegistry()
166        add_ctxs(ureg)
167        l = UnitsContainer({"[length]": 1.0})  # noqa: E741
168        t = UnitsContainer({"[time]": -1.0})
169        c = UnitsContainer({"[current]": 1.0})
170
171        g_sp = defaultdict(set)
172        g_sp.update({l: {t}, t: {l}})
173
174        g_ab = defaultdict(set)
175        g_ab.update({l: {c}, c: {l}})
176
177        g = defaultdict(set)
178        g.update({l: {t, c}, t: {l}, c: {l}})
179
180        ureg.enable_contexts("lc")
181        assert ureg._active_ctx.graph == g_sp
182        ureg.disable_contexts(1)
183
184        ureg.enable_contexts("lc", n=1)
185        assert ureg._active_ctx.graph == g_sp
186        ureg.disable_contexts(1)
187
188        ureg.enable_contexts("ab")
189        assert ureg._active_ctx.graph == g_ab
190        ureg.disable_contexts(1)
191
192        ureg.enable_contexts("lc")
193        ureg.enable_contexts("ab")
194        assert ureg._active_ctx.graph == g
195        ureg.disable_contexts(2)
196
197        ureg.enable_contexts("ab")
198        ureg.enable_contexts("lc")
199        assert ureg._active_ctx.graph == g
200        ureg.disable_contexts(2)
201
202        ureg.enable_contexts("lc", "ab")
203        assert ureg._active_ctx.graph == g
204        ureg.disable_contexts(2)
205
206        ureg.enable_contexts("ab", "lc")
207        assert ureg._active_ctx.graph == g
208        ureg.disable_contexts(2)
209
210    def test_known_nested_context(self, func_registry):
211        ureg = UnitRegistry()
212        add_ctxs(ureg)
213
214        with ureg.context("lc"):
215            x = dict(ureg._active_ctx)
216            y = dict(ureg._active_ctx.graph)
217            assert ureg._active_ctx
218            assert ureg._active_ctx.graph
219
220            with ureg.context("ab"):
221                assert ureg._active_ctx
222                assert ureg._active_ctx.graph
223                assert x != ureg._active_ctx
224                assert y != ureg._active_ctx.graph
225
226            assert x == ureg._active_ctx
227            assert y == ureg._active_ctx.graph
228
229        assert not ureg._active_ctx
230        assert not ureg._active_ctx.graph
231
232    def test_unknown_context(self, func_registry):
233        ureg = func_registry
234        add_ctxs(ureg)
235        with pytest.raises(KeyError):
236            with ureg.context("la"):
237                pass
238        assert not ureg._active_ctx
239        assert not ureg._active_ctx.graph
240
241    def test_unknown_nested_context(self, func_registry):
242        ureg = UnitRegistry()
243        add_ctxs(ureg)
244
245        with ureg.context("lc"):
246            x = dict(ureg._active_ctx)
247            y = dict(ureg._active_ctx.graph)
248            with pytest.raises(KeyError):
249                with ureg.context("la"):
250                    pass
251
252            assert x == ureg._active_ctx
253            assert y == ureg._active_ctx.graph
254
255        assert not ureg._active_ctx
256        assert not ureg._active_ctx.graph
257
258    def test_one_context(self, func_registry):
259        ureg = UnitRegistry()
260
261        add_ctxs(ureg)
262
263        q = 500 * ureg.meter
264        s = (ureg.speed_of_light / q).to("Hz")
265
266        meter_units = ureg.get_compatible_units(ureg.meter)
267        hertz_units = ureg.get_compatible_units(ureg.hertz)
268
269        with pytest.raises(DimensionalityError):
270            q.to("Hz")
271        with ureg.context("lc"):
272            assert q.to("Hz") == s
273            assert ureg.get_compatible_units(q) == meter_units | hertz_units
274        with pytest.raises(DimensionalityError):
275            q.to("Hz")
276        assert ureg.get_compatible_units(q) == meter_units
277
278    def test_multiple_context(self, func_registry):
279        ureg = UnitRegistry()
280
281        add_ctxs(ureg)
282
283        q = 500 * ureg.meter
284        s = (ureg.speed_of_light / q).to("Hz")
285
286        meter_units = ureg.get_compatible_units(ureg.meter)
287        hertz_units = ureg.get_compatible_units(ureg.hertz)
288        ampere_units = ureg.get_compatible_units(ureg.ampere)
289
290        with pytest.raises(DimensionalityError):
291            q.to("Hz")
292        with ureg.context("lc", "ab"):
293            assert q.to("Hz") == s
294            assert (
295                ureg.get_compatible_units(q) == meter_units | hertz_units | ampere_units
296            )
297        with pytest.raises(DimensionalityError):
298            q.to("Hz")
299        assert ureg.get_compatible_units(q) == meter_units
300
301    def test_nested_context(self, func_registry):
302        ureg = UnitRegistry()
303
304        add_ctxs(ureg)
305
306        q = 500 * ureg.meter
307        s = (ureg.speed_of_light / q).to("Hz")
308
309        with pytest.raises(DimensionalityError):
310            q.to("Hz")
311        with ureg.context("lc"):
312            assert q.to("Hz") == s
313            with ureg.context("ab"):
314                assert q.to("Hz") == s
315            assert q.to("Hz") == s
316
317        with ureg.context("ab"):
318            with pytest.raises(DimensionalityError):
319                q.to("Hz")
320            with ureg.context("lc"):
321                assert q.to("Hz") == s
322            with pytest.raises(DimensionalityError):
323                q.to("Hz")
324
325    def test_context_with_arg(self, func_registry):
326
327        ureg = UnitRegistry()
328
329        add_arg_ctxs(ureg)
330
331        q = 500 * ureg.meter
332        s = (ureg.speed_of_light / q).to("Hz")
333
334        with pytest.raises(DimensionalityError):
335            q.to("Hz")
336        with ureg.context("lc", n=1):
337            assert q.to("Hz") == s
338            with ureg.context("ab"):
339                assert q.to("Hz") == s
340            assert q.to("Hz") == s
341
342        with ureg.context("ab"):
343            with pytest.raises(DimensionalityError):
344                q.to("Hz")
345            with ureg.context("lc", n=1):
346                assert q.to("Hz") == s
347            with pytest.raises(DimensionalityError):
348                q.to("Hz")
349
350        with ureg.context("lc"):
351            with pytest.raises(TypeError):
352                q.to("Hz")
353
354    def test_enable_context_with_arg(self, func_registry):
355
356        ureg = UnitRegistry()
357
358        add_arg_ctxs(ureg)
359
360        q = 500 * ureg.meter
361        s = (ureg.speed_of_light / q).to("Hz")
362
363        with pytest.raises(DimensionalityError):
364            q.to("Hz")
365        ureg.enable_contexts("lc", n=1)
366        assert q.to("Hz") == s
367        ureg.enable_contexts("ab")
368        assert q.to("Hz") == s
369        assert q.to("Hz") == s
370        ureg.disable_contexts(1)
371        ureg.disable_contexts(1)
372
373        ureg.enable_contexts("ab")
374        with pytest.raises(DimensionalityError):
375            q.to("Hz")
376        ureg.enable_contexts("lc", n=1)
377        assert q.to("Hz") == s
378        ureg.disable_contexts(1)
379        with pytest.raises(DimensionalityError):
380            q.to("Hz")
381        ureg.disable_contexts(1)
382
383        ureg.enable_contexts("lc")
384        with pytest.raises(TypeError):
385            q.to("Hz")
386        ureg.disable_contexts(1)
387
388    def test_context_with_arg_def(self, func_registry):
389
390        ureg = UnitRegistry()
391
392        add_argdef_ctxs(ureg)
393
394        q = 500 * ureg.meter
395        s = (ureg.speed_of_light / q).to("Hz")
396
397        with pytest.raises(DimensionalityError):
398            q.to("Hz")
399        with ureg.context("lc"):
400            assert q.to("Hz") == s
401            with ureg.context("ab"):
402                assert q.to("Hz") == s
403            assert q.to("Hz") == s
404
405        with ureg.context("ab"):
406            with pytest.raises(DimensionalityError):
407                q.to("Hz")
408            with ureg.context("lc"):
409                assert q.to("Hz") == s
410            with pytest.raises(DimensionalityError):
411                q.to("Hz")
412
413        with pytest.raises(DimensionalityError):
414            q.to("Hz")
415        with ureg.context("lc", n=2):
416            assert q.to("Hz") == s / 2
417            with ureg.context("ab"):
418                assert q.to("Hz") == s / 2
419            assert q.to("Hz") == s / 2
420
421        with ureg.context("ab"):
422            with pytest.raises(DimensionalityError):
423                q.to("Hz")
424            with ureg.context("lc", n=2):
425                assert q.to("Hz") == s / 2
426            with pytest.raises(DimensionalityError):
427                q.to("Hz")
428
429    def test_context_with_sharedarg_def(self, func_registry):
430
431        ureg = UnitRegistry()
432
433        add_sharedargdef_ctxs(ureg)
434
435        q = 500 * ureg.meter
436        s = (ureg.speed_of_light / q).to("Hz")
437        u = (1 / 500) * ureg.ampere
438
439        with ureg.context("lc"):
440            assert q.to("Hz") == s
441            with ureg.context("ab"):
442                assert q.to("ampere") == u
443
444        with ureg.context("ab"):
445            assert q.to("ampere") == 0 * u
446            with ureg.context("lc"):
447                with pytest.raises(ZeroDivisionError):
448                    ureg.Quantity.to(q, "Hz")
449
450        with ureg.context("lc", n=2):
451            assert q.to("Hz") == s / 2
452            with ureg.context("ab"):
453                assert q.to("ampere") == 2 * u
454
455        with ureg.context("ab", n=3):
456            assert q.to("ampere") == 3 * u
457            with ureg.context("lc"):
458                assert q.to("Hz") == s / 3
459
460        with ureg.context("lc", n=2):
461            assert q.to("Hz") == s / 2
462            with ureg.context("ab", n=4):
463                assert q.to("ampere") == 4 * u
464
465        with ureg.context("ab", n=3):
466            assert q.to("ampere") == 3 * u
467            with ureg.context("lc", n=6):
468                assert q.to("Hz") == s / 6
469
470    def test_anonymous_context(self, func_registry):
471        ureg = UnitRegistry()
472        c = Context()
473        c.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("5 cm/s"))
474        with pytest.raises(ValueError):
475            ureg.add_context(c)
476
477        x = ureg("10 cm")
478        expect = ureg("2 s")
479        helpers.assert_quantity_equal(x.to("s", c), expect)
480
481        with ureg.context(c):
482            helpers.assert_quantity_equal(x.to("s"), expect)
483
484        ureg.enable_contexts(c)
485        helpers.assert_quantity_equal(x.to("s"), expect)
486        ureg.disable_contexts(1)
487        with pytest.raises(DimensionalityError):
488            x.to("s")
489
490        # Multiple anonymous contexts
491        c2 = Context()
492        c2.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("10 cm/s"))
493        c2.add_transformation("[mass]", "[time]", lambda ureg, x: x / ureg("10 kg/s"))
494        with ureg.context(c2, c):
495            helpers.assert_quantity_equal(x.to("s"), expect)
496            # Transformations only in c2 are still working even if c takes priority
497            helpers.assert_quantity_equal(ureg("100 kg").to("s"), ureg("10 s"))
498        with ureg.context(c, c2):
499            helpers.assert_quantity_equal(x.to("s"), ureg("1 s"))
500
501    def _test_ctx(self, ctx):
502        ureg = UnitRegistry()
503        q = 500 * ureg.meter
504        s = (ureg.speed_of_light / q).to("Hz")
505
506        nctx = len(ureg._contexts)
507
508        assert ctx.name not in ureg._contexts
509        ureg.add_context(ctx)
510
511        assert ctx.name in ureg._contexts
512        assert len(ureg._contexts) == nctx + 1 + len(ctx.aliases)
513
514        with ureg.context(ctx.name):
515            assert q.to("Hz") == s
516            assert s.to("meter") == q
517
518        ureg.remove_context(ctx.name)
519        assert ctx.name not in ureg._contexts
520        assert len(ureg._contexts) == nctx
521
522    @pytest.mark.parametrize(
523        "badrow",
524        (
525            "[length] = 1 / [time]: c / value",
526            "1 / [time] = [length]: c / value",
527            "[length] <- [time] = c / value",
528            "[length] - [time] = c / value",
529        ),
530    )
531    def test_parse_invalid(self, badrow):
532        with pytest.raises(DefinitionSyntaxError):
533            Context.from_lines(["@context c", badrow])
534
535    def test_parse_simple(self):
536
537        a = Context.__keytransform__(
538            UnitsContainer({"[time]": -1}), UnitsContainer({"[length]": 1})
539        )
540        b = Context.__keytransform__(
541            UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1})
542        )
543
544        s = [
545            "@context longcontextname",
546            "[length] -> 1 / [time]: c / value",
547            "1 / [time] -> [length]: c / value",
548        ]
549
550        c = Context.from_lines(s)
551        assert c.name == "longcontextname"
552        assert c.aliases == ()
553        assert c.defaults == {}
554        assert c.funcs.keys() == {a, b}
555        self._test_ctx(c)
556
557        s = ["@context longcontextname = lc", "[length] <-> 1 / [time]: c / value"]
558
559        c = Context.from_lines(s)
560        assert c.name == "longcontextname"
561        assert c.aliases == ("lc",)
562        assert c.defaults == {}
563        assert c.funcs.keys() == {a, b}
564        self._test_ctx(c)
565
566        s = [
567            "@context longcontextname = lc = lcn",
568            "[length] <-> 1 / [time]: c / value",
569        ]
570
571        c = Context.from_lines(s)
572        assert c.name == "longcontextname"
573        assert c.aliases == ("lc", "lcn")
574        assert c.defaults == {}
575        assert c.funcs.keys() == {a, b}
576        self._test_ctx(c)
577
578    def test_parse_auto_inverse(self):
579
580        a = Context.__keytransform__(
581            UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0})
582        )
583        b = Context.__keytransform__(
584            UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0})
585        )
586
587        s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"]
588
589        c = Context.from_lines(s)
590        assert c.defaults == {}
591        assert c.funcs.keys() == {a, b}
592        self._test_ctx(c)
593
594    def test_parse_define(self):
595        a = Context.__keytransform__(
596            UnitsContainer({"[time]": -1}), UnitsContainer({"[length]": 1.0})
597        )
598        b = Context.__keytransform__(
599            UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1.0})
600        )
601
602        s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"]
603        c = Context.from_lines(s)
604        assert c.defaults == {}
605        assert c.funcs.keys() == {a, b}
606        self._test_ctx(c)
607
608    def test_parse_parameterized(self):
609        a = Context.__keytransform__(
610            UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0})
611        )
612        b = Context.__keytransform__(
613            UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0})
614        )
615
616        s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: n * c / value"]
617
618        c = Context.from_lines(s)
619        assert c.defaults == {"n": 1}
620        assert c.funcs.keys() == {a, b}
621        self._test_ctx(c)
622
623        s = [
624            "@context(n=1, bla=2) longcontextname",
625            "[length] <-> 1 / [time]: n * c / value / bla",
626        ]
627
628        c = Context.from_lines(s)
629        assert c.defaults == {"n": 1, "bla": 2}
630        assert c.funcs.keys() == {a, b}
631
632        # If the variable is not present in the definition, then raise an error
633        s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: c / value"]
634        with pytest.raises(DefinitionSyntaxError):
635            Context.from_lines(s)
636
637    def test_warnings(self, caplog):
638
639        ureg = UnitRegistry()
640
641        with caplog.at_level(logging.DEBUG, "pint"):
642            add_ctxs(ureg)
643
644            d = Context("ab")
645            ureg.add_context(d)
646
647            assert len(caplog.records) == 1
648            assert "ab" in str(caplog.records[-1].args)
649
650            d = Context("ab1", aliases=("ab",))
651            ureg.add_context(d)
652
653            assert len(caplog.records) == 2
654            assert "ab" in str(caplog.records[-1].args)
655
656
657class TestDefinedContexts:
658    @classmethod
659    def setup_class(cls):
660        cls.ureg = UnitRegistry()
661
662    @classmethod
663    def teardown_class(cls):
664        cls.ureg = None
665
666    def test_defined(self):
667        ureg = self.ureg
668        with ureg.context("sp"):
669            pass
670
671        a = Context.__keytransform__(
672            UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0})
673        )
674        b = Context.__keytransform__(
675            UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0})
676        )
677        assert a in ureg._contexts["sp"].funcs
678        assert b in ureg._contexts["sp"].funcs
679        with ureg.context("sp"):
680            assert a in ureg._active_ctx
681            assert b in ureg._active_ctx
682
683    def test_spectroscopy(self):
684        ureg = self.ureg
685        eq = (532.0 * ureg.nm, 563.5 * ureg.terahertz, 2.33053 * ureg.eV)
686        with ureg.context("sp"):
687            from pint.util import find_shortest_path
688
689            for a, b in itertools.product(eq, eq):
690                for x in range(2):
691                    if x == 1:
692                        a = a.to_base_units()
693                        b = b.to_base_units()
694                    da, db = Context.__keytransform__(
695                        a.dimensionality, b.dimensionality
696                    )
697                    p = find_shortest_path(ureg._active_ctx.graph, da, db)
698                    assert p
699                    msg = "{} <-> {}".format(a, b)
700                    # assertAlmostEqualRelError converts second to first
701                    helpers.assert_quantity_almost_equal(b, a, rtol=0.01, msg=msg)
702
703        for a, b in itertools.product(eq, eq):
704            helpers.assert_quantity_almost_equal(a.to(b.units, "sp"), b, rtol=0.01)
705
706    def test_textile(self):
707        ureg = self.ureg
708        qty_direct = 1.331 * ureg.tex
709        with pytest.raises(DimensionalityError):
710            qty_indirect = qty_direct.to("Nm")
711
712        with ureg.context("textile"):
713            from pint.util import find_shortest_path
714
715            qty_indirect = qty_direct.to("Nm")
716            a = qty_direct.to_base_units()
717            b = qty_indirect.to_base_units()
718            da, db = Context.__keytransform__(a.dimensionality, b.dimensionality)
719            p = find_shortest_path(ureg._active_ctx.graph, da, db)
720            assert p
721            msg = "{} <-> {}".format(a, b)
722            helpers.assert_quantity_almost_equal(b, a, rtol=0.01, msg=msg)
723
724            # Check RKM <-> cN/tex conversion
725            helpers.assert_quantity_almost_equal(
726                1 * ureg.RKM, 0.980665 * ureg.cN / ureg.tex
727            )
728            helpers.assert_quantity_almost_equal(
729                (1 / 0.980665) * ureg.RKM, 1 * ureg.cN / ureg.tex
730            )
731            assert (
732                round(abs((1 * ureg.RKM).to(ureg.cN / ureg.tex).m - 0.980665), 7) == 0
733            )
734            assert (
735                round(abs((1 * ureg.cN / ureg.tex).to(ureg.RKM).m - 1 / 0.980665), 7)
736                == 0
737            )
738
739    def test_decorator(self):
740        ureg = self.ureg
741
742        a = 532.0 * ureg.nm
743        with ureg.context("sp"):
744            b = a.to("terahertz")
745
746        def f(wl):
747            return wl.to("terahertz")
748
749        with pytest.raises(DimensionalityError):
750            f(a)
751
752        @ureg.with_context("sp")
753        def g(wl):
754            return wl.to("terahertz")
755
756        assert b == g(a)
757
758    def test_decorator_composition(self):
759        ureg = self.ureg
760
761        a = 532.0 * ureg.nm
762        with ureg.context("sp"):
763            b = a.to("terahertz")
764
765        @ureg.with_context("sp")
766        @ureg.check("[length]")
767        def f(wl):
768            return wl.to("terahertz")
769
770        @ureg.with_context("sp")
771        @ureg.check("[length]")
772        def g(wl):
773            return wl.to("terahertz")
774
775        assert b == f(a)
776        assert b == g(a)
777
778
779def test_redefine(subtests):
780    ureg = UnitRegistry(
781        """
782        foo = [d] = f = foo_alias
783        bar = 2 foo = b = bar_alias
784        baz = 3 bar = _ = baz_alias
785        asd = 4 baz
786
787        @context c
788            # Note how we're redefining a symbol, not the base name, as a
789            # function of another name
790            b = 5 f
791        """.splitlines()
792    )
793    # Units that are somehow directly or indirectly defined as a function of the
794    # overridden unit are also affected
795    foo = ureg.Quantity(1, "foo")
796    bar = ureg.Quantity(1, "bar")
797    asd = ureg.Quantity(1, "asd")
798
799    # Test without context before and after, to verify that the cache and units have
800    # not been polluted
801    for enable_ctx in (False, True, False):
802        with subtests.test(enable_ctx):
803            if enable_ctx:
804                ureg.enable_contexts("c")
805                k = 5
806            else:
807                k = 2
808
809            assert foo.to("b").magnitude == 1 / k
810            assert foo.to("bar").magnitude == 1 / k
811            assert foo.to("bar_alias").magnitude == 1 / k
812            assert foo.to("baz").magnitude == 1 / k / 3
813            assert bar.to("foo").magnitude == k
814            assert bar.to("baz").magnitude == 1 / 3
815            assert asd.to("foo").magnitude == 4 * 3 * k
816            assert asd.to("bar").magnitude == 4 * 3
817            assert asd.to("baz").magnitude == 4
818
819        ureg.disable_contexts()
820
821
822def test_define_nan():
823    ureg = UnitRegistry(
824        """
825        USD = [currency]
826        EUR = nan USD
827        GBP = nan USD
828
829        @context c
830            EUR = 1.11 USD
831            # Note that we're changing which unit GBP is defined against
832            GBP = 1.18 EUR
833        @end
834        """.splitlines()
835    )
836
837    q = ureg.Quantity("10 GBP")
838    assert q.magnitude == 10
839    assert q.units.dimensionality == {"[currency]": 1}
840    assert q.to("GBP").magnitude == 10
841    assert math.isnan(q.to("USD").magnitude)
842    assert math.isclose(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11)
843
844
845def test_non_multiplicative(subtests):
846    ureg = UnitRegistry(
847        """
848        kelvin = [temperature]
849        fahrenheit = 5 / 9 * kelvin; offset: 255
850        bogodegrees = 9 * kelvin
851
852        @context nonmult_to_nonmult
853            fahrenheit = 7 * kelvin; offset: 123
854        @end
855        @context nonmult_to_mult
856            fahrenheit = 123 * kelvin
857        @end
858        @context mult_to_nonmult
859            bogodegrees = 5 * kelvin; offset: 123
860        @end
861        """.splitlines()
862    )
863    k = ureg.Quantity(100, "kelvin")
864
865    with subtests.test("baseline"):
866        helpers.assert_quantity_almost_equal(
867            k.to("fahrenheit").magnitude, (100 - 255) * 9 / 5
868        )
869        helpers.assert_quantity_almost_equal(k.to("bogodegrees").magnitude, 100 / 9)
870
871    with subtests.test("nonmult_to_nonmult"):
872        with ureg.context("nonmult_to_nonmult"):
873            helpers.assert_quantity_almost_equal(
874                k.to("fahrenheit").magnitude, (100 - 123) / 7
875            )
876
877    with subtests.test("nonmult_to_mult"):
878        with ureg.context("nonmult_to_mult"):
879            helpers.assert_quantity_almost_equal(
880                k.to("fahrenheit").magnitude, 100 / 123
881            )
882
883    with subtests.test("mult_to_nonmult"):
884        with ureg.context("mult_to_nonmult"):
885            helpers.assert_quantity_almost_equal(
886                k.to("bogodegrees").magnitude, (100 - 123) / 5
887            )
888
889
890def test_stack_contexts():
891    ureg = UnitRegistry(
892        """
893        a = [dim1]
894        b = 1/2 a
895        c = 1/3 a
896        d = [dim2]
897
898        @context c1
899            b = 1/4 a
900            c = 1/6 a
901            [dim1]->[dim2]: value * 2 d/a
902        @end
903        @context c2
904            b = 1/5 a
905            [dim1]->[dim2]: value * 3 d/a
906        @end
907        """.splitlines()
908    )
909    q = ureg.Quantity(1, "a")
910    assert q.to("b").magnitude == 2
911    assert q.to("c").magnitude == 3
912    assert q.to("b", "c1").magnitude == 4
913    assert q.to("c", "c1").magnitude == 6
914    assert q.to("d", "c1").magnitude == 2
915    assert q.to("b", "c2").magnitude == 5
916    assert q.to("c", "c2").magnitude == 3
917    assert q.to("d", "c2").magnitude == 3
918    assert q.to("b", "c1", "c2").magnitude == 5  # c2 takes precedence
919    assert q.to("c", "c1", "c2").magnitude == 6  # c2 doesn't change it, so use c1
920    assert q.to("d", "c1", "c2").magnitude == 3  # c2 takes precedence
921
922
923def test_err_change_base_unit():
924    ureg = UnitRegistry(
925        """
926        foo = [d1]
927        bar = [d2]
928
929        @context c
930            bar = foo
931        @end
932        """.splitlines()
933    )
934
935    expected = "Can't redefine a base unit to a derived one"
936    with pytest.raises(ValueError, match=expected):
937        ureg.enable_contexts("c")
938
939
940def test_err_to_base_unit():
941    expected = "Can't define base units within a context"
942    with pytest.raises(DefinitionSyntaxError, match=expected):
943        Context.from_lines(["@context c", "x = [d]"])
944
945
946def test_err_change_dimensionality():
947    ureg = UnitRegistry(
948        """
949        foo = [d1]
950        bar = [d2]
951        baz = foo
952
953        @context c
954            baz = bar
955        @end
956        """.splitlines()
957    )
958
959    expected = re.escape(
960        "Can't change dimensionality of baz from [d1] to [d2] in a context"
961    )
962    with pytest.raises(ValueError, match=expected):
963        ureg.enable_contexts("c")
964
965
966def test_err_cyclic_dependency():
967    ureg = UnitRegistry(
968        """
969        foo = [d]
970        bar = foo
971        baz = bar
972
973        @context c
974            bar = baz
975        @end
976        """.splitlines()
977    )
978    # TODO align this exception and the one you get when you implement a cyclic
979    #      dependency within the base registry. Ideally this exception should be
980    #      raised by enable_contexts.
981    ureg.enable_contexts("c")
982    q = ureg.Quantity("bar")
983    with pytest.raises(RecursionError):
984        q.to("foo")
985
986
987def test_err_dimension_redefinition():
988    expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
989    with pytest.raises(DefinitionSyntaxError, match=expected):
990        Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
991
992
993def test_err_prefix_redefinition():
994    expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
995    with pytest.raises(DefinitionSyntaxError, match=expected):
996        Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
997
998
999def test_err_redefine_alias(subtests):
1000    expected = "Can't change a unit's symbol or aliases within a context"
1001    for s in ("foo = bar = f", "foo = bar = _ = baz"):
1002        with subtests.test(s):
1003            with pytest.raises(DefinitionSyntaxError, match=expected):
1004                Context.from_lines(["@context c", s])
1005
1006
1007def test_err_redefine_with_prefix():
1008    ureg = UnitRegistry(
1009        """
1010        kilo- = 1000
1011        gram = [mass]
1012        pound = 454 gram
1013
1014        @context c
1015            kilopound = 500000 gram
1016        @end
1017        """.splitlines()
1018    )
1019
1020    expected = "Can't redefine a unit with a prefix: kilopound"
1021    with pytest.raises(ValueError, match=expected):
1022        ureg.enable_contexts("c")
1023
1024
1025def test_err_new_unit():
1026    ureg = UnitRegistry(
1027        """
1028        foo = [d]
1029        @context c
1030            bar = foo
1031        @end
1032        """.splitlines()
1033    )
1034    expected = "'bar' is not defined in the unit registry"
1035    with pytest.raises(UndefinedUnitError, match=expected):
1036        ureg.enable_contexts("c")
1037