diff --git a/.gitignore b/.gitignore index d2fddea12..610ed68ff 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ test/ # test csv which should be user generated notebooks/pandas_test.csv + +# dask stuff +dask-worker-space diff --git a/CHANGES b/CHANGES index 9922eda99..13947daea 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Pint Changelog 0.15 (unreleased) ----------------- +- Change `Quantity` and `Unit` HTML (i.e., Jupyter notebook) repr away from LaTeX to a + simpler, more performant pretty-text and table based repr inspired by Sparse and Dask. + (Issue #654) - Implement Dask collection interface to support Pint Quantity wrapped Dask arrays. - Started automatically testing examples in the documentation - Fixed right operand power for dimensionless Quantity to reflect numpy behavior. (Issue #1136) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index e473ac4f5..1c64c4ecf 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -319,10 +319,10 @@ Pint also supports `f-strings`_ from python>=3.6 : >>> print(f'The str is {accel:~.3e}') The str is 1.300e+00 m / s ** 2 >>> print(f'The str is {accel:~H}') # HTML format (displays well in Jupyter) - The str is \[1.3\ m/{s}^{2}\] + The str is 1.3 m/s2 -But Pint also extends the standard formatting capabilities for unicode and -LaTeX representations: +But Pint also extends the standard formatting capabilities for unicode, LaTeX, and HTML +representations: .. doctest:: @@ -335,7 +335,7 @@ LaTeX representations: 'The LaTeX representation is 1.3\\ \\frac{\\mathrm{meter}}{\\mathrm{second}^{2}}' >>> # HTML print - good for Jupyter notebooks >>> 'The HTML representation is {:H}'.format(accel) - 'The HTML representation is \\[1.3\\ meter/{second}^{2}\\]' + 'The HTML representation is 1.3 meter/second2' If you want to use abbreviated unit names, prefix the specification with `~`: diff --git a/pint/formatting.py b/pint/formatting.py index 495dfec8c..3cc7643ec 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -95,7 +95,7 @@ def _pretty_fmt_exponent(num): "single_denominator": True, "product_fmt": r" ", "division_fmt": r"{}/{}", - "power_fmt": "{{{}}}^{{{}}}", # braces superscript whole exponent + "power_fmt": r"{}{}", "parentheses_fmt": r"({})", }, "": { # Default format. @@ -270,12 +270,8 @@ def format_unit(unit, spec, **kwspec): (r"\mathrm{{{}}}".format(u.replace("_", r"\_")), p) for u, p in unit.items() ] return formatter(rm, **fmt).replace("[", "{").replace("]", "}") - elif spec == "H": - # HTML (Jupyter Notebook) - rm = [(u.replace("_", r"\_"), p) for u, p in unit.items()] - return formatter(rm, **fmt) else: - # Plain text + # HTML and Text return formatter(unit.items(), **fmt) diff --git a/pint/measurement.py b/pint/measurement.py index 826253fa5..1da1b6447 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -147,7 +147,7 @@ def __format__(self, spec): if "L" in newspec and "S" in newspec: mag = mag.replace("(", r"\left(").replace(")", r"\right)") - if "L" in newspec or "H" in spec: + if "L" in newspec: space = r"\ " else: space = " " @@ -158,14 +158,10 @@ def __format__(self, spec): if "H" in spec: # Fix exponential format - mag = re.sub(r"\)e\+0?(\d+)", r")×10^{\1}", mag) - mag = re.sub(r"\)e-0?(\d+)", r")×10^{-\1}", mag) + mag = re.sub(r"\)e\+0?(\d+)", r")×10\1", mag) + mag = re.sub(r"\)e-0?(\d+)", r")×10-\1", mag) - assert ustr[:2] == r"\[" - assert ustr[-2:] == r"\]" - return r"\[" + mag + space + ustr[2:] - else: - return mag + space + ustr + return mag + space + ustr _Measurement = Measurement diff --git a/pint/quantity.py b/pint/quantity.py index 66642a3e1..5d6ac46f0 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -47,7 +47,6 @@ from .formatting import ( _pretty_fmt_exponent, ndarray_to_latex, - ndarray_to_latex_parts, remove_custom_flags, siunitx_format_unit, ) @@ -66,6 +65,7 @@ SharedRegistryObject, UnitsContainer, infer_base_unit, + iterable, logger, to_units_container, ) @@ -311,11 +311,6 @@ def __format__(self, spec): spec = spec or self.default_format - if "L" in spec: - allf = plain_allf = r"{}\ {}" - else: - allf = plain_allf = "{} {}" - # If Compact is selected, do it at the beginning if "#" in spec: spec = spec.replace("#", "") @@ -323,36 +318,65 @@ def __format__(self, spec): else: obj = self - # the LaTeX siunitx code + if "L" in spec: + allf = plain_allf = r"{}\ {}" + elif "H" in spec: + allf = plain_allf = "{} {}" + if iterable(obj.magnitude): + # Use HTML table instead of plain text template for array-likes + allf = ( + "" + "" + "" + "" + "
Magnitude{}
Units{}
" + ) + else: + allf = plain_allf = "{} {}" + if "Lx" in spec: + # the LaTeX siunitx code spec = spec.replace("Lx", "") # TODO: add support for extracting options opts = "" ustr = siunitx_format_unit(obj.units) allf = r"\SI[%s]{{{}}}{{{}}}" % opts - elif "H" in spec: - ustr = format(obj.units, spec) - assert ustr[:2] == r"\[" - assert ustr[-2:] == r"\]" - ustr = ustr[2:-2] - allf = r"\[{}\ {}\]" else: + # Hand off to unit formatting ustr = format(obj.units, spec) mspec = remove_custom_flags(spec) - if isinstance(self.magnitude, ndarray): + if "H" in spec: + # HTML formatting + if hasattr(obj.magnitude, "_repr_html_"): + # If magnitude has an HTML repr, nest it within Pint's + mstr = obj.magnitude._repr_html_() + else: + if isinstance(self.magnitude, ndarray): + # Use custom ndarray text formatting with monospace font + formatter = "{{:{}}}".format(mspec) + with printoptions(formatter={"float_kind": formatter.format}): + mstr = ( + "
"
+                            + format(obj.magnitude).replace("\n", "
") + + "
" + ) + elif not iterable(obj.magnitude): + # Use plain text for scalars + mstr = format(obj.magnitude, mspec) + else: + # Use monospace font for other array-likes + mstr = ( + "
"
+                        + format(obj.magnitude, mspec).replace("\n", "
") + + "
" + ) + elif isinstance(self.magnitude, ndarray): if "L" in spec: + # Use ndarray LaTeX special formatting mstr = ndarray_to_latex(obj.magnitude, mspec) - elif "H" in spec: - allf = r"\[{} {}\]" - # this is required to have the magnitude and unit in the same line - parts = ndarray_to_latex_parts(obj.magnitude, mspec) - - if len(parts) > 1: - return "\n".join(allf.format(part, ustr) for part in parts) - - mstr = parts[0] else: + # Use custom ndarray text formatting formatter = "{{:{}}}".format(mspec) with printoptions(formatter={"float_kind": formatter.format}): mstr = format(obj.magnitude).replace("\n", "") @@ -361,13 +385,14 @@ def __format__(self, spec): if "L" in spec: mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr) - elif "H" in spec: - mstr = self._exp_pattern.sub(r"\1×10^{\2\3}", mstr) - elif "P" in spec: + elif "H" in spec or "P" in spec: m = self._exp_pattern.match(mstr) + _exp_formatter = ( + _pretty_fmt_exponent if "P" in spec else lambda s: f"{s}" + ) if m: exp = int(m.group(2) + m.group(3)) - mstr = self._exp_pattern.sub(r"\1×10" + _pretty_fmt_exponent(exp), mstr) + mstr = self._exp_pattern.sub(r"\1×10" + _exp_formatter(exp), mstr) if allf == plain_allf and ustr.startswith("1 /"): # Write e.g. "3 / s" instead of "3 1 / s" diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 78d48a73b..b5acff0e3 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -48,13 +48,13 @@ def test_format(self): ("{!r}", ""), ("{:P}", "(4.00 ± 0.10) second²"), ("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), - ("{:H}", r"\[(4.00 ± 0.10)\ {second}^{2}\]"), + ("{:H}", "(4.00 ± 0.10) second2"), ("{:C}", "(4.00+/-0.10) second**2"), ("{:Lx}", r"\SI{4.00 +- 0.10}{\second\squared}"), ("{:.1f}", "(4.0 +/- 0.1) second ** 2"), ("{:.1fP}", "(4.0 ± 0.1) second²"), ("{:.1fL}", r"\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}"), - ("{:.1fH}", r"\[(4.0 ± 0.1)\ {second}^{2}\]"), + ("{:.1fH}", "(4.0 ± 0.1) second2"), ("{:.1fC}", "(4.0+/-0.1) second**2"), ("{:.1fLx}", r"\SI{4.0 +- 0.1}{\second\squared}"), ): @@ -70,7 +70,7 @@ def test_format_paru(self): ("{:.3uS}", "0.2000(100) second ** 2"), ("{:.3uSP}", "0.2000(100) second²"), ("{:.3uSL}", r"0.2000\left(100\right)\ \mathrm{second}^{2}"), - ("{:.3uSH}", r"\[0.2000(100)\ {second}^{2}\]"), + ("{:.3uSH}", "0.2000(100) second2"), ("{:.3uSC}", "0.2000(100) second**2"), ): with self.subTest(spec): @@ -84,7 +84,7 @@ def test_format_u(self): ("{:.3u}", "(0.2000 +/- 0.0100) second ** 2"), ("{:.3uP}", "(0.2000 ± 0.0100) second²"), ("{:.3uL}", r"\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}"), - ("{:.3uH}", r"\[(0.2000 ± 0.0100)\ {second}^{2}\]"), + ("{:.3uH}", "(0.2000 ± 0.0100) second2"), ("{:.3uC}", "(0.2000+/-0.0100) second**2"), ("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}",), ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), @@ -101,7 +101,7 @@ def test_format_percu(self): ("{:.1u%}", "(20 +/- 1)% second ** 2"), ("{:.1u%P}", "(20 ± 1)% second²"), ("{:.1u%L}", r"\left(20 \pm 1\right) \%\ \mathrm{second}^{2}"), - ("{:.1u%H}", r"\[(20 ± 1)%\ {second}^{2}\]"), + ("{:.1u%H}", "(20 ± 1)% second2"), ("{:.1u%C}", "(20+/-1)% second**2"), ): with self.subTest(spec): @@ -117,7 +117,7 @@ def test_format_perce(self): "{:.1ueL}", r"\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}", ), - ("{:.1ueH}", r"\[(2.0 ± 0.1)×10^{-1}\ {second}^{2}\]"), + ("{:.1ueH}", "(2.0 ± 0.1)×10-1 second2"), ("{:.1ueC}", "(2.0+/-0.1)e-01 second**2"), ): with self.subTest(spec): @@ -132,7 +132,7 @@ def test_format_exponential_pos(self): ("{!r}", ""), ("{:P}", "(4.00 ± 0.10)×10²⁰ second²"), ("{:L}", r"\left(4.00 \pm 0.10\right) \times 10^{20}\ \mathrm{second}^{2}"), - ("{:H}", r"\[(4.00 ± 0.10)×10^{20}\ {second}^{2}\]"), + ("{:H}", "(4.00 ± 0.10)×1020 second2"), ("{:C}", "(4.00+/-0.10)e+20 second**2"), ("{:Lx}", r"\SI{4.00 +- 0.10 e+20}{\second\squared}"), ): @@ -149,7 +149,7 @@ def test_format_exponential_neg(self): "{:L}", r"\left(4.00 \pm 0.10\right) \times 10^{-20}\ \mathrm{second}^{2}", ), - ("{:H}", r"\[(4.00 ± 0.10)×10^{-20}\ {second}^{2}\]"), + ("{:H}", "(4.00 ± 0.10)×10-20 second2"), ("{:C}", "(4.00+/-0.10)e-20 second**2"), ("{:Lx}", r"\SI{4.00 +- 0.10 e-20}{\second\squared}"), ): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fa18fe8fc..b5780e568 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -134,7 +134,7 @@ def test_quantity_format(self): r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("{:P}", "4.12345678 kilogram·meter²/second"), - ("{:H}", r"\[4.12345678\ kilogram\ {meter}^{2}/second\]"), + ("{:H}", "4.12345678 kilogram meter2/second"), ("{:C}", "4.12345678 kilogram*meter**2/second"), ("{:~}", "4.12345678 kg * m ** 2 / s"), ( @@ -142,7 +142,7 @@ def test_quantity_format(self): r"4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}", ), ("{:P~}", "4.12345678 kg·m²/s"), - ("{:H~}", r"\[4.12345678\ kg\ {m}^{2}/s\]"), + ("{:H~}", "4.12345678 kg m2/s"), ("{:C~}", "4.12345678 kg*m**2/s"), ("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"), ): @@ -176,6 +176,15 @@ def test_quantity_array_format(self): ), ("{:.2f~P}", "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kg·m²"), ("{:g~P}", "[1e-16 1 1e+07 1e+12 nan inf] kg·m²"), + ( + "{:.2f~H}", + ( + "" + "" + "
Magnitude" + "
[0.00 1.00 10000000.00 1000000000000.00 nan inf]
Unitskg m2
" + ), + ), ): with self.subTest(spec): self.assertEqual(spec.format(x), result) @@ -209,12 +218,12 @@ def test_default_formatting(self): r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("P", "4.12345678 kilogram·meter²/second"), - ("H", r"\[4.12345678\ kilogram\ {meter}^{2}/second\]"), + ("H", "4.12345678 kilogram meter2/second"), ("C", "4.12345678 kilogram*meter**2/second"), ("~", "4.12345678 kg * m ** 2 / s"), ("L~", r"4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}"), ("P~", "4.12345678 kg·m²/s"), - ("H~", r"\[4.12345678\ kg\ {m}^{2}/s\]"), + ("H~", "4.12345678 kg m2/s"), ("C~", "4.12345678 kg*m**2/s"), ): with self.subTest(spec): @@ -224,12 +233,12 @@ def test_default_formatting(self): def test_exponent_formatting(self): ureg = UnitRegistry() x = ureg.Quantity(1e20, "meter") - self.assertEqual(f"{x:~H}", r"\[1×10^{20}\ m\]") + self.assertEqual(f"{x:~H}", r"1×1020 m") self.assertEqual(f"{x:~L}", r"1\times 10^{20}\ \mathrm{m}") self.assertEqual(f"{x:~P}", r"1×10²⁰ m") x /= 1e40 - self.assertEqual(f"{x:~H}", r"\[1×10^{-20}\ m\]") + self.assertEqual(f"{x:~H}", r"1×10-20 m") self.assertEqual(f"{x:~L}", r"1\times 10^{-20}\ \mathrm{m}") self.assertEqual(f"{x:~P}", r"1×10⁻²⁰ m") @@ -250,7 +259,7 @@ def pretty(cls, data): ureg = UnitRegistry() x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), r"\[3.5\ kilogram\ {meter}^{2}/second\]") + self.assertEqual(x._repr_html_(), "3.5 kilogram meter2/second") self.assertEqual( x._repr_latex_(), r"$3.5\ \frac{\mathrm{kilogram} \cdot " @@ -259,7 +268,7 @@ def pretty(cls, data): x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "3.5 kilogram·meter²/second") ureg.default_format = "~" - self.assertEqual(x._repr_html_(), r"\[3.5\ kg\ {m}^{2}/s\]") + self.assertEqual(x._repr_html_(), "3.5 kg m2/s") self.assertEqual( x._repr_latex_(), r"$3.5\ \frac{\mathrm{kg} \cdot " r"\mathrm{m}^{2}}{\mathrm{s}}$", diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 2cce7e0d1..749a60f5b 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -42,13 +42,13 @@ def test_unit_formatting(self): r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("{:P}", "kilogram·meter²/second"), - ("{:H}", r"\[kilogram\ {meter}^{2}/second\]"), + ("{:H}", "kilogram meter2/second"), ("{:C}", "kilogram*meter**2/second"), ("{:Lx}", r"\si[]{\kilo\gram\meter\squared\per\second}"), ("{:~}", "kg * m ** 2 / s"), ("{:L~}", r"\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}"), ("{:P~}", "kg·m²/s"), - ("{:H~}", r"\[kg\ {m}^{2}/s\]"), + ("{:H~}", "kg m2/s"), ("{:C~}", "kg*m**2/s"), ): with self.subTest(spec): @@ -63,12 +63,12 @@ def test_unit_default_formatting(self): r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("P", "kilogram·meter²/second"), - ("H", r"\[kilogram\ {meter}^{2}/second\]"), + ("H", "kilogram meter2/second"), ("C", "kilogram*meter**2/second"), ("~", "kg * m ** 2 / s"), ("L~", r"\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}"), ("P~", "kg·m²/s"), - ("H~", r"\[kg\ {m}^{2}/s\]"), + ("H~", "kg m2/s"), ("C~", "kg*m**2/s"), ): with self.subTest(spec): @@ -82,12 +82,12 @@ def test_unit_formatting_snake_case(self): for spec, result in ( ("L", r"\mathrm{oil\_barrel}"), ("P", "oil_barrel"), - ("H", r"\[oil\_barrel\]"), + ("H", "oil_barrel"), ("C", "oil_barrel"), ("~", "oil_bbl"), ("L~", r"\mathrm{oil\_bbl}"), ("P~", "oil_bbl"), - ("H~", r"\[oil\_bbl\]"), + ("H~", "oil_bbl"), ("C~", "oil_bbl"), ): with self.subTest(spec): @@ -104,7 +104,7 @@ def text(text): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), r"\[kilogram\ {meter}^{2}/second\]") + self.assertEqual(x._repr_html_(), "kilogram meter2/second") self.assertEqual( x._repr_latex_(), r"$\frac{\mathrm{kilogram} \cdot " r"\mathrm{meter}^{2}}{\mathrm{second}}$", @@ -112,7 +112,7 @@ def text(text): x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "kilogram·meter²/second") ureg.default_format = "~" - self.assertEqual(x._repr_html_(), r"\[kg\ {m}^{2}/s\]") + self.assertEqual(x._repr_html_(), "kg m2/s") self.assertEqual( x._repr_latex_(), r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" ) diff --git a/pint/unit.py b/pint/unit.py index 25084b46c..f09f39e31 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -91,10 +91,6 @@ def __format__(self, spec): else: units = self._units - if "H" in spec: - # HTML / Jupyter Notebook - return r"\[" + format(units, spec).replace(" ", r"\ ") + r"\]" - return format(units, spec) def format_babel(self, spec="", **kwspec):