# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst """ Regression tests for the units.format package """ import warnings from contextlib import nullcontext from fractions import Fraction import pytest import numpy as np from numpy.testing import assert_allclose from astropy import units as u from astropy.constants import si from astropy.units import core, dex, UnitsWarning from astropy.units import format as u_format from astropy.units.utils import is_effectively_unity @pytest.mark.parametrize('strings, unit', [ (["m s", "m*s", "m.s"], u.m * u.s), (["m/s", "m*s**-1", "m /s", "m / s", "m/ s"], u.m / u.s), (["m**2", "m2", "m**(2)", "m**+2", "m+2", "m^(+2)"], u.m ** 2), (["m**-3", "m-3", "m^(-3)", "/m3"], u.m ** -3), (["m**(1.5)", "m(3/2)", "m**(3/2)", "m^(3/2)"], u.m ** 1.5), (["2.54 cm"], u.Unit(u.cm * 2.54)), (["10+8m"], u.Unit(u.m * 1e8)), # This is the VOUnits documentation, but doesn't seem to follow the # unity grammar (["3.45 10**(-4)Jy"], 3.45 * 1e-4 * u.Jy) (["sqrt(m)"], u.m ** 0.5), (["dB(mW)", "dB (mW)"], u.DecibelUnit(u.mW)), (["mag"], u.mag), (["mag(ct/s)"], u.MagUnit(u.ct / u.s)), (["dex"], u.dex), (["dex(cm s**-2)", "dex(cm/s2)"], u.DexUnit(u.cm / u.s**2)), ]) def test_unit_grammar(strings, unit): for s in strings: print(s) unit2 = u_format.Generic.parse(s) assert unit2 == unit @pytest.mark.parametrize('string', ['sin( /pixel /s)', 'mag(mag)', 'dB(dB(mW))', 'dex()']) def test_unit_grammar_fail(string): with pytest.raises(ValueError): print(string) u_format.Generic.parse(string) @pytest.mark.parametrize('strings, unit', [ (["0.1nm"], u.AA), (["mW/m2"], u.Unit(u.erg / u.cm ** 2 / u.s)), (["mW/(m2)"], u.Unit(u.erg / u.cm ** 2 / u.s)), (["km/s", "km.s-1"], u.km / u.s), (["10pix/nm"], u.Unit(10 * u.pix / u.nm)), (["1.5x10+11m"], u.Unit(1.5e11 * u.m)), (["1.5×10+11m"], u.Unit(1.5e11 * u.m)), (["m2"], u.m ** 2), (["10+21m"], u.Unit(u.m * 1e21)), (["2.54cm"], u.Unit(u.cm * 2.54)), (["20%"], 0.20 * u.dimensionless_unscaled), (["10+9"], 1.e9 * u.dimensionless_unscaled), (["2x10-9"], 2.e-9 * u.dimensionless_unscaled), (["---"], u.dimensionless_unscaled), (["ma"], u.ma), (["mAU"], u.mAU), (["uarcmin"], u.uarcmin), (["uarcsec"], u.uarcsec), (["kbarn"], u.kbarn), (["Gbit"], u.Gbit), (["Gibit"], 2 ** 30 * u.bit), (["kbyte"], u.kbyte), (["mRy"], 0.001 * u.Ry), (["mmag"], u.mmag), (["Mpc"], u.Mpc), (["Gyr"], u.Gyr), (["°"], u.degree), (["°/s"], u.degree / u.s), (["Å"], u.AA), (["Å/s"], u.AA / u.s), (["\\h"], si.h), (["[cm/s2]"], dex(u.cm / u.s ** 2)), (["[K]"], dex(u.K)), (["[-]"], dex(u.dimensionless_unscaled))]) def test_cds_grammar(strings, unit): for s in strings: print(s) unit2 = u_format.CDS.parse(s) assert unit2 == unit @pytest.mark.parametrize('string', [ '0.1 nm', 'solMass(3/2)', 'km / s', 'km s-1', 'pix0.1nm', 'pix/(0.1nm)', 'km*s', 'km**2', '5x8+3m', '0.1---', '---m', 'm---', '--', '0.1-', '-m', 'm-', 'mag(s-1)', 'dB(mW)', 'dex(cm s-2)', '[--]']) def test_cds_grammar_fail(string): with pytest.raises(ValueError): print(string) u_format.CDS.parse(string) def test_cds_dimensionless(): assert u.Unit('---', format='cds') == u.dimensionless_unscaled assert u.dimensionless_unscaled.to_string(format='cds') == "---" def test_cds_log10_dimensionless(): assert u.Unit('[-]', format='cds') == u.dex(u.dimensionless_unscaled) assert u.dex(u.dimensionless_unscaled).to_string(format='cds') == "[-]" # These examples are taken from the EXAMPLES section of # https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/ @pytest.mark.parametrize('strings, unit', [ (["count /s", "count/s", "count s**(-1)", "count / s", "count /s "], u.count / u.s), (["/pixel /s", "/(pixel * s)"], (u.pixel * u.s) ** -1), (["count /m**2 /s /eV", "count m**(-2) * s**(-1) * eV**(-1)", "count /(m**2 * s * eV)"], u.count * u.m ** -2 * u.s ** -1 * u.eV ** -1), (["erg /pixel /s /GHz", "erg /s /GHz /pixel", "erg /pixel /(s * GHz)"], u.erg / (u.s * u.GHz * u.pixel)), (["keV**2 /yr /angstrom", "10**(10) keV**2 /yr /m"], # Though this is given as an example, it seems to violate the rules # of not raising scales to powers, so I'm just excluding it # "(10**2 MeV)**2 /yr /m" u.keV**2 / (u.yr * u.angstrom)), (["10**(46) erg /s", "10**46 erg /s", "10**(39) J /s", "10**(39) W", "10**(15) YW", "YJ /fs"], 10**46 * u.erg / u.s), (["10**(-7) J /cm**2 /MeV", "10**(-9) J m**(-2) eV**(-1)", "nJ m**(-2) eV**(-1)", "nJ /m**2 /eV"], 10 ** -7 * u.J * u.cm ** -2 * u.MeV ** -1), (["sqrt(erg /pixel /s /GHz)", "(erg /pixel /s /GHz)**(0.5)", "(erg /pixel /s /GHz)**(1/2)", "erg**(0.5) pixel**(-0.5) s**(-0.5) GHz**(-0.5)"], (u.erg * u.pixel ** -1 * u.s ** -1 * u.GHz ** -1) ** 0.5), (["(count /s) (/pixel /s)", "(count /s) * (/pixel /s)", "count /pixel /s**2"], (u.count / u.s) * (1.0 / (u.pixel * u.s)))]) def test_ogip_grammar(strings, unit): for s in strings: print(s) unit2 = u_format.OGIP.parse(s) assert unit2 == unit @pytest.mark.parametrize('string', [ 'log(photon /m**2 /s /Hz)', 'sin( /pixel /s)', 'log(photon /cm**2 /s /Hz) /(sin( /pixel /s))', 'log(photon /cm**2 /s /Hz) (sin( /pixel /s))**(-1)', 'dB(mW)', 'dex(cm/s**2)']) def test_ogip_grammar_fail(string): with pytest.raises(ValueError): print(string) u_format.OGIP.parse(string) class RoundtripBase: deprecated_units = set() def check_roundtrip(self, unit, output_format=None): if output_format is None: output_format = self.format_ with warnings.catch_warnings(): warnings.simplefilter('ignore') # Same warning shows up multiple times s = unit.to_string(output_format) if s in self.deprecated_units: with pytest.warns(UnitsWarning, match='deprecated') as w: a = core.Unit(s, format=self.format_) assert len(w) == 1 else: a = core.Unit(s, format=self.format_) # No warning assert_allclose(a.decompose().scale, unit.decompose().scale, rtol=1e-9) def check_roundtrip_decompose(self, unit): ud = unit.decompose() s = ud.to_string(self.format_) assert ' ' not in s a = core.Unit(s, format=self.format_) assert_allclose(a.decompose().scale, ud.scale, rtol=1e-5) class TestRoundtripGeneric(RoundtripBase): format_ = 'generic' @pytest.mark.parametrize('unit', [ unit for unit in u.__dict__.values() if (isinstance(unit, core.UnitBase) and not isinstance(unit, core.PrefixUnit))]) def test_roundtrip(self, unit): self.check_roundtrip(unit) self.check_roundtrip(unit, output_format='unicode') self.check_roundtrip_decompose(unit) class TestRoundtripVOUnit(RoundtripBase): format_ = 'vounit' deprecated_units = u_format.VOUnit._deprecated_units @pytest.mark.parametrize('unit', [ unit for unit in u_format.VOUnit._units.values() if (isinstance(unit, core.UnitBase) and not isinstance(unit, core.PrefixUnit))]) def test_roundtrip(self, unit): self.check_roundtrip(unit) if unit not in (u.mag, u.dB): self.check_roundtrip_decompose(unit) class TestRoundtripFITS(RoundtripBase): format_ = 'fits' deprecated_units = u_format.Fits._deprecated_units @pytest.mark.parametrize('unit', [ unit for unit in u_format.Fits._units.values() if (isinstance(unit, core.UnitBase) and not isinstance(unit, core.PrefixUnit))]) def test_roundtrip(self, unit): self.check_roundtrip(unit) class TestRoundtripCDS(RoundtripBase): format_ = 'cds' @pytest.mark.parametrize('unit', [ unit for unit in u_format.CDS._units.values() if (isinstance(unit, core.UnitBase) and not isinstance(unit, core.PrefixUnit))]) def test_roundtrip(self, unit): self.check_roundtrip(unit) if unit == u.mag: # Skip mag: decomposes into dex, which is unknown to CDS. return self.check_roundtrip_decompose(unit) @pytest.mark.parametrize('unit', [u.dex(unit) for unit in (u.cm/u.s**2, u.K, u.Lsun)]) def test_roundtrip_dex(self, unit): string = unit.to_string(format='cds') recovered = u.Unit(string, format='cds') assert recovered == unit class TestRoundtripOGIP(RoundtripBase): format_ = 'ogip' deprecated_units = u_format.OGIP._deprecated_units | {'d'} @pytest.mark.parametrize('unit', [ unit for unit in u_format.OGIP._units.values() if (isinstance(unit, core.UnitBase) and not isinstance(unit, core.PrefixUnit))]) def test_roundtrip(self, unit): if str(unit) in ('d', '0.001 Crab'): # Special-case day, which gets auto-converted to hours, and mCrab, # which the default check does not recognize as a deprecated unit. with pytest.warns(UnitsWarning): s = unit.to_string(self.format_) a = core.Unit(s, format=self.format_) assert_allclose(a.decompose().scale, unit.decompose().scale, rtol=1e-9) else: self.check_roundtrip(unit) if str(unit) in ('mag', 'byte', 'Crab'): # Skip mag and byte, which decompose into dex and bit, resp., # both of which are unknown to OGIP, as well as Crab, which does # not decompose, and thus gives a deprecated unit warning. return power_of_ten = np.log10(unit.decompose().scale) if abs(power_of_ten - round(power_of_ten)) > 1e-3: ctx = pytest.warns(UnitsWarning, match='power of 10') elif str(unit) == '0.001 Crab': ctx = pytest.warns(UnitsWarning, match='deprecated') else: ctx = nullcontext() with ctx: self.check_roundtrip_decompose(unit) def test_fits_units_available(): u_format.Fits._units def test_vo_units_available(): u_format.VOUnit._units def test_cds_units_available(): u_format.CDS._units def test_cds_non_ascii_unit(): """Regression test for #5350. This failed with a decoding error as μas could not be represented in ascii.""" from astropy.units import cds with cds.enable(): u.radian.find_equivalent_units(include_prefix_units=True) def test_latex(): fluxunit = u.erg / (u.cm ** 2 * u.s) assert fluxunit.to_string('latex') == r'$\mathrm{\frac{erg}{s\,cm^{2}}}$' def test_new_style_latex(): fluxunit = u.erg / (u.cm ** 2 * u.s) assert f"{fluxunit:latex}" == r'$\mathrm{\frac{erg}{s\,cm^{2}}}$' def test_latex_scale(): fluxunit = u.Unit(1.e-24 * u.erg / (u.cm ** 2 * u.s * u.Hz)) latex = r'$\mathrm{1 \times 10^{-24}\,\frac{erg}{Hz\,s\,cm^{2}}}$' assert fluxunit.to_string('latex') == latex def test_latex_inline_scale(): fluxunit = u.Unit(1.e-24 * u.erg / (u.cm ** 2 * u.s * u.Hz)) latex_inline = (r'$\mathrm{1 \times 10^{-24}\,erg' r'\,Hz^{-1}\,s^{-1}\,cm^{-2}}$') assert fluxunit.to_string('latex_inline') == latex_inline @pytest.mark.parametrize('format_spec, string', [ ('generic', 'erg / (cm2 s)'), ('s', 'erg / (cm2 s)'), ('console', ' erg \n ------\n s cm^2'), ('latex', '$\\mathrm{\\frac{erg}{s\\,cm^{2}}}$'), ('latex_inline', '$\\mathrm{erg\\,s^{-1}\\,cm^{-2}}$'), ('>20s', ' erg / (cm2 s)')]) def test_format_styles(format_spec, string): fluxunit = u.erg / (u.cm ** 2 * u.s) assert format(fluxunit, format_spec) == string def test_flatten_to_known(): myunit = u.def_unit("FOOBAR_One", u.erg / u.Hz) assert myunit.to_string('fits') == 'erg Hz-1' myunit2 = myunit * u.bit ** 3 assert myunit2.to_string('fits') == 'bit3 erg Hz-1' def test_flatten_impossible(): myunit = u.def_unit("FOOBAR_Two") with u.add_enabled_units(myunit), pytest.raises(ValueError): myunit.to_string('fits') def test_console_out(): """ Issue #436. """ u.Jy.decompose().to_string('console') def test_flexible_float(): assert u.min._represents.to_string('latex') == r'$\mathrm{60\,s}$' def test_fits_to_string_function_error(): """Test function raises TypeError on bad input. This instead of returning None, see gh-11825. """ with pytest.raises(TypeError, match='unit argument must be'): u_format.Fits.to_string(None) def test_fraction_repr(): area = u.cm ** 2.0 assert '.' not in area.to_string('latex') fractional = u.cm ** 2.5 assert '5/2' in fractional.to_string('latex') assert fractional.to_string('unicode') == 'cm⁵⸍²' def test_scale_effectively_unity(): """Scale just off unity at machine precision level is OK. Ensures #748 does not recur """ a = (3. * u.N).cgs assert is_effectively_unity(a.unit.scale) assert len(a.__repr__().split()) == 3 def test_percent(): """Test that the % unit is properly recognized. Since % is a special symbol, this goes slightly beyond the round-tripping tested above.""" assert u.Unit('%') == u.percent == u.Unit(0.01) assert u.Unit('%', format='cds') == u.Unit(0.01) assert u.Unit(0.01).to_string('cds') == '%' with pytest.raises(ValueError): u.Unit('%', format='fits') with pytest.raises(ValueError): u.Unit('%', format='vounit') def test_scaled_dimensionless(): """Test that scaled dimensionless units are properly recognized in generic and CDS, but not in fits and vounit.""" assert u.Unit('0.1') == u.Unit(0.1) == 0.1 * u.dimensionless_unscaled assert u.Unit('1.e-4') == u.Unit(1.e-4) assert u.Unit('10-4', format='cds') == u.Unit(1.e-4) assert u.Unit('10+8').to_string('cds') == '10+8' with pytest.raises(ValueError): u.Unit(0.15).to_string('fits') assert u.Unit(0.1).to_string('fits') == '10**-1' with pytest.raises(ValueError): u.Unit(0.1).to_string('vounit') def test_deprecated_did_you_mean_units(): with pytest.raises(ValueError) as exc_info: u.Unit('ANGSTROM', format='fits') assert 'Did you mean Angstrom or angstrom?' in str(exc_info.value) with pytest.raises(ValueError) as exc_info: u.Unit('crab', format='ogip') assert 'Crab (deprecated)' in str(exc_info.value) assert 'mCrab (deprecated)' in str(exc_info.value) with pytest.warns(UnitsWarning, match=r'.* Did you mean 0\.1nm, Angstrom ' r'\(deprecated\) or angstrom \(deprecated\)\?') as w: u.Unit('ANGSTROM', format='vounit') assert len(w) == 1 assert str(w[0].message).count('0.1nm') == 1 with pytest.warns(UnitsWarning, match=r'.* 0\.1nm\.') as w: u.Unit('angstrom', format='vounit') assert len(w) == 1 @pytest.mark.parametrize('string', ['mag(ct/s)', 'dB(mW)', 'dex(cm s**-2)']) def test_fits_function(string): # Function units cannot be written, so ensure they're not parsed either. with pytest.raises(ValueError): print(string) u_format.Fits().parse(string) @pytest.mark.parametrize('string', ['mag(ct/s)', 'dB(mW)', 'dex(cm s**-2)']) def test_vounit_function(string): # Function units cannot be written, so ensure they're not parsed either. with pytest.raises(ValueError), warnings.catch_warnings(): warnings.simplefilter('ignore') # ct, dex also raise warnings - irrelevant here. u_format.VOUnit().parse(string) def test_vounit_binary_prefix(): u.Unit('KiB', format='vounit') == u.Unit('1024 B') u.Unit('Kibyte', format='vounit') == u.Unit('1024 B') u.Unit('Kibit', format='vounit') == u.Unit('1024 B') with pytest.warns(UnitsWarning) as w: u.Unit('kibibyte', format='vounit') assert len(w) == 1 def test_vounit_unknown(): assert u.Unit('unknown', format='vounit') is None assert u.Unit('UNKNOWN', format='vounit') is None assert u.Unit('', format='vounit') is u.dimensionless_unscaled def test_vounit_details(): with pytest.warns(UnitsWarning, match='deprecated') as w: assert u.Unit('Pa', format='vounit') is u.Pascal assert len(w) == 1 # The da- prefix is not allowed, and the d- prefix is discouraged assert u.dam.to_string('vounit') == '10m' assert u.Unit('dam dag').to_string('vounit') == '100g.m' # Parse round-trip with pytest.warns(UnitsWarning, match='deprecated'): flam = u.erg / u.cm / u.cm / u.s / u.AA x = u.format.VOUnit.to_string(flam) assert x == 'Angstrom**-1.cm**-2.erg.s**-1' new_flam = u.format.VOUnit.parse(x) assert new_flam == flam @pytest.mark.parametrize('unit, vounit, number, scale, voscale', [('nm', 'nm', 0.1, '10^-1', '0.1'), ('fm', 'fm', 100.0, '10+2', '100'), ('m^2', 'm**2', 100.0, '100.0', '100'), ('cm', 'cm', 2.54, '2.54', '2.54'), ('kg', 'kg', 1.898124597e27, '1.898124597E27', '1.8981246e+27'), ('m/s', 'm.s**-1', 299792458.0, '299792458', '2.9979246e+08'), ('cm2', 'cm**2', 1.e-20, '10^(-20)', '1e-20')]) def test_vounit_scale_factor(unit, vounit, number, scale, voscale): x = u.Unit(f'{scale} {unit}') assert x == number * u.Unit(unit) assert x.to_string(format='vounit') == voscale + vounit def test_vounit_custom(): x = u.Unit("'foo' m", format='vounit') x_vounit = x.to_string('vounit') assert x_vounit == "'foo'.m" x_string = x.to_string() assert x_string == "foo m" x = u.Unit("m'foo' m", format='vounit') assert x.bases[1]._represents.scale == 0.001 x_vounit = x.to_string('vounit') assert x_vounit == "m.m'foo'" x_string = x.to_string() assert x_string == 'm mfoo' def test_vounit_implicit_custom(): # Yikes, this becomes "femto-urlong"... But at least there's a warning. with pytest.warns(UnitsWarning) as w: x = u.Unit("furlong/week", format="vounit") assert x.bases[0]._represents.scale == 1e-15 assert x.bases[0]._represents.bases[0].name == 'urlong' assert len(w) == 2 assert 'furlong' in str(w[0].message) assert 'week' in str(w[1].message) @pytest.mark.parametrize('scale, number, string', [('10+2', 100, '10**2'), ('10(+2)', 100, '10**2'), ('10**+2', 100, '10**2'), ('10**(+2)', 100, '10**2'), ('10^+2', 100, '10**2'), ('10^(+2)', 100, '10**2'), ('10**2', 100, '10**2'), ('10**(2)', 100, '10**2'), ('10^2', 100, '10**2'), ('10^(2)', 100, '10**2'), ('10-20', 10**(-20), '10**-20'), ('10(-20)', 10**(-20), '10**-20'), ('10**-20', 10**(-20), '10**-20'), ('10**(-20)', 10**(-20), '10**-20'), ('10^-20', 10**(-20), '10**-20'), ('10^(-20)', 10**(-20), '10**-20'), ]) def test_fits_scale_factor(scale, number, string): x = u.Unit(scale + ' erg/(s cm**2 Angstrom)', format='fits') assert x == number * (u.erg / u.s / u.cm ** 2 / u.Angstrom) assert x.to_string(format='fits') == string + ' Angstrom-1 cm-2 erg s-1' x = u.Unit(scale + '*erg/(s cm**2 Angstrom)', format='fits') assert x == number * (u.erg / u.s / u.cm ** 2 / u.Angstrom) assert x.to_string(format='fits') == string + ' Angstrom-1 cm-2 erg s-1' def test_fits_scale_factor_errors(): with pytest.raises(ValueError): x = u.Unit('1000 erg/(s cm**2 Angstrom)', format='fits') with pytest.raises(ValueError): x = u.Unit('12 erg/(s cm**2 Angstrom)', format='fits') x = u.Unit(1.2 * u.erg) with pytest.raises(ValueError): x.to_string(format='fits') x = u.Unit(100.0 * u.erg) assert x.to_string(format='fits') == '10**2 erg' def test_double_superscript(): """Regression test for #5870, #8699, #9218; avoid double superscripts.""" assert (u.deg).to_string("latex") == r'$\mathrm{{}^{\circ}}$' assert (u.deg**2).to_string("latex") == r'$\mathrm{deg^{2}}$' assert (u.arcmin).to_string("latex") == r'$\mathrm{{}^{\prime}}$' assert (u.arcmin**2).to_string("latex") == r'$\mathrm{arcmin^{2}}$' assert (u.arcsec).to_string("latex") == r'$\mathrm{{}^{\prime\prime}}$' assert (u.arcsec**2).to_string("latex") == r'$\mathrm{arcsec^{2}}$' assert (u.hourangle).to_string("latex") == r'$\mathrm{{}^{h}}$' assert (u.hourangle**2).to_string("latex") == r'$\mathrm{hourangle^{2}}$' assert (u.electron).to_string("latex") == r'$\mathrm{e^{-}}$' assert (u.electron**2).to_string("latex") == r'$\mathrm{electron^{2}}$' @pytest.mark.parametrize('power,expected', ( (1., 'm'), (2., 'm2'), (-10, '1 / m10'), (1.5, 'm(3/2)'), (2/3, 'm(2/3)'), (7/11, 'm(7/11)'), (-1/64, '1 / m(1/64)'), (1/100, 'm(1/100)'), (2/101, 'm(0.019801980198019802)'), (Fraction(2, 101), 'm(2/101)'))) def test_powers(power, expected): """Regression test for #9279 - powers should not be oversimplified.""" unit = u.m ** power s = unit.to_string() assert s == expected assert unit == s @pytest.mark.parametrize('string,unit', [ ('\N{MICRO SIGN}g', u.microgram), ('\N{GREEK SMALL LETTER MU}g', u.microgram), ('g\N{MINUS SIGN}1', u.g**(-1)), ('m\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT ONE}', 1 / u.m), ('m s\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT ONE}', u.m / u.s), ('m\N{SUPERSCRIPT TWO}', u.m**2), ('m\N{SUPERSCRIPT PLUS SIGN}\N{SUPERSCRIPT TWO}', u.m**2), ('m\N{SUPERSCRIPT THREE}', u.m**3), ('m\N{SUPERSCRIPT ONE}\N{SUPERSCRIPT ZERO}', u.m**10), ('\N{GREEK CAPITAL LETTER OMEGA}', u.ohm), ('\N{OHM SIGN}', u.ohm), # deprecated but for compatibility ('\N{MICRO SIGN}\N{GREEK CAPITAL LETTER OMEGA}', u.microOhm), ('\N{ANGSTROM SIGN}', u.Angstrom), ('\N{ANGSTROM SIGN} \N{OHM SIGN}', u.Angstrom * u.Ohm), ('\N{LATIN CAPITAL LETTER A WITH RING ABOVE}', u.Angstrom), ('\N{LATIN CAPITAL LETTER A}\N{COMBINING RING ABOVE}', u.Angstrom), ('m\N{ANGSTROM SIGN}', u.milliAngstrom), ('°C', u.deg_C), ('°', u.deg), ('M⊙', u.Msun), # \N{CIRCLED DOT OPERATOR} ('L☉', u.Lsun), # \N{SUN} ('M⊕', u.Mearth), # normal earth symbol = \N{CIRCLED PLUS} ('M♁', u.Mearth), # be generous with \N{EARTH} ('R♃', u.Rjup), # \N{JUPITER} ('′', u.arcmin), # \N{PRIME} ('R∞', u.Ry), ('Mₚ', u.M_p), ]) def test_unicode(string, unit): assert u_format.Generic.parse(string) == unit assert u.Unit(string) == unit @pytest.mark.parametrize('string', [ 'g\N{MICRO SIGN}', 'g\N{MINUS SIGN}', 'm\N{SUPERSCRIPT MINUS}1', 'm+\N{SUPERSCRIPT ONE}', 'm\N{MINUS SIGN}\N{SUPERSCRIPT ONE}', 'k\N{ANGSTROM SIGN}', ]) def test_unicode_failures(string): with pytest.raises(ValueError): u.Unit(string) @pytest.mark.parametrize('format_', ('unicode', 'latex', 'latex_inline')) def test_parse_error_message_for_output_only_format(format_): with pytest.raises(NotImplementedError, match='not parse'): u.Unit('m', format=format_) def test_unknown_parser(): with pytest.raises(ValueError, match=r"Unknown.*unicode'\] for output only"): u.Unit('m', format='foo')