diff options
Diffstat (limited to 'pint')
-rw-r--r-- | pint/compat.py | 27 | ||||
-rw-r--r-- | pint/constants_en.txt | 2 | ||||
-rw-r--r-- | pint/default_en.txt | 2 | ||||
-rw-r--r-- | pint/default_en_0.6.txt | 360 | ||||
-rw-r--r-- | pint/definitions.py | 16 | ||||
-rw-r--r-- | pint/numpy_func.py | 18 | ||||
-rwxr-xr-x | pint/pint-convert | 120 | ||||
-rw-r--r-- | pint/quantity.py | 83 | ||||
-rw-r--r-- | pint/registry.py | 101 | ||||
-rw-r--r-- | pint/testsuite/helpers.py | 4 | ||||
-rw-r--r-- | pint/testsuite/test_babel.py | 35 | ||||
-rw-r--r-- | pint/testsuite/test_issues.py | 18 | ||||
-rw-r--r-- | pint/testsuite/test_numpy.py | 38 | ||||
-rw-r--r-- | pint/testsuite/test_quantity.py | 9 | ||||
-rw-r--r-- | pint/testsuite/test_unit.py | 41 | ||||
-rw-r--r-- | pint/unit.py | 35 | ||||
-rw-r--r-- | pint/util.py | 16 |
17 files changed, 450 insertions, 475 deletions
diff --git a/pint/compat.py b/pint/compat.py index 6fb27ee..e8e1a1b 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -7,7 +7,6 @@ :copyright: 2013 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -import os import tokenize from decimal import Decimal from io import BytesIO @@ -37,27 +36,6 @@ class BehaviorChangeWarning(UserWarning): pass -array_function_change_msg = """The way Pint handles NumPy operations has changed with the -implementation of NEP 18. Unimplemented NumPy operations will now fail instead of making -assumptions about units. Some functions, eg concat, will now return Quanties with units, where -they returned ndarrays previously. See https://github.com/hgrecco/pint/pull/905. - -To hide this warning, wrap your first creation of an array Quantity with -warnings.catch_warnings(), like the following: - -import numpy as np -import warnings -from pint import Quantity - -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - Quantity([]) - -To disable the new behavior, see -https://www.numpy.org/neps/nep-0018-array-function-protocol.html#implementation -""" - - try: import numpy as np from numpy import ndarray @@ -93,12 +71,9 @@ try: return False HAS_NUMPY_ARRAY_FUNCTION = _test_array_function_protocol() - SKIP_ARRAY_FUNCTION_CHANGE_WARNING = not HAS_NUMPY_ARRAY_FUNCTION NP_NO_VALUE = np._NoValue - ARRAY_FALLBACK = bool(int(os.environ.get("PINT_ARRAY_PROTOCOL_FALLBACK", 1))) - except ImportError: np = None @@ -110,9 +85,7 @@ except ImportError: NUMPY_VER = "0" NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False - SKIP_ARRAY_FUNCTION_CHANGE_WARNING = True NP_NO_VALUE = None - ARRAY_FALLBACK = False def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): if force_ndarray or force_ndarray_like: diff --git a/pint/constants_en.txt b/pint/constants_en.txt index fa485fa..c3ecbf1 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -8,7 +8,7 @@ #### MATHEMATICAL CONSTANTS #### # As computed by Maxima with fpprec:50 -pi = 3.1415926535897932384626433832795028841971693993751 = π # pi +pi = 3.1415926535897932384626433832795028841971693993751 = π # pi tansec = 4.8481368111333441675396429478852851658848753880815e-6 # tangent of 1 arc-second ~ arc_second/radian ln10 = 2.3025850929940456840179914546843642076011014886288 # natural logarithm of 10 wien_x = 4.9651142317442763036987591313228939440555849867973 # solution to (x-5)*exp(x)+5 = 0 => x = W(5/exp(5))+5 diff --git a/pint/default_en.txt b/pint/default_en.txt index 6d5fe6a..8bd4133 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -36,7 +36,7 @@ # [density] = [mass] / [volume] # # Note that primary dimensions don't need to be declared; they can be -# defined or the first time in a unit definition. +# defined for the first time in a unit definition. # E.g. see below `meter = [length]` # # diff --git a/pint/default_en_0.6.txt b/pint/default_en_0.6.txt deleted file mode 100644 index fb722c0..0000000 --- a/pint/default_en_0.6.txt +++ /dev/null @@ -1,360 +0,0 @@ -# Default Pint units definition file -# Based on the International System of Units -# Language: english -# :copyright: 2013 by Pint Authors, see AUTHORS for more details. - -# decimal prefixes -yocto- = 1e-24 = y- -zepto- = 1e-21 = z- -atto- = 1e-18 = a- -femto- = 1e-15 = f- -pico- = 1e-12 = p- -nano- = 1e-9 = n- -micro- = 1e-6 = u- = µ- -milli- = 1e-3 = m- -centi- = 1e-2 = c- -deci- = 1e-1 = d- -deca- = 1e+1 = da- -hecto- = 1e2 = h- -kilo- = 1e3 = k- -mega- = 1e6 = M- -giga- = 1e9 = G- -tera- = 1e12 = T- -peta- = 1e15 = P- -exa- = 1e18 = E- -zetta- = 1e21 = Z- -yotta- = 1e24 = Y- - -# binary_prefixes -kibi- = 2**10 = Ki- -mebi- = 2**20 = Mi- -gibi- = 2**30 = Gi- -tebi- = 2**40 = Ti- -pebi- = 2**50 = Pi- -exbi- = 2**60 = Ei- -zebi- = 2**70 = Zi- -yobi- = 2**80 = Yi- - -# reference -meter = [length] = m = metre -second = [time] = s = sec -ampere = [current] = A = amp -candela = [luminosity] = cd = candle -gram = [mass] = g -mole = [substance] = mol -kelvin = [temperature]; offset: 0 = K = degK -radian = [] = rad -bit = [] -count = [] - -@import constants_en.txt - -# acceleration -[acceleration] = [length] / [time] ** 2 - -# Angle -turn = 2 * pi * radian = revolution = cycle = circle -degree = pi / 180 * radian = deg = arcdeg = arcdegree = angular_degree -arcminute = arcdeg / 60 = arcmin = arc_minute = angular_minute -arcsecond = arcmin / 60 = arcsec = arc_second = angular_second -steradian = radian ** 2 = sr - -# Area -[area] = [length] ** 2 -are = 100 * m**2 -barn = 1e-28 * m ** 2 = b -cmil = 5.067075e-10 * m ** 2 = circular_mils -darcy = 9.869233e-13 * m ** 2 -acre = 4046.8564224 * m ** 2 = international_acre -hectare = 100 * are = ha -US_survey_acre = 160 * rod ** 2 - -# EM -esu = 1 * erg**0.5 * centimeter**0.5 = statcoulombs = statC = franklin = Fr -esu_per_second = 1 * esu / second = statampere -ampere_turn = 1 * A -gilbert = 10 / (4 * pi ) * ampere_turn -coulomb = ampere * second = C -volt = joule / coulomb = V -farad = coulomb / volt = F -ohm = volt / ampere = Ω -siemens = ampere / volt = S = mho -weber = volt * second = Wb -tesla = weber / meter ** 2 = T -henry = weber / ampere = H -elementary_charge = 1.602176487e-19 * coulomb = e -chemical_faraday = 9.64957e4 * coulomb -physical_faraday = 9.65219e4 * coulomb -faraday = 96485.3399 * coulomb = C12_faraday -gamma = 1e-9 * tesla -gauss = 1e-4 * tesla -maxwell = 1e-8 * weber = mx -oersted = 1000 / (4 * pi) * A / m = Oe -statfarad = 1.112650e-12 * farad = statF = stF -stathenry = 8.987554e11 * henry = statH = stH -statmho = 1.112650e-12 * siemens = statS = stS -statohm = 8.987554e11 * ohm -statvolt = 2.997925e2 * volt = statV = stV -unit_pole = 1.256637e-7 * weber - -# Energy -[energy] = [force] * [length] -joule = newton * meter = J -erg = dyne * centimeter -btu = 1.05505585262e3 * joule = Btu = BTU = british_thermal_unit -electron_volt = 1.60217653e-19 * J = eV -quadrillion_btu = 10**15 * btu = quad -thm = 100000 * BTU = therm = EC_therm -cal = 4.184 * joule = calorie = thermochemical_calorie -international_steam_table_calorie = 4.1868 * joule -ton_TNT = 4.184e9 * joule = tTNT -US_therm = 1.054804e8 * joule -watt_hour = watt * hour = Wh = watthour -hartree = 4.35974394e-18 * joule = E_h = hartree_energy - -# Force -[force] = [mass] * [acceleration] -newton = kilogram * meter / second ** 2 = N -dyne = gram * centimeter / second ** 2 = dyn -force_kilogram = g_0 * kilogram = kgf = kilogram_force = pond -force_gram = g_0 * gram = gf = gram_force -force_ounce = g_0 * ounce = ozf = ounce_force -force_pound = g_0 * lb = lbf = pound_force -force_ton = 2000 * force_pound = ton_force -poundal = lb * feet / second ** 2 = pdl -kip = 1000*lbf - -# Frequency -[frequency] = 1 / [time] -hertz = 1 / second = Hz = rps -revolutions_per_minute = revolution / minute = rpm -counts_per_second = count / second = cps - -# Heat -#RSI = degK * meter ** 2 / watt -#clo = 0.155 * RSI = clos -#R_value = foot ** 2 * degF * hour / btu - -# Information -byte = 8 * bit = B = octet -baud = bit / second = Bd = bps - -# Irradiance -peak_sun_hour = 1000 * watt_hour / meter**2 = PSH -langley = thermochemical_calorie / centimeter**2 = Langley - -# Length -angstrom = 1e-10 * meter = Å = ångström = Å -inch = 2.54 * centimeter = in = international_inch = inches = international_inches -foot = 12 * inch = ft = international_foot = feet = international_feet -mile = 5280 * foot = mi = international_mile -yard = 3 * feet = yd = international_yard -mil = inch / 1000 = thou -parsec = 3.08568025e16 * meter = pc -light_year = speed_of_light * julian_year = ly = lightyear -astronomical_unit = 149597870691 * meter = au -nautical_mile = 1.852e3 * meter = nmi -printers_point = 127 * millimeter / 360 = point -printers_pica = 12 * printers_point = pica -US_survey_foot = 1200 * meter / 3937 -US_survey_yard = 3 * US_survey_foot -US_survey_mile = 5280 * US_survey_foot = US_statute_mile -rod = 16.5 * US_survey_foot = pole = perch -furlong = 660 * US_survey_foot -fathom = 6 * US_survey_foot -chain = 66 * US_survey_foot -barleycorn = inch / 3 -arpentlin = 191.835 * feet -kayser = 1 / centimeter = wavenumber - -# Mass -dram = oz / 16 = dr = avoirdupois_dram -ounce = 28.349523125 * gram = oz = avoirdupois_ounce -pound = 0.45359237 * kilogram = lb = avoirdupois_pound -stone = 14 * lb = st -carat = 200 * milligram -grain = 64.79891 * milligram = gr -long_hundredweight = 112 * lb -short_hundredweight = 100 * lb -metric_ton = 1000 * kilogram = t = tonne -pennyweight = 24 * gram = dwt -slug = 14.59390 * kilogram -troy_ounce = 480 * grain = toz = apounce = apothecary_ounce -troy_pound = 12 * toz = tlb = appound = apothecary_pound -drachm = 60 * grain = apdram = apothecary_dram -atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da -scruple = 20 * grain -bag = 94 * lb -ton = 2000 * lb = short_ton - -# Textile -denier = gram / (9000 * meter) -tex = gram / (1000 * meter) -dtex = decitex - -# Photometry -lumen = candela * steradian = lm -lux = lumen / meter ** 2 = lx - -# Power -[power] = [energy] / [time] -watt = joule / second = W = volt_ampere = VA -horsepower = 33000 * ft * lbf / min = hp = UK_horsepower = British_horsepower -boiler_horsepower = 33475 * btu / hour -metric_horsepower = 75 * force_kilogram * meter / second -electric_horsepower = 746 * watt -hydraulic_horsepower = 550 * feet * lbf / second -refrigeration_ton = 12000 * btu / hour = ton_of_refrigeration - -# Pressure -[pressure] = [force] / [area] -Hg = gravity * 13.59510 * gram / centimeter ** 3 = mercury = conventional_mercury -mercury_60F = gravity * 13.5568 * gram / centimeter ** 3 -H2O = gravity * 1000 * kilogram / meter ** 3 = h2o = water = conventional_water -water_4C = gravity * 999.972 * kilogram / meter ** 3 = water_39F -water_60F = gravity * 999.001 * kilogram / m ** 3 -pascal = newton / meter ** 2 = Pa -bar = 100000 * pascal -atmosphere = 101325 * pascal = atm = standard_atmosphere -technical_atmosphere = kilogram * gravity / centimeter ** 2 = at -torr = atm / 760 -pound_force_per_square_inch = pound * gravity / inch ** 2 = psi -kip_per_square_inch = kip / inch ** 2 = ksi -barye = 0.1 * newton / meter ** 2 = barie = barad = barrie = baryd = Ba -mm_Hg = millimeter * Hg = mmHg = millimeter_Hg = millimeter_Hg_0C -cm_Hg = centimeter * Hg = cmHg = centimeter_Hg -in_Hg = inch * Hg = inHg = inch_Hg = inch_Hg_32F -inch_Hg_60F = inch * mercury_60F -inch_H2O_39F = inch * water_39F -inch_H2O_60F = inch * water_60F -footH2O = ft * water -cmH2O = centimeter * water -foot_H2O = ft * water = ftH2O -standard_liter_per_minute = 1.68875 * Pa * m ** 3 / s = slpm = slm - -# Radiation -Bq = Hz = becquerel -curie = 3.7e10 * Bq = Ci -rutherford = 1e6*Bq = rd = Rd -Gy = joule / kilogram = gray = Sv = sievert -rem = 1e-2 * sievert -rads = 1e-2 * gray -roentgen = 2.58e-4 * coulomb / kilogram - -# Temperature -degC = kelvin; offset: 273.15 = celsius -degR = 5 / 9 * kelvin; offset: 0 = rankine -degF = 5 / 9 * kelvin; offset: 255.372222 = fahrenheit - -# Time -minute = 60 * second = min -hour = 60 * minute = hr -day = 24 * hour -week = 7 * day -fortnight = 2 * week -year = 31556925.9747 * second -month = year / 12 -shake = 1e-8 * second -sidereal_day = day / 1.00273790935079524 -sidereal_hour = sidereal_day / 24 -sidereal_minute = sidereal_hour / 60 -sidereal_second = sidereal_minute / 60 -sidereal_year = 366.25636042 * sidereal_day -sidereal_month = 27.321661 * sidereal_day -tropical_month = 27.321661 * day -synodic_month = 29.530589 * day = lunar_month -common_year = 365 * day -leap_year = 366 * day -julian_year = 365.25 * day -gregorian_year = 365.2425 * day -millenium = 1000 * year = millenia = milenia = milenium -eon = 1e9 * year -work_year = 2056 * hour -work_month = work_year / 12 - -# Velocity -[speed] = [length] / [time] -knot = nautical_mile / hour = kt = knot_international = international_knot = nautical_miles_per_hour -mph = mile / hour = MPH -kph = kilometer / hour = KPH - -# Viscosity -[viscosity] = [pressure] * [time] -poise = 1e-1 * Pa * second = P -stokes = 1e-4 * meter ** 2 / second = St -rhe = 10 / (Pa * s) - -# Volume -[volume] = [length] ** 3 -liter = 1e-3 * m ** 3 = l = L = litre -cc = centimeter ** 3 = cubic_centimeter -stere = meter ** 3 -gross_register_ton = 100 * foot ** 3 = register_ton = GRT -acre_foot = acre * foot = acre_feet -board_foot = foot ** 2 * inch = FBM -bushel = 2150.42 * inch ** 3 = bu = US_bushel -dry_gallon = bushel / 8 = US_dry_gallon -dry_quart = dry_gallon / 4 = US_dry_quart -dry_pint = dry_quart / 2 = US_dry_pint -gallon = 231 * inch ** 3 = liquid_gallon = US_liquid_gallon -quart = gallon / 4 = liquid_quart = US_liquid_quart -pint = quart / 2 = pt = liquid_pint = US_liquid_pint -cup = pint / 2 = liquid_cup = US_liquid_cup -gill = cup / 2 = liquid_gill = US_liquid_gill -fluid_ounce = gill / 4 = floz = US_fluid_ounce = US_liquid_ounce -imperial_bushel = 36.36872 * liter = UK_bushel -imperial_gallon = imperial_bushel / 8 = UK_gallon -imperial_quart = imperial_gallon / 4 = UK_quart -imperial_pint = imperial_quart / 2 = UK_pint -imperial_cup = imperial_pint / 2 = UK_cup -imperial_gill = imperial_cup / 2 = UK_gill -imperial_floz = imperial_gill / 5 = UK_fluid_ounce = imperial_fluid_ounce -barrel = 42 * gallon = bbl -tablespoon = floz / 2 = tbsp = Tbsp = Tblsp = tblsp = tbs = Tbl -teaspoon = tablespoon / 3 = tsp -peck = bushel / 4 = pk -fluid_dram = floz / 8 = fldr = fluidram -firkin = barrel / 4 - - -@context(n=1) spectroscopy = sp - # n index of refraction of the medium. - [length] <-> [frequency]: speed_of_light / n / value - [frequency] -> [energy]: planck_constant * value - [energy] -> [frequency]: value / planck_constant -@end - -@context boltzmann - [temperature] -> [energy]: boltzmann_constant * value - [energy] -> [temperature]: value / boltzmann_constant -@end - -@context(mw=0,volume=0,solvent_mass=0) chemistry = chem - # mw is the molecular weight of the species - # volume is the volume of the solution - # solvent_mass is the mass of solvent in the solution - - # moles -> mass require the molecular weight - [substance] -> [mass]: value * mw - [mass] -> [substance]: value / mw - - # moles/volume -> mass/volume and moles/mass -> mass / mass - # require the molecular weight - [substance] / [volume] -> [mass] / [volume]: value * mw - [mass] / [volume] -> [substance] / [volume]: value / mw - [substance] / [mass] -> [mass] / [mass]: value * mw - [mass] / [mass] -> [substance] / [mass]: value / mw - - # moles/volume -> moles requires the solution volume - [substance] / [volume] -> [substance]: value * volume - [substance] -> [substance] / [volume]: value / volume - - # moles/mass -> moles requires the solvent (usually water) mass - [substance] / [mass] -> [substance]: value * solvent_mass - [substance] -> [substance] / [mass]: value / solvent_mass - - # moles/mass -> moles/volume require the solvent mass and the volume - [substance] / [mass] -> [substance]/[volume]: value * solvent_mass / volume - [substance] / [volume] -> [substance] / [mass]: value / solvent_mass * volume - -@end diff --git a/pint/definitions.py b/pint/definitions.py index 327cc87..338d1fc 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -15,8 +15,8 @@ from .errors import DefinitionSyntaxError from .util import ParserHelper, UnitsContainer, _is_dim -class ParsedDefinition( - namedtuple("ParsedDefinition", "name symbol aliases value rhs_parts") +class PreprocessedDefinition( + namedtuple("PreprocessedDefinition", "name symbol aliases value rhs_parts") ): """Splits a definition into the constitutive parts. @@ -126,7 +126,7 @@ class Definition: Parameters ---------- - definition : str or ParsedDefinition + definition : str or PreprocessedDefinition non_int_type : type Returns @@ -135,7 +135,7 @@ class Definition: """ if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) if definition.name.startswith("@alias "): return AliasDefinition.from_string(definition, non_int_type) @@ -186,7 +186,7 @@ class PrefixDefinition(Definition): @classmethod def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) aliases = tuple(alias.strip("-") for alias in definition.aliases) if definition.symbol: @@ -230,7 +230,7 @@ class UnitDefinition(Definition): @classmethod def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) if ";" in definition.value: [converter, modifiers] = definition.value.split(";", 2) @@ -296,7 +296,7 @@ class DimensionDefinition(Definition): @classmethod def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) converter = ParserHelper.from_string(definition.value, non_int_type) @@ -338,7 +338,7 @@ class AliasDefinition(Definition): def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) name = definition.name[len("@alias ") :].lstrip() return AliasDefinition(name, tuple(definition.rhs_parts)) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 1fd7d8c..c03887b 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -654,20 +654,22 @@ def _pad(array, pad_width, mode="constant", **kwargs): def _recursive_convert(arg, unit): if iterable(arg): return tuple(_recursive_convert(a, unit=unit) for a in arg) - elif _is_quantity(arg): - return arg.m_as(unit) - else: - return arg + elif not _is_quantity(arg): + if arg == 0 or np.isnan(arg): + arg = unit._REGISTRY.Quantity(arg, unit) + else: + arg = unit._REGISTRY.Quantity(arg, "dimensionless") + + return arg.m_as(unit) # pad only dispatches on array argument, so we know it is a Quantity units = array.units # Handle flexible constant_values and end_values, converting to units if Quantity # and ignoring if not - if mode == "constant": - kwargs["constant_values"] = _recursive_convert(kwargs["constant_values"], units) - elif mode == "linear_ramp": - kwargs["end_values"] = _recursive_convert(kwargs["end_values"], units) + for key in ("constant_values", "end_values"): + if key in kwargs: + kwargs[key] = _recursive_convert(kwargs[key], units) return units._REGISTRY.Quantity( np.pad(array._magnitude, pad_width, mode=mode, **kwargs), units diff --git a/pint/pint-convert b/pint/pint-convert new file mode 100755 index 0000000..da7df23 --- /dev/null +++ b/pint/pint-convert @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +""" + pint-convert + ~~~~~~~~~~~~ + + :copyright: 2020 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +import argparse +import re +import sys + +from pint import UnitRegistry + +parser = argparse.ArgumentParser(description='Unit converter.', usage=argparse.SUPPRESS) +parser.add_argument('-s', '--system', metavar='sys', default='SI', help='unit system to convert to (default: SI)') +parser.add_argument('-p', '--prec', metavar='n', type=int, default=12, help='number of maximum significant figures (default: 12)') +parser.add_argument('-u', '--prec-unc', metavar='n', type=int, default=2, help='number of maximum uncertainty digits (default: 2)') +parser.add_argument('-U', '--no-unc', dest='unc', action='store_false', help='ignore uncertainties in constants') +parser.add_argument('-C', '--no-corr', dest='corr', action='store_false', help='ignore correlations between constants') +parser.add_argument('fr', metavar='from', type=str, help='unit or quantity to convert from') +parser.add_argument('to', type=str, nargs='?', help='unit to convert to') +try: + args = parser.parse_args() +except SystemExit: + parser.print_help() + raise + +ureg = UnitRegistry() +ureg.auto_reduce_dimensions = True +ureg.autoconvert_offset_to_baseunit = True +ureg.enable_contexts('Gau', 'ESU', 'sp', 'energy', 'boltzmann') +ureg.default_system = args.system + +if args.unc: + import uncertainties + # Measured constans subject to correlation + # R_i: Rydberg constant + # g_e: Electron g factor + # m_u: Atomic mass constant + # m_e: Electron mass + # m_p: Proton mass + # m_n: Neutron mass + R_i = (ureg._units['R_inf'].converter.scale, 0.0000000000021e7) + g_e = (ureg._units['g_e'].converter.scale, 0.00000000000035) + m_u = (ureg._units['m_u'].converter.scale, 0.00000000050e-27) + m_e = (ureg._units['m_e'].converter.scale, 0.00000000028e-30) + m_p = (ureg._units['m_p'].converter.scale, 0.00000000051e-27) + m_n = (ureg._units['m_n'].converter.scale, 0.00000000095e-27) + if args.corr: + # Correlation matrix between measured constants (to be completed below) + # R_i g_e m_u m_e m_p m_n + corr = [[ 1.0 , -0.00206, 0.00369, 0.00436, 0.00194, 0.00233], # R_i + [ -0.00206, 1.0 , 0.99029, 0.99490, 0.97560, 0.52445], # g_e + [ 0.00369, 0.99029, 1.0 , 0.99536, 0.98516, 0.52959], # m_u + [ 0.00436, 0.99490, 0.99536, 1.0 , 0.98058, 0.52714], # m_e + [ 0.00194, 0.97560, 0.98516, 0.98058, 1.0 , 0.51521], # m_p + [ 0.00233, 0.52445, 0.52959, 0.52714, 0.51521, 1.0 ]] # m_n + (R_i, g_e, m_u, m_e, m_p, m_n) = uncertainties.correlated_values_norm([R_i, g_e, m_u, m_e, m_p, m_n], corr) + else: + R_i = uncertainties.ufloat(*R_i) + g_e = uncertainties.ufloat(*g_e) + m_u = uncertainties.ufloat(*m_u) + m_e = uncertainties.ufloat(*m_e) + m_p = uncertainties.ufloat(*m_p) + m_n = uncertainties.ufloat(*m_n) + ureg._units['R_inf'].converter.scale = R_i + ureg._units['g_e'].converter.scale = g_e + ureg._units['m_u'].converter.scale = m_u + ureg._units['m_e'].converter.scale = m_e + ureg._units['m_p'].converter.scale = m_p + ureg._units['m_n'].converter.scale = m_n + + # Measured constants with zero correlation + ureg._units['gravitational_constant'].converter.scale = uncertainties.ufloat(ureg._units['gravitational_constant'].converter.scale, 0.00015e-11) + ureg._units['d_220'].converter.scale = uncertainties.ufloat(ureg._units['d_220'].converter.scale, 0.000000032e-10) + ureg._units['K_alpha_Cu_d_220'].converter.scale = uncertainties.ufloat(ureg._units['K_alpha_Cu_d_220'].converter.scale, 0.00000022) + ureg._units['K_alpha_Mo_d_220'].converter.scale = uncertainties.ufloat(ureg._units['K_alpha_Mo_d_220'].converter.scale, 0.00000019) + ureg._units['K_alpha_W_d_220'].converter.scale = uncertainties.ufloat(ureg._units['K_alpha_W_d_220'].converter.scale, 0.000000098) + + ureg._root_units_cache = dict() + ureg._build_cache() + +def convert(u_from, u_to=None, unc=None, factor=None): + q = ureg.Quantity(u_from) + fmt = '.{}g'.format(args.prec) + if unc: + q = q.plus_minus(unc) + if u_to: + nq = q.to(u_to) + else: + nq = q.to_base_units() + if (factor): + q *= ureg.Quantity(factor) + nq *= ureg.Quantity(factor).to_base_units() + prec_unc = use_unc(nq.magnitude, fmt, args.prec_unc) + if (prec_unc > 0): + fmt = '.{}uS'.format(prec_unc) + else: + try: + nq = nq.magnitude.n * nq.units + except: + pass + fmt = '{:' + fmt + '} {:~P}' + print(('{:} = ' + fmt).format(q, nq.magnitude, nq.units)) + +def use_unc(num, fmt, prec_unc): + unc = 0 + try: + if (isinstance(num, uncertainties.UFloat)): + full = ('{:'+fmt+'}').format(num) + unc = re.search(r'\+\/-[0.]*([\d.]*)', full).group(1) + unc = len(unc.replace('.', '')) + except: + pass + return max(0, min(prec_unc, unc)) + +convert(args.fr, args.to) diff --git a/pint/quantity.py b/pint/quantity.py index 1d9ed21..d434a0f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -20,13 +20,9 @@ import warnings from pkg_resources.extern.packaging import version -from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 from .compat import ( - ARRAY_FALLBACK, NUMPY_VER, - BehaviorChangeWarning, _to_magnitude, - array_function_change_msg, babel_parse, eq, is_duck_array_type, @@ -165,8 +161,6 @@ class Quantity(PrettyIPython, SharedRegistryObject): return _unpickle, (Quantity, self.magnitude, self._units) def __new__(cls, value, units=None): - global SKIP_ARRAY_FUNCTION_CHANGE_WARNING - if is_upcast_type(type(value)): raise TypeError(f"Quantity cannot wrap upcast type {type(value)}") elif units is None: @@ -219,12 +213,6 @@ class Quantity(PrettyIPython, SharedRegistryObject): inst.__used = False inst.__handling = None - if not SKIP_ARRAY_FUNCTION_CHANGE_WARNING and isinstance( - inst._magnitude, ndarray - ): - warnings.warn(array_function_change_msg, BehaviorChangeWarning) - SKIP_ARRAY_FUNCTION_CHANGE_WARNING = True - return inst @property @@ -256,6 +244,9 @@ class Quantity(PrettyIPython, SharedRegistryObject): return ret def __str__(self): + if self._REGISTRY.fmt_locale is not None: + return self.format_babel() + return format(self) def __bytes__(self): @@ -274,6 +265,9 @@ class Quantity(PrettyIPython, SharedRegistryObject): _exp_pattern = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") def __format__(self, spec): + if self._REGISTRY.fmt_locale is not None: + return self.format_babel(spec) + spec = spec or self.default_format if "L" in spec: @@ -508,6 +502,40 @@ class Quantity(PrettyIPython, SharedRegistryObject): return self._REGISTRY.get_compatible_units(self._units) + def is_compatible_with(self, other, *contexts, **ctx_kwargs): + """ check if the other object is compatible + + Parameters + ---------- + other + The object to check. Treated as dimensionless if not a + Quantity, Unit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + if contexts: + try: + self.to(other, *contexts, **ctx_kwargs) + return True + except DimensionalityError: + return False + + if isinstance(other, (self._REGISTRY.Quantity, self._REGISTRY.Unit)): + return self.dimensionality == other.dimensionality + + if isinstance(other, str): + return ( + self.dimensionality == self._REGISTRY.parse_units(other).dimensionality + ) + + return self.dimensionless + def _convert_magnitude_not_inplace(self, other, *contexts, **ctx_kwargs): if contexts: with self._REGISTRY.context(*contexts, **ctx_kwargs): @@ -693,6 +721,8 @@ class Quantity(PrettyIPython, SharedRegistryObject): if unit is None: unit = infer_base_unit(self) + else: + unit = infer_base_unit(self.__class__(1, unit)) q_base = self.to(unit) @@ -1665,34 +1695,7 @@ class Quantity(PrettyIPython, SharedRegistryObject): def __getattr__(self, item): if item.startswith("__array_"): # Handle array protocol attributes other than `__array__` - if ARRAY_FALLBACK: - # Deprecated fallback behavior - warnings.warn( - ( - f"Array protocol attribute {item} accessed, with unit of the " - "Quantity being stripped. This attribute will become unavailable " - "in the next minor version of Pint. To make this potentially " - "incorrect attribute unavailable now, set the " - "PINT_ARRAY_PROTOCOL_FALLBACK environment variable to 0 before " - "importing Pint." - ), - DeprecationWarning, - stacklevel=2, - ) - - if is_duck_array_type(type(self._magnitude)): - # Defer to magnitude, and don't catch any AttributeErrors - return getattr(self._magnitude, item) - else: - # If an `__array_` attribute is requested but the magnitude is not - # a duck array, we convert the magnitude to a numpy ndarray. - magnitude_as_array = _to_magnitude( - self._magnitude, force_ndarray=True - ) - return getattr(magnitude_as_array, item) - else: - # TODO (next minor version): ARRAY_FALLBACK is removed and this becomes the standard behavior - raise AttributeError(f"Array protocol attribute {item} not available.") + raise AttributeError(f"Array protocol attribute {item} not available.") elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods: magnitude_as_duck_array = _to_magnitude( self._magnitude, force_ndarray_like=True diff --git a/pint/registry.py b/pint/registry.py index 9379770..99873ed 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -84,6 +84,19 @@ from .util import ( _BLOCK_RE = re.compile(r" |\(") +@functools.lru_cache() +def pattern_to_regex(pattern): + if hasattr(pattern, "finditer"): + pattern = pattern.pattern + + # Replace "{unit_name}" match string with float regex with unit_name as group + pattern = re.sub( + r"{(\w+)}", r"(?P<\1>[+-]?[0-9]+(?:.[0-9]+)?(?:[Ee][+-]?[0-9]+)?)", pattern + ) + + return re.compile(pattern) + + class RegistryMeta(type): """This is just to call after_init at the right time instead of asking the developer to do it when subclassing. @@ -161,6 +174,9 @@ class BaseRegistry(metaclass=RegistryMeta): #: type: Dict[str, (SourceIterator -> None)] _parsers = None + #: Babel.Locale instance or None + fmt_locale = None + #: List to be used in addition of units when dir(registry) is called. #: Also used for autocompletion in IPython. _dir = [ @@ -207,7 +223,7 @@ class BaseRegistry(metaclass=RegistryMeta): self.auto_reduce_dimensions = auto_reduce_dimensions #: Default locale identifier string, used when calling format_babel without explicit locale. - self.fmt_locale = self.set_fmt_locale(fmt_locale) + self.set_fmt_locale(fmt_locale) #: Numerical type used for non integer values. self.non_int_type = non_int_type @@ -871,6 +887,33 @@ class BaseRegistry(metaclass=RegistryMeta): src_dim = self._get_dimensionality(input_units) return self._cache.dimensional_equivalents[src_dim] + def is_compatible_with(self, obj1, obj2, *contexts, **ctx_kwargs): + """ check if the other object is compatible + + Parameters + ---------- + obj1, obj2 + The objects to check against each other. Treated as + dimensionless if not a Quantity, Unit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + if isinstance(obj1, (self.Quantity, self.Unit)): + return obj1.is_compatible_with(obj2, *contexts, **ctx_kwargs) + + if isinstance(obj1, str): + return self.parse_expression(obj1).is_compatible_with( + obj2, *contexts, **ctx_kwargs + ) + + return not isinstance(obj2, (self.Quantity, self.Unit)) + def convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. @@ -1114,6 +1157,62 @@ class BaseRegistry(metaclass=RegistryMeta): else: raise Exception("unknown token type") + def parse_pattern( + self, input_string, pattern, case_sensitive=True, use_decimal=False, many=False + ): + """Parse a string with a given regex pattern and returns result. + + Parameters + ---------- + input_string : + + pattern_string: + The regex parse string + case_sensitive : + (Default value = True) + use_decimal : + (Default value = False) + many : + Match many results + (Default value = False) + + + Returns + ------- + + """ + + if not input_string: + return self.Quantity(1) + + # Parse string + pattern = pattern_to_regex(pattern) + matched = re.finditer(pattern, input_string) + + # Extract result(s) + results = [] + for match in matched: + # Extract units from result + match = match.groupdict() + + # Parse units + units = [] + for unit, value in match.items(): + # Construct measure by multiplying value by unit + units.append( + float(value) + * self.parse_expression(unit, case_sensitive, use_decimal) + ) + + # Add to results + results.append(units) + + # Return first match only + if not many: + return results[0] + + return results + def parse_expression( self, input_string, case_sensitive=True, use_decimal=False, **values, ): diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 5a91391..2bf743c 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -67,6 +67,10 @@ def requires_babel(): return unittest.skipUnless(HAS_BABEL, "Requires Babel with units support") +def requires_not_babel(): + return unittest.skipIf(HAS_BABEL, "Requires Babel is not installed") + + def requires_uncertainties(): return unittest.skipUnless(HAS_UNCERTAINTIES, "Requires Uncertainties") diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 2aac0cd..def7c83 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -5,6 +5,14 @@ from pint.testsuite import BaseTestCase, helpers class TestBabel(BaseTestCase): + @helpers.requires_not_babel() + def test_no_babel(self): + ureg = UnitRegistry() + distance = 24.0 * ureg.meter + self.assertRaises( + Exception, distance.format_babel, locale="fr_FR", length="long" + ) + @helpers.requires_babel() def test_format(self): ureg = UnitRegistry() @@ -46,9 +54,32 @@ class TestBabel(BaseTestCase): mks = ureg.get_system("mks") self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") - def test_nobabel(self): + @helpers.requires_babel() + def test_no_registry_locale(self): ureg = UnitRegistry() distance = 24.0 * ureg.meter self.assertRaises( - Exception, distance.format_babel, locale="fr_FR", length="long" + Exception, distance.format_babel, ) + + @helpers.requires_babel() + def test_str(self): + ureg = UnitRegistry() + d = 24.0 * ureg.meter + + s = "24.0 meter" + self.assertEqual(str(d), s) + self.assertEqual("%s" % d, s) + self.assertEqual("{}".format(d), s) + + ureg.set_fmt_locale("fr_FR") + s = "24.0 mètres" + self.assertEqual(str(d), s) + self.assertEqual("%s" % d, s) + self.assertEqual("{}".format(d), s) + + ureg.set_fmt_locale(None) + s = "24.0 meter" + self.assertEqual(str(d), s) + self.assertEqual("%s" % d, s) + self.assertEqual("{}".format(d), s) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 3036022..5f42717 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -697,6 +697,24 @@ class TestIssues(QuantityTestCase): with self.assertRaises(DimensionalityError): q.to("joule") + def test_issue507(self): + # leading underscore in unit works with numbers + ureg.define("_100km = 100 * kilometer") + battery_ec = 16 * ureg.kWh / ureg._100km # noqa: F841 + # ... but not with text + ureg.define("_home = 4700 * kWh / year") + with self.assertRaises(AttributeError): + home_elec_power = 1 * ureg._home # noqa: F841 + # ... or with *only* underscores + ureg.define("_ = 45 * km") + with self.assertRaises(AttributeError): + one_blank = 1 * ureg._ # noqa: F841 + + def test_issue960(self): + q = (1 * ureg.nanometer).to_compact("micrometer") + assert q.units == ureg.nanometer + assert q.magnitude == 1 + try: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0efb476..8e3d116 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,7 +1,6 @@ import copy import operator as op import unittest -from unittest.mock import patch from pint import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning from pint.compat import np @@ -885,7 +884,7 @@ class TestNumpyUnclassified(TestNumpyMethods): @helpers.requires_array_function_protocol() def test_result_type_numpy_func(self): - self.assertEqual(np.result_type(self.q), np.dtype("int64")) + self.assertEqual(np.result_type(self.q), np.dtype("int")) @helpers.requires_array_function_protocol() def test_nan_to_num_numpy_func(self): @@ -1031,28 +1030,15 @@ class TestNumpyUnclassified(TestNumpyMethods): np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m, ) - @patch("pint.quantity.ARRAY_FALLBACK", False) def test_ndarray_downcast(self): with self.assertWarns(UnitStrippedWarning): np.asarray(self.q) - @patch("pint.quantity.ARRAY_FALLBACK", False) def test_ndarray_downcast_with_dtype(self): with self.assertWarns(UnitStrippedWarning): qarr = np.asarray(self.q, dtype=np.float64) self.assertEqual(qarr.dtype, np.float64) - def test_array_protocol_fallback(self): - with self.assertWarns(DeprecationWarning) as cm: - for attr in ("__array_struct__", "__array_interface__"): - getattr(self.q, attr) - warning_text = str(cm.warnings[0].message) - self.assertTrue( - f"unit of the Quantity being stripped" in warning_text - and "will become unavailable" in warning_text - ) - - @patch("pint.quantity.ARRAY_FALLBACK", False) def test_array_protocol_unavailable(self): for attr in ("__array_struct__", "__array_interface__"): self.assertRaises(AttributeError, getattr, self.q, attr) @@ -1067,14 +1053,32 @@ class TestNumpyUnclassified(TestNumpyMethods): def test_pad(self): # Tests reproduced with modification from NumPy documentation a = [1, 2, 3, 4, 5] * self.ureg.m + b = self.Q_([4.0, 6.0, 8.0, 9.0, -3.0], "degC") + + self.assertQuantityEqual( + np.pad(a, (2, 3), "constant"), [0, 0, 1, 2, 3, 4, 5, 0, 0, 0] * self.ureg.m, + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "constant", constant_values=(0, 600 * self.ureg.cm)), + [0, 0, 1, 2, 3, 4, 5, 6, 6, 6] * self.ureg.m, + ) self.assertQuantityEqual( - np.pad(a, (2, 3), "constant", constant_values=(4, 600 * self.ureg.cm)), - [4, 4, 1, 2, 3, 4, 5, 6, 6, 6] * self.ureg.m, + np.pad( + b, (2, 1), "constant", constant_values=(np.nan, self.Q_(10, "degC")) + ), + self.Q_([np.nan, np.nan, 4, 6, 8, 9, -3, 10], "degC"), + ) + self.assertRaises( + DimensionalityError, np.pad, a, (2, 3), "constant", constant_values=4 ) self.assertQuantityEqual( np.pad(a, (2, 3), "edge"), [1, 1, 1, 2, 3, 4, 5, 5, 5, 5] * self.ureg.m ) self.assertQuantityEqual( + np.pad(a, (2, 3), "linear_ramp"), + [0, 0, 1, 2, 3, 4, 5, 3, 1, 0] * self.ureg.m, + ) + self.assertQuantityEqual( np.pad(a, (2, 3), "linear_ramp", end_values=(5, -4) * self.ureg.m), [5, 3, 1, 2, 3, 4, 5, 2, -1, -4] * self.ureg.m, ) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fbfd773..51faf1a 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -6,7 +6,7 @@ import warnings from unittest.mock import patch from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry -from pint.compat import BehaviorChangeWarning, np +from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase from pint.unit import UnitsContainer @@ -531,11 +531,8 @@ class TestQuantity(QuantityTestCase): iter(x) @helpers.requires_array_function_protocol() - @patch("pint.quantity.SKIP_ARRAY_FUNCTION_CHANGE_WARNING", False) - def test_array_function_warning_on_creation(self): - # Test that warning is raised on first creation, but not second - with self.assertWarns(BehaviorChangeWarning): - self.Q_([]) + def test_no_longer_array_function_warning_on_creation(self): + # Test that warning is no longer raised on first creation with warnings.catch_warnings(): warnings.filterwarnings("error") self.Q_([]) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 0b6c086..d7eb649 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -658,6 +658,47 @@ class TestRegistry(QuantityTestCase): self.assertEqual(ureg.parse_units(""), ureg.Unit("")) self.assertRaises(ValueError, ureg.parse_units, "2 * meter") + def test_parse_string_pattern(self): + ureg = self.ureg + self.assertEqual( + ureg.parse_pattern("10'11", r"{foot}'{inch}"), + [ureg.Quantity(10.0, "foot"), ureg.Quantity(11.0, "inch")], + ) + + def test_parse_string_pattern_no_preprocess(self): + """Were preprocessors enabled, this would be interpreted as 10*11, not + two separate units, and thus cause the parsing to fail""" + ureg = self.ureg + self.assertEqual( + ureg.parse_pattern("10 11", r"{kg} {lb}"), + [ureg.Quantity(10.0, "kilogram"), ureg.Quantity(11.0, "pound")], + ) + + def test_parse_pattern_many_results(self): + ureg = self.ureg + self.assertEqual( + ureg.parse_pattern( + "1.5kg or 2kg will be fine, if you do not have 3kg", + r"{kg}kg", + many=True, + ), + [ + [ureg.Quantity(1.5, "kilogram")], + [ureg.Quantity(2.0, "kilogram")], + [ureg.Quantity(3.0, "kilogram")], + ], + ) + + def test_parse_pattern_many_results_two_units(self): + ureg = self.ureg + self.assertEqual( + ureg.parse_pattern("10'10 or 10'11", "{foot}'{inch}", many=True), + [ + [ureg.Quantity(10.0, "foot"), ureg.Quantity(10.0, "inch")], + [ureg.Quantity(10.0, "foot"), ureg.Quantity(11.0, "inch")], + ], + ) + class TestCompatibleUnits(QuantityTestCase): FORCE_NDARRAY = False diff --git a/pint/unit.py b/pint/unit.py index 5db27cd..25084b4 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -15,6 +15,7 @@ from numbers import Number from .compat import NUMERIC_TYPES, is_upcast_type from .definitions import UnitDefinition +from .errors import DimensionalityError from .formatting import siunitx_format_unit from .util import PrettyIPython, SharedRegistryObject, UnitsContainer @@ -143,6 +144,40 @@ class Unit(PrettyIPython, SharedRegistryObject): return self._REGISTRY.get_compatible_units(self) + def is_compatible_with(self, other, *contexts, **ctx_kwargs): + """ check if the other object is compatible + + Parameters + ---------- + other + The object to check. Treated as dimensionless if not a + Quantity, Unit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + if contexts: + try: + (1 * self).to(other, *contexts, **ctx_kwargs) + return True + except DimensionalityError: + return False + + if isinstance(other, (self._REGISTRY.Quantity, self._REGISTRY.Unit)): + return self.dimensionality == other.dimensionality + + if isinstance(other, str): + return ( + self.dimensionality == self._REGISTRY.parse_units(other).dimensionality + ) + + return self.dimensionless + def __mul__(self, other): if self._check(other): if isinstance(other, self.__class__): diff --git a/pint/util.py b/pint/util.py index 68bed2e..aeebff1 100644 --- a/pint/util.py +++ b/pint/util.py @@ -897,12 +897,16 @@ def infer_base_unit(q): def getattr_maybe_raise(self, item): - """Helper function to invoke at the beginning of all overridden ``__getattr__`` - methods. Raise AttributeError if the user tries to ask for a _ or __ attribute. + """Helper function invoked at start of all overridden ``__getattr__``. + + Raise AttributeError if the user tries to ask for a _ or __ attribute, + *unless* it is immediately followed by a number, to enable units + encompassing constants, such as ``L / _100km``. Parameters ---------- - item : + item : string + Item to be found. Returns @@ -911,7 +915,11 @@ def getattr_maybe_raise(self, item): """ # Double-underscore attributes are tricky to detect because they are # automatically prefixed with the class name - which may be a subclass of self - if item.startswith("_") or item.endswith("__"): + if ( + item.endswith("__") + or len(item.lstrip("_")) == 0 + or (item.startswith("_") and not item.lstrip("_")[0].isdigit()) + ): raise AttributeError("%r object has no attribute %r" % (self, item)) |