From 6edc65799ab073f48e78a397d91e0e877913ef19 Mon Sep 17 00:00:00 2001 From: Jonas Haag Date: Mon, 29 Oct 2018 19:13:37 +0100 Subject: [PATCH 001/612] 1.0 -> 1 --- pint/quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 90a9076ae..2abfc9929 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -804,7 +804,7 @@ def _imul_div(self, other, magnitude_op, units_op=None): return self if isinstance(other, self._REGISTRY.Unit): - other = 1.0 * other + other = 1 * other if not self._ok_for_muldiv(no_offset_units_self): raise OffsetUnitCalculusError(self._units, other._units) @@ -864,7 +864,7 @@ def _mul_div(self, other, magnitude_op, units_op=None): return self.__class__(magnitude, units) if isinstance(other, self._REGISTRY.Unit): - other = 1.0 * other + other = 1 * other new_self = self From 8f2908b07d24dca0dd722ef35bb334e08502aeeb Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 12 Jan 2019 20:31:19 -0300 Subject: [PATCH 002/612] Back to development: 0.10 --- CHANGES | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ce6e0bfe2..6e56d90e1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.10 (unreleased) +----------------- + +- Nothing changed yet. + + 0.9 (2019-01-12) ---------------- diff --git a/setup.py b/setup.py index 8350f1137..a6d83d6fc 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def read(filename): setup( name='Pint', - version='0.9', + version='0.10.dev0', description='Physical quantities module', long_description=long_description, keywords='physical quantities unit conversion science', From a997cd9db454774df9e30b7670f42619eaa6f121 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Thu, 17 Jan 2019 22:46:02 +0000 Subject: [PATCH 003/612] add pint-pandas notebook docs --- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/pint-pandas.ipynb | 1545 ++++++++++++++++++++++++++++++++++++++++ requirements_docs.txt | 1 + 4 files changed, 1548 insertions(+), 2 deletions(-) create mode 100644 docs/pint-pandas.ipynb diff --git a/docs/conf.py b/docs/conf.py index 626dd4ce5..6d8b8470a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', 'matplotlib.sphinxext.plot_directive'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', 'matplotlib.sphinxext.plot_directive', 'nbsphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index 8f3ec1c6e..05cb13af1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,7 +130,7 @@ User Guide defining performance systems - + pint-pandas.ipynb More information ---------------- diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb new file mode 100644 index 000000000..390831ed3 --- /dev/null +++ b/docs/pint-pandas.ipynb @@ -0,0 +1,1545 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Pandas support\n", + "\n", + "It is convenient to use the Pandas package when dealing with numerical data, so Pint provides PintArray. A PintArray is a Pandas Extension Array, which allows Pandas to recognise the Quantity and store it in Pandas DataFrames and Series." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", + "\n", + "First some imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd \n", + "import pint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we create a DataFrame with PintArrays as columns." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
torqueangular_velocity
011
122
222
333
\n", + "
" + ], + "text/plain": [ + " torque angular_velocity\n", + "0 1 1\n", + "1 2 2\n", + "2 2 2\n", + "3 3 3" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame({\n", + " \"torque\": pd.Series([1, 2, 2, 3], dtype=\"pint[lbf ft]\"),\n", + " \"angular_velocity\": pd.Series([1, 2, 2, 3], dtype=\"pint[rpm]\"),\n", + "})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Operations with columns are units aware so behave as we would intuitively expect." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
torqueangular_velocitypower
0111
1224
2224
3339
\n", + "
" + ], + "text/plain": [ + " torque angular_velocity power\n", + "0 1 1 1\n", + "1 2 2 4\n", + "2 2 2 4\n", + "3 3 3 9" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['power'] = df['torque'] * df['angular_velocity']\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the columns' units in the dtypes attribute" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torque pint[foot * force_pound]\n", + "angular_velocity pint[revolutions_per_minute]\n", + "power pint[foot * force_pound * revolutions_per_minute]\n", + "dtype: object" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each column can be accessed as a Pandas Series" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1\n", + "1 4\n", + "2 4\n", + "3 9\n", + "Name: power, dtype: pint[foot * force_pound * revolutions_per_minute]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.power" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which contains a PintArray" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PintArray([1 foot * force_pound * revolutions_per_minute,\n", + " 4 foot * force_pound * revolutions_per_minute,\n", + " 4 foot * force_pound * revolutions_per_minute,\n", + " 9 foot * force_pound * revolutions_per_minute],\n", + " dtype='pint[foot * force_pound * revolutions_per_minute]')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.power.values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The PintArray contains a Quantity" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\\[\\begin{pmatrix}1 & 4 & 4 & 9\\end{pmatrix} foot force_pound revolutions_per_minute\\]" + ], + "text/latex": [ + "$\\begin{pmatrix}1 & 4 & 4 & 9\\end{pmatrix}\\ \\mathrm{foot} \\cdot \\mathrm{force_pound} \\cdot \\mathrm{revolutions_per_minute}$" + ], + "text/plain": [ + "array([1, 4, 4, 9]) " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.power.values.quantity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pandas Series accessors are provided for most Quantity properties and methods, which will convert the result to a Series where possible." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "foot force_pound revolutions_per_minute" + ], + "text/latex": [ + "$\\mathrm{foot} \\cdot \\mathrm{force_pound} \\cdot \\mathrm{revolutions_per_minute}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.power.pint.units" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PintArray([0.00014198092353610376 kilowatt, 0.000567923694144415 kilowatt,\n", + " 0.000567923694144415 kilowatt, 0.0012778283118249339 kilowatt],\n", + " dtype='pint[kilowatt]')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.power.pint.to(\"kW\").values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reading from csv\n", + "\n", + "Reading from files is the far more standard way to use pandas. To facilitate this, DataFrame accessors are provided to make it easy to get to PintArrays. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd \n", + "import pint\n", + "import io" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's the contents of the csv file." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "test_data = '''speed,mech power,torque,rail pressure,fuel flow rate,fluid power\n", + "rpm,kW,N m,bar,l/min,kW\n", + "1000.0,,10.0,1000.0,10.0,\n", + "1100.0,,10.0,100000000.0,10.0,\n", + "1200.0,,10.0,1000.0,10.0,\n", + "1200.0,,10.0,1000.0,10.0,'''" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's read that into a DataFrame.\n", + "Here io.StringIO is used in place of reading a file from disk, whereas a csv file path would typically be used and is shown commented." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
rpmkWN mbarl/minkW
01000.0NaN10.01000.010.0NaN
11100.0NaN10.0100000000.010.0NaN
21200.0NaN10.01000.010.0NaN
31200.0NaN10.01000.010.0NaN
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate fluid power\n", + " rpm kW N m bar l/min kW\n", + "0 1000.0 NaN 10.0 1000.0 10.0 NaN\n", + "1 1100.0 NaN 10.0 100000000.0 10.0 NaN\n", + "2 1200.0 NaN 10.0 1000.0 10.0 NaN\n", + "3 1200.0 NaN 10.0 1000.0 10.0 NaN" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_csv(io.StringIO(test_data),header=[0,1])\n", + "# df = pd.read_csv(\"/path/to/test_data.csv\",header=[0,1])\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then use the DataFrame's pint accessor's quantify method to convert the columns from `np.ndarray`s to PintArrays, with units from the bottom column level." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "speed rpm float64\n", + "mech power kW float64\n", + "torque N m float64\n", + "rail pressure bar float64\n", + "fuel flow rate l/min float64\n", + "fluid power kW float64\n", + "dtype: object" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.dtypes" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.0nan10.01000.010.0nan
11100.0nan10.0100000000.010.0nan
21200.0nan10.01000.010.0nan
31200.0nan10.01000.010.0nan
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate fluid power\n", + "0 1000.0 nan 10.0 1000.0 10.0 nan\n", + "1 1100.0 nan 10.0 100000000.0 10.0 nan\n", + "2 1200.0 nan 10.0 1000.0 10.0 nan\n", + "3 1200.0 nan 10.0 1000.0 10.0 nan" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_ = df.pint.quantify(level=-1)\n", + "df_" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As previously, operations between DataFrame columns are unit aware" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 10000.0\n", + "1 11000.0\n", + "2 12000.0\n", + "3 12000.0\n", + "dtype: pint[meter * newton * revolutions_per_minute]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_.speed*df_.torque" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.0nan10.01000.010.0nan
11100.0nan10.0100000000.010.0nan
21200.0nan10.01000.010.0nan
31200.0nan10.01000.010.0nan
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate fluid power\n", + "0 1000.0 nan 10.0 1000.0 10.0 nan\n", + "1 1100.0 nan 10.0 100000000.0 10.0 nan\n", + "2 1200.0 nan 10.0 1000.0 10.0 nan\n", + "3 1200.0 nan 10.0 1000.0 10.0 nan" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.010000.010.01000.010.010000.0
11100.011000.010.0100000000.010.01000000000.0
21200.012000.010.01000.010.010000.0
31200.012000.010.01000.010.010000.0
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate fluid power\n", + "0 1000.0 10000.0 10.0 1000.0 10.0 10000.0\n", + "1 1100.0 11000.0 10.0 100000000.0 10.0 1000000000.0\n", + "2 1200.0 12000.0 10.0 1000.0 10.0 10000.0\n", + "3 1200.0 12000.0 10.0 1000.0 10.0 10000.0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_['mech power'] = df_.speed*df_.torque\n", + "df_['fluid power'] = df_['fuel flow rate'] * df_['rail pressure']\n", + "df_" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The DataFrame's `pint.dequantify` method then allows us to retrieve the units information as a header row once again." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrevolutions_per_minutemeter * newton * revolutions_per_minutemeter * newtonbarliter / minutebar * liter / minute
01000.010000.010.01000.010.01.000000e+04
11100.011000.010.0100000000.010.01.000000e+09
21200.012000.010.01000.010.01.000000e+04
31200.012000.010.01000.010.01.000000e+04
\n", + "
" + ], + "text/plain": [ + " speed mech power \\\n", + "unit revolutions_per_minute meter * newton * revolutions_per_minute \n", + "0 1000.0 10000.0 \n", + "1 1100.0 11000.0 \n", + "2 1200.0 12000.0 \n", + "3 1200.0 12000.0 \n", + "\n", + " torque rail pressure fuel flow rate fluid power \n", + "unit meter * newton bar liter / minute bar * liter / minute \n", + "0 10.0 1000.0 10.0 1.000000e+04 \n", + "1 10.0 100000000.0 10.0 1.000000e+09 \n", + "2 10.0 1000.0 10.0 1.000000e+04 \n", + "3 10.0 1000.0 10.0 1.000000e+04 " + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_.pint.dequantify()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This allows for some rather powerful abilities. For example, to change single column units" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrevolutions_per_minutekilowattmeter * newtonbarliter / minutekilowatt
01000.01.04719810.01000.010.01.666667e+01
11100.01.15191710.0100000000.010.01.666667e+06
21200.01.25663710.01000.010.01.666667e+01
31200.01.25663710.01000.010.01.666667e+01
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure \\\n", + "unit revolutions_per_minute kilowatt meter * newton bar \n", + "0 1000.0 1.047198 10.0 1000.0 \n", + "1 1100.0 1.151917 10.0 100000000.0 \n", + "2 1200.0 1.256637 10.0 1000.0 \n", + "3 1200.0 1.256637 10.0 1000.0 \n", + "\n", + " fuel flow rate fluid power \n", + "unit liter / minute kilowatt \n", + "0 10.0 1.666667e+01 \n", + "1 10.0 1.666667e+06 \n", + "2 10.0 1.666667e+01 \n", + "3 10.0 1.666667e+01 " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_['fluid power'] = df_['fluid power'].pint.to(\"kW\")\n", + "df_['mech power'] = df_['mech power'].pint.to(\"kW\")\n", + "df_.pint.dequantify()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The units are harder to read than they need be, so lets change pints default format for displaying units." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrpmkWN·mbarl/minkW
01000.01.04719810.01000.010.01.666667e+01
11100.01.15191710.0100000000.010.01.666667e+06
21200.01.25663710.01000.010.01.666667e+01
31200.01.25663710.01000.010.01.666667e+01
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate fluid power\n", + "unit rpm kW N·m bar l/min kW\n", + "0 1000.0 1.047198 10.0 1000.0 10.0 1.666667e+01\n", + "1 1100.0 1.151917 10.0 100000000.0 10.0 1.666667e+06\n", + "2 1200.0 1.256637 10.0 1000.0 10.0 1.666667e+01\n", + "3 1200.0 1.256637 10.0 1000.0 10.0 1.666667e+01" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pint.PintType.ureg.default_format = \"~P\"\n", + "df_.pint.dequantify()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or the entire table's units" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrad/skg·m²/s³kg·m²/s²kg/m/s²m³/skg·m²/s³
0104.7197551047.19755110.01.000000e+080.0001671.666667e+04
1115.1917311151.91730610.01.000000e+130.0001671.666667e+09
2125.6637061256.63706110.01.000000e+080.0001671.666667e+04
3125.6637061256.63706110.01.000000e+080.0001671.666667e+04
\n", + "
" + ], + "text/plain": [ + " speed mech power torque rail pressure fuel flow rate \\\n", + "unit rad/s kg·m²/s³ kg·m²/s² kg/m/s² m³/s \n", + "0 104.719755 1047.197551 10.0 1.000000e+08 0.000167 \n", + "1 115.191731 1151.917306 10.0 1.000000e+13 0.000167 \n", + "2 125.663706 1256.637061 10.0 1.000000e+08 0.000167 \n", + "3 125.663706 1256.637061 10.0 1.000000e+08 0.000167 \n", + "\n", + " fluid power \n", + "unit kg·m²/s³ \n", + "0 1.666667e+04 \n", + "1 1.666667e+09 \n", + "2 1.666667e+04 \n", + "3 1.666667e+04 " + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_.pint.to_base_units().pint.dequantify()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced example\n", + "This example shows alternative ways to use pint with pandas and other features.\n", + "\n", + "Start with the same imports." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd \n", + "import pint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll be use a shorthand for PintArray" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "PA_ = pint.PintArray" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And set up a unit registry and quantity shorthand." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "ureg=pint.UnitRegistry()\n", + "Q_=ureg.Quantity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Operations between PintArrays of different unit registry will not work. We can change the unit registry that will be used in creating new PintArrays to prevent this issue." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "pint.PintType.ureg = ureg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are the possible ways to create a PintArray.\n", + "\n", + "Note that pint[unit] must be used for the Series constuctor, whereas the PintArray constructor allows the unit string or object." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
lengthwidthdistanceheightdepth
012222
123333
\n", + "
" + ], + "text/plain": [ + " length width distance height depth\n", + "0 1 2 2 2 2\n", + "1 2 3 3 3 3" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame({\n", + " \"length\" : pd.Series([1,2], dtype=\"pint[m]\"),\n", + " \"width\" : PA_([2,3], dtype=\"pint[m]\"),\n", + " \"distance\" : PA_([2,3], dtype=\"m\"),\n", + " \"height\" : PA_([2,3], dtype=ureg.m),\n", + " \"depth\" : PA_.from_1darray_quantity(Q_([2,3],ureg.m)),\n", + " })\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "meter" + ], + "text/latex": [ + "$\\mathrm{meter}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.length.values.units" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements_docs.txt b/requirements_docs.txt index 019388deb..1cbe5b677 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,2 +1,3 @@ matplotlib>=2 numpy +nbsphinx \ No newline at end of file From bfc74ef9dabbf695f29d5c85a494c8c14faf151f Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 21 Jan 2019 15:40:51 -0700 Subject: [PATCH 004/612] Fix import on Python 2.7 Need to enable absolute imports so that when we do `import matplotlib.units`, it doesn't look at the current module (matplotlib) first. --- pint/matplotlib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index d1b543c74..bda251d46 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -9,6 +9,8 @@ :copyright: 2017 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +from __future__ import absolute_import + import matplotlib.units From 74d2ff491a68a57490a596f8eff1f3c7ebcdb44f Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 2 Feb 2019 15:28:01 +0100 Subject: [PATCH 005/612] Add ndarray formatting support for float_kind data --- pint/quantity.py | 20 +++++++++++++++++++- pint/testsuite/test_quantity.py | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index c88bba1d7..782abc4e6 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -9,6 +9,7 @@ from __future__ import division, unicode_literals, print_function, absolute_import +import contextlib import copy import datetime import math @@ -77,6 +78,21 @@ def wrapped(self, *args, **kwargs): return wrapped +@contextlib.contextmanager +def printoptions(*args, **kwargs): + """ + Numpy printoptions context manager released with version 1.15.0 + https://docs.scipy.org/doc/numpy/reference/generated/numpy.printoptions.html + """ + + opts = np.get_printoptions() + try: + np.set_printoptions(*args, **kwargs) + yield np.get_printoptions() + finally: + np.set_printoptions(**opts) + + @fix_str_conversions class _Quantity(PrettyIPython, SharedRegistryObject): """Implements a class to describe a physical quantity: @@ -215,7 +231,9 @@ def __format__(self, spec): mstr = parts[0] else: - mstr = format(obj.magnitude, mspec).replace('\n', '') + formatter = "{{:{}}}".format(mspec) + with printoptions(formatter={"float_kind": formatter.format}): + mstr = format(obj.magnitude).replace('\n', '') else: mstr = format(obj.magnitude, mspec).replace('\n', '') diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index c7f41cbf7..94cc1da81 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -129,6 +129,19 @@ def test_quantity_format(self): x = self.Q_(3, UnitsContainer(second=-1)) self.assertEqual('{0}'.format(x), '3 / second') + @helpers.requires_numpy() + def test_quantity_array_format(self): + x = self.Q_(np.array([1e-16, 1.0000001, 10000000.0, 1e12, np.nan, np.inf]), "kg * m ** 2") + for spec, result in (('{0}', str(x)), + ('{0.magnitude}', str(x.magnitude)), + ('{0:e}', "[1.000000e-16 1.000000e+00 1.000000e+07 1.000000e+12 nan inf] kilogram * meter ** 2"), + ('{0:E}', "[1.000000E-16 1.000000E+00 1.000000E+07 1.000000E+12 NAN INF] kilogram * meter ** 2"), + ('{0:.2f}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kilogram * meter ** 2"), + ('{0:.2f~P}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kg·m²"), + ('{0:g~P}', "[1e-16 1 1e+07 1e+12 nan inf] kg·m²"), + ): + self.assertEqual(spec.format(x), result) + def test_format_compact(self): q1 = (200e-9 * self.ureg.s).to_compact() q1b = self.Q_(200., 'nanosecond') From 03066e2d6c128a7ac9a859a906312bb91a23f74d Mon Sep 17 00:00:00 2001 From: Jules Date: Tue, 5 Feb 2019 20:52:59 +0100 Subject: [PATCH 006/612] Add documentation about numpy array formatting --- docs/tutorial.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index e35ab5148..b11e608e8 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -292,6 +292,17 @@ Pint's physical quantities can be easily printed: >>> print('The magnitude is {0.magnitude} with units {0.units}'.format(accel)) The magnitude is 1.3 with units meter / second ** 2 +Pint supports float formatting for numpy arrays as well: + +.. doctest:: + + >>> accel = np.array([-1.1, 1e-6, 1.2505, 1.3]) * ureg['meter/second**2'] + >>> # float formatting numpy arrays + >>> print('The array is {:.2f}'.format(accel)) + The array is [-1.10 0.00 1.25 1.30] meter / second ** 2 + >>> # scientific form formatting with unit pretty printing + >>> print('The array is {:+.2E~P}'.format(accel)) + The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: From aee62455e78fecbcadabd7f591fc1e1bb1eec8bd Mon Sep 17 00:00:00 2001 From: Deniz Bozyigit Date: Wed, 6 Feb 2019 00:29:28 +0100 Subject: [PATCH 007/612] added custom constructors from_sequence and from_list to Quantity, as discussed in issue #761. added tests for these constructors to test_quantity.py --- pint/quantity.py | 47 +++++++++++++++++++++++++++++++++ pint/testsuite/test_quantity.py | 24 +++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index c88bba1d7..91e02d60b 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -321,6 +321,53 @@ def check(self, dimension): """ return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) + + @classmethod + def from_list(cls, quant_list, units=None): + """Transforms a list of Quantities into an numpy.array quantity. + If no units are specified, the unit of the first element will be used. + Same as from_sequence. + + If units is not specified and list is empty, the unit cannot be determined + and a ValueError is raised. + + :param quant_list: list of Quantities + :type quant_list: list of Quantity + :param units: units of the physical quantity to be created. + :type units: UnitsContainer, str or Quantity. + """ + return cls.from_sequence(quant_list, units=units) + + @classmethod + def from_sequence(cls, seq, units=None): + """Transforms a sequence of Quantities into an numpy.array quantity. + If no units are specified, the unit of the first element will be used. + + If units is not specified and sequence is empty, the unit cannot be determined + and a ValueError is raised. + + :param seq: sequence of Quantities + :type seq: sequence of Quantity + :param units: units of the physical quantity to be created. + :type units: UnitsContainer, str or Quantity. + """ + + len_seq = len(seq) + if units is None: + if len_seq: + units = seq[0].u + else: + raise ValueError('Cannot determine units from empty sequence!') + + a = np.empty(len_seq) + + for i, seq_i in enumerate(seq): + a[i] = seq_i.m_as(units) + # raises DimensionalityError if incompatible units are used in the sequence + + return cls(a, units) + + @classmethod def from_tuple(cls, tup): return cls(tup[0], UnitsContainer(tup[1])) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index c7f41cbf7..d2004c64c 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -355,6 +355,30 @@ def pickle_test(q): pickle_test(self.Q_(2.4, 'm/s')) + def test_from_sequence(self): + u_array_ref = self.Q_([200, 1000], 'g') + u_array_ref_reversed = self.Q_([1000, 200], 'g') + u_seq = [self.Q_('200g'), self.Q_('1kg')] + u_seq_reversed = u_seq[::-1] + + u_array = self.Q_.from_sequence(u_seq) + self.assertTrue(all(u_array == u_array_ref)) + + u_array_2 = self.Q_.from_sequence(u_seq_reversed) + self.assertTrue(all(u_array_2 == u_array_ref_reversed)) + self.assertFalse(u_array_2.u == u_array_ref_reversed.u) + + u_array_3 = self.Q_.from_sequence(u_seq_reversed, units='g') + self.assertTrue(all(u_array_3 == u_array_ref_reversed)) + self.assertTrue(u_array_3.u == u_array_ref_reversed.u) + + with self.assertRaises(ValueError): + self.Q_.from_sequence([]) + + u_array_5 = self.Q_.from_list(u_seq) + self.assertTrue(all(u_array_5 == u_array_ref)) + + class TestQuantityToCompact(QuantityTestCase): def assertQuantityAlmostIdentical(self, q1, q2): From 935b8d29f24e05ac51c445bb81966d406d59e3f5 Mon Sep 17 00:00:00 2001 From: Deniz Bozyigit Date: Wed, 6 Feb 2019 03:44:34 +0100 Subject: [PATCH 008/612] Modified default constructor to process sequences of Quantities. This behavior can be turned off by class property distill_sequences=False (default: True). Alternative constructor from_sequence has same functionality independent of distill_sequences. Adjusted testcase.' --- pint/compat/__init__.py | 60 +++++++++++++++++++++++++++------ pint/quantity.py | 22 ++++-------- pint/testsuite/test_quantity.py | 8 +++-- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/pint/compat/__init__.py b/pint/compat/__init__.py index 1fc438d27..449e8eae4 100644 --- a/pint/compat/__init__.py +++ b/pint/compat/__init__.py @@ -66,6 +66,14 @@ def u(x): except ImportError: from itertools import izip_longest as zip_longest + + +def _raise_invalid_magnitude(value): + if isinstance(value, (dict, bool)) or value is None: + raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) + elif isinstance(value, string_types) and value == '': + raise ValueError('Quantity magnitude cannot be an empty string.') + try: import numpy as np from numpy import ndarray @@ -74,13 +82,41 @@ def u(x): NUMPY_VER = np.__version__ NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) + def _distill_sequence(value, units=None): + """ + Distills sequences of quantities to magnitude arrays with a single unit. + """ + + _raise_invalid_magnitude(value) + + try: + # might trigger IndexError if sequence empty + new_units = value[0].units if units is None else units + + # might trigger TypeError if not a sequence (no __len__) + a = np.empty(len(value)) + + for i, seq_i in enumerate(value): + # might raise AttributeError if not a quantity + a[i] = seq_i.m_as(new_units) + # raises DimensionalityError if incompatible units are used in the sequence + + return a, new_units + + except (TypeError, IndexError, KeyError, AttributeError): + # if any element is not a quantity we pass value on + return value, units + + def _to_magnitude(value, force_ndarray=False): - if isinstance(value, (dict, bool)) or value is None: - raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, string_types) and value == '': - raise ValueError('Quantity magnitude cannot be an empty string.') - elif isinstance(value, (list, tuple)): - return np.asarray(value) + _raise_invalid_magnitude(value) + + if isinstance(value, (list, tuple)): + try: + a = np.asarray(value) + except ValueError: + raise ValueError('Given magnitude value cannot be used as an numpy array! Use e.g. scalars, Quantities, lists of scalars or lists of Quantities!') from None + return a if force_ndarray: return np.asarray(value) return value @@ -96,12 +132,14 @@ class ndarray(object): NUMPY_VER = '0' NUMERIC_TYPES = (Number, Decimal) + def _distill_sequence(value, units=None, ignore_mix=True): + _raise_invalid_magnitude(value) + return value, units + def _to_magnitude(value, force_ndarray=False): - if isinstance(value, (dict, bool)) or value is None: - raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, string_types) and value == '': - raise ValueError('Quantity magnitude cannot be an empty string.') - elif isinstance(value, (list, tuple)): + _raise_invalid_magnitude(value) + + if isinstance(value, (list, tuple)): raise TypeError('lists and tuples are valid magnitudes for ' 'Quantity only when NumPy is present.') return value diff --git a/pint/quantity.py b/pint/quantity.py index 91e02d60b..62d5d2434 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -24,7 +24,7 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import string_types, ndarray, np, _to_magnitude, long_type +from .compat import string_types, ndarray, np, _to_magnitude, _distill_sequence, long_type from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, fix_str_conversions) @@ -90,12 +90,16 @@ class _Quantity(PrettyIPython, SharedRegistryObject): #: Default formatting string. default_format = '' + distill_sequences = True def __reduce__(self): from . import _build_quantity return _build_quantity, (self.magnitude, self._units) def __new__(cls, value, units=None): + if cls.distill_sequences: + value, units = _distill_sequence(value, units) + if units is None: if isinstance(value, string_types): if value == '': @@ -352,20 +356,8 @@ def from_sequence(cls, seq, units=None): :type units: UnitsContainer, str or Quantity. """ - len_seq = len(seq) - if units is None: - if len_seq: - units = seq[0].u - else: - raise ValueError('Cannot determine units from empty sequence!') - - a = np.empty(len_seq) - - for i, seq_i in enumerate(seq): - a[i] = seq_i.m_as(units) - # raises DimensionalityError if incompatible units are used in the sequence - - return cls(a, units) + # moved sequence distillation into default constructor + return cls(*_distill_sequence(seq, units)) @classmethod diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index d2004c64c..b3d0fc02d 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -372,8 +372,12 @@ def test_from_sequence(self): self.assertTrue(all(u_array_3 == u_array_ref_reversed)) self.assertTrue(u_array_3.u == u_array_ref_reversed.u) - with self.assertRaises(ValueError): - self.Q_.from_sequence([]) + u_seq_np = np.array(u_seq, dtype=object) + u_array_4 = self.Q_.from_sequence(u_seq_np) + self.assertTrue(all(u_array_4 == u_array_ref)) + + # with self.assertRaises(ValueError): + # self.Q_.from_sequence([]) u_array_5 = self.Q_.from_list(u_seq) self.assertTrue(all(u_array_5 == u_array_ref)) From 81facee15b991207d1b93f3c0e8c06637dcbbb87 Mon Sep 17 00:00:00 2001 From: Deniz Bozyigit Date: Fri, 8 Feb 2019 00:51:35 +0100 Subject: [PATCH 009/612] added @helpers.requires_numpy() to test_from_sequence. (also cleaned up some forking issues) --- pint/compat/__init__.py | 60 ++++++--------------------------- pint/quantity.py | 22 ++++++++---- pint/testsuite/test_quantity.py | 9 ++--- 3 files changed, 29 insertions(+), 62 deletions(-) diff --git a/pint/compat/__init__.py b/pint/compat/__init__.py index 449e8eae4..1fc438d27 100644 --- a/pint/compat/__init__.py +++ b/pint/compat/__init__.py @@ -66,14 +66,6 @@ def u(x): except ImportError: from itertools import izip_longest as zip_longest - - -def _raise_invalid_magnitude(value): - if isinstance(value, (dict, bool)) or value is None: - raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, string_types) and value == '': - raise ValueError('Quantity magnitude cannot be an empty string.') - try: import numpy as np from numpy import ndarray @@ -82,41 +74,13 @@ def _raise_invalid_magnitude(value): NUMPY_VER = np.__version__ NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) - def _distill_sequence(value, units=None): - """ - Distills sequences of quantities to magnitude arrays with a single unit. - """ - - _raise_invalid_magnitude(value) - - try: - # might trigger IndexError if sequence empty - new_units = value[0].units if units is None else units - - # might trigger TypeError if not a sequence (no __len__) - a = np.empty(len(value)) - - for i, seq_i in enumerate(value): - # might raise AttributeError if not a quantity - a[i] = seq_i.m_as(new_units) - # raises DimensionalityError if incompatible units are used in the sequence - - return a, new_units - - except (TypeError, IndexError, KeyError, AttributeError): - # if any element is not a quantity we pass value on - return value, units - - def _to_magnitude(value, force_ndarray=False): - _raise_invalid_magnitude(value) - - if isinstance(value, (list, tuple)): - try: - a = np.asarray(value) - except ValueError: - raise ValueError('Given magnitude value cannot be used as an numpy array! Use e.g. scalars, Quantities, lists of scalars or lists of Quantities!') from None - return a + if isinstance(value, (dict, bool)) or value is None: + raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) + elif isinstance(value, string_types) and value == '': + raise ValueError('Quantity magnitude cannot be an empty string.') + elif isinstance(value, (list, tuple)): + return np.asarray(value) if force_ndarray: return np.asarray(value) return value @@ -132,14 +96,12 @@ class ndarray(object): NUMPY_VER = '0' NUMERIC_TYPES = (Number, Decimal) - def _distill_sequence(value, units=None, ignore_mix=True): - _raise_invalid_magnitude(value) - return value, units - def _to_magnitude(value, force_ndarray=False): - _raise_invalid_magnitude(value) - - if isinstance(value, (list, tuple)): + if isinstance(value, (dict, bool)) or value is None: + raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) + elif isinstance(value, string_types) and value == '': + raise ValueError('Quantity magnitude cannot be an empty string.') + elif isinstance(value, (list, tuple)): raise TypeError('lists and tuples are valid magnitudes for ' 'Quantity only when NumPy is present.') return value diff --git a/pint/quantity.py b/pint/quantity.py index 62d5d2434..91e02d60b 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -24,7 +24,7 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import string_types, ndarray, np, _to_magnitude, _distill_sequence, long_type +from .compat import string_types, ndarray, np, _to_magnitude, long_type from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, fix_str_conversions) @@ -90,16 +90,12 @@ class _Quantity(PrettyIPython, SharedRegistryObject): #: Default formatting string. default_format = '' - distill_sequences = True def __reduce__(self): from . import _build_quantity return _build_quantity, (self.magnitude, self._units) def __new__(cls, value, units=None): - if cls.distill_sequences: - value, units = _distill_sequence(value, units) - if units is None: if isinstance(value, string_types): if value == '': @@ -356,8 +352,20 @@ def from_sequence(cls, seq, units=None): :type units: UnitsContainer, str or Quantity. """ - # moved sequence distillation into default constructor - return cls(*_distill_sequence(seq, units)) + len_seq = len(seq) + if units is None: + if len_seq: + units = seq[0].u + else: + raise ValueError('Cannot determine units from empty sequence!') + + a = np.empty(len_seq) + + for i, seq_i in enumerate(seq): + a[i] = seq_i.m_as(units) + # raises DimensionalityError if incompatible units are used in the sequence + + return cls(a, units) @classmethod diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index b3d0fc02d..669150bc4 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -355,6 +355,7 @@ def pickle_test(q): pickle_test(self.Q_(2.4, 'm/s')) + @helpers.requires_numpy() def test_from_sequence(self): u_array_ref = self.Q_([200, 1000], 'g') u_array_ref_reversed = self.Q_([1000, 200], 'g') @@ -372,12 +373,8 @@ def test_from_sequence(self): self.assertTrue(all(u_array_3 == u_array_ref_reversed)) self.assertTrue(u_array_3.u == u_array_ref_reversed.u) - u_seq_np = np.array(u_seq, dtype=object) - u_array_4 = self.Q_.from_sequence(u_seq_np) - self.assertTrue(all(u_array_4 == u_array_ref)) - - # with self.assertRaises(ValueError): - # self.Q_.from_sequence([]) + with self.assertRaises(ValueError): + self.Q_.from_sequence([]) u_array_5 = self.Q_.from_list(u_seq) self.assertTrue(all(u_array_5 == u_array_ref)) From d1f2aa3060e835d06063a9ef25b7003731f05152 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Sun, 24 Feb 2019 00:46:48 +0100 Subject: [PATCH 010/612] Docs: Fix Warning Formatting Fix a sphinx/rst `warning` formatting in the systems docs. --- docs/systems.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/systems.rst b/docs/systems.rst index 5d8e01b42..7c095c5d3 100644 --- a/docs/systems.rst +++ b/docs/systems.rst @@ -28,10 +28,10 @@ or more drastically to: >>> '{:.3f}'.format(q.to_base_units()) '1.094 yard / second' -..warning: In versions previous to 0.7 `to_base_units` returns quantities in the - units of the definition files (which are called root units). For the definition file - bundled with pint this is meter/gram/second. To get back this behaviour use `to_root_units`, - set `ureg.system = None` +.. warning:: In versions previous to 0.7 ``to_base_units`` returns quantities in the + units of the definition files (which are called root units). For the definition file + bundled with pint this is meter/gram/second. To get back this behaviour use ``to_root_units``, + set ``ureg.system = None`` You can also use system to narrow down the list of compatible units: From 7034b39d7109efd497ad6512ac7de45eeb91adfc Mon Sep 17 00:00:00 2001 From: Jules Date: Sun, 24 Feb 2019 17:00:53 +0100 Subject: [PATCH 011/612] Add only units that are not defined to any other groups to the default group Fix #766 --- pint/registry.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 32baa024a..2e00639f1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1293,12 +1293,27 @@ def __init__(self, system=None, **kwargs): self._default_system = system def _after_init(self): + """After init function + + Create default group. + Add all orphan units to it. + Set default system. + """ super(SystemRegistry, self)._after_init() - #: Copy units in root group to the default group + #: Copy units not defined in any group to the default group if 'group' in self._defaults: grp = self.get_group(self._defaults['group'], True) - grp.add_units(*self.get_group('root', False).non_inherited_unit_names) + group_units = frozenset( + [ + member + for group in self._groups.values() + if group.name != "root" + for member in group.members + ] + ) + all_units = self.get_group("root", False).members + grp.add_units(*(all_units - group_units)) #: System name to be used by default. self._default_system = self._default_system or self._defaults.get('system', None) From 6f344b3816f948e36c875dace8478adc3260400b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Sun, 10 Mar 2019 17:52:26 +0100 Subject: [PATCH 012/612] formatting: revert unwanted removal of positional While this was a nice change in Python, babel locale data are still using positional formatting. This should fix the test suite for us but it's a mystery the build is passing on the master branch. This change was introduced in commit 546ddaae991fcae400db22f87e82fffdaf5b2316. --- pint/formatting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/formatting.py b/pint/formatting.py index 787cfae73..c71437acc 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -149,7 +149,8 @@ def formatter(items, as_ratio=True, single_denominator=False, for _babel_length in [babel_length] + other_lengths: pat = unit_patterns.get(_key, {}).get(_babel_length, {}).get(plural) if pat is not None: - key = pat.replace('{}', '').strip() + # Don't remove this positional! This is the format used in Babel + key = pat.replace('{0}', '').strip() break division_fmt = compound_unit_patterns.get("per", {}).get(babel_length, division_fmt) power_fmt = '{}{}' From e2b6aacff86bbae61b2c7825edbe409bcfb94097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Wed, 13 Mar 2019 13:01:31 +0100 Subject: [PATCH 013/612] babel_names: fix typo in unit I was wondering why grams were not translated. --- pint/babel_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/babel_names.py b/pint/babel_names.py index 45601bd1b..05b550980 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -68,7 +68,7 @@ inch='length-inch', centiliter='volume-centiliter', troy_ounce='mass-ounce-troy', - gream='mass-gram', + gram='mass-gram', kilowatt='power-kilowatt', knot='speed-knot', lux='light-lux', From 97b637fe089ee75103b721385512f0de2f3964f7 Mon Sep 17 00:00:00 2001 From: Wes Turner <50891+westurner@users.noreply.github.com> Date: Fri, 22 Mar 2019 08:33:58 -0400 Subject: [PATCH 014/612] DOC: index: pint-pandas Jupyter notebook --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 05cb13af1..4f796b015 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -102,7 +102,7 @@ points, like positions on a map or absolute temperature scales. **Python 2 and 3**: a single codebase that runs unchanged in Python 2.7+ and Python 3.3+. -**Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `Pandas Support Documentation`_. +**Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. When you choose to use a NumPy_ ndarray, its methods and @@ -160,4 +160,4 @@ One last thing .. _`PEP 3101`: https://www.python.org/dev/peps/pep-3101/ .. _`Babel`: http://babel.pocoo.org/ .. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types -.. _`Pandas Support Documentation`: ./pandas.rst +.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb From 414d400fd8519aace0f0123d9557e352c6bdb705 Mon Sep 17 00:00:00 2001 From: alexbhandari Date: Mon, 25 Mar 2019 23:57:06 -0500 Subject: [PATCH 015/612] In the check_implemented function in quanity.py added a check for empty lists to prevent IndexError. --- pint/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index a5b40d79b..3373552c7 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -71,7 +71,7 @@ def wrapped(self, *args, **kwargs): return NotImplemented # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] # and expects Quantity * array[Quantity] should return NotImplemented - elif isinstance(other, list) and isinstance(other[0], type(self)): + elif isinstance(other, list) and other and isinstance(other[0], type(self)): return NotImplemented result = f(self, *args, **kwargs) return result From 022337d58b46ae7537b1f019b3d6a0fa8951549e Mon Sep 17 00:00:00 2001 From: alexbhandari Date: Tue, 26 Mar 2019 00:06:56 -0500 Subject: [PATCH 016/612] Added test for issue #783 --- pint/testsuite/test_issues.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index efbaec420..df9722862 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -653,6 +653,10 @@ def test_issue655a(self): self.assertEqual(velocity.check('[length] / [time]'), True) self.assertEqual(velocity.check('1 / [time] * [length]'), True) + def test_issue783(self): + ureg = UnitRegistry() + assert not ureg('g') == [] + def test_issue(self): import math try: From 4a58aae55c2118da3f58f94aa0deb4f8364bfa5e Mon Sep 17 00:00:00 2001 From: Francisco Couzo Date: Fri, 12 Apr 2019 15:07:29 -0300 Subject: [PATCH 017/612] Pass use_decimal to eval_token --- AUTHORS | 1 + pint/registry.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index d33a75cf7..12fcbd8bf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,6 +17,7 @@ Other contributors, listed alphabetically, are: * Eduard Bopp * Eli * Felix Hummel +* Francisco Couzo * Giel van Schijndel * James Rowe * Jim Turner diff --git a/pint/registry.py b/pint/registry.py index 2e00639f1..a078656a8 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -831,7 +831,7 @@ def _parse_units(self, input_string, as_delta=None): return ret - def _eval_token(self, token, case_sensitive=True, **values): + def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): token_type = token[0] token_text = token[1] if token_type == NAME: @@ -845,11 +845,11 @@ def _eval_token(self, token, case_sensitive=True, **values): return self.Quantity(1, UnitsContainer({self.get_name(token_text, case_sensitive=case_sensitive) : 1})) elif token_type == NUMBER: - return ParserHelper.eval_token(token) + return ParserHelper.eval_token(token, use_decimal=use_decimal) else: raise Exception('unknown token type') - def parse_expression(self, input_string, case_sensitive=True, **values): + def parse_expression(self, input_string, case_sensitive=True, use_decimal=False, **values): """Parse a mathematical expression including units and return a quantity object. Numerical constants can be specified as keyword arguments and will take precedence @@ -864,6 +864,7 @@ def parse_expression(self, input_string, case_sensitive=True, **values): return build_eval_tree(gen).evaluate(lambda x: self._eval_token(x, case_sensitive=case_sensitive, + use_decimal=use_decimal, **values)) __call__ = parse_expression From cb8a3ea624124124e640e5ecf7f934648e79ea47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= Date: Tue, 16 Apr 2019 21:05:20 +0000 Subject: [PATCH 018/612] Fix instantiation of errors.DefinitionSyntaxError in context.py The original message led to the following being displayed: pint.errors.DefinitionSyntaxError: While opening units.txt, in line 3: Could not parse Context definition defaults: '%s' instead of the intended: pint.errors.DefinitionSyntaxError: While opening units.txt, in line 3: Could not parse Context definition defaults: '(factor=xyz)' --- pint/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/context.py b/pint/context.py index 780605b1f..f492bceb4 100644 --- a/pint/context.py +++ b/pint/context.py @@ -125,7 +125,7 @@ def to_num(val): defaults = dict((str(k).strip(), to_num(v)) for k, v in defaults) except (ValueError, TypeError): - raise DefinitionSyntaxError("Could not parse Context definition defaults: '%s'", _txt, + raise DefinitionSyntaxError("Could not parse Context definition defaults: '%s'" % _txt, lineno=lineno) ctx = cls(name, aliases, defaults) From 1438ec8f6dd76b6b5d0669558013f9fcc4d594c3 Mon Sep 17 00:00:00 2001 From: JHdJ Date: Sat, 20 Apr 2019 21:55:46 +0200 Subject: [PATCH 019/612] Expanded ton_force definitions Adds definitions for metric ton force and long ton force to the default definitions. Fully backward compatible (i.e. ton_force, force_ton and US_ton_force are still available, and behave the same as before). Added definitions: - force_metric_ton, metric_ton_force, force_t, t_force - force_short_ton, short_ton_force - force_long_ton, long_ton_force, UK_ton_force --- pint/default_en.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index ff96de9a7..1ca5c83b7 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -132,7 +132,7 @@ 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 +force_metric_ton = g_0 * t = metric_ton_force = force_t = t_force poundal = lb * feet / second ** 2 = pdl kip = 1000*lbf @@ -405,6 +405,8 @@ stere = meter ** 3 long_hunderweight = 112 avoirdupois_pound = lg_cwt short_ton = 2000 avoirdupois_pound long_ton = 2240 avoirdupois_pound + force_short_ton = short_ton * g_0 = short_ton_force + force_long_ton = long_ton * g_0 = long_ton_force @end @group Troy @@ -425,11 +427,13 @@ stere = meter ** 3 quarter = 28 stone UK_hundredweight = long_hunderweight = UK_cwt UK_ton = long_ton + UK_ton_force = force_long_ton @end @group AvoirdupoisUS using Avoirdupois US_hundredweight = short_hunderdweight = US_cwt US_ton = short_ton = ton + US_ton_force = force_short_ton = ton_force = force_ton @end @group Printer From fbbe7865a10fa4af0fefb57531c5db9aba1ba5f1 Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 23 Apr 2019 09:07:25 +0200 Subject: [PATCH 020/612] Remove __name__ method definition in BaseRegistry It serves no known purpose, causes issues and goes against conventions (see #787). Fixes #787 --- pint/registry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index a078656a8..0992bd9ab 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -194,9 +194,6 @@ def _parse_defaults(self, ifile): k, v = part.split('=') self._defaults[k.strip()] = v.strip() - def __name__(self): - return 'UnitRegistry' - def __getattr__(self, item): if item[0] == '_': return super(BaseRegistry, self).__getattribute__(item) From 513459e9f74b66863d80c16f9144091b3e49d619 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Thu, 23 Aug 2018 16:54:50 +0100 Subject: [PATCH 021/612] =?UTF-8?q?Use=20=C2=B5=20as=20default=20abbreviat?= =?UTF-8?q?ion=20for=20micro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pint/default_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index 1ca5c83b7..8c658e45c 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -15,7 +15,7 @@ atto- = 1e-18 = a- femto- = 1e-15 = f- pico- = 1e-12 = p- nano- = 1e-9 = n- -micro- = 1e-6 = u- = µ- +micro- = 1e-6 = µ- = u- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- From 14a348324db711792b840a50f341010cea93cd56 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 23 Apr 2019 14:20:28 +0100 Subject: [PATCH 022/612] Remove unsupported unicode character (in aliases) from definition for python 2.7. --- pint/definitions.py | 8 ++++++++ pint/testsuite/test_issues.py | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 51446de58..4ed39ea4b 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -11,6 +11,7 @@ from __future__ import (division, unicode_literals, print_function, absolute_import) +import sys from .converters import ScaleConverter, OffsetConverter from .util import UnitsContainer, _is_dim, ParserHelper @@ -44,6 +45,13 @@ def from_string(cls, definition): name = name.strip() result = [res.strip() for res in definition.split('=')] + # For python 2.7, remove unsupported unicode character + if (sys.version_info < (3, 0)): + for value in result: + try: + value.decode('utf-8') + except UnicodeEncodeError: + result.remove(value) value, aliases = result[0], tuple(result[1:]) symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index df9722862..c8bfa90e0 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -5,6 +5,7 @@ import math import copy import unittest +import sys from pint import UnitRegistry from pint.unit import UnitsContainer @@ -281,25 +282,24 @@ def test_issue170b(self): def test_angstrom_creation(self): ureg = UnitRegistry() - try: + if (sys.version_info < (3, 0)): + self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'Å') + else: ureg.Quantity(2, 'Å') - except SyntaxError: - self.fail('Quantity with Å could not be created.') def test_alternative_angstrom_definition(self): ureg = UnitRegistry() - try: + if (sys.version_info < (3, 0)): + self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, '\u212B') + else: ureg.Quantity(2, '\u212B') - except UndefinedUnitError: - self.fail('Quantity with Å could not be created.') def test_micro_creation(self): ureg = UnitRegistry() - try: + if (sys.version_info < (3, 0)): + self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'µm') + else: ureg.Quantity(2, 'µm') - except SyntaxError: - self.fail('Quantity with µ prefix could not be created.') - @helpers.requires_numpy() class TestIssuesNP(QuantityTestCase): From 45914e6c389f8c1f6ec9298932e7a78e25cbba26 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 7 May 2019 10:08:45 +0100 Subject: [PATCH 023/612] Fix unicode tests on python27. --- pint/testsuite/test_issues.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index c8bfa90e0..c3fcb311e 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -282,21 +282,36 @@ def test_issue170b(self): def test_angstrom_creation(self): ureg = UnitRegistry() - if (sys.version_info < (3, 0)): + try: + # Check if the install supports unicode, travis python27 seems to + # support it... + if (sys.version_info < (3, 0)): + 'Å'.decode('utf-8') + except UnicodeEncodeError: self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'Å') else: ureg.Quantity(2, 'Å') def test_alternative_angstrom_definition(self): ureg = UnitRegistry() - if (sys.version_info < (3, 0)): + try: + # Check if the install supports unicode, travis python27 seems to + # support it... + if (sys.version_info < (3, 0)): + 'Å'.decode('utf-8') + except UnicodeEncodeError: self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, '\u212B') else: ureg.Quantity(2, '\u212B') def test_micro_creation(self): ureg = UnitRegistry() - if (sys.version_info < (3, 0)): + try: + # Check if the install supports unicode, travis python27 seems to + # support it... + if (sys.version_info < (3, 0)): + 'µ'.decode('utf-8') + except UnicodeEncodeError: self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'µm') else: ureg.Quantity(2, 'µm') From 1b37334951358ee47946d99a56ef71ed90dbe673 Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Thu, 9 May 2019 09:35:27 +0200 Subject: [PATCH 024/612] Added RKM unit (used in textile industry) --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index 8c658e45c..f2951f147 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -171,6 +171,7 @@ bag = 94 * lb denier = gram / (9000 * meter) tex = gram / (1000 * meter) dtex = decitex +RKM = kgf * 1000 / tex # These are Indirect yarn numbering systems (length/unit mass) jute = lb / (14400 * yd) = Tj From 515621b9e2d0daaa61f380c6031f7d1dbecd7c61 Mon Sep 17 00:00:00 2001 From: Jellby Date: Fri, 10 May 2019 19:38:40 +0200 Subject: [PATCH 025/612] Fix definition of dimensionless constants --- pint/constants_en.txt | 2 ++ pint/definitions.py | 6 +++--- pint/registry.py | 8 +++----- pint/testsuite/test_unit.py | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index 2b67545ac..19616a044 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -4,6 +4,8 @@ # Source: http://physics.nist.gov/cuu/Constants/Table/allascii.txt # :copyright: 2013 by Pint Authors, see AUTHORS for more details. +ln10 = 2.302585092994046 + speed_of_light = 299792458 * meter / second = c standard_gravity = 9.806650 * meter / second ** 2 = g_0 = g_n = gravity vacuum_permeability = 4 * pi * 1e-7 * newton / ampere ** 2 = mu_0 = magnetic_constant diff --git a/pint/definitions.py b/pint/definitions.py index 4ed39ea4b..e8e07c956 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -123,10 +123,10 @@ def __init__(self, name, symbol, aliases, converter, modifiers = {} converter = ParserHelper.from_string(converter) - if all(_is_dim(key) for key in converter.keys()): - self.is_base = True - elif not any(_is_dim(key) for key in converter.keys()): + if not any(_is_dim(key) for key in converter.keys()): self.is_base = False + elif all(_is_dim(key) for key in converter.keys()): + self.is_base = True else: raise ValueError('Cannot mix dimensions and units in the same definition. ' 'Base units must be referenced only to dimensions. ' diff --git a/pint/registry.py b/pint/registry.py index 0992bd9ab..6c9d953b8 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -171,13 +171,13 @@ def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', aut def _after_init(self): """This should be called after all __init__ """ + self.define(UnitDefinition('pi', 'π', (), ScaleConverter(math.pi))) + if self._filename == '': self.load_definitions('default_en.txt', True) elif self._filename is not None: self.load_definitions(self._filename) - self.define(UnitDefinition('pi', 'π', (), ScaleConverter(math.pi))) - self._build_cache() self._initialized = True @@ -832,9 +832,7 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): token_type = token[0] token_text = token[1] if token_type == NAME: - if token_text == 'pi': - return self.Quantity(math.pi) - elif token_text == 'dimensionless': + if token_text == 'dimensionless': return 1 * self.dimensionless elif token_text in values: return self.Quantity(values[token_text]) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index b9e3b0650..5ce35c51a 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -562,6 +562,8 @@ def test_context_sp(self): def test_get_base_units(self): ureg = UnitRegistry() self.assertEqual(ureg.get_base_units(''), (1, ureg.Unit(''))) + self.assertEqual(ureg.get_base_units('pi'), (math.pi, ureg.Unit(''))) + self.assertEqual(ureg.get_base_units('ln10'), (math.log(10), ureg.Unit(''))) self.assertEqual(ureg.get_base_units('meter'), ureg.get_base_units(ParserHelper(meter=1))) def test_get_compatible_units(self): From f52122cf7e17beb26d7107fc70833d9df7587e74 Mon Sep 17 00:00:00 2001 From: Jellby Date: Fri, 10 May 2019 19:39:43 +0200 Subject: [PATCH 026/612] Fix typos in default units --- pint/default_en.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index 8c658e45c..e7966367b 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -19,7 +19,7 @@ micro- = 1e-6 = µ- = u- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- -deca- = 1e+1 = da- = deka +deca- = 1e+1 = da- = deka- hecto- = 1e2 = h- kilo- = 1e3 = k- mega- = 1e6 = M- @@ -343,8 +343,6 @@ stere = meter ** 3 cubic_inch = 1 in ** 3 = cu_in cubic_foot = 1 ft ** 3 = cu_ft = cubic_feet cubic_yard = 1 yd ** 3 = cu_yd - - acre_foot = acre * foot = acre_feet @end @group USCSLengthSurvey @@ -361,6 +359,8 @@ stere = meter ** 3 us_statute_mile = 5280 survey_foot league = 3 us_statute_mile furlong = us_statute_mile / 8 + + acre_foot = acre * survey_foot = acre_feet @end @group USCSDryVolume @@ -369,7 +369,7 @@ stere = meter ** 3 dry_gallon = 8 dry_pint = dgal = US_dry_gallon peck = 16 dry_pint = pk bushel = 64 dry_pint = bu - dry_barrel = 7065 cubic_inch = US_dry_barrel + dry_barrel = 7056 cubic_inch = US_dry_barrel @end @group USCSLiquidVolume @@ -401,8 +401,8 @@ stere = meter ** 3 ounce = pound / 16 = oz = avoirdupois_ounce = avdp_ounce pound = 453.59237 gram = lb = avoirdupois_pound = avdp_pound - short_hunderdweight = 100 avoirdupois_pound = ch_cwt - long_hunderweight = 112 avoirdupois_pound = lg_cwt + short_hundredweight = 100 avoirdupois_pound = ch_cwt + long_hundredweight = 112 avoirdupois_pound = lg_cwt short_ton = 2000 avoirdupois_pound long_ton = 2240 avoirdupois_pound force_short_ton = short_ton * g_0 = short_ton_force @@ -425,13 +425,13 @@ stere = meter ** 3 @group AvoirdupoisUK using Avoirdupois stone = 14 pound quarter = 28 stone - UK_hundredweight = long_hunderweight = UK_cwt + UK_hundredweight = long_hundredweight = UK_cwt UK_ton = long_ton UK_ton_force = force_long_ton @end @group AvoirdupoisUS using Avoirdupois - US_hundredweight = short_hunderdweight = US_cwt + US_hundredweight = short_hundredweight = US_cwt US_ton = short_ton = ton US_ton_force = force_short_ton = ton_force = force_ton @end From 7e3483181901144bdf3a9b66db8ee5e244a6f8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= Date: Fri, 26 Apr 2019 17:27:43 -0400 Subject: [PATCH 027/612] Fix call to logger.warning in registry.py --- pint/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 0992bd9ab..eb0344623 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -318,7 +318,7 @@ def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): if self._on_redefinition == 'raise': raise RedefinitionError(key, type(value)) elif self._on_redefinition == 'warn': - logger.warning("Redefining '%s' (%s)", key, type(value)) + logger.warning("Redefining '%s' (%s)" % (key, type(value))) unit_dict[key] = value if casei_unit_dict is not None: From 1d60fbed740628b556c809f6a5a4279ee048c06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= Date: Fri, 26 Apr 2019 17:28:52 -0400 Subject: [PATCH 028/612] Preserve caches across context changes (fixes #795) --- AUTHORS | 1 + pint/context.py | 9 ++++-- pint/registry.py | 83 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/AUTHORS b/AUTHORS index 12fcbd8bf..a89af23af 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Other contributors, listed alphabetically, are: * Brend Wanders * choloepus * coutinho +* Clément Pit-Claudel * Daniel Sokolowski * Dave Brooks * David Linke diff --git a/pint/context.py b/pint/context.py index f492bceb4..6a9169ed7 100644 --- a/pint/context.py +++ b/pint/context.py @@ -192,6 +192,8 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) + def __hash__(self): + return hash(self.name) class ContextChain(ChainMap): """A specialized ChainMap for contexts that simplifies finding rules @@ -201,7 +203,7 @@ class ContextChain(ChainMap): def __init__(self, *args, **kwargs): super(ContextChain, self).__init__(*args, **kwargs) self._graph = None - self._contexts = [] + self._contexts = () def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. @@ -210,7 +212,7 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts.insert(0, contexts) + self._contexts = tuple(contexts) + self._contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None @@ -244,3 +246,6 @@ def transform(self, src, dst, registry, value): :raises: KeyError if the rule is not found. """ return self[(src, dst)].transform(src, dst, registry, value) + + def __hash__(self): + return hash(self._contexts) diff --git a/pint/registry.py b/pint/registry.py index eb0344623..ad1e2c76c 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -75,6 +75,21 @@ def __call__(self, *args, **kwargs): obj._after_init() return obj +class RegistryCache(): + """Cache to speed up unit registries""" + + def __init__(self): + #: Maps dimensionality (UnitsContainer) to Units (str) + self.dimensional_equivalents = dict() + + #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) + self.root_units = dict() + + #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) + self.dimensionality = dict() + + #: Cache the unit name associated to user input. ('mV' -> 'millivolt') + self.parse_unit = dict() class BaseRegistry(meta.with_metaclass(_Meta)): """Base class for all registries. @@ -154,17 +169,8 @@ def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', aut #: Map suffix name (string) to canonical , and unit alias to canonical unit name self._suffixes = {'': None, 's': ''} - #: Maps dimensionality (UnitsContainer) to Units (str) - self._dimensional_equivalents = dict() - - #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) - self._root_units_cache = dict() - - #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) - self._dimensionality_cache = dict() - - #: Cache the unit name associated to user input. ('mV' -> 'millivolt') - self._parse_unit_cache = dict() + #: Map contexts to RegistryCache + self._cache = RegistryCache() self._initialized = False @@ -404,7 +410,10 @@ def load_definitions(self, file, is_resource=False): def _build_cache(self): """Build a cache of dimensionality and base units. """ - self._dimensional_equivalents = dict() + self._cache = RegistryCache() + root_units = self._cache.root_units + dimensionality = self._cache.dimensionality + dimensional_equivalents = self._cache.dimensional_equivalents deps = dict((name, set(definition.reference.keys() if definition.reference else {})) for name, definition in self._units.items()) @@ -426,14 +435,14 @@ def _build_cache(self): bu = self._get_root_units(uc) di = self._get_dimensionality(uc) - self._root_units_cache[uc] = bu - self._dimensionality_cache[uc] = di + root_units[uc] = bu + dimensionality[uc] = di if not prefixed: - if di not in self._dimensional_equivalents: - self._dimensional_equivalents[di] = set() + if di not in dimensional_equivalents: + dimensional_equivalents[di] = set() - self._dimensional_equivalents[di].add(self._units[base_name]._name) + dimensional_equivalents[di].add(self._units[base_name]._name) except Exception as e: logger.warning('Could not resolve {0}: {1!r}'.format(unit_name, e)) @@ -528,8 +537,10 @@ def _get_dimensionality(self, input_units): if not input_units: return UnitsContainer() - if input_units in self._dimensionality_cache: - return self._dimensionality_cache[input_units] + cache = self._cache.dimensionality + + if input_units in cache: + return cache[input_units] accumulator = defaultdict(float) self._get_dimensionality_recurse(input_units, 1.0, accumulator) @@ -540,7 +551,7 @@ def _get_dimensionality(self, input_units): dims = UnitsContainer(dict((k, v) for k, v in accumulator.items() if v != 0.0)) - self._dimensionality_cache[input_units] = dims + cache[input_units] = dims return dims @@ -615,9 +626,11 @@ def _get_root_units(self, input_units, check_nonmult=True): if not input_units: return 1., UnitsContainer() + cache = self._cache.root_units + # The cache is only done for check_nonmult=True - if check_nonmult and input_units in self._root_units_cache: - return self._root_units_cache[input_units] + if check_nonmult and input_units in cache: + return cache[input_units] accumulators = [1., defaultdict(float)] self._get_root_units_recurse(input_units, 1.0, accumulators) @@ -633,7 +646,7 @@ def _get_root_units(self, input_units, check_nonmult=True): return None, units if check_nonmult: - self._root_units_cache[input_units] = factor, units + cache[input_units] = factor, units return factor, units @@ -683,7 +696,7 @@ def _get_compatible_units(self, input_units, group_or_system): src_dim = self._get_dimensionality(input_units) - ret = self._dimensional_equivalents[src_dim] + ret = self._cache.dimensional_equivalents[src_dim] return ret @@ -795,8 +808,10 @@ def _parse_units(self, input_string, as_delta=None): if as_delta is None: as_delta = True - if as_delta and input_string in self._parse_unit_cache: - return self._parse_unit_cache[input_string] + cache = self._cache.parse_unit + + if as_delta and input_string in cache: + return cache[input_string] if not input_string: return UnitsContainer() @@ -824,7 +839,7 @@ def _parse_units(self, input_string, as_delta=None): ret = UnitsContainer(ret) if as_delta: - self._parse_unit_cache[input_string] = ret + cache[input_string] = ret return ret @@ -1042,6 +1057,9 @@ def __init__(self, **kwargs): #: Stores active contexts. self._active_ctx = ContextChain() + #: Map context chain to cache + self._caches = { self._active_ctx: self._cache } + def _register_parsers(self): super(ContextRegistry, self)._register_parsers() self._register_parser('@context', self._parse_context) @@ -1083,6 +1101,15 @@ def remove_context(self, name_or_alias): return context + def _build_cache(self): + """""" + cache = self._caches.get(self._active_ctx) + if cache is None: + super()._build_cache() + self._caches[self._active_ctx] = self._cache + else: + self._cache = cache + def enable_contexts(self, *names_or_contexts, **kwargs): """Enable contexts provided by name or by object. @@ -1251,7 +1278,7 @@ def _get_compatible_units(self, input_units, group_or_system): nodes = find_connected_nodes(self._active_ctx.graph, src_dim) if nodes: for node in nodes: - ret |= self._dimensional_equivalents[node] + ret |= self._cache.dimensional_equivalents[node] return ret From 2cfe32f3697305c4141cd3b62e11bdc0d840e2e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Mon, 13 May 2019 16:18:24 +0200 Subject: [PATCH 029/612] denier unit is often abbreviated as 'den' --- pint/default_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index f2951f147..b6e4b1967 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -168,7 +168,7 @@ atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da bag = 94 * lb # Textile -denier = gram / (9000 * meter) +denier = gram / (9000 * meter) = den tex = gram / (1000 * meter) dtex = decitex RKM = kgf * 1000 / tex From 1f05b4852e5af06af21b7c98903fdb1fd0aa3960 Mon Sep 17 00:00:00 2001 From: Jellby Date: Thu, 16 May 2019 20:16:30 +0200 Subject: [PATCH 030/612] Allow defining units with aliases, but without symbol Using "_" as a symbol sets symbol no None, using "_" as an alias is ignored. Update the default definitions file accordingly. --- pint/constants_en.txt | 4 +- pint/default_en.txt | 88 +++++++++++++++--------------- pint/definitions.py | 5 +- pint/testsuite/test_definitions.py | 10 ++++ 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index 19616a044..dba75ae70 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -10,7 +10,7 @@ speed_of_light = 299792458 * meter / second = c standard_gravity = 9.806650 * meter / second ** 2 = g_0 = g_n = gravity vacuum_permeability = 4 * pi * 1e-7 * newton / ampere ** 2 = mu_0 = magnetic_constant vacuum_permittivity = 1 / (mu_0 * c **2 ) = epsilon_0 = electric_constant -Z_0 = mu_0 * c = impedance_of_free_space = characteristic_impedance_of_vacuum +impedance_of_free_space = mu_0 * c = Z_0 = characteristic_impedance_of_vacuum # 0.000 000 29 e-34 planck_constant = 6.62606957e-34 J s = h @@ -29,7 +29,7 @@ molar_gas_constant = 8.3144621 J mol^-1 K^-1 = R fine_structure_constant = 7.2973525698e-3 # 0.000 000 27 e23 -avogadro_number = 6.02214129e23 mol^-1 =N_A +avogadro_number = 6.02214129e23 mol^-1 = N_A # 0.000 0013 e-23 boltzmann_constant = 1.3806488e-23 J K^-1 = k diff --git a/pint/default_en.txt b/pint/default_en.txt index e7966367b..d89bca566 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -58,17 +58,17 @@ count = [] [acceleration] = [length] / [time] ** 2 # Angle -turn = 2 * pi * radian = revolution = cycle = circle +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 +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 +circular_mil = 5.067075e-10 * m ** 2 = cmil = circular_mils darcy = 9.869233e-13 * m ** 2 hectare = 100 * are = ha @@ -81,8 +81,8 @@ molar = mol / (1e-3 * m ** 3) = M katal = mole / second = kat # EM -esu = 1 * erg**0.5 * centimeter**0.5 = statcoulombs = statC = franklin = Fr -esu_per_second = 1 * esu / second = statampere +esu = 1 * erg**0.5 * centimeter**0.5 = statcoulomb = statC = franklin = Fr +esu_per_second = 1 * esu / second = _ = statampere = statA ampere_turn = 1 * A gilbert = 10 / (4 * pi ) * ampere_turn coulomb = ampere * second = C @@ -96,15 +96,15 @@ 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 +faraday = 96485.3399 * coulomb = _ = C12_faraday gamma = 1e-9 * tesla gauss = 1e-4 * tesla -maxwell = 1e-8 * weber = mx +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 +statohm = 8.987554e11 * ohm = statΩ = stΩ statvolt = 2.997925e2 * volt = statV = stV unit_pole = 1.256637e-7 * weber @@ -112,17 +112,17 @@ unit_pole = 1.256637e-7 * weber [energy] = [force] * [length] joule = newton * meter = J erg = dyne * centimeter -btu = 1.05505585262e3 * joule = Btu = BTU = british_thermal_unit +british_thermal_unit = 1.05505585262e3 * joule = Btu = BTU = btu electron_volt = 1.60217653e-19 * J = eV quadrillion_btu = 10**15 * btu = quad -thm = 100000 * BTU = therm = EC_therm +therm = 100000 * BTU = thm = EC_therm calorie = 4.184 * joule = cal = 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 = = Eh = E_h = hartree_energy -toe = 41.868e9 * joule = tonne_of_oil_equivalent +hartree = 4.35974394e-18 * joule = E_h = Eh = hartree_energy +tonne_of_oil_equivalent = 41.868e9 * joule = toe # Force [force] = [mass] * [acceleration] @@ -132,7 +132,7 @@ 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_metric_ton = g_0 * t = metric_ton_force = force_t = t_force +force_metric_ton = g_0 * t = tf = metric_ton_force = force_t = t_force poundal = lb * feet / second ** 2 = pdl kip = 1000*lbf @@ -144,7 +144,7 @@ counts_per_second = count / second = cps # Heat #RSI = degK * meter ** 2 / watt -#clo = 0.155 * RSI = clos +#clo = 0.155 * RSI = _ = clos #R_value = foot ** 2 * degF * hour / btu # Information @@ -153,7 +153,7 @@ baud = bit / second = Bd = bps # Irradiance peak_sun_hour = 1000 * watt_hour / meter**2 = PSH -langley = thermochemical_calorie / centimeter**2 = Langley +langley = thermochemical_calorie / centimeter**2 = Ly # Length angstrom = 1e-10 * meter = Å = ångström = Å @@ -164,7 +164,7 @@ astronomical_unit = 149597870691 * meter = au # Mass carat = 200 * milligram metric_ton = 1000 * kilogram = t = tonne -atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da +atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da bag = 94 * lb # Textile @@ -195,13 +195,13 @@ 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 +refrigeration_ton = 12000 * btu / hour = _ = ton_of_refrigeration # Pressure [pressure] = [force] / [area] -Hg = gravity * 13.59510 * gram / centimeter ** 3 = mercury = conventional_mercury +mercury = gravity * 13.59510 * gram / centimeter ** 3 = Hg = conventional_mercury mercury_60F = gravity * 13.5568 * gram / centimeter ** 3 -H2O = gravity * 1000 * kilogram / meter ** 3 = h2o = water = conventional_water +water = gravity * 1000 * kilogram / meter ** 3 = H2O = h2o = 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 @@ -211,31 +211,31 @@ 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 +barye = 0.1 * newton / meter ** 2 = Ba = barie = barad = barrie = baryd +millimeter_Hg = millimeter * Hg = mmHg = mm_Hg = millimeter_Hg_0C +centimeter_Hg = centimeter * Hg = cmHg = cm_Hg +inch_Hg = inch * Hg = inHg = in_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 +centimeter_water = centimeter * water = cmH2O +foot_H2O = ft * water = ftH2O = footH2O standard_liter_per_minute = 1.68875 * Pa * m ** 3 / s = slpm = slm # Radiation -Bq = Hz = becquerel +becquerel = Hz = Bq curie = 3.7e10 * Bq = Ci rutherford = 1e6*Bq = Rd -Gy = joule / kilogram = gray = Sv = sievert +gray = joule / kilogram = Gy +sievert = joule / kilogram = Sv 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 +degC = kelvin; offset: 273.15 = °C = degree_Celsius = celsius +degR = 5 / 9 * kelvin; offset: 0 = °R = degree_Rankine = rankine +degF = 5 / 9 * kelvin; offset: 255.372222 = °F = degree_Fahrenheit = fahrenheit # Time minute = 60 * second = min @@ -253,12 +253,12 @@ 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 +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 +millennium = 1000 * year = _ = millennia eon = 1e9 * year work_year = 2056 * hour work_month = work_year / 12 @@ -279,7 +279,7 @@ rhe = 10 / (Pa * s) # Volume [volume] = [length] ** 3 liter = 1e-3 * m ** 3 = l = L = litre -cc = centimeter ** 3 = cubic_centimeter +cubic_centimeter = centimeter ** 3 = cc stere = meter ** 3 @@ -353,23 +353,23 @@ stere = meter ** 3 survey_mile = 5280 survey_foot acre = 43560 survey_foot ** 2 - square_rod = 1 rod ** 2 = sq_rod = sq_pole = sq_perch + square_rod = 1 rod ** 2 = sq_rd = sq_rod = sq_pole = sq_perch fathom = 6 survey_foot us_statute_mile = 5280 survey_foot league = 3 us_statute_mile furlong = us_statute_mile / 8 - acre_foot = acre * survey_foot = acre_feet + acre_foot = acre * survey_foot = _ =acre_feet @end @group USCSDryVolume - dry_pint = 33.6003125 cubic_inch = dpi = US_dry_pint + dry_pint = 33.6003125 cubic_inch = dpt = US_dry_pint dry_quart = 2 dry_pint = dqt = US_dry_quart dry_gallon = 8 dry_pint = dgal = US_dry_gallon peck = 16 dry_pint = pk bushel = 64 dry_pint = bu - dry_barrel = 7056 cubic_inch = US_dry_barrel + dry_barrel = 7056 cubic_inch = _ = US_dry_barrel @end @group USCSLiquidVolume @@ -405,8 +405,8 @@ stere = meter ** 3 long_hundredweight = 112 avoirdupois_pound = lg_cwt short_ton = 2000 avoirdupois_pound long_ton = 2240 avoirdupois_pound - force_short_ton = short_ton * g_0 = short_ton_force - force_long_ton = long_ton * g_0 = long_ton_force + force_short_ton = short_ton * g_0 = _ = short_ton_force + force_long_ton = long_ton * g_0 = _ = long_ton_force @end @group Troy @@ -433,14 +433,14 @@ stere = meter ** 3 @group AvoirdupoisUS using Avoirdupois US_hundredweight = short_hundredweight = US_cwt US_ton = short_ton = ton - US_ton_force = force_short_ton = ton_force = force_ton + US_ton_force = force_short_ton = _ = ton_force = force_ton @end @group Printer # Length - pixel = [printing_unit] = dot = px = pel = picture_element + pixel = [printing_unit] = _ = dot = px = pel = picture_element pixels_per_centimeter = pixel / cm = PPCM - pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi + pixels_per_inch = pixel / inch = ppi = dots_per_inch = PPI = DPI = printers_dpi bits_per_pixel = bit / pixel = bpp point = yard / 216 / 12 = pp = printers_point @@ -450,7 +450,7 @@ stere = meter ** 3 @group ImperialVolume imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce - imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fluid_dram + imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fldr = imperial_fluid_dram imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup imperial_pint = 568.26125 * milliliter = imperial_pt = UK_pint diff --git a/pint/definitions.py b/pint/definitions.py index e8e07c956..e510a1b10 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -52,9 +52,12 @@ def from_string(cls, definition): value.decode('utf-8') except UnicodeEncodeError: result.remove(value) - value, aliases = result[0], tuple(result[1:]) + value, aliases = result[0], tuple([x for x in result[1:] if x != '']) symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) + if symbol == '_': + symbol = None + aliases = tuple([x for x in aliases if x != '_']) if name.startswith('['): return DimensionDefinition(name, symbol, aliases, value) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 573ee2189..3b73fd8e0 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -70,6 +70,16 @@ def test_unit_definition(self): self.assertEqual(x.converter.offset, 255.372222) self.assertEqual(x.reference, UnitsContainer(kelvin=1)) + x = Definition.from_string('turn = 6.28 * radian = _ = revolution = = cycle = _') + self.assertIsInstance(x, UnitDefinition) + self.assertEqual(x.name, 'turn') + self.assertEqual(x.aliases, ('revolution', 'cycle')) + self.assertEqual(x.symbol, 'turn') + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, ScaleConverter) + self.assertEqual(x.converter.scale, 6.28) + self.assertEqual(x.reference, UnitsContainer(radian=1)) + def test_dimension_definition(self): x = DimensionDefinition('[time]', '', (), converter='') self.assertTrue(x.is_base) From 0f9acf7d0ed48d85b73deb493a8b986d303e9686 Mon Sep 17 00:00:00 2001 From: Jellby Date: Thu, 16 May 2019 20:52:30 +0200 Subject: [PATCH 031/612] Add "delta_" prefix to aliases too --- pint/default_en.txt | 6 +++--- pint/registry.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index d89bca566..b32908731 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -233,9 +233,9 @@ rads = 1e-2 * gray roentgen = 2.58e-4 * coulomb / kilogram # Temperature -degC = kelvin; offset: 273.15 = °C = degree_Celsius = celsius -degR = 5 / 9 * kelvin; offset: 0 = °R = degree_Rankine = rankine -degF = 5 / 9 * kelvin; offset: 255.372222 = °F = degree_Fahrenheit = fahrenheit +degree_Celsius = kelvin; offset: 273.15 = °C = degC = celsius +degree_Rankine = 5 / 9 * kelvin; offset: 0 = °R = degR = rankine +degree_Fahrenheit = 5 / 9 * kelvin; offset: 255.372222 = °F = degF = fahrenheit # Time minute = 60 * second = min diff --git a/pint/registry.py b/pint/registry.py index 6c9d953b8..0129ba5d5 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -279,7 +279,8 @@ def _define(self, definition): else: d_symbol = None - d_aliases = tuple('Δ' + alias for alias in definition.aliases) + d_aliases = tuple('Δ' + alias for alias in definition.aliases) + \ + tuple('delta_' + alias for alias in definition.aliases) d_reference = UnitsContainer(dict((ref, value) for ref, value in definition.reference.items())) From 3ad5c2bb24ca92cb69353af9a84458da9bebc8f3 Mon Sep 17 00:00:00 2001 From: Jellby Date: Wed, 22 May 2019 19:29:50 +0200 Subject: [PATCH 032/612] Rewrite default units and constants Adopt new SI definition and CODATA 2018 values, create Gaussian context --- AUTHORS | 1 + pint/constants_en.txt | 100 ++-- pint/default_en.txt | 930 ++++++++++++++++++++++------------ pint/testsuite/test_issues.py | 4 +- 4 files changed, 674 insertions(+), 361 deletions(-) diff --git a/AUTHORS b/AUTHORS index 12fcbd8bf..df980c206 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,6 +19,7 @@ Other contributors, listed alphabetically, are: * Felix Hummel * Francisco Couzo * Giel van Schijndel +* Ignacio Fdez. Galván * James Rowe * Jim Turner * Joel B. Mohler diff --git a/pint/constants_en.txt b/pint/constants_en.txt index 19616a044..673a21b18 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -1,53 +1,73 @@ # Default Pint constants definition file # Based on the International System of Units # Language: english -# Source: http://physics.nist.gov/cuu/Constants/Table/allascii.txt -# :copyright: 2013 by Pint Authors, see AUTHORS for more details. +# Source: https://physics.nist.gov/cuu/Constants/ +# https://physics.nist.gov/PhysRefData/XrayTrans/Html/search.html +# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. -ln10 = 2.302585092994046 +#### MATHEMATICAL CONSTANTS #### +# As computed by Maxima with fpprec:50 -speed_of_light = 299792458 * meter / second = c -standard_gravity = 9.806650 * meter / second ** 2 = g_0 = g_n = gravity -vacuum_permeability = 4 * pi * 1e-7 * newton / ampere ** 2 = mu_0 = magnetic_constant -vacuum_permittivity = 1 / (mu_0 * c **2 ) = epsilon_0 = electric_constant -Z_0 = mu_0 * c = impedance_of_free_space = characteristic_impedance_of_vacuum +#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 +wien_u = 2.8214393721220788934031913302944851953458817440731 # solution to (u-3)*exp(u)+3 = 0 => u = W(3/exp(3))+3 -# 0.000 000 29 e-34 -planck_constant = 6.62606957e-34 J s = h -hbar = planck_constant / (2 * pi) = ħ +#### DEFINED EXACT CONSTANTS #### -# 0.000 80 e-11 -newtonian_constant_of_gravitation = 6.67384e-11 m^3 kg^-1 s^-2 +speed_of_light = 299792458 m/s = c = c_0 # since 1983 +planck_constant = 6.62607015e-34 J s = h # since May 2019 +elementary_charge = 1.602176634e-19 C = e # since May 2019 +avogadro_number = 6.02214076e23 # since May 2019 +boltzmann_constant = 1.380649e-23 J K^-1 = k = k_B # since May 2019 +standard_gravity = 9.80665 m/s^2 = g_0 = g0 = g_n = gravity # since 1901 +standard_atmosphere = 1.01325e5 Pa = atm = atmosphere # since 1954 +conventional_josephson_constant = 4.835979e14 Hz / V = K_J90 # since Jan 1990 +conventional_von_klitzing_constant = 2.5812807e4 ohm = R_K90 # since Jan 1990 -# 0.000 000 035 e-19 -# elementary_charge = 1.602176565e-19 C = e +#### DERIVED EXACT CONSTANTS #### +# Floating-point conversion may introduce inaccuracies -# 0.000 0075 -molar_gas_constant = 8.3144621 J mol^-1 K^-1 = R +zeta = c / (cm/s) = ζ +dirac_constant = h / (2 * π) = ħ = hbar = atomic_unit_of_action = a_u_action +avogadro_constant = avogadro_number * mol^-1 = N_A +molar_gas_constant = k * N_A = R +faraday_constant = e * N_A +conductance_quantum = 2 * e ** 2 / h = G_0 +magnetic_flux_quantum = h / (2 * e) = Φ_0 = Phi_0 +josephson_constant = 2 * e / h = K_J +von_klitzing_constant = h / e ** 2 = R_K +stefan_boltzmann_constant = 2 / 15 * π ** 5 * k ** 4 / (h ** 3 * c ** 2) = σ = sigma +first_radiation_constant = 2 * π * h * c ** 2 = c_1 +second_radiation_constant = h * c / k = c_2 +wien_wavelength_displacement_law_constant = h * c / (k * wien_x) +wien_frequency_displacement_law_constant = wien_u * k / h -# 0.000 000 0024 e-3 -fine_structure_constant = 7.2973525698e-3 +#### MEASURED CONSTANTS #### +# Recommended CODATA-2018 values +# To some extent, what is measured and what is derived is a bit arbitrary. +# The choice of measured constants is based on convenience and on available uncertainty. +# The uncertainty in the last significant digits is given in parentheses as a comment. -# 0.000 000 27 e23 -avogadro_number = 6.02214129e23 mol^-1 =N_A +newtonian_constant_of_gravitation = 6.67408e-11 m^3/(kg s^2) = = gravitational_constant # (15) +rydberg_constant = 1.0973731568160e7 * m^-1 = R_∞ = R_inf # (21) +electron_g_factor = -2.00231930436256 = g_e # (35) +atomic_mass_constant = 1.66053906660e-27 kg = m_u # (50) +electron_mass = 9.1093837015e-31 kg = m_e = atomic_unit_of_mass = a_u_mass # (28) +proton_mass = 1.67262192369e-27 kg = m_p # (51) +neutron_mass = 1.67492749804e-27 kg = m_n # (95) +lattice_spacing_of_Si = 1.920155716 m = d_220 # (32) +K_alpha_Cu_d_220 = 0.80232719 # (22) +K_alpha_Mo_d_220 = 0.36940604 # (19) +K_alpha_W_d_220 = 0.108852175 # (98) -# 0.000 0013 e-23 -boltzmann_constant = 1.3806488e-23 J K^-1 = k +#### DERIVED CONSTANTS #### -# 0.000 021 e-8 -stefan_boltzmann_constant = 5.670373e-8 W m^-2 K^-4 = σ - -# 0.000 0053 e10 -wien_frequency_displacement_law_constant = 5.8789254e10 Hz K^-1 - -# 0.000 055 -rydberg_constant = 10973731.568539 m^-1 - -# 0.000 000 40 e-31 -electron_mass = 9.10938291e-31 kg = m_e - -# 0.000 000 074 e-27 -neutron_mass = 1.674927351e-27 kg = m_n - -# 0.000 000 074 e-27 -proton_mass = 1.672621777e-27 kg = m_p +fine_structure_constant = (2 * h * R_inf / (m_e * c)) ** 0.5 = α = alpha +vacuum_permeability = 2 * α * h / (e ** 2 * c) = µ_0 = mu_0 = mu0 = magnetic_constant +vacuum_permittivity = e ** 2 / (2 * α * h * c) = ε_0 = epsilon_0 = eps_0 = eps0 = electric_constant +impedance_of_free_space = 2 * α * h / e ** 2 = Z_0 = characteristic_impedance_of_vacuum +coulomb_constant = α * hbar * c / e ** 2 = k_C +classical_electron_radius = α * hbar / (m_e * c) = r_e +thomson_cross_section = 8 / 3 * π * r_e ** 2 = σ_e = sigma_e diff --git a/pint/default_en.txt b/pint/default_en.txt index e7966367b..175bc8734 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -1,13 +1,16 @@ # Default Pint units definition file # Based on the International System of Units # Language: english -# :copyright: 2013 by Pint Authors, see AUTHORS for more details. +# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. @defaults group = international system = mks @end + +#### PREFIXES #### + # decimal prefixes yocto- = 1e-24 = y- zepto- = 1e-21 = z- @@ -40,89 +43,149 @@ exbi- = 2**60 = Ei- zebi- = 2**70 = Zi- yobi- = 2**80 = Yi- -# reference +# extra_prefixes +semi- = 0.5 = = demi- +sesqui- = 1.5 + + +#### BASE UNITS #### + 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 +kelvin = [temperature]; offset: 0 = K = degK = °K = degree_Kelvin = degreeK # older names supported for compatibility radian = [] = rad +neper = [] = Np bit = [] count = [] + +#### CONSTANTS #### + @import constants_en.txt -# acceleration -[acceleration] = [length] / [time] ** 2 + +#### UNITS #### +# Common and less common, grouped by quantity. +# Conversion factors are exact (except when noted), +# although floating-point conversion may introduce inaccuracies # 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 +turn = 2 * π * radian = = revolution = cycle = circle +degree = π / 180 * radian = deg = arcdeg = arcdegree = angular_degree +arcminute = degree / 60 = arcmin = arc_minute = angular_minute +arcsecond = arcminute / 60 = arcsec = arc_second = angular_second +milliarcsecond = 1e-3 * arcsecond = mas +grade = π / 200 * radian = grad = gon +mil = π / 32000 * radian + +# Solid angle steradian = radian ** 2 = sr +square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg + +# Logarithmic ratio +bel = 0.5 * ln10 * neper + +# Information +byte = 8 * bit = B = octet +baud = bit / second = Bd = bps + +# Length +angstrom = 1e-10 * meter = Å = ångström = Å +micron = micrometer = µ +fermi = femtometer = fm +light_year = speed_of_light * julian_year = ly = lightyear +astronomical_unit = 149597870700 * meter = au # since Aug 2012 +parsec = 1 / tansec * astronomical_unit = pc +nautical_mile = 1852 * meter = nmi +bohr = hbar / (alpha * m_e * c) = a_0 = a0 = bohr_radius = atomic_unit_of_length = a_u_length +x_unit_Cu = K_alpha_Cu_d_220 * d_220 / 1537.4 = Xu_Cu +x_unit_Mo = K_alpha_Mo_d_220 * d_220 / 707.831 = Xu_Mo +angstrom_star = K_alpha_W_d_220 * d_220 / 0.2090100 = Å_star +planck_length = (hbar * gravitational_constant / c ** 3) ** 0.5 + +# Mass +metric_ton = 1e3 * kilogram = t = tonne +unified_atomic_mass_unit = atomic_mass_constant = u = amu +dalton = atomic_mass_constant = Da +grain = 64.79891 * milligram = gr +gamma_mass = microgram +carat = 200 * milligram = ct = karat +planck_mass = (hbar * c / gravitational_constant) ** 0.5 + +# Time +minute = 60 * second = min +hour = 60 * minute = hr +day = 24 * hour = d +week = 7 * day +fortnight = 2 * week +year = 365.25 * day = a = yr = julian_year +month = year / 12 +decade = 10 * year +century = 100 * year = = centuries +millennium = 1e3 * year = = millennia +eon = 1e9 * year +shake = 1e-8 * second +svedberg = 1e-13 * second +atomic_unit_of_time = hbar / E_h = a_u_time +gregorian_year = 365.2425 * day +sidereal_year = 365.256363004 * day # approximate, as of J2000 epoch +tropical_year = 365.242190402 * day # approximate, as of J2000 epoch +common_year = 365 * day +leap_year = 366 * day +sidereal_day = day / 1.00273790935079524 # approximate +sidereal_month = 27.32166155 * day # approximate +tropical_month = 27.321582 * day # approximate +synodic_month = 29.530589 * day = = lunar_month # approximate +planck_time = (hbar * gravitational_constant / c ** 5) ** 0.5 + +# Temperature +degC = kelvin; offset: 273.15 = °C = celsius = degree_Celsius = degreeC +degR = 5 / 9 * kelvin; offset: 0 = °R = rankine = degree_Rankine = degreeR +degF = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degree_Fahrenheit = degreeF +degRe = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degree_Reaumur = degreeRe = degree_Réaumur = réaumur +atomic_unit_of_temperature = E_h / k = a_u_temp +planck_temperature = (hbar * c ** 5 / gravitational_constant / k ** 2) ** 0.5 # 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 +are = 100 * meter ** 2 +barn = 1e-28 * meter ** 2 = b +darcy = centipoise * centimeter ** 2 / (second * atmosphere) hectare = 100 * are = ha -# Concentration -[concentration] = [substance] / [volume] -molar = mol / (1e-3 * m ** 3) = M +# Volume +[volume] = [length] ** 3 +liter = decimeter ** 3 = l = L = litre +cubic_centimeter = centimeter ** 3 = cc +lambda = microliter = λ +stere = meter ** 3 -# Activity -[activity] = [substance] / [time] -katal = mole / second = kat +# Frequency +[frequency] = 1 / [time] +hertz = 1 / second = Hz +revolutions_per_minute = revolution / minute = rpm +counts_per_second = count / second = cps -# 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 +# Wavenumber +[wavenumber] = 1 / [length] +reciprocal_centimeter = 1 / cm = cm_1 = kayser -# 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 -calorie = 4.184 * joule = cal = 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 = = Eh = E_h = hartree_energy -toe = 41.868e9 * joule = tonne_of_oil_equivalent +# Velocity +[velocity] = [length] / [time] = [speed] +knot = nautical_mile / hour = kt = knot_international = international_knot +mile_per_hour = mile / hour = mph = MPH +kilometer_per_hour = kilometer / hour = kph = KPH +kilometer_per_second = kilometer / second = kps +meter_per_second = meter / second = mps +foot_per_second = foot / second = fps + +# Acceleration +[acceleration] = [velocity] / [time] +galileo = centimeter / second ** 2 = Gal # Force [force] = [mass] * [acceleration] @@ -130,265 +193,292 @@ 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_metric_ton = g_0 * t = metric_ton_force = force_t = t_force -poundal = lb * feet / second ** 2 = pdl -kip = 1000*lbf +force_metric_ton = g_0 * metric_ton = tf = metric_ton_force = force_t = t_force +atomic_unit_of_force = E_h / a_0 = a_u_force -# Frequency -[frequency] = 1 / [time] -hertz = 1 / second = Hz = rps -revolutions_per_minute = revolution / minute = rpm -counts_per_second = count / second = cps +# Energy +[energy] = [force] * [length] +joule = newton * meter = J +erg = dyne * centimeter +watt_hour = watt * hour = Wh = watthour +electron_volt = e * volt = eV +rydberg = h * c * R_inf = Ry +hartree = 2 * rydberg = E_h = Eh = hartree_energy = atomic_unit_of_energy = a_u_energy +calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th +international_calorie = 4.1868 * joule = cal_it = international_steam_table_calorie +fifteen_degree_calorie = 4.1855 * joule = cal_15 +british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso +international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it +thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th +quadrillion_Btu = 1e15 * Btu = quad +therm = 1e5 * Btu = thm = EC_therm +US_therm = 1.054804e8 * joule # approximate, no exact definition +ton_TNT = 1e9 * calorie = tTNT +tonne_of_oil_equivalent = 1e10 * international_calorie = toe +atmosphere_liter = atmosphere * liter = atm_l -# Heat -#RSI = degK * meter ** 2 / watt -#clo = 0.155 * RSI = clos -#R_value = foot ** 2 * degF * hour / btu +# Power +[power] = [energy] / [time] +watt = joule / second = W +volt_ampere = volt * ampere = VA +horsepower = 550 * foot * force_pound / second = hp = UK_horsepower = hydraulic_horsepower +boiler_horsepower = 33475 * Btu / hour # unclear which Btu +metric_horsepower = 75 * force_kilogram * meter / second +electrical_horsepower = 746 * watt +refrigeration_ton = 12e3 * Btu / hour = = ton_of_refrigeration # approximate, no exact definition +standard_liter_per_minute = atmosphere * liter / minute = slpm = slm +conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 + +# Density (as auxiliary for pressure) +[density] = [mass] / [volume] +mercury = 13.5951 * kilogram / liter = Hg = Hg_0C = Hg_32F = conventional_mercury +water = 1.0 * kilogram / liter = H2O = conventional_water +mercury_60F = 13.5568 * kilogram / liter = Hg_60F # approximate +water_39F = 0.999972 * kilogram / liter = water_4C # approximate +water_60F = 0.999001 * kilogram / liter # approximate -# Information -byte = 8 * bit = B = octet -baud = bit / second = Bd = bps +# Pressure +[pressure] = [force] / [area] +pascal = newton / meter ** 2 = Pa +barye = dyne / centimeter ** 2 = Ba = barie = barad = barrie = baryd +bar = 1e5 * pascal +technical_atmosphere = kilogram * g_0 / centimeter ** 2 = at +torr = atm / 760 +pound_force_per_square_inch = force_pound / inch ** 2 = psi +kip_per_square_inch = kip / inch ** 2 = ksi +millimeter_Hg = millimeter * Hg * g_0 = mmHg = mm_Hg = millimeter_Hg_0C +centimeter_Hg = centimeter * Hg * g_0 = cmHg = cm_Hg = centimeter_Hg_0C +inch_Hg = inch * Hg * g_0 = inHg = in_Hg = inch_Hg_32F +inch_Hg_60F = inch * Hg_60F * g_0 +inch_H2O_39F = inch * water_39F * g_0 +inch_H2O_60F = inch * water_60F * g_0 +foot_H2O = foot * water * g_0 = ftH2O = feet_H2O +centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O + +# Torque +[torque] = [force] * [length] +foot_pound = foot * force_pound = ft_lb = footpound -# Irradiance -peak_sun_hour = 1000 * watt_hour / meter**2 = PSH -langley = thermochemical_calorie / centimeter**2 = Langley +# Viscosity +[viscosity] = [pressure] * [time] +poise = 0.1 * Pa * second = P +reyn = psi * second -# Length -angstrom = 1e-10 * meter = Å = ångström = Å -parsec = 3.08568025e16 * meter = pc -light_year = speed_of_light * julian_year = ly = lightyear -astronomical_unit = 149597870691 * meter = au +# Kinematic viscosity +[kinematic_viscosity] = [area] / [time] +stokes = centimeter ** 2 / second = St -# Mass -carat = 200 * milligram -metric_ton = 1000 * kilogram = t = tonne -atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da -bag = 94 * lb +# Fluidity +[fluidity] = 1 / [viscosity] +rhe = 1 / poise -# Textile -denier = gram / (9000 * meter) -tex = gram / (1000 * meter) -dtex = decitex +# Amount of substance +particle = 1 / N_A = = molec = molecule -# These are Indirect yarn numbering systems (length/unit mass) -jute = lb / (14400 * yd) = Tj -Ne = 1/590.5 / tex -Nm = 1/1000 / tex +# Concentration +[concentration] = [substance] / [volume] +molar = mole / liter = M -@context textile - # Allow switching between Direct count system (i.e. tex) and - # Indirect count system (i.e. Ne, Nm) - [mass] / [length] <-> [length] / [mass]: 1 / value -@end +# Catalytic activity +[activity] = [substance] / [time] +katal = mole / second = kat +enzyme_unit = micromole / minute = U = enzymeunit + +# Entropy +[entropy] = [energy] / [temperature] +clausius = calorie / kelvin = Cl -# Photometry +# Molar entropy +[molar_entropy] = [entropy] / [substance] +entropy_unit = calorie / kelvin / mole = eu + +# Radiation +becquerel = counts_per_second = Bq +curie = 3.7e10 * becquerel = Ci +rutherford = 1e6 * becquerel = Rd +gray = joule / kilogram = Gy +sievert = joule / kilogram = Sv +rads = 0.01 * gray +rem = 0.01 * sievert +roentgen = 2.58e-4 * coulomb / kilogram = = röntgen # approximate, depends on medium + +# Heat transimission +[heat_transmission] = [energy] / [area] +peak_sun_hour = 1e3 * watt_hour / meter ** 2 = PSH +langley = thermochemical_calorie / centimeter ** 2 = Ly + +# Luminance +[luminance] = [luminosity] / [area] +nit = candela / meter ** 2 +stilb = candela / centimeter ** 2 +lambert = 1 / π * candela / centimeter ** 2 + +# Luminous flux +[luminous_flux] = [luminosity] lumen = candela * steradian = lm + +# Illuminance +[illuminance] = [luminous_flux] / [area] 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 +# Current +biot = 10 * ampere = Bi +abampere = biot = abA +atomic_unit_of_current = e / atomic_unit_of_time = a_u_current +mean_international_ampere = mean_international_volt / mean_international_ohm = A_it +US_international_ampere = US_international_volt / US_international_ohm = A_US +conventional_ampere_90 = K_J90 * R_K90 / (K_J * R_K) * ampere = A_90 +planck_current = (c ** 6 / gravitational_constant / k_C) ** 0.5 + +# Charge +[charge] = [current] * [time] +coulomb = ampere * second = C +abcoulomb = 10 * C = abC +faraday = e * N_A * mole +conventional_coulomb_90 = K_J90 * R_K90 / (K_J * R_K) * coulomb = C_90 -# 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 +# Electric potential +[electric_potential] = [energy] / [charge] +volt = joule / coulomb = V +abvolt = 1e-8 * volt = abV +mean_international_volt = 1.00034 * volt = V_it # approximate +US_international_volt = 1.00033 * volt = V_US # approximate +conventional_volt_90 = K_J90 / K_J * volt = V_90 -# Radiation -Bq = Hz = becquerel -curie = 3.7e10 * Bq = Ci -rutherford = 1e6*Bq = Rd -Gy = joule / kilogram = gray = Sv = sievert -rem = 1e-2 * sievert -rads = 1e-2 * gray -roentgen = 2.58e-4 * coulomb / kilogram +# Electric field +[electric_field] = [electric_potential] / [length] -# Temperature -degC = kelvin; offset: 273.15 = celsius -degR = 5 / 9 * kelvin; offset: 0 = rankine -degF = 5 / 9 * kelvin; offset: 255.372222 = fahrenheit +# Electric displacement field +[electric_displacement_field] = [charge] / [area] -# 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 +# Resistance +[resistance] = [electric_potential] / [current] +ohm = volt / ampere = Ω +abohm = 1e-9 * ohm = abΩ +mean_international_ohm = 1.00049 * ohm = Ω_it = ohm_it # approximate +US_international_ohm = 1.000495 * ohm = Ω_US = ohm_US # approximate +conventional_ohm_90 = R_K / R_K90 * ohm = Ω_90 = ohm_90 -# Velocity -[speed] = [length] / [time] -nautical_mile = 1852 m = nmi # exact -knot = nautical_mile / hour = kt = knot_international = international_knot = nautical_miles_per_hour -mph = mile / hour = MPH -kph = kilometer / hour = KPH +# Resistivity +[resistivity] = [resistance] * [length] -# Viscosity -[viscosity] = [pressure] * [time] -poise = 1e-1 * Pa * second = P -stokes = 1e-4 * meter ** 2 / second = St -rhe = 10 / (Pa * s) +# Conductance +[conductance] = [current] / [electric_potential] +siemens = ampere / volt = S = mho +absiemens = 1e9 * siemens = abS = abmho -# Volume -[volume] = [length] ** 3 -liter = 1e-3 * m ** 3 = l = L = litre -cc = centimeter ** 3 = cubic_centimeter -stere = meter ** 3 +# Capacitance +[capacitance] = [charge] / [electric_potential] +farad = coulomb / volt = F +abfarad = 1e9 * farad = abF +conventional_farad_90 = R_K90 / R_K * farad = F_90 +# Inductance +[inductance] = [magnetic_flux] / [current] +henry = weber / ampere = H +abhenry = 1e-9 * henry = abH +conventional_henry_90 = R_K / R_K90 * henry = H_90 -@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 - # allow wavenumber / kayser - 1 / [length] <-> [length]: 1 / value -@end +# Magnetic flux +[magnetic_flux] = [electric_potential] * [time] +weber = volt * second = Wb +unit_pole = µ_0 * biot * centimeter -@context boltzmann - [temperature] -> [energy]: boltzmann_constant * value - [energy] -> [temperature]: value / boltzmann_constant -@end +# Magnetic field +[magnetic_field] = [magnetic_flux] / [area] +tesla = weber / meter ** 2 = T +gamma = 1e-9 * tesla = γ -@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 +# Magnetomotive force +[magnetomotive_force] = [current] +ampere_turn = ampere = At +biot_turn = biot +gilbert = 1 / (4 * π) * biot_turn = Gb - # moles -> mass require the molecular weight - [substance] -> [mass]: value * mw - [mass] -> [substance]: value / mw +# Magnetic field strength +[magnetic_field_strength] = [current] / [length] - # 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 +# Electric dipole moment +[electric_dipole] = [charge] * [length] +debye = 1e-9 / ζ * coulomb * angstrom = D # formally 1 D = 1e-10 Fr*Å, but we generally want to use it outside the Gaussian context - # moles/volume -> moles requires the solution volume - [substance] / [volume] -> [substance]: value * volume - [substance] -> [substance] / [volume]: value / volume +# Electric quadrupole moment +[electric_quadrupole] = [charge] * [area] +buckingham = debye * angstrom - # moles/mass -> moles requires the solvent (usually water) mass - [substance] / [mass] -> [substance]: value * solvent_mass - [substance] -> [substance] / [mass]: value / solvent_mass +# Magnetic dipole moment +[magnetic_dipole] = [current] * [area] +bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B +nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N - # 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 +#### UNIT GROUPS #### +# Mostly for length, area, volume, mass, force +# (customary or specialized units) -# Most of the definitions that follows are derived from: -# See http://www.nist.gov/pml/wmd/pubs/hb44.cfm @group USCSLengthInternational + thou = 1e-3 * inch = th = mil_length inch = yard / 36 = in = international_inch = inches = international_inches + hand = 4 * inch foot = yard / 3 = ft = international_foot = feet = international_feet - yard = 0.9144 metres = yd = international_yard - mile = 1760 yard = mi = international_mile - - square_inch = 1 inch ** 2 = sq_in = square_inches - square_foot = 1 foot ** 2 = sq_ft = square_feet - square_yard = 1 yard ** 2 = sq_yd - square_mile = 1 mile ** 2 = sq_mi - - cubic_inch = 1 in ** 3 = cu_in - cubic_foot = 1 ft ** 3 = cu_ft = cubic_feet - cubic_yard = 1 yd ** 3 = cu_yd + yard = 0.9144 * meter = yd = international_yard # since Jul 1959 + mile = 1760 * yard = mi = international_mile + + circular_mil = π / 4 * mil_length ** 2 = cmil + square_inch = inch ** 2 = sq_in = square_inches + square_foot = foot ** 2 = sq_ft = square_feet + square_yard = yard ** 2 = sq_yd + square_mile = mile ** 2 = sq_mi + + cubic_inch = in ** 3 = cu_in + cubic_foot = ft ** 3 = cu_ft = cubic_feet + cubic_yard = yd ** 3 = cu_yd @end @group USCSLengthSurvey - link = 0.66 survey_foot = li = survey_link - survey_foot = foot / 0.999998 = sft - rod = 16.5 survey_foot = rd = pole = perch - chain = 66 survey_foot - survey_mile = 5280 survey_foot - - acre = 43560 survey_foot ** 2 - square_rod = 1 rod ** 2 = sq_rod = sq_pole = sq_perch - - fathom = 6 survey_foot - us_statute_mile = 5280 survey_foot - league = 3 us_statute_mile - furlong = us_statute_mile / 8 - - acre_foot = acre * survey_foot = acre_feet + link = 1e-2 * chain = li = survey_link + survey_foot = 1200 / 3937 * meter = sft + fathom = 6 * survey_foot + rod = 16.5 * survey_foot = rd = pole = perch + chain = 4 * rod + furlong = 40 * rod = fur + cables_length = 120 * fathom + survey_mile = 5280 * survey_foot = smi = us_statute_mile + league = 3 * survey_mile + + square_rod = rod ** 2 = sq_rod = sq_pole = sq_perch + acre = 10 * chain ** 2 + square_survey_mile = survey_mile ** 2 = = section + square_league = league ** 2 + + acre_foot = acre * survey_foot = = acre_feet @end @group USCSDryVolume - dry_pint = 33.6003125 cubic_inch = dpi = US_dry_pint - dry_quart = 2 dry_pint = dqt = US_dry_quart - dry_gallon = 8 dry_pint = dgal = US_dry_gallon - peck = 16 dry_pint = pk - bushel = 64 dry_pint = bu - dry_barrel = 7056 cubic_inch = US_dry_barrel + dry_pint = bushel / 64 = dpi = US_dry_pint + dry_quart = bushel / 32 = dqt = US_dry_quart + dry_gallon = bushel / 8 = dgal = US_dry_gallon + peck = bushel / 4 = pk + bushel = 2150.42 cubic_inch = bu + dry_barrel = 7056 cubic_inch = = US_dry_barrel @end @group USCSLiquidVolume - minim = liquid_pint / 7680 - fluid_dram = liquid_pint / 128 = fldr = fluidram = US_fluid_dram - fluid_ounce = liquid_pint / 16 = floz = US_fluid_ounce = US_liquid_ounce - gill = liquid_pint / 4 = gi = liquid_gill = US_liquid_gill - - pint = 28.875 cubic_inch = pt = liquid_pint = US_pint - - quart = 2 liquid_pint = qt = liquid_quart = US_liquid_quart - gallon = 8 liquid_pint = gal = liquid_gallon = US_liquid_gallon + minim = pint / 7680 + fluid_dram = pint / 128 = fldr = fluidram = US_fluid_dram = US_liquid_dram + fluid_ounce = pint / 16 = floz = US_fluid_ounce = US_liquid_ounce + gill = pint / 4 = gi = liquid_gill = US_liquid_gill + pint = quart / 2 = pt = liquid_pint = US_pint + fifth = gallon / 5 = = US_liquid_fifth + quart = gallon / 4 = qt = liquid_quart = US_liquid_quart + gallon = 231 * cubic_inch = gal = liquid_gallon = US_liquid_gallon @end @group USCSVolumeOther - teaspoon = tablespoon / 3 = tsp - tablespoon = floz / 2 = tbsp = Tbsp = Tblsp = tblsp = tbs = Tbl + teaspoon = fluid_ounce / 6 = tsp + tablespoon = fluid_ounce / 2 = tbsp shot = 3 * tablespoon = jig = US_shot - cup = 8 fluid_ounce = cp = liquid_cup = US_liquid_cup + cup = pint / 2 = cp = liquid_cup = US_liquid_cup barrel = 31.5 * gallon = bbl oil_barrel = 42 * gallon = oil_bbl beer_barrel = 31 * gallon = beer_bbl @@ -396,83 +486,285 @@ stere = meter ** 3 @end @group Avoirdupois - grain = avdp_pound / 7000 = gr - drachm = pound / 256 = dr = avoirdupois_dram = avdp_dram = dram + dram = pound / 256 = dr = avoirdupois_dram = avdp_dram = drachm ounce = pound / 16 = oz = avoirdupois_ounce = avdp_ounce - pound = 453.59237 gram = lb = avoirdupois_pound = avdp_pound - - short_hundredweight = 100 avoirdupois_pound = ch_cwt - long_hundredweight = 112 avoirdupois_pound = lg_cwt - short_ton = 2000 avoirdupois_pound - long_ton = 2240 avoirdupois_pound - force_short_ton = short_ton * g_0 = short_ton_force - force_long_ton = long_ton * g_0 = long_ton_force -@end - -@group Troy - pennyweight = 24 grain = dwt - troy_ounce = 480 grain = toz - troy_pound = 12 troy_ounce = tlb -@end - -@group Apothecary - scruple = 20 grain - apothecary_dram = 3 scruple = ap_dr - apothecary_ounce = 8 apothecary_dram = ap_oz - apothecary_pound = 12 apothecary_ounce = ap_lb + pound = 7e3 * grain = lb = avoirdupois_pound = avdp_pound + stone = 14 * pound + quarter = 28 * stone + bag = 94 * pound + hundredweight = 100 * pound = cwt = short_hundredweight + long_hundredweight = 112 * pound + ton = 2e3 * pound = = short_ton + long_ton = 2240 * pound + slug = g_0 * pound * second ** 2 / foot + + force_ounce = g_0 * ounce = ozf = ounce_force + force_pound = g_0 * pound = lbf = pound_force + force_ton = g_0 * ton = = ton_force = force_short_ton = short_ton_force + force_long_ton = g_0 * long_ton = = long_ton_force + kip = 1e3 * force_pound + poundal = pound * foot / second ** 2 = pdl @end @group AvoirdupoisUK using Avoirdupois - stone = 14 pound - quarter = 28 stone UK_hundredweight = long_hundredweight = UK_cwt UK_ton = long_ton - UK_ton_force = force_long_ton + UK_force_ton = force_long_ton = = UK_ton_force @end @group AvoirdupoisUS using Avoirdupois - US_hundredweight = short_hundredweight = US_cwt - US_ton = short_ton = ton - US_ton_force = force_short_ton = ton_force = force_ton + US_hundredweight = hundredweight = US_cwt + US_ton = ton + US_force_ton = force_ton = = US_ton_force @end -@group Printer - # Length - pixel = [printing_unit] = dot = px = pel = picture_element - pixels_per_centimeter = pixel / cm = PPCM - pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi - bits_per_pixel = bit / pixel = bpp +@group Troy + pennyweight = 24 * grain = dwt + troy_ounce = 480 * grain = toz = ozt + troy_pound = 12 * troy_ounce = tlb = lbt +@end - point = yard / 216 / 12 = pp = printers_point - thou = yard / 36000 = th = mil - pica = yard / 216 = P̸ = printers_pica +@group Apothecary + scruple = 20 * grain + apothecary_dram = 3 * scruple = ap_dr + apothecary_ounce = 8 * apothecary_dram = ap_oz + apothecary_pound = 12 * apothecary_ounce = ap_lb @end @group ImperialVolume + imperial_minim = imperial_fluid_ounce / 480 + imperial_fluid_scruple = imperial_fluid_ounce / 24 + imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fldr = imperial_fluid_dram imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce - imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fluid_dram imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup - imperial_pint = 568.26125 * milliliter = imperial_pt = UK_pint - imperial_quart = 2 * imperial_pint = imperial_qt = UK_quart - imperial_gallon = 8 * imperial_pint = imperial_gal = UK_gallon - imperial_peck = 16 * imperial_pint = imperial_pk = UK_pk - imperial_bushel = 64 * imperial_pint = imperial_bu = UK_bushel - imperial_barrel = 288 * imperial_pint = imperial_bbl = UK_bbl + imperial_pint = imperial_gallon / 8 = imperial_pt = UK_pint + imperial_quart = imperial_gallon / 4 = imperial_qt = UK_quart + imperial_gallon = 4.54609 * liter = imperial_gal = UK_gallon + imperial_peck = 2 * imperial_gallon = imperial_pk = UK_pk + imperial_bushel = 8 * imperial_gallon = imperial_bu = UK_bushel + imperial_barrel = 36 * imperial_gallon = imperial_bbl = UK_bbl +@end + +@group Printer + pica = inch / 6 = = printers_pica + point = pica / 12 = pp = printers_point = big_point = bp + didot = 1 / 2660 * m + cicero = 12 * didot + tex_point = inch / 72.27 + tex_pica = 12 * tex_point + tex_didot = 1238 / 1157 * tex_point + tex_cicero = 12 * tex_didot + scaled_point = tex_point / 65536 + css_pixel = inch / 96 = px + + pixel = [printing_unit] = = dot = pel = picture_element + pixels_per_centimeter = pixel / cm = PPCM + pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi + bits_per_pixel = bit / pixel = bpp +@end + +@group Textile + tex = gram / kilometer = Tt + dtex = decitex + denier = gram / (9 * kilometer) = Td + jute = pound / (14400 * yard) = Tj + aberdeen = jute = Ta + + number_english = 840 * yard / pound = Ne = NeC = ECC + number_meter = kilometer / kilogram = Nm @end + +#### CGS ELECTROMAGNETIC UNITS #### + +# === Gaussian system of units === +@group Gaussian + franklin = erg ** 0.5 * centimeter ** 0.5 = Fr = statcoulomb = statC = esu + statvolt = erg / franklin = statV + statampere = franklin / second = statA + gauss = dyne / franklin = G + maxwell = gauss * centimeter ** 2 = Mx + oersted = dyne / maxwell = Oe = ørsted + statohm = statvolt / statampere = statΩ + statfarad = franklin / statvolt = statF + statmho = statampere / statvolt +@end +# Note this system is not commensurate with SI, as ε_0 and µ_0 disappear; +# some quantities with different dimensions in SI have the same +# dimensions in the Gaussian system (e.g. [Mx] = [Fr], but [Wb] != [C]), +# and therefore the conversion factors depend on the context (not in pint sense) +[gaussian_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[gaussian_current] = [gaussian_charge] / [time] +[gaussian_electric_potential] = [gaussian_charge] / [length] +[gaussian_electric_field] = [gaussian_electric_potential] / [length] +[gaussian_electric_displacement_field] = [gaussian_charge] / [area] +[gaussian_electric_flux] = [gaussian_charge] +[gaussian_electric_dipole] = [gaussian_charge] * [length] +[gaussian_electric_quadrupole] = [gaussian_charge] * [area] +[gaussian_magnetic_field] = [force] / [gaussian_charge] +[gaussian_magnetic_field_strength] = [gaussian_magnetic_field] +[gaussian_magnetic_flux] = [gaussian_magnetic_field] * [area] +[gaussian_magnetic_dipole] = [energy] / [gaussian_magnetic_field] +[gaussian_resistance] = [gaussian_electric_potential] / [gaussian_current] +[gaussian_resistivity] = [gaussian_resistance] * [length] +[gaussian_capacitance] = [gaussian_charge] / [gaussian_electric_potential] +[gaussian_inductance] = [gaussian_electric_potential] * [time] / [gaussian_current] +[gaussian_conductance] = [gaussian_current] / [gaussian_electric_potential] +@context Gaussian = Gau + [gaussian_charge] -> [charge]: value / k_C ** 0.5 + [charge] -> [gaussian_charge]: value * k_C ** 0.5 + [gaussian_current] -> [current]: value / k_C ** 0.5 + [current] -> [gaussian_current]: value * k_C ** 0.5 + [gaussian_electric_potential] -> [electric_potential]: value * k_C ** 0.5 + [electric_potential] -> [gaussian_electric_potential]: value / k_C ** 0.5 + [gaussian_electric_field] -> [electric_field]: value * k_C ** 0.5 + [electric_field] -> [gaussian_electric_field]: value / k_C ** 0.5 + [gaussian_electric_displacement_field] -> [electric_displacement_field]: value / (4 * π / ε_0) ** 0.5 + [electric_displacement_field] -> [gaussian_electric_displacement_field]: value * (4 * π / ε_0) ** 0.5 + [gaussian_electric_dipole] -> [electric_dipole]: value / k_C ** 0.5 + [electric_dipole] -> [gaussian_electric_dipole]: value * k_C ** 0.5 + [gaussian_electric_quadrupole] -> [electric_quadrupole]: value / k_C ** 0.5 + [electric_quadrupole] -> [gaussian_electric_quadrupole]: value * k_C ** 0.5 + [gaussian_magnetic_field] -> [magnetic_field]: value / (4 * π / µ_0) ** 0.5 + [magnetic_field] -> [gaussian_magnetic_field]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_flux] -> [magnetic_flux]: value / (4 * π / µ_0) ** 0.5 + [magnetic_flux] -> [gaussian_magnetic_flux]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π * µ_0) ** 0.5 + [magnetic_field_strength] -> [gaussian_magnetic_field_strength]: value * (4 * π * µ_0) ** 0.5 + [gaussian_magnetic_dipole] -> [magnetic_dipole]: value * (4 * π / µ_0) ** 0.5 + [magnetic_dipole] -> [gaussian_magnetic_dipole]: value / (4 * π / µ_0) ** 0.5 + [gaussian_resistance] -> [resistance]: value * k_C + [resistance] -> [gaussian_resistance]: value / k_C + [gaussian_resistivity] -> [resistivity]: value * k_C + [resistivity] -> [gaussian_resistivity]: value / k_C + [gaussian_capacitance] -> [capacitance]: value / k_C + [capacitance] -> [gaussian_capacitance]: value * k_C + [gaussian_inductance] -> [inductance]: value * k_C + [inductance] -> [gaussian_inductance]: value / k_C + [gaussian_conductance] -> [conductance]: value / k_C + [conductance] -> [gaussian_conductance]: value * k_C +@end + +# === ESU system of units === +# (where different from Gaussian) +# See note for Gaussian system too +@group ESU using Gaussian + statweber = statvolt * second = statWb + stattesla = statweber / centimeter ** 2 = statT + stathenry = statweber / statampere = statH +@end +[esu_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[esu_current] = [esu_charge] / [time] +[esu_electric_potential] = [esu_charge] / [length] +[esu_magnetic_flux] = [esu_electric_potential] * [time] +[esu_magnetic_field] = [esu_magnetic_flux] / [area] +[esu_magnetic_field_strength] = [esu_current] / [length] +[esu_magnetic_dipole] = [esu_current] * [area] +@context ESU = esu + [esu_magnetic_field] -> [magnetic_field]: value * k_C ** 0.5 + [magnetic_field] -> [esu_magnetic_field]: value / k_C ** 0.5 + [esu_magnetic_flux] -> [magnetic_flux]: value * k_C ** 0.5 + [magnetic_flux] -> [esu_magnetic_flux]: value / k_C ** 0.5 + [esu_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π / ε_0) ** 0.5 + [magnetic_field_strength] -> [esu_magnetic_field_strength]: value * (4 * π / ε_0) ** 0.5 + [esu_magnetic_dipole] -> [magnetic_dipole]: value / k_C ** 0.5 + [magnetic_dipole] -> [esu_magnetic_dipole]: value * k_C ** 0.5 +@end + + +#### CONVERSION CONTEXTS #### + +@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 + # allow wavenumber / kayser + [wavenumber] <-> [length]: 1 / value +@end + +@context boltzmann + [temperature] -> [energy]: boltzmann_constant * value + [energy] -> [temperature]: value / boltzmann_constant +@end + +@context energy + [energy] -> [energy] / [substance]: value * N_A + [energy] / [substance] -> [energy]: value / N_A + [energy] -> [mass]: value / c ** 2 + [mass] -> [energy]: value * c ** 2 +@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 + +@context textile + # Allow switching between Direct count system (i.e. tex) and + # Indirect count system (i.e. Ne, Nm) + [mass] / [length] <-> [length] / [mass]: 1 / value +@end + + +#### SYSTEMS OF UNITS #### + @system mks using international meter kilogram second @end -@system cgs using international +@system cgs using international, Gaussian, ESU centimeter gram second @end +@system atomic using international + # based on unit m_e, e, hbar, k_C, k + bohr: meter + electron_mass: gram + atomic_unit_of_time: second + atomic_unit_of_current: ampere + atomic_unit_of_temperature: kelvin +@end + +@system Planck using international + # based on unit c, gravitational_constant, hbar, k_C, k + planck_length: meter + planck_mass: gram + planck_time: second + planck_current: ampere + planck_temperature: kelvin +@end + @system imperial using ImperialVolume, USCSLengthInternational, AvoirdupoisUK yard pound diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index c3fcb311e..7b5b3ccd6 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -43,7 +43,7 @@ def test_issue29(self): t = 4 * ureg('mM') self.assertEqual(t.magnitude, 4) self.assertEqual(t._units, UnitsContainer(millimolar=1)) - self.assertEqual(t.to('mole / liter'), 4e-3 * ureg('M')) + self.assertAlmostEqual(t.to('mole / liter'), 4e-3 * ureg('M')) def test_issue52(self): u1 = UnitRegistry() @@ -127,7 +127,7 @@ def test_issue85(self): except: self.assertTrue(False, 'Error while trying to get base units for {}'.format(va)) - boltmk = 1.3806488e-23*ureg.J/ureg.K + boltmk = 1.380649e-23*ureg.J/ureg.K vb = 2. * boltmk * T / m self.assertQuantityAlmostEqual(va.to_base_units(), vb.to_base_units()) From 7a57f5ccfdb0b628a29e44529dfad9e5920a38c8 Mon Sep 17 00:00:00 2001 From: Jellby Date: Sat, 25 May 2019 09:00:03 +0200 Subject: [PATCH 033/612] Some updates Add _ for no-symbol, update degrees, remove duplicates in xtranslated.txt, change test from M to W. --- pint/constants_en.txt | 2 +- pint/default_en.txt | 56 +++++++++++++++++++++-------------- pint/testsuite/test_issues.py | 6 ++-- pint/xtranslated.txt | 10 +------ 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index 673a21b18..47a835e86 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -50,7 +50,7 @@ wien_frequency_displacement_law_constant = wien_u * k / h # The choice of measured constants is based on convenience and on available uncertainty. # The uncertainty in the last significant digits is given in parentheses as a comment. -newtonian_constant_of_gravitation = 6.67408e-11 m^3/(kg s^2) = = gravitational_constant # (15) +newtonian_constant_of_gravitation = 6.67408e-11 m^3/(kg s^2) = _ = gravitational_constant # (15) rydberg_constant = 1.0973731568160e7 * m^-1 = R_∞ = R_inf # (21) electron_g_factor = -2.00231930436256 = g_e # (35) atomic_mass_constant = 1.66053906660e-27 kg = m_u # (50) diff --git a/pint/default_en.txt b/pint/default_en.txt index 175bc8734..722ee3630 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -44,7 +44,7 @@ zebi- = 2**70 = Zi- yobi- = 2**80 = Yi- # extra_prefixes -semi- = 0.5 = = demi- +semi- = 0.5 = _ = demi- sesqui- = 1.5 @@ -74,7 +74,7 @@ count = [] # although floating-point conversion may introduce inaccuracies # Angle -turn = 2 * π * radian = = revolution = cycle = circle +turn = 2 * π * radian = _ = revolution = cycle = circle degree = π / 180 * radian = deg = arcdeg = arcdegree = angular_degree arcminute = degree / 60 = arcmin = arc_minute = angular_minute arcsecond = arcminute / 60 = arcsec = arc_second = angular_second @@ -125,8 +125,8 @@ fortnight = 2 * week year = 365.25 * day = a = yr = julian_year month = year / 12 decade = 10 * year -century = 100 * year = = centuries -millennium = 1e3 * year = = millennia +century = 100 * year = _ = centuries +millennium = 1e3 * year = _ = millennia eon = 1e9 * year shake = 1e-8 * second svedberg = 1e-13 * second @@ -139,14 +139,14 @@ leap_year = 366 * day sidereal_day = day / 1.00273790935079524 # approximate sidereal_month = 27.32166155 * day # approximate tropical_month = 27.321582 * day # approximate -synodic_month = 29.530589 * day = = lunar_month # approximate +synodic_month = 29.530589 * day = _ = lunar_month # approximate planck_time = (hbar * gravitational_constant / c ** 5) ** 0.5 # Temperature -degC = kelvin; offset: 273.15 = °C = celsius = degree_Celsius = degreeC -degR = 5 / 9 * kelvin; offset: 0 = °R = rankine = degree_Rankine = degreeR -degF = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degree_Fahrenheit = degreeF -degRe = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degree_Reaumur = degreeRe = degree_Réaumur = réaumur +degree_Celsius = kelvin; offset: 273.15 = °C = celsius = degC = degreeC +degree_Rankine = 5 / 9 * kelvin; offset: 0 = °R = rankine = degR = degreeR +degree_Fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degF = degreeF +degree_Reaumur = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degRe = degreeRe = degree_Réaumur = réaumur atomic_unit_of_temperature = E_h / k = a_u_temp planck_temperature = (hbar * c ** 5 / gravitational_constant / k ** 2) ** 0.5 @@ -225,7 +225,7 @@ horsepower = 550 * foot * force_pound / second = hp = UK_horsepower = hydraulic_ boiler_horsepower = 33475 * Btu / hour # unclear which Btu metric_horsepower = 75 * force_kilogram * meter / second electrical_horsepower = 746 * watt -refrigeration_ton = 12e3 * Btu / hour = = ton_of_refrigeration # approximate, no exact definition +refrigeration_ton = 12e3 * Btu / hour = _ = ton_of_refrigeration # approximate, no exact definition standard_liter_per_minute = atmosphere * liter / minute = slpm = slm conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 @@ -273,7 +273,7 @@ stokes = centimeter ** 2 / second = St rhe = 1 / poise # Amount of substance -particle = 1 / N_A = = molec = molecule +particle = 1 / N_A = _ = molec = molecule # Concentration [concentration] = [substance] / [volume] @@ -300,7 +300,7 @@ gray = joule / kilogram = Gy sievert = joule / kilogram = Sv rads = 0.01 * gray rem = 0.01 * sievert -roentgen = 2.58e-4 * coulomb / kilogram = = röntgen # approximate, depends on medium +roentgen = 2.58e-4 * coulomb / kilogram = _ = röntgen # approximate, depends on medium # Heat transimission [heat_transmission] = [energy] / [area] @@ -448,10 +448,10 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N square_rod = rod ** 2 = sq_rod = sq_pole = sq_perch acre = 10 * chain ** 2 - square_survey_mile = survey_mile ** 2 = = section + square_survey_mile = survey_mile ** 2 = _ = section square_league = league ** 2 - acre_foot = acre * survey_foot = = acre_feet + acre_foot = acre * survey_foot = _ = acre_feet @end @group USCSDryVolume @@ -460,7 +460,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N dry_gallon = bushel / 8 = dgal = US_dry_gallon peck = bushel / 4 = pk bushel = 2150.42 cubic_inch = bu - dry_barrel = 7056 cubic_inch = = US_dry_barrel + dry_barrel = 7056 cubic_inch = _ = US_dry_barrel @end @group USCSLiquidVolume @@ -469,7 +469,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N fluid_ounce = pint / 16 = floz = US_fluid_ounce = US_liquid_ounce gill = pint / 4 = gi = liquid_gill = US_liquid_gill pint = quart / 2 = pt = liquid_pint = US_pint - fifth = gallon / 5 = = US_liquid_fifth + fifth = gallon / 5 = _ = US_liquid_fifth quart = gallon / 4 = qt = liquid_quart = US_liquid_quart gallon = 231 * cubic_inch = gal = liquid_gallon = US_liquid_gallon @end @@ -494,14 +494,14 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N bag = 94 * pound hundredweight = 100 * pound = cwt = short_hundredweight long_hundredweight = 112 * pound - ton = 2e3 * pound = = short_ton + ton = 2e3 * pound = _ = short_ton long_ton = 2240 * pound slug = g_0 * pound * second ** 2 / foot force_ounce = g_0 * ounce = ozf = ounce_force force_pound = g_0 * pound = lbf = pound_force - force_ton = g_0 * ton = = ton_force = force_short_ton = short_ton_force - force_long_ton = g_0 * long_ton = = long_ton_force + force_ton = g_0 * ton = _ = ton_force = force_short_ton = short_ton_force + force_long_ton = g_0 * long_ton = _ = long_ton_force kip = 1e3 * force_pound poundal = pound * foot / second ** 2 = pdl @end @@ -509,13 +509,13 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N @group AvoirdupoisUK using Avoirdupois UK_hundredweight = long_hundredweight = UK_cwt UK_ton = long_ton - UK_force_ton = force_long_ton = = UK_ton_force + UK_force_ton = force_long_ton = _ = UK_ton_force @end @group AvoirdupoisUS using Avoirdupois US_hundredweight = hundredweight = US_cwt US_ton = ton - US_force_ton = force_ton = = US_ton_force + US_force_ton = force_ton = _ = US_ton_force @end @group Troy @@ -547,7 +547,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N @end @group Printer - pica = inch / 6 = = printers_pica + pica = inch / 6 = _ = printers_pica point = pica / 12 = pp = printers_point = big_point = bp didot = 1 / 2660 * m cicero = 12 * didot @@ -558,7 +558,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N scaled_point = tex_point / 65536 css_pixel = inch / 96 = px - pixel = [printing_unit] = = dot = pel = picture_element + pixel = [printing_unit] = _ = dot = pel = picture_element pixels_per_centimeter = pixel / cm = PPCM pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi bits_per_pixel = bit / pixel = bpp @@ -735,6 +735,16 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N #### SYSTEMS OF UNITS #### +@system SI + second + meter + kilogram + ampere + kelvin + mole + candela +@end + @system mks using international meter kilogram diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 7b5b3ccd6..bb79699ce 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -40,10 +40,10 @@ def test_issue25(self): def test_issue29(self): ureg = UnitRegistry() - t = 4 * ureg('mM') + t = 4 * ureg('mW') self.assertEqual(t.magnitude, 4) - self.assertEqual(t._units, UnitsContainer(millimolar=1)) - self.assertAlmostEqual(t.to('mole / liter'), 4e-3 * ureg('M')) + self.assertEqual(t._units, UnitsContainer(milliwatt=1)) + self.assertEqual(t.to('joule / second'), 4e-3 * ureg('W')) def test_issue52(self): u1 = UnitRegistry() diff --git a/pint/xtranslated.txt b/pint/xtranslated.txt index 3ccfd9de8..3c4d03252 100644 --- a/pint/xtranslated.txt +++ b/pint/xtranslated.txt @@ -1,22 +1,14 @@ # a few unit definitions added to use the translations by unicode cldr -dietary_calorie = 1000 * calorie = Calorie +dietary_calorie = 1000 * calorie = Cal = Calorie metric_cup = liter / 4 -mps = meter / second -square_inch = inch ** 2 = sq_in -square_mile = mile ** 2 = sq_mile square_meter = kilometer ** 2 = sq_m square_kilometer = kilometer ** 2 = sq_km mile_scandinavian = 10000 * meter -century = 100 * year cubic_mile = 1 * mile ** 3 = cu_mile = cubic_miles -cubic_yard = 1 * yard ** 3 = cu_yd = cubic_yards -cubic_foot = 1 * foot ** 3 = cu_ft = cubic_feet -cubic_inch = 1 * inch ** 3 = cu_in = cubic_inches cubic_meter = 1 * meter ** 3 = cu_m cubic_kilometer = 1 * kilometer ** 3 = cu_km -karat = [purity] = Karat [consumption] = [volume] / [length] liter_per_kilometer = liter / kilometer From 620e5d1de8be5b03ff1d33673325b213fc65da98 Mon Sep 17 00:00:00 2001 From: Jellby Date: Sat, 25 May 2019 17:26:01 +0200 Subject: [PATCH 034/612] Fix mistake in d_220 constant --- pint/constants_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index 47a835e86..a1d22d4d4 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -57,7 +57,7 @@ atomic_mass_constant = 1.66053906660e-27 kg = m_u electron_mass = 9.1093837015e-31 kg = m_e = atomic_unit_of_mass = a_u_mass # (28) proton_mass = 1.67262192369e-27 kg = m_p # (51) neutron_mass = 1.67492749804e-27 kg = m_n # (95) -lattice_spacing_of_Si = 1.920155716 m = d_220 # (32) +lattice_spacing_of_Si = 1.920155716e-10 m = d_220 # (32) K_alpha_Cu_d_220 = 0.80232719 # (22) K_alpha_Mo_d_220 = 0.36940604 # (19) K_alpha_W_d_220 = 0.108852175 # (98) From 8011ad97934a41fb6a1b8291b438940185e1e181 Mon Sep 17 00:00:00 2001 From: A Brooks Date: Fri, 31 May 2019 12:38:53 -0500 Subject: [PATCH 035/612] Add similar library and remove duplicate. --- docs/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index e92a630b9..dbc7f0edf 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -31,13 +31,13 @@ You mention other similar Python libraries. Can you point me to those? `SymPy `_ -`Units `_ - `cf units `_ `astropy units `_ `yt `_ +`measurement `_ + If your are aware of another one, please contribute a patch to the docs. From 9614ec6ba0e7e9f32b1d7f70fb0f8e4b0a0820ee Mon Sep 17 00:00:00 2001 From: A Brooks Date: Fri, 31 May 2019 12:40:46 -0500 Subject: [PATCH 036/612] Fix a typo, too. --- docs/faq.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index dbc7f0edf..d16051850 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -39,5 +39,4 @@ You mention other similar Python libraries. Can you point me to those? `measurement `_ - -If your are aware of another one, please contribute a patch to the docs. +If you're aware of another one, please contribute a patch to the docs. From 6765c2f662bd84e9a5669abe79b2db989187be28 Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Wed, 19 Jun 2019 16:14:38 +0200 Subject: [PATCH 037/612] Updating PR #802 to the status of default_en.txt as of 7a57f5c Added 'den' as 'denier' abbreviation. --- pint/default_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index 722ee3630..cfc43f808 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -567,7 +567,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N @group Textile tex = gram / kilometer = Tt dtex = decitex - denier = gram / (9 * kilometer) = Td + denier = gram / (9 * kilometer) = den = Td jute = pound / (14400 * yard) = Tj aberdeen = jute = Ta From aed701c9c3ef4c59a498cee477c02599eba3a8c1 Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Wed, 19 Jun 2019 16:24:10 +0200 Subject: [PATCH 038/612] Added RKM unit (used in textile industry) --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index cfc43f808..268a1a174 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -570,6 +570,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N denier = gram / (9 * kilometer) = den = Td jute = pound / (14400 * yard) = Tj aberdeen = jute = Ta + RKM = kgf * 1000 / tex number_english = 840 * yard / pound = Ne = NeC = ECC number_meter = kilometer / kilogram = Nm From 9cf83be59781b1a0ea08a958c1dbf4282e70b65c Mon Sep 17 00:00:00 2001 From: Jellby Date: Fri, 28 Jun 2019 22:17:40 +0200 Subject: [PATCH 039/612] Fix gravitational constant --- pint/constants_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index a1d22d4d4..a7c22fcaa 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -50,7 +50,7 @@ wien_frequency_displacement_law_constant = wien_u * k / h # The choice of measured constants is based on convenience and on available uncertainty. # The uncertainty in the last significant digits is given in parentheses as a comment. -newtonian_constant_of_gravitation = 6.67408e-11 m^3/(kg s^2) = _ = gravitational_constant # (15) +newtonian_constant_of_gravitation = 6.67430e-11 m^3/(kg s^2) = _ = gravitational_constant # (15) rydberg_constant = 1.0973731568160e7 * m^-1 = R_∞ = R_inf # (21) electron_g_factor = -2.00231930436256 = g_e # (35) atomic_mass_constant = 1.66053906660e-27 kg = m_u # (50) From 6c880bb8360a3792e0f163c9e2f2f6b2f427c866 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Sat, 29 Jun 2019 11:32:50 +0100 Subject: [PATCH 040/612] allow np arrays of scalar quantities to be plotted --- pint/matplotlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index bda251d46..0d28d433d 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -31,7 +31,7 @@ def __init__(self, registry): def convert(self, value, unit, axis): """Convert :`Quantity` instances for matplotlib to use.""" - if isinstance(value, (tuple, list)): + if hasattr(value,"__iter__"): return [self._convert_value(v, unit, axis) for v in value] else: return self._convert_value(value, unit, axis) @@ -51,6 +51,8 @@ def axisinfo(unit, axis): @staticmethod def default_units(x, axis): """Get the default unit to use for the given combination of unit and axis.""" + if hasattr(x,"__iter__") and len(x) > 0: + return getattr(x[0], 'units', None) return getattr(x, 'units', None) From f97ad9eff5424d8b46e4b4cb18a709a334547bc8 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 6 Jul 2019 21:19:37 -0400 Subject: [PATCH 041/612] Correct PDF link to MCO report The FTP link was dead. This link appears to be from another canonical NASA source, and is the same link used on the Wikipedia page for the MCO. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 4f796b015..eb2ab91c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -150,7 +150,7 @@ One last thing The MCO MIB has determined that the root cause for the loss of the MCO spacecraft was the failure to use metric units in the coding of a ground software file, “Small Forces,” used in trajectory models. Specifically, thruster performance data in English units instead of metric units was used in the software application code titled SM_FORCES (small forces). The output from the SM_FORCES application code as required by a MSOP Project Software Interface Specification (SIS) was to be in metric units of Newtonseconds (N-s). Instead, the data was reported in English units of pound-seconds (lbf-s). The Angular Momentum Desaturation (AMD) file contained the output data from the SM_FORCES software. The SIS, which was not followed, defines both the format and units of the AMD file generated by ground-based computers. Subsequent processing of the data from AMD file by the navigation software algorithm therefore, underestimated the effect on the spacecraft trajectory by a factor of 4.45, which is the required conversion factor from force in pounds to Newtons. An erroneous trajectory was computed using this incorrect data. `Mars Climate Orbiter Mishap Investigation Phase I Report` - `PDF `_ + `PDF `_ From 44a8c5a97e18665214dca37b9be876f85927e93c Mon Sep 17 00:00:00 2001 From: Jellby Date: Sun, 7 Jul 2019 11:58:15 +0200 Subject: [PATCH 042/612] Add free conda channel for testing --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a759926d6..033439fe6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ env: - UNCERTAINTIES="N" PYTHON="3.5" NUMPY_VERSION=0 PANDAS=0 # Test with the latest numpy version - UNCERTAINTIES="N" PYTHON="2.7" NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.4" NUMPY_VERSION=1.14 PANDAS=0 + #- UNCERTAINTIES="N" PYTHON="3.4" NUMPY_VERSION=1.14 PANDAS=0 - UNCERTAINTIES="N" PYTHON="3.5" NUMPY_VERSION=1.14 PANDAS=0 - UNCERTAINTIES="Y" PYTHON="3.5" NUMPY_VERSION=1.14 PANDAS=0 - UNCERTAINTIES="N" PYTHON="3.6" NUMPY_VERSION=1.14 PANDAS=0 @@ -35,6 +35,9 @@ before_install: - hash -r - conda config --set always_yes yes --set changeps1 no - conda update -q conda + - if [[ "$PYTHON" != "2.7" ]]; then + conda config --set restore_free_channel yes; + fi # Useful for debugging any issues with conda - conda info -a From 3fa6a2f40be1acedcca759167b5ec39d5ef1c722 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Thu, 11 Jul 2019 09:39:54 +0100 Subject: [PATCH 043/612] Update wrapping.rst Lowercase `UREG` to `ureg` --- docs/wrapping.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 31eb10bdc..84cfc8a55 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -144,7 +144,7 @@ the extra outputs. For example, given the NREL SOLPOS calculator that outputs solar zenith, azimuth and air mass, the following wrapper assumes no units for airmass:: - @UREG.wraps(('deg', 'deg'), ('deg', 'deg', 'millibar', 'degC')) + @ureg.wraps(('deg', 'deg'), ('deg', 'deg', 'millibar', 'degC')) def solar_position(lat, lon, press, tamb, timestamp): return zenith, azimuth, airmass From 25734d75e807024b3315e4d17734a6cdb37a77e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Sigmundson=20Sch=C3=B8yen?= Date: Thu, 18 Jul 2019 23:02:15 +0200 Subject: [PATCH 044/612] Add atomic units for intensity and electric field --- pint/default_en.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index 722ee3630..55daf6b5e 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -321,6 +321,10 @@ lumen = candela * steradian = lm [illuminance] = [luminous_flux] / [area] lux = lumen / meter ** 2 = lx +# Intensity +[intensity] = [power] / [area] +atomic_unit_of_intensity = 0.5 * ε_0 * c * atomic_unit_of_electric_field ** 2 = a_u_intensity + # Current biot = 10 * ampere = Bi abampere = biot = abA @@ -347,6 +351,7 @@ conventional_volt_90 = K_J90 / K_J * volt = V_90 # Electric field [electric_field] = [electric_potential] / [length] +atomic_unit_of_electric_field = e * k_C / a_0 ** 2 = a_u_electric_field # Electric displacement field [electric_displacement_field] = [charge] / [area] From 61b017f9b4d1d3bf93c8a70a7bf72693a4c6ebe5 Mon Sep 17 00:00:00 2001 From: Lars Schellhas Date: Mon, 22 Jul 2019 20:14:46 +0200 Subject: [PATCH 045/612] added automatic developer reference using sphinx --- docs/developers_reference.rst | 120 ++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 121 insertions(+) create mode 100644 docs/developers_reference.rst diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst new file mode 100644 index 000000000..9e318bbcf --- /dev/null +++ b/docs/developers_reference.rst @@ -0,0 +1,120 @@ +=================== +Developer reference +=================== + +Pint +==== + +.. automodule:: pint.babel_names + :members: + +.. automodule:: pint.context + :members: + +.. automodule:: pint.converters + :members: + +.. automodule:: pint.definitions + :members: + +.. automodule:: pint.errors + :members: + +.. automodule:: pint.formatting + :members: + +.. automodule:: pint.matplotlib + :members: + +.. automodule:: pint.measurement + :members: + +.. automodule:: pint.pint_eval + :members: + +.. automodule:: pint.quantity + :members: + +.. automodule:: pint.registry + :members: + +.. automodule:: pint.registry_helpers + :members: + +.. automodule:: pint.systems + :members: + +.. automodule:: pint.unit + :members: + +.. automodule:: pint.util + :members: + +.. automodule:: pint.compat.chainmap + :members: + +.. automodule:: pint.compat.lrucache + :members: + +.. automodule:: pint.compat.meta + :members: + +.. automodule:: pint.compat.tokenize + :members: + +.. automodule:: pint.testsuite.helpers + :members: + +.. automodule:: pint.testsuite.parametrized + :members: + +.. automodule:: pint.testsuite.test_babel + :members: + +.. automodule:: pint.testsuite.test_contexts + :members: + +.. automodule:: pint.testsuite.test_converters + :members: + +.. automodule:: pint.testsuite.test_definitions + :members: + +.. automodule:: pint.testsuite.test_errors + :members: + +.. automodule:: pint.testsuite.test_formatter + :members: + +.. automodule:: pint.testsuite.test_infer_base_unit + :members: + +.. automodule:: pint.testsuite.test_issues + :members: + +.. automodule:: pint.testsuite.test_measurements + :members: + +.. automodule:: pint.testsuite.test_numpy + :members: + +.. automodule:: pint.testsuite.test_pint_eval + :members: + +.. automodule:: pint.testsuite.test_pitheorem + :members: + +.. automodule:: pint.testsuite.test_quantity + :members: + +.. automodule:: pint.testsuite.test_systems + :members: + +.. automodule:: pint.testsuite.test_umath + :members: + +.. automodule:: pint.testsuite.test_unit + :members: + +.. automodule:: pint.testsuite.test_util + :members: \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index eb2ab91c0..b6eac5dba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -138,6 +138,7 @@ More information .. toctree:: :maxdepth: 1 + developers_reference contributing faq From 624adf7167fa7c74e202661ae7e791da446164fc Mon Sep 17 00:00:00 2001 From: good597 Date: Wed, 24 Jul 2019 16:41:21 -0700 Subject: [PATCH 046/612] Updated documentation for string formatting with info on abbreviated units and scientific notation - also updated the abbreviation for becquerel from 'becquerel' to 'Bq' --- docs/tutorial.rst | 24 +- pint/default_en.txt | 934 ++++++++++++++------------------------------ 2 files changed, 317 insertions(+), 641 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b11e608e8..9e5d381a9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -292,17 +292,6 @@ Pint's physical quantities can be easily printed: >>> print('The magnitude is {0.magnitude} with units {0.units}'.format(accel)) The magnitude is 1.3 with units meter / second ** 2 -Pint supports float formatting for numpy arrays as well: - -.. doctest:: - - >>> accel = np.array([-1.1, 1e-6, 1.2505, 1.3]) * ureg['meter/second**2'] - >>> # float formatting numpy arrays - >>> print('The array is {:.2f}'.format(accel)) - The array is [-1.10 0.00 1.25 1.30] meter / second ** 2 - >>> # scientific form formatting with unit pretty printing - >>> print('The array is {:+.2E~P}'.format(accel)) - The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: @@ -336,6 +325,18 @@ If you want to use abbreviated unit names, prefix the specification with `~`: The same is true for latex (`L`) and HTML (`H`) specs. +.. note:: + The abbreviated unit is drawn from the unit registry where the 3rd item in the + equivalence chain (ie 1 = 2 = **3**) will be returned when the prefix '~' is + used. The 1st item in the chain is the canonical name of the unit. + +The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatting +syntax'_ for custom float representations. For example, scientific notation: + +..doctest:: + >>> 'Scientific notation: {:.3e~L}'.format(accel) + 'Scientific notation: 1.300\\times 10^{0}\\ \\frac{\\mathrm{m}}{\\mathrm{s}^{2}}' + Pint also supports the LaTeX siunitx package: .. doctest:: @@ -409,3 +410,4 @@ also define the registry as the application registry:: .. _eval: http://docs.python.org/3/library/functions.html#eval .. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html .. _`Babel`: http://babel.pocoo.org/ +.. _'formatting syntax': https://docs.python.org/3/library/string.html#format-specification-mini-language \ No newline at end of file diff --git a/pint/default_en.txt b/pint/default_en.txt index 55daf6b5e..f54650901 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -1,16 +1,13 @@ # Default Pint units definition file # Based on the International System of Units # Language: english -# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. +# :copyright: 2013 by Pint Authors, see AUTHORS for more details. @defaults group = international system = mks @end - -#### PREFIXES #### - # decimal prefixes yocto- = 1e-24 = y- zepto- = 1e-21 = z- @@ -18,11 +15,11 @@ atto- = 1e-18 = a- femto- = 1e-15 = f- pico- = 1e-12 = p- nano- = 1e-9 = n- -micro- = 1e-6 = µ- = u- +micro- = 1e-6 = u- = µ- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- -deca- = 1e+1 = da- = deka- +deca- = 1e+1 = da- = deka hecto- = 1e2 = h- kilo- = 1e3 = k- mega- = 1e6 = M- @@ -43,149 +40,85 @@ exbi- = 2**60 = Ei- zebi- = 2**70 = Zi- yobi- = 2**80 = Yi- -# extra_prefixes -semi- = 0.5 = _ = demi- -sesqui- = 1.5 - - -#### BASE UNITS #### - +# 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 = °K = degree_Kelvin = degreeK # older names supported for compatibility +kelvin = [temperature]; offset: 0 = K = degK radian = [] = rad -neper = [] = Np bit = [] count = [] - -#### CONSTANTS #### - @import constants_en.txt - -#### UNITS #### -# Common and less common, grouped by quantity. -# Conversion factors are exact (except when noted), -# although floating-point conversion may introduce inaccuracies +# acceleration +[acceleration] = [length] / [time] ** 2 # Angle -turn = 2 * π * radian = _ = revolution = cycle = circle -degree = π / 180 * radian = deg = arcdeg = arcdegree = angular_degree -arcminute = degree / 60 = arcmin = arc_minute = angular_minute -arcsecond = arcminute / 60 = arcsec = arc_second = angular_second -milliarcsecond = 1e-3 * arcsecond = mas -grade = π / 200 * radian = grad = gon -mil = π / 32000 * radian - -# Solid 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 -square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg - -# Logarithmic ratio -bel = 0.5 * ln10 * neper - -# Information -byte = 8 * bit = B = octet -baud = bit / second = Bd = bps - -# Length -angstrom = 1e-10 * meter = Å = ångström = Å -micron = micrometer = µ -fermi = femtometer = fm -light_year = speed_of_light * julian_year = ly = lightyear -astronomical_unit = 149597870700 * meter = au # since Aug 2012 -parsec = 1 / tansec * astronomical_unit = pc -nautical_mile = 1852 * meter = nmi -bohr = hbar / (alpha * m_e * c) = a_0 = a0 = bohr_radius = atomic_unit_of_length = a_u_length -x_unit_Cu = K_alpha_Cu_d_220 * d_220 / 1537.4 = Xu_Cu -x_unit_Mo = K_alpha_Mo_d_220 * d_220 / 707.831 = Xu_Mo -angstrom_star = K_alpha_W_d_220 * d_220 / 0.2090100 = Å_star -planck_length = (hbar * gravitational_constant / c ** 3) ** 0.5 - -# Mass -metric_ton = 1e3 * kilogram = t = tonne -unified_atomic_mass_unit = atomic_mass_constant = u = amu -dalton = atomic_mass_constant = Da -grain = 64.79891 * milligram = gr -gamma_mass = microgram -carat = 200 * milligram = ct = karat -planck_mass = (hbar * c / gravitational_constant) ** 0.5 - -# Time -minute = 60 * second = min -hour = 60 * minute = hr -day = 24 * hour = d -week = 7 * day -fortnight = 2 * week -year = 365.25 * day = a = yr = julian_year -month = year / 12 -decade = 10 * year -century = 100 * year = _ = centuries -millennium = 1e3 * year = _ = millennia -eon = 1e9 * year -shake = 1e-8 * second -svedberg = 1e-13 * second -atomic_unit_of_time = hbar / E_h = a_u_time -gregorian_year = 365.2425 * day -sidereal_year = 365.256363004 * day # approximate, as of J2000 epoch -tropical_year = 365.242190402 * day # approximate, as of J2000 epoch -common_year = 365 * day -leap_year = 366 * day -sidereal_day = day / 1.00273790935079524 # approximate -sidereal_month = 27.32166155 * day # approximate -tropical_month = 27.321582 * day # approximate -synodic_month = 29.530589 * day = _ = lunar_month # approximate -planck_time = (hbar * gravitational_constant / c ** 5) ** 0.5 - -# Temperature -degree_Celsius = kelvin; offset: 273.15 = °C = celsius = degC = degreeC -degree_Rankine = 5 / 9 * kelvin; offset: 0 = °R = rankine = degR = degreeR -degree_Fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degF = degreeF -degree_Reaumur = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degRe = degreeRe = degree_Réaumur = réaumur -atomic_unit_of_temperature = E_h / k = a_u_temp -planck_temperature = (hbar * c ** 5 / gravitational_constant / k ** 2) ** 0.5 # Area [area] = [length] ** 2 -are = 100 * meter ** 2 -barn = 1e-28 * meter ** 2 = b -darcy = centipoise * centimeter ** 2 / (second * atmosphere) +are = 100 * m**2 +barn = 1e-28 * m ** 2 = b +cmil = 5.067075e-10 * m ** 2 = circular_mils +darcy = 9.869233e-13 * m ** 2 hectare = 100 * are = ha -# Volume -[volume] = [length] ** 3 -liter = decimeter ** 3 = l = L = litre -cubic_centimeter = centimeter ** 3 = cc -lambda = microliter = λ -stere = meter ** 3 - -# Frequency -[frequency] = 1 / [time] -hertz = 1 / second = Hz -revolutions_per_minute = revolution / minute = rpm -counts_per_second = count / second = cps +# Concentration +[concentration] = [substance] / [volume] +molar = mol / (1e-3 * m ** 3) = M -# Wavenumber -[wavenumber] = 1 / [length] -reciprocal_centimeter = 1 / cm = cm_1 = kayser +# 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 -# Velocity -[velocity] = [length] / [time] = [speed] -knot = nautical_mile / hour = kt = knot_international = international_knot -mile_per_hour = mile / hour = mph = MPH -kilometer_per_hour = kilometer / hour = kph = KPH -kilometer_per_second = kilometer / second = kps -meter_per_second = meter / second = mps -foot_per_second = foot / second = fps - -# Acceleration -[acceleration] = [velocity] / [time] -galileo = centimeter / second ** 2 = Gal +# 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 +calorie = 4.184 * joule = cal = 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 +toe = 41.868e9 * joule = tonne_of_oil_equivalent # Force [force] = [mass] * [acceleration] @@ -193,297 +126,254 @@ 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_metric_ton = g_0 * metric_ton = tf = metric_ton_force = force_t = t_force -atomic_unit_of_force = E_h / a_0 = a_u_force - -# Energy -[energy] = [force] * [length] -joule = newton * meter = J -erg = dyne * centimeter -watt_hour = watt * hour = Wh = watthour -electron_volt = e * volt = eV -rydberg = h * c * R_inf = Ry -hartree = 2 * rydberg = E_h = Eh = hartree_energy = atomic_unit_of_energy = a_u_energy -calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th -international_calorie = 4.1868 * joule = cal_it = international_steam_table_calorie -fifteen_degree_calorie = 4.1855 * joule = cal_15 -british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso -international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it -thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th -quadrillion_Btu = 1e15 * Btu = quad -therm = 1e5 * Btu = thm = EC_therm -US_therm = 1.054804e8 * joule # approximate, no exact definition -ton_TNT = 1e9 * calorie = tTNT -tonne_of_oil_equivalent = 1e10 * international_calorie = toe -atmosphere_liter = atmosphere * liter = atm_l - -# Power -[power] = [energy] / [time] -watt = joule / second = W -volt_ampere = volt * ampere = VA -horsepower = 550 * foot * force_pound / second = hp = UK_horsepower = hydraulic_horsepower -boiler_horsepower = 33475 * Btu / hour # unclear which Btu -metric_horsepower = 75 * force_kilogram * meter / second -electrical_horsepower = 746 * watt -refrigeration_ton = 12e3 * Btu / hour = _ = ton_of_refrigeration # approximate, no exact definition -standard_liter_per_minute = atmosphere * liter / minute = slpm = slm -conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 - -# Density (as auxiliary for pressure) -[density] = [mass] / [volume] -mercury = 13.5951 * kilogram / liter = Hg = Hg_0C = Hg_32F = conventional_mercury -water = 1.0 * kilogram / liter = H2O = conventional_water -mercury_60F = 13.5568 * kilogram / liter = Hg_60F # approximate -water_39F = 0.999972 * kilogram / liter = water_4C # approximate -water_60F = 0.999001 * kilogram / liter # approximate - -# Pressure -[pressure] = [force] / [area] -pascal = newton / meter ** 2 = Pa -barye = dyne / centimeter ** 2 = Ba = barie = barad = barrie = baryd -bar = 1e5 * pascal -technical_atmosphere = kilogram * g_0 / centimeter ** 2 = at -torr = atm / 760 -pound_force_per_square_inch = force_pound / inch ** 2 = psi -kip_per_square_inch = kip / inch ** 2 = ksi -millimeter_Hg = millimeter * Hg * g_0 = mmHg = mm_Hg = millimeter_Hg_0C -centimeter_Hg = centimeter * Hg * g_0 = cmHg = cm_Hg = centimeter_Hg_0C -inch_Hg = inch * Hg * g_0 = inHg = in_Hg = inch_Hg_32F -inch_Hg_60F = inch * Hg_60F * g_0 -inch_H2O_39F = inch * water_39F * g_0 -inch_H2O_60F = inch * water_60F * g_0 -foot_H2O = foot * water * g_0 = ftH2O = feet_H2O -centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O - -# Torque -[torque] = [force] * [length] -foot_pound = foot * force_pound = ft_lb = footpound - -# Viscosity -[viscosity] = [pressure] * [time] -poise = 0.1 * Pa * second = P -reyn = psi * second +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 -# Kinematic viscosity -[kinematic_viscosity] = [area] / [time] -stokes = centimeter ** 2 / second = St +# Frequency +[frequency] = 1 / [time] +hertz = 1 / second = Hz = rps +revolutions_per_minute = revolution / minute = rpm +counts_per_second = count / second = cps -# Fluidity -[fluidity] = 1 / [viscosity] -rhe = 1 / poise +# Heat +#RSI = degK * meter ** 2 / watt +#clo = 0.155 * RSI = clos +#R_value = foot ** 2 * degF * hour / btu -# Amount of substance -particle = 1 / N_A = _ = molec = molecule +# Information +byte = 8 * bit = B = octet +baud = bit / second = Bd = bps -# Concentration -[concentration] = [substance] / [volume] -molar = mole / liter = M +# Irradiance +peak_sun_hour = 1000 * watt_hour / meter**2 = PSH +langley = thermochemical_calorie / centimeter**2 = Langley -# Catalytic activity -[activity] = [substance] / [time] -katal = mole / second = kat -enzyme_unit = micromole / minute = U = enzymeunit +# Length +angstrom = 1e-10 * meter = Å = ångström = Å +parsec = 3.08568025e16 * meter = pc +light_year = speed_of_light * julian_year = ly = lightyear +astronomical_unit = 149597870691 * meter = au -# Entropy -[entropy] = [energy] / [temperature] -clausius = calorie / kelvin = Cl +# Mass +carat = 200 * milligram +metric_ton = 1000 * kilogram = t = tonne +atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da +bag = 94 * lb -# Molar entropy -[molar_entropy] = [entropy] / [substance] -entropy_unit = calorie / kelvin / mole = eu +# Textile +denier = gram / (9000 * meter) +tex = gram / (1000 * meter) +dtex = decitex -# Radiation -becquerel = counts_per_second = Bq -curie = 3.7e10 * becquerel = Ci -rutherford = 1e6 * becquerel = Rd -gray = joule / kilogram = Gy -sievert = joule / kilogram = Sv -rads = 0.01 * gray -rem = 0.01 * sievert -roentgen = 2.58e-4 * coulomb / kilogram = _ = röntgen # approximate, depends on medium - -# Heat transimission -[heat_transmission] = [energy] / [area] -peak_sun_hour = 1e3 * watt_hour / meter ** 2 = PSH -langley = thermochemical_calorie / centimeter ** 2 = Ly - -# Luminance -[luminance] = [luminosity] / [area] -nit = candela / meter ** 2 -stilb = candela / centimeter ** 2 -lambert = 1 / π * candela / centimeter ** 2 - -# Luminous flux -[luminous_flux] = [luminosity] +# Photometry lumen = candela * steradian = lm - -# Illuminance -[illuminance] = [luminous_flux] / [area] lux = lumen / meter ** 2 = lx -# Intensity -[intensity] = [power] / [area] -atomic_unit_of_intensity = 0.5 * ε_0 * c * atomic_unit_of_electric_field ** 2 = a_u_intensity - -# Current -biot = 10 * ampere = Bi -abampere = biot = abA -atomic_unit_of_current = e / atomic_unit_of_time = a_u_current -mean_international_ampere = mean_international_volt / mean_international_ohm = A_it -US_international_ampere = US_international_volt / US_international_ohm = A_US -conventional_ampere_90 = K_J90 * R_K90 / (K_J * R_K) * ampere = A_90 -planck_current = (c ** 6 / gravitational_constant / k_C) ** 0.5 - -# Charge -[charge] = [current] * [time] -coulomb = ampere * second = C -abcoulomb = 10 * C = abC -faraday = e * N_A * mole -conventional_coulomb_90 = K_J90 * R_K90 / (K_J * R_K) * coulomb = C_90 +# 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 -# Electric potential -[electric_potential] = [energy] / [charge] -volt = joule / coulomb = V -abvolt = 1e-8 * volt = abV -mean_international_volt = 1.00034 * volt = V_it # approximate -US_international_volt = 1.00033 * volt = V_US # approximate -conventional_volt_90 = K_J90 / K_J * volt = V_90 +# 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 -# Electric field -[electric_field] = [electric_potential] / [length] -atomic_unit_of_electric_field = e * k_C / a_0 ** 2 = a_u_electric_field +# Radiation +Bq = Hz = Bq = becquerel +curie = 3.7e10 * Bq = Ci +rutherford = 1e6*Bq = Rd +Gy = joule / kilogram = gray = Sv = sievert +rem = 1e-2 * sievert +rads = 1e-2 * gray +roentgen = 2.58e-4 * coulomb / kilogram -# Electric displacement field -[electric_displacement_field] = [charge] / [area] +# Temperature +degC = kelvin; offset: 273.15 = celsius +degR = 5 / 9 * kelvin; offset: 0 = rankine +degF = 5 / 9 * kelvin; offset: 255.372222 = fahrenheit -# Resistance -[resistance] = [electric_potential] / [current] -ohm = volt / ampere = Ω -abohm = 1e-9 * ohm = abΩ -mean_international_ohm = 1.00049 * ohm = Ω_it = ohm_it # approximate -US_international_ohm = 1.000495 * ohm = Ω_US = ohm_US # approximate -conventional_ohm_90 = R_K / R_K90 * ohm = Ω_90 = ohm_90 +# 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 -# Resistivity -[resistivity] = [resistance] * [length] +# Velocity +[speed] = [length] / [time] +nautical_mile = 1852 m = nmi # exact +knot = nautical_mile / hour = kt = knot_international = international_knot = nautical_miles_per_hour +mph = mile / hour = MPH +kph = kilometer / hour = KPH -# Conductance -[conductance] = [current] / [electric_potential] -siemens = ampere / volt = S = mho -absiemens = 1e9 * siemens = abS = abmho +# Viscosity +[viscosity] = [pressure] * [time] +poise = 1e-1 * Pa * second = P +stokes = 1e-4 * meter ** 2 / second = St +rhe = 10 / (Pa * s) -# Capacitance -[capacitance] = [charge] / [electric_potential] -farad = coulomb / volt = F -abfarad = 1e9 * farad = abF -conventional_farad_90 = R_K90 / R_K * farad = F_90 +# Volume +[volume] = [length] ** 3 +liter = 1e-3 * m ** 3 = l = L = litre +cc = centimeter ** 3 = cubic_centimeter +stere = meter ** 3 -# Inductance -[inductance] = [magnetic_flux] / [current] -henry = weber / ampere = H -abhenry = 1e-9 * henry = abH -conventional_henry_90 = R_K / R_K90 * henry = H_90 -# Magnetic flux -[magnetic_flux] = [electric_potential] * [time] -weber = volt * second = Wb -unit_pole = µ_0 * biot * centimeter +@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 + # allow wavenumber / kayser + 1 / [length] <-> [length]: 1 / value +@end -# Magnetic field -[magnetic_field] = [magnetic_flux] / [area] -tesla = weber / meter ** 2 = T -gamma = 1e-9 * tesla = γ +@context boltzmann + [temperature] -> [energy]: boltzmann_constant * value + [energy] -> [temperature]: value / boltzmann_constant +@end -# Magnetomotive force -[magnetomotive_force] = [current] -ampere_turn = ampere = At -biot_turn = biot -gilbert = 1 / (4 * π) * biot_turn = Gb +@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 -# Magnetic field strength -[magnetic_field_strength] = [current] / [length] + # moles -> mass require the molecular weight + [substance] -> [mass]: value * mw + [mass] -> [substance]: value / mw -# Electric dipole moment -[electric_dipole] = [charge] * [length] -debye = 1e-9 / ζ * coulomb * angstrom = D # formally 1 D = 1e-10 Fr*Å, but we generally want to use it outside the Gaussian context + # 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 -# Electric quadrupole moment -[electric_quadrupole] = [charge] * [area] -buckingham = debye * angstrom + # moles/volume -> moles requires the solution volume + [substance] / [volume] -> [substance]: value * volume + [substance] -> [substance] / [volume]: value / volume -# Magnetic dipole moment -[magnetic_dipole] = [current] * [area] -bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B -nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N + # 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 -#### UNIT GROUPS #### -# Mostly for length, area, volume, mass, force -# (customary or specialized units) +@end +# Most of the definitions that follows are derived from: +# See http://www.nist.gov/pml/wmd/pubs/hb44.cfm @group USCSLengthInternational - thou = 1e-3 * inch = th = mil_length inch = yard / 36 = in = international_inch = inches = international_inches - hand = 4 * inch foot = yard / 3 = ft = international_foot = feet = international_feet - yard = 0.9144 * meter = yd = international_yard # since Jul 1959 - mile = 1760 * yard = mi = international_mile - - circular_mil = π / 4 * mil_length ** 2 = cmil - square_inch = inch ** 2 = sq_in = square_inches - square_foot = foot ** 2 = sq_ft = square_feet - square_yard = yard ** 2 = sq_yd - square_mile = mile ** 2 = sq_mi - - cubic_inch = in ** 3 = cu_in - cubic_foot = ft ** 3 = cu_ft = cubic_feet - cubic_yard = yd ** 3 = cu_yd + yard = 0.9144 metres = yd = international_yard + mile = 1760 yard = mi = international_mile + + square_inch = 1 inch ** 2 = sq_in = square_inches + square_foot = 1 foot ** 2 = sq_ft = square_feet + square_yard = 1 yard ** 2 = sq_yd + square_mile = 1 mile ** 2 = sq_mi + + cubic_inch = 1 in ** 3 = cu_in + cubic_foot = 1 ft ** 3 = cu_ft = cubic_feet + cubic_yard = 1 yd ** 3 = cu_yd + + acre_foot = acre * foot = acre_feet @end @group USCSLengthSurvey - link = 1e-2 * chain = li = survey_link - survey_foot = 1200 / 3937 * meter = sft - fathom = 6 * survey_foot - rod = 16.5 * survey_foot = rd = pole = perch - chain = 4 * rod - furlong = 40 * rod = fur - cables_length = 120 * fathom - survey_mile = 5280 * survey_foot = smi = us_statute_mile - league = 3 * survey_mile - - square_rod = rod ** 2 = sq_rod = sq_pole = sq_perch - acre = 10 * chain ** 2 - square_survey_mile = survey_mile ** 2 = _ = section - square_league = league ** 2 - - acre_foot = acre * survey_foot = _ = acre_feet + link = 0.66 survey_foot = li = survey_link + survey_foot = foot / 0.999998 = sft + rod = 16.5 survey_foot = rd = pole = perch + chain = 66 survey_foot + survey_mile = 5280 survey_foot + + acre = 43560 survey_foot ** 2 + square_rod = 1 rod ** 2 = sq_rod = sq_pole = sq_perch + + fathom = 6 survey_foot + us_statute_mile = 5280 survey_foot + league = 3 us_statute_mile + furlong = us_statute_mile / 8 @end @group USCSDryVolume - dry_pint = bushel / 64 = dpi = US_dry_pint - dry_quart = bushel / 32 = dqt = US_dry_quart - dry_gallon = bushel / 8 = dgal = US_dry_gallon - peck = bushel / 4 = pk - bushel = 2150.42 cubic_inch = bu - dry_barrel = 7056 cubic_inch = _ = US_dry_barrel + dry_pint = 33.6003125 cubic_inch = dpi = US_dry_pint + dry_quart = 2 dry_pint = dqt = US_dry_quart + dry_gallon = 8 dry_pint = dgal = US_dry_gallon + peck = 16 dry_pint = pk + bushel = 64 dry_pint = bu + dry_barrel = 7065 cubic_inch = US_dry_barrel @end @group USCSLiquidVolume - minim = pint / 7680 - fluid_dram = pint / 128 = fldr = fluidram = US_fluid_dram = US_liquid_dram - fluid_ounce = pint / 16 = floz = US_fluid_ounce = US_liquid_ounce - gill = pint / 4 = gi = liquid_gill = US_liquid_gill - pint = quart / 2 = pt = liquid_pint = US_pint - fifth = gallon / 5 = _ = US_liquid_fifth - quart = gallon / 4 = qt = liquid_quart = US_liquid_quart - gallon = 231 * cubic_inch = gal = liquid_gallon = US_liquid_gallon + minim = liquid_pint / 7680 + fluid_dram = liquid_pint / 128 = fldr = fluidram = US_fluid_dram + fluid_ounce = liquid_pint / 16 = floz = US_fluid_ounce = US_liquid_ounce + gill = liquid_pint / 4 = gi = liquid_gill = US_liquid_gill + + pint = 28.875 cubic_inch = pt = liquid_pint = US_pint + + quart = 2 liquid_pint = qt = liquid_quart = US_liquid_quart + gallon = 8 liquid_pint = gal = liquid_gallon = US_liquid_gallon @end @group USCSVolumeOther - teaspoon = fluid_ounce / 6 = tsp - tablespoon = fluid_ounce / 2 = tbsp + teaspoon = tablespoon / 3 = tsp + tablespoon = floz / 2 = tbsp = Tbsp = Tblsp = tblsp = tbs = Tbl shot = 3 * tablespoon = jig = US_shot - cup = pint / 2 = cp = liquid_cup = US_liquid_cup + cup = 8 fluid_ounce = cp = liquid_cup = US_liquid_cup barrel = 31.5 * gallon = bbl oil_barrel = 42 * gallon = oil_bbl beer_barrel = 31 * gallon = beer_bbl @@ -491,263 +381,65 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N @end @group Avoirdupois - dram = pound / 256 = dr = avoirdupois_dram = avdp_dram = drachm + grain = avdp_pound / 7000 = gr + drachm = pound / 256 = dr = avoirdupois_dram = avdp_dram = dram ounce = pound / 16 = oz = avoirdupois_ounce = avdp_ounce - pound = 7e3 * grain = lb = avoirdupois_pound = avdp_pound - stone = 14 * pound - quarter = 28 * stone - bag = 94 * pound - hundredweight = 100 * pound = cwt = short_hundredweight - long_hundredweight = 112 * pound - ton = 2e3 * pound = _ = short_ton - long_ton = 2240 * pound - slug = g_0 * pound * second ** 2 / foot - - force_ounce = g_0 * ounce = ozf = ounce_force - force_pound = g_0 * pound = lbf = pound_force - force_ton = g_0 * ton = _ = ton_force = force_short_ton = short_ton_force - force_long_ton = g_0 * long_ton = _ = long_ton_force - kip = 1e3 * force_pound - poundal = pound * foot / second ** 2 = pdl -@end + pound = 453.59237 gram = lb = avoirdupois_pound = avdp_pound -@group AvoirdupoisUK using Avoirdupois - UK_hundredweight = long_hundredweight = UK_cwt - UK_ton = long_ton - UK_force_ton = force_long_ton = _ = UK_ton_force -@end - -@group AvoirdupoisUS using Avoirdupois - US_hundredweight = hundredweight = US_cwt - US_ton = ton - US_force_ton = force_ton = _ = US_ton_force + short_hunderdweight = 100 avoirdupois_pound = ch_cwt + long_hunderweight = 112 avoirdupois_pound = lg_cwt + short_ton = 2000 avoirdupois_pound + long_ton = 2240 avoirdupois_pound @end @group Troy - pennyweight = 24 * grain = dwt - troy_ounce = 480 * grain = toz = ozt - troy_pound = 12 * troy_ounce = tlb = lbt + pennyweight = 24 grain = dwt + troy_ounce = 480 grain = toz + troy_pound = 12 troy_ounce = tlb @end @group Apothecary - scruple = 20 * grain - apothecary_dram = 3 * scruple = ap_dr - apothecary_ounce = 8 * apothecary_dram = ap_oz - apothecary_pound = 12 * apothecary_ounce = ap_lb + scruple = 20 grain + apothecary_dram = 3 scruple = ap_dr + apothecary_ounce = 8 apothecary_dram = ap_oz + apothecary_pound = 12 apothecary_ounce = ap_lb @end -@group ImperialVolume - imperial_minim = imperial_fluid_ounce / 480 - imperial_fluid_scruple = imperial_fluid_ounce / 24 - imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fldr = imperial_fluid_dram - imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce - imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill - imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup - imperial_pint = imperial_gallon / 8 = imperial_pt = UK_pint - imperial_quart = imperial_gallon / 4 = imperial_qt = UK_quart - imperial_gallon = 4.54609 * liter = imperial_gal = UK_gallon - imperial_peck = 2 * imperial_gallon = imperial_pk = UK_pk - imperial_bushel = 8 * imperial_gallon = imperial_bu = UK_bushel - imperial_barrel = 36 * imperial_gallon = imperial_bbl = UK_bbl +@group AvoirdupoisUK using Avoirdupois + stone = 14 pound + quarter = 28 stone + UK_hundredweight = long_hunderweight = UK_cwt + UK_ton = long_ton +@end + +@group AvoirdupoisUS using Avoirdupois + US_hundredweight = short_hunderdweight = US_cwt + US_ton = short_ton = ton @end @group Printer - pica = inch / 6 = _ = printers_pica - point = pica / 12 = pp = printers_point = big_point = bp - didot = 1 / 2660 * m - cicero = 12 * didot - tex_point = inch / 72.27 - tex_pica = 12 * tex_point - tex_didot = 1238 / 1157 * tex_point - tex_cicero = 12 * tex_didot - scaled_point = tex_point / 65536 - css_pixel = inch / 96 = px - - pixel = [printing_unit] = _ = dot = pel = picture_element + # Length + pixel = [printing_unit] = dot = px = pel = picture_element pixels_per_centimeter = pixel / cm = PPCM pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi bits_per_pixel = bit / pixel = bpp -@end - -@group Textile - tex = gram / kilometer = Tt - dtex = decitex - denier = gram / (9 * kilometer) = Td - jute = pound / (14400 * yard) = Tj - aberdeen = jute = Ta - - number_english = 840 * yard / pound = Ne = NeC = ECC - number_meter = kilometer / kilogram = Nm -@end - - -#### CGS ELECTROMAGNETIC UNITS #### - -# === Gaussian system of units === -@group Gaussian - franklin = erg ** 0.5 * centimeter ** 0.5 = Fr = statcoulomb = statC = esu - statvolt = erg / franklin = statV - statampere = franklin / second = statA - gauss = dyne / franklin = G - maxwell = gauss * centimeter ** 2 = Mx - oersted = dyne / maxwell = Oe = ørsted - statohm = statvolt / statampere = statΩ - statfarad = franklin / statvolt = statF - statmho = statampere / statvolt -@end -# Note this system is not commensurate with SI, as ε_0 and µ_0 disappear; -# some quantities with different dimensions in SI have the same -# dimensions in the Gaussian system (e.g. [Mx] = [Fr], but [Wb] != [C]), -# and therefore the conversion factors depend on the context (not in pint sense) -[gaussian_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] -[gaussian_current] = [gaussian_charge] / [time] -[gaussian_electric_potential] = [gaussian_charge] / [length] -[gaussian_electric_field] = [gaussian_electric_potential] / [length] -[gaussian_electric_displacement_field] = [gaussian_charge] / [area] -[gaussian_electric_flux] = [gaussian_charge] -[gaussian_electric_dipole] = [gaussian_charge] * [length] -[gaussian_electric_quadrupole] = [gaussian_charge] * [area] -[gaussian_magnetic_field] = [force] / [gaussian_charge] -[gaussian_magnetic_field_strength] = [gaussian_magnetic_field] -[gaussian_magnetic_flux] = [gaussian_magnetic_field] * [area] -[gaussian_magnetic_dipole] = [energy] / [gaussian_magnetic_field] -[gaussian_resistance] = [gaussian_electric_potential] / [gaussian_current] -[gaussian_resistivity] = [gaussian_resistance] * [length] -[gaussian_capacitance] = [gaussian_charge] / [gaussian_electric_potential] -[gaussian_inductance] = [gaussian_electric_potential] * [time] / [gaussian_current] -[gaussian_conductance] = [gaussian_current] / [gaussian_electric_potential] -@context Gaussian = Gau - [gaussian_charge] -> [charge]: value / k_C ** 0.5 - [charge] -> [gaussian_charge]: value * k_C ** 0.5 - [gaussian_current] -> [current]: value / k_C ** 0.5 - [current] -> [gaussian_current]: value * k_C ** 0.5 - [gaussian_electric_potential] -> [electric_potential]: value * k_C ** 0.5 - [electric_potential] -> [gaussian_electric_potential]: value / k_C ** 0.5 - [gaussian_electric_field] -> [electric_field]: value * k_C ** 0.5 - [electric_field] -> [gaussian_electric_field]: value / k_C ** 0.5 - [gaussian_electric_displacement_field] -> [electric_displacement_field]: value / (4 * π / ε_0) ** 0.5 - [electric_displacement_field] -> [gaussian_electric_displacement_field]: value * (4 * π / ε_0) ** 0.5 - [gaussian_electric_dipole] -> [electric_dipole]: value / k_C ** 0.5 - [electric_dipole] -> [gaussian_electric_dipole]: value * k_C ** 0.5 - [gaussian_electric_quadrupole] -> [electric_quadrupole]: value / k_C ** 0.5 - [electric_quadrupole] -> [gaussian_electric_quadrupole]: value * k_C ** 0.5 - [gaussian_magnetic_field] -> [magnetic_field]: value / (4 * π / µ_0) ** 0.5 - [magnetic_field] -> [gaussian_magnetic_field]: value * (4 * π / µ_0) ** 0.5 - [gaussian_magnetic_flux] -> [magnetic_flux]: value / (4 * π / µ_0) ** 0.5 - [magnetic_flux] -> [gaussian_magnetic_flux]: value * (4 * π / µ_0) ** 0.5 - [gaussian_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π * µ_0) ** 0.5 - [magnetic_field_strength] -> [gaussian_magnetic_field_strength]: value * (4 * π * µ_0) ** 0.5 - [gaussian_magnetic_dipole] -> [magnetic_dipole]: value * (4 * π / µ_0) ** 0.5 - [magnetic_dipole] -> [gaussian_magnetic_dipole]: value / (4 * π / µ_0) ** 0.5 - [gaussian_resistance] -> [resistance]: value * k_C - [resistance] -> [gaussian_resistance]: value / k_C - [gaussian_resistivity] -> [resistivity]: value * k_C - [resistivity] -> [gaussian_resistivity]: value / k_C - [gaussian_capacitance] -> [capacitance]: value / k_C - [capacitance] -> [gaussian_capacitance]: value * k_C - [gaussian_inductance] -> [inductance]: value * k_C - [inductance] -> [gaussian_inductance]: value / k_C - [gaussian_conductance] -> [conductance]: value / k_C - [conductance] -> [gaussian_conductance]: value * k_C -@end - -# === ESU system of units === -# (where different from Gaussian) -# See note for Gaussian system too -@group ESU using Gaussian - statweber = statvolt * second = statWb - stattesla = statweber / centimeter ** 2 = statT - stathenry = statweber / statampere = statH -@end -[esu_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] -[esu_current] = [esu_charge] / [time] -[esu_electric_potential] = [esu_charge] / [length] -[esu_magnetic_flux] = [esu_electric_potential] * [time] -[esu_magnetic_field] = [esu_magnetic_flux] / [area] -[esu_magnetic_field_strength] = [esu_current] / [length] -[esu_magnetic_dipole] = [esu_current] * [area] -@context ESU = esu - [esu_magnetic_field] -> [magnetic_field]: value * k_C ** 0.5 - [magnetic_field] -> [esu_magnetic_field]: value / k_C ** 0.5 - [esu_magnetic_flux] -> [magnetic_flux]: value * k_C ** 0.5 - [magnetic_flux] -> [esu_magnetic_flux]: value / k_C ** 0.5 - [esu_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π / ε_0) ** 0.5 - [magnetic_field_strength] -> [esu_magnetic_field_strength]: value * (4 * π / ε_0) ** 0.5 - [esu_magnetic_dipole] -> [magnetic_dipole]: value / k_C ** 0.5 - [magnetic_dipole] -> [esu_magnetic_dipole]: value * k_C ** 0.5 -@end - - -#### CONVERSION CONTEXTS #### - -@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 - # allow wavenumber / kayser - [wavenumber] <-> [length]: 1 / value -@end - -@context boltzmann - [temperature] -> [energy]: boltzmann_constant * value - [energy] -> [temperature]: value / boltzmann_constant -@end - -@context energy - [energy] -> [energy] / [substance]: value * N_A - [energy] / [substance] -> [energy]: value / N_A - [energy] -> [mass]: value / c ** 2 - [mass] -> [energy]: value * c ** 2 -@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 + point = yard / 216 / 12 = pp = printers_point + thou = yard / 36000 = th = mil + pica = yard / 216 = P̸ = printers_pica @end -@context textile - # Allow switching between Direct count system (i.e. tex) and - # Indirect count system (i.e. Ne, Nm) - [mass] / [length] <-> [length] / [mass]: 1 / value -@end - - -#### SYSTEMS OF UNITS #### - -@system SI - second - meter - kilogram - ampere - kelvin - mole - candela +@group ImperialVolume + imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce + imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fluid_dram + imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill + imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup + imperial_pint = 568.26125 * milliliter = imperial_pt = UK_pint + imperial_quart = 2 * imperial_pint = imperial_qt = UK_quart + imperial_gallon = 8 * imperial_pint = imperial_gal = UK_gallon + imperial_peck = 16 * imperial_pint = imperial_pk = UK_pk + imperial_bushel = 64 * imperial_pint = imperial_bu = UK_bushel + imperial_barrel = 288 * imperial_pint = imperial_bbl = UK_bbl @end @system mks using international @@ -756,30 +448,12 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N second @end -@system cgs using international, Gaussian, ESU +@system cgs using international centimeter gram second @end -@system atomic using international - # based on unit m_e, e, hbar, k_C, k - bohr: meter - electron_mass: gram - atomic_unit_of_time: second - atomic_unit_of_current: ampere - atomic_unit_of_temperature: kelvin -@end - -@system Planck using international - # based on unit c, gravitational_constant, hbar, k_C, k - planck_length: meter - planck_mass: gram - planck_time: second - planck_current: ampere - planck_temperature: kelvin -@end - @system imperial using ImperialVolume, USCSLengthInternational, AvoirdupoisUK yard pound From 7a640f70008467c2961f56256926d208d060191d Mon Sep 17 00:00:00 2001 From: good597 Date: Tue, 6 Aug 2019 15:40:15 -0700 Subject: [PATCH 047/612] rebase to hgrecco pint master --- pint/default_en.txt | 934 ++++++++++++++++++++++++++++++-------------- 1 file changed, 630 insertions(+), 304 deletions(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index f54650901..55daf6b5e 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -1,13 +1,16 @@ # Default Pint units definition file # Based on the International System of Units # Language: english -# :copyright: 2013 by Pint Authors, see AUTHORS for more details. +# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. @defaults group = international system = mks @end + +#### PREFIXES #### + # decimal prefixes yocto- = 1e-24 = y- zepto- = 1e-21 = z- @@ -15,11 +18,11 @@ atto- = 1e-18 = a- femto- = 1e-15 = f- pico- = 1e-12 = p- nano- = 1e-9 = n- -micro- = 1e-6 = u- = µ- +micro- = 1e-6 = µ- = u- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- -deca- = 1e+1 = da- = deka +deca- = 1e+1 = da- = deka- hecto- = 1e2 = h- kilo- = 1e3 = k- mega- = 1e6 = M- @@ -40,85 +43,149 @@ exbi- = 2**60 = Ei- zebi- = 2**70 = Zi- yobi- = 2**80 = Yi- -# reference +# extra_prefixes +semi- = 0.5 = _ = demi- +sesqui- = 1.5 + + +#### BASE UNITS #### + 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 +kelvin = [temperature]; offset: 0 = K = degK = °K = degree_Kelvin = degreeK # older names supported for compatibility radian = [] = rad +neper = [] = Np bit = [] count = [] + +#### CONSTANTS #### + @import constants_en.txt -# acceleration -[acceleration] = [length] / [time] ** 2 + +#### UNITS #### +# Common and less common, grouped by quantity. +# Conversion factors are exact (except when noted), +# although floating-point conversion may introduce inaccuracies # 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 +turn = 2 * π * radian = _ = revolution = cycle = circle +degree = π / 180 * radian = deg = arcdeg = arcdegree = angular_degree +arcminute = degree / 60 = arcmin = arc_minute = angular_minute +arcsecond = arcminute / 60 = arcsec = arc_second = angular_second +milliarcsecond = 1e-3 * arcsecond = mas +grade = π / 200 * radian = grad = gon +mil = π / 32000 * radian + +# Solid angle steradian = radian ** 2 = sr +square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg + +# Logarithmic ratio +bel = 0.5 * ln10 * neper + +# Information +byte = 8 * bit = B = octet +baud = bit / second = Bd = bps + +# Length +angstrom = 1e-10 * meter = Å = ångström = Å +micron = micrometer = µ +fermi = femtometer = fm +light_year = speed_of_light * julian_year = ly = lightyear +astronomical_unit = 149597870700 * meter = au # since Aug 2012 +parsec = 1 / tansec * astronomical_unit = pc +nautical_mile = 1852 * meter = nmi +bohr = hbar / (alpha * m_e * c) = a_0 = a0 = bohr_radius = atomic_unit_of_length = a_u_length +x_unit_Cu = K_alpha_Cu_d_220 * d_220 / 1537.4 = Xu_Cu +x_unit_Mo = K_alpha_Mo_d_220 * d_220 / 707.831 = Xu_Mo +angstrom_star = K_alpha_W_d_220 * d_220 / 0.2090100 = Å_star +planck_length = (hbar * gravitational_constant / c ** 3) ** 0.5 + +# Mass +metric_ton = 1e3 * kilogram = t = tonne +unified_atomic_mass_unit = atomic_mass_constant = u = amu +dalton = atomic_mass_constant = Da +grain = 64.79891 * milligram = gr +gamma_mass = microgram +carat = 200 * milligram = ct = karat +planck_mass = (hbar * c / gravitational_constant) ** 0.5 + +# Time +minute = 60 * second = min +hour = 60 * minute = hr +day = 24 * hour = d +week = 7 * day +fortnight = 2 * week +year = 365.25 * day = a = yr = julian_year +month = year / 12 +decade = 10 * year +century = 100 * year = _ = centuries +millennium = 1e3 * year = _ = millennia +eon = 1e9 * year +shake = 1e-8 * second +svedberg = 1e-13 * second +atomic_unit_of_time = hbar / E_h = a_u_time +gregorian_year = 365.2425 * day +sidereal_year = 365.256363004 * day # approximate, as of J2000 epoch +tropical_year = 365.242190402 * day # approximate, as of J2000 epoch +common_year = 365 * day +leap_year = 366 * day +sidereal_day = day / 1.00273790935079524 # approximate +sidereal_month = 27.32166155 * day # approximate +tropical_month = 27.321582 * day # approximate +synodic_month = 29.530589 * day = _ = lunar_month # approximate +planck_time = (hbar * gravitational_constant / c ** 5) ** 0.5 + +# Temperature +degree_Celsius = kelvin; offset: 273.15 = °C = celsius = degC = degreeC +degree_Rankine = 5 / 9 * kelvin; offset: 0 = °R = rankine = degR = degreeR +degree_Fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degF = degreeF +degree_Reaumur = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degRe = degreeRe = degree_Réaumur = réaumur +atomic_unit_of_temperature = E_h / k = a_u_temp +planck_temperature = (hbar * c ** 5 / gravitational_constant / k ** 2) ** 0.5 # 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 +are = 100 * meter ** 2 +barn = 1e-28 * meter ** 2 = b +darcy = centipoise * centimeter ** 2 / (second * atmosphere) hectare = 100 * are = ha -# Concentration -[concentration] = [substance] / [volume] -molar = mol / (1e-3 * m ** 3) = M +# Volume +[volume] = [length] ** 3 +liter = decimeter ** 3 = l = L = litre +cubic_centimeter = centimeter ** 3 = cc +lambda = microliter = λ +stere = meter ** 3 -# 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 +# Frequency +[frequency] = 1 / [time] +hertz = 1 / second = Hz +revolutions_per_minute = revolution / minute = rpm +counts_per_second = count / second = cps -# 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 -calorie = 4.184 * joule = cal = 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 -toe = 41.868e9 * joule = tonne_of_oil_equivalent +# Wavenumber +[wavenumber] = 1 / [length] +reciprocal_centimeter = 1 / cm = cm_1 = kayser + +# Velocity +[velocity] = [length] / [time] = [speed] +knot = nautical_mile / hour = kt = knot_international = international_knot +mile_per_hour = mile / hour = mph = MPH +kilometer_per_hour = kilometer / hour = kph = KPH +kilometer_per_second = kilometer / second = kps +meter_per_second = meter / second = mps +foot_per_second = foot / second = fps + +# Acceleration +[acceleration] = [velocity] / [time] +galileo = centimeter / second ** 2 = Gal # Force [force] = [mass] * [acceleration] @@ -126,254 +193,297 @@ 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 +force_metric_ton = g_0 * metric_ton = tf = metric_ton_force = force_t = t_force +atomic_unit_of_force = E_h / a_0 = a_u_force -# Frequency -[frequency] = 1 / [time] -hertz = 1 / second = Hz = rps -revolutions_per_minute = revolution / minute = rpm -counts_per_second = count / second = cps +# Energy +[energy] = [force] * [length] +joule = newton * meter = J +erg = dyne * centimeter +watt_hour = watt * hour = Wh = watthour +electron_volt = e * volt = eV +rydberg = h * c * R_inf = Ry +hartree = 2 * rydberg = E_h = Eh = hartree_energy = atomic_unit_of_energy = a_u_energy +calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th +international_calorie = 4.1868 * joule = cal_it = international_steam_table_calorie +fifteen_degree_calorie = 4.1855 * joule = cal_15 +british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso +international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it +thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th +quadrillion_Btu = 1e15 * Btu = quad +therm = 1e5 * Btu = thm = EC_therm +US_therm = 1.054804e8 * joule # approximate, no exact definition +ton_TNT = 1e9 * calorie = tTNT +tonne_of_oil_equivalent = 1e10 * international_calorie = toe +atmosphere_liter = atmosphere * liter = atm_l -# Heat -#RSI = degK * meter ** 2 / watt -#clo = 0.155 * RSI = clos -#R_value = foot ** 2 * degF * hour / btu +# Power +[power] = [energy] / [time] +watt = joule / second = W +volt_ampere = volt * ampere = VA +horsepower = 550 * foot * force_pound / second = hp = UK_horsepower = hydraulic_horsepower +boiler_horsepower = 33475 * Btu / hour # unclear which Btu +metric_horsepower = 75 * force_kilogram * meter / second +electrical_horsepower = 746 * watt +refrigeration_ton = 12e3 * Btu / hour = _ = ton_of_refrigeration # approximate, no exact definition +standard_liter_per_minute = atmosphere * liter / minute = slpm = slm +conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 + +# Density (as auxiliary for pressure) +[density] = [mass] / [volume] +mercury = 13.5951 * kilogram / liter = Hg = Hg_0C = Hg_32F = conventional_mercury +water = 1.0 * kilogram / liter = H2O = conventional_water +mercury_60F = 13.5568 * kilogram / liter = Hg_60F # approximate +water_39F = 0.999972 * kilogram / liter = water_4C # approximate +water_60F = 0.999001 * kilogram / liter # approximate -# Information -byte = 8 * bit = B = octet -baud = bit / second = Bd = bps +# Pressure +[pressure] = [force] / [area] +pascal = newton / meter ** 2 = Pa +barye = dyne / centimeter ** 2 = Ba = barie = barad = barrie = baryd +bar = 1e5 * pascal +technical_atmosphere = kilogram * g_0 / centimeter ** 2 = at +torr = atm / 760 +pound_force_per_square_inch = force_pound / inch ** 2 = psi +kip_per_square_inch = kip / inch ** 2 = ksi +millimeter_Hg = millimeter * Hg * g_0 = mmHg = mm_Hg = millimeter_Hg_0C +centimeter_Hg = centimeter * Hg * g_0 = cmHg = cm_Hg = centimeter_Hg_0C +inch_Hg = inch * Hg * g_0 = inHg = in_Hg = inch_Hg_32F +inch_Hg_60F = inch * Hg_60F * g_0 +inch_H2O_39F = inch * water_39F * g_0 +inch_H2O_60F = inch * water_60F * g_0 +foot_H2O = foot * water * g_0 = ftH2O = feet_H2O +centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O + +# Torque +[torque] = [force] * [length] +foot_pound = foot * force_pound = ft_lb = footpound -# Irradiance -peak_sun_hour = 1000 * watt_hour / meter**2 = PSH -langley = thermochemical_calorie / centimeter**2 = Langley +# Viscosity +[viscosity] = [pressure] * [time] +poise = 0.1 * Pa * second = P +reyn = psi * second -# Length -angstrom = 1e-10 * meter = Å = ångström = Å -parsec = 3.08568025e16 * meter = pc -light_year = speed_of_light * julian_year = ly = lightyear -astronomical_unit = 149597870691 * meter = au +# Kinematic viscosity +[kinematic_viscosity] = [area] / [time] +stokes = centimeter ** 2 / second = St -# Mass -carat = 200 * milligram -metric_ton = 1000 * kilogram = t = tonne -atomic_mass_unit = 1.660538782e-27 * kilogram = u = amu = dalton = Da -bag = 94 * lb +# Fluidity +[fluidity] = 1 / [viscosity] +rhe = 1 / poise + +# Amount of substance +particle = 1 / N_A = _ = molec = molecule + +# Concentration +[concentration] = [substance] / [volume] +molar = mole / liter = M -# Textile -denier = gram / (9000 * meter) -tex = gram / (1000 * meter) -dtex = decitex +# Catalytic activity +[activity] = [substance] / [time] +katal = mole / second = kat +enzyme_unit = micromole / minute = U = enzymeunit -# Photometry +# Entropy +[entropy] = [energy] / [temperature] +clausius = calorie / kelvin = Cl + +# Molar entropy +[molar_entropy] = [entropy] / [substance] +entropy_unit = calorie / kelvin / mole = eu + +# Radiation +becquerel = counts_per_second = Bq +curie = 3.7e10 * becquerel = Ci +rutherford = 1e6 * becquerel = Rd +gray = joule / kilogram = Gy +sievert = joule / kilogram = Sv +rads = 0.01 * gray +rem = 0.01 * sievert +roentgen = 2.58e-4 * coulomb / kilogram = _ = röntgen # approximate, depends on medium + +# Heat transimission +[heat_transmission] = [energy] / [area] +peak_sun_hour = 1e3 * watt_hour / meter ** 2 = PSH +langley = thermochemical_calorie / centimeter ** 2 = Ly + +# Luminance +[luminance] = [luminosity] / [area] +nit = candela / meter ** 2 +stilb = candela / centimeter ** 2 +lambert = 1 / π * candela / centimeter ** 2 + +# Luminous flux +[luminous_flux] = [luminosity] lumen = candela * steradian = lm + +# Illuminance +[illuminance] = [luminous_flux] / [area] 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 +# Intensity +[intensity] = [power] / [area] +atomic_unit_of_intensity = 0.5 * ε_0 * c * atomic_unit_of_electric_field ** 2 = a_u_intensity + +# Current +biot = 10 * ampere = Bi +abampere = biot = abA +atomic_unit_of_current = e / atomic_unit_of_time = a_u_current +mean_international_ampere = mean_international_volt / mean_international_ohm = A_it +US_international_ampere = US_international_volt / US_international_ohm = A_US +conventional_ampere_90 = K_J90 * R_K90 / (K_J * R_K) * ampere = A_90 +planck_current = (c ** 6 / gravitational_constant / k_C) ** 0.5 + +# Charge +[charge] = [current] * [time] +coulomb = ampere * second = C +abcoulomb = 10 * C = abC +faraday = e * N_A * mole +conventional_coulomb_90 = K_J90 * R_K90 / (K_J * R_K) * coulomb = C_90 -# 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 +# Electric potential +[electric_potential] = [energy] / [charge] +volt = joule / coulomb = V +abvolt = 1e-8 * volt = abV +mean_international_volt = 1.00034 * volt = V_it # approximate +US_international_volt = 1.00033 * volt = V_US # approximate +conventional_volt_90 = K_J90 / K_J * volt = V_90 -# Radiation -Bq = Hz = Bq = becquerel -curie = 3.7e10 * Bq = Ci -rutherford = 1e6*Bq = Rd -Gy = joule / kilogram = gray = Sv = sievert -rem = 1e-2 * sievert -rads = 1e-2 * gray -roentgen = 2.58e-4 * coulomb / kilogram +# Electric field +[electric_field] = [electric_potential] / [length] +atomic_unit_of_electric_field = e * k_C / a_0 ** 2 = a_u_electric_field -# Temperature -degC = kelvin; offset: 273.15 = celsius -degR = 5 / 9 * kelvin; offset: 0 = rankine -degF = 5 / 9 * kelvin; offset: 255.372222 = fahrenheit +# Electric displacement field +[electric_displacement_field] = [charge] / [area] -# 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 +# Resistance +[resistance] = [electric_potential] / [current] +ohm = volt / ampere = Ω +abohm = 1e-9 * ohm = abΩ +mean_international_ohm = 1.00049 * ohm = Ω_it = ohm_it # approximate +US_international_ohm = 1.000495 * ohm = Ω_US = ohm_US # approximate +conventional_ohm_90 = R_K / R_K90 * ohm = Ω_90 = ohm_90 -# Velocity -[speed] = [length] / [time] -nautical_mile = 1852 m = nmi # exact -knot = nautical_mile / hour = kt = knot_international = international_knot = nautical_miles_per_hour -mph = mile / hour = MPH -kph = kilometer / hour = KPH +# Resistivity +[resistivity] = [resistance] * [length] -# Viscosity -[viscosity] = [pressure] * [time] -poise = 1e-1 * Pa * second = P -stokes = 1e-4 * meter ** 2 / second = St -rhe = 10 / (Pa * s) +# Conductance +[conductance] = [current] / [electric_potential] +siemens = ampere / volt = S = mho +absiemens = 1e9 * siemens = abS = abmho -# Volume -[volume] = [length] ** 3 -liter = 1e-3 * m ** 3 = l = L = litre -cc = centimeter ** 3 = cubic_centimeter -stere = meter ** 3 +# Capacitance +[capacitance] = [charge] / [electric_potential] +farad = coulomb / volt = F +abfarad = 1e9 * farad = abF +conventional_farad_90 = R_K90 / R_K * farad = F_90 +# Inductance +[inductance] = [magnetic_flux] / [current] +henry = weber / ampere = H +abhenry = 1e-9 * henry = abH +conventional_henry_90 = R_K / R_K90 * henry = H_90 -@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 - # allow wavenumber / kayser - 1 / [length] <-> [length]: 1 / value -@end +# Magnetic flux +[magnetic_flux] = [electric_potential] * [time] +weber = volt * second = Wb +unit_pole = µ_0 * biot * centimeter -@context boltzmann - [temperature] -> [energy]: boltzmann_constant * value - [energy] -> [temperature]: value / boltzmann_constant -@end +# Magnetic field +[magnetic_field] = [magnetic_flux] / [area] +tesla = weber / meter ** 2 = T +gamma = 1e-9 * tesla = γ -@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 +# Magnetomotive force +[magnetomotive_force] = [current] +ampere_turn = ampere = At +biot_turn = biot +gilbert = 1 / (4 * π) * biot_turn = Gb - # moles -> mass require the molecular weight - [substance] -> [mass]: value * mw - [mass] -> [substance]: value / mw +# Magnetic field strength +[magnetic_field_strength] = [current] / [length] - # 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 +# Electric dipole moment +[electric_dipole] = [charge] * [length] +debye = 1e-9 / ζ * coulomb * angstrom = D # formally 1 D = 1e-10 Fr*Å, but we generally want to use it outside the Gaussian context - # moles/volume -> moles requires the solution volume - [substance] / [volume] -> [substance]: value * volume - [substance] -> [substance] / [volume]: value / volume +# Electric quadrupole moment +[electric_quadrupole] = [charge] * [area] +buckingham = debye * angstrom - # moles/mass -> moles requires the solvent (usually water) mass - [substance] / [mass] -> [substance]: value * solvent_mass - [substance] -> [substance] / [mass]: value / solvent_mass +# Magnetic dipole moment +[magnetic_dipole] = [current] * [area] +bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B +nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N - # 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 +#### UNIT GROUPS #### +# Mostly for length, area, volume, mass, force +# (customary or specialized units) -# Most of the definitions that follows are derived from: -# See http://www.nist.gov/pml/wmd/pubs/hb44.cfm @group USCSLengthInternational + thou = 1e-3 * inch = th = mil_length inch = yard / 36 = in = international_inch = inches = international_inches + hand = 4 * inch foot = yard / 3 = ft = international_foot = feet = international_feet - yard = 0.9144 metres = yd = international_yard - mile = 1760 yard = mi = international_mile - - square_inch = 1 inch ** 2 = sq_in = square_inches - square_foot = 1 foot ** 2 = sq_ft = square_feet - square_yard = 1 yard ** 2 = sq_yd - square_mile = 1 mile ** 2 = sq_mi - - cubic_inch = 1 in ** 3 = cu_in - cubic_foot = 1 ft ** 3 = cu_ft = cubic_feet - cubic_yard = 1 yd ** 3 = cu_yd - - acre_foot = acre * foot = acre_feet + yard = 0.9144 * meter = yd = international_yard # since Jul 1959 + mile = 1760 * yard = mi = international_mile + + circular_mil = π / 4 * mil_length ** 2 = cmil + square_inch = inch ** 2 = sq_in = square_inches + square_foot = foot ** 2 = sq_ft = square_feet + square_yard = yard ** 2 = sq_yd + square_mile = mile ** 2 = sq_mi + + cubic_inch = in ** 3 = cu_in + cubic_foot = ft ** 3 = cu_ft = cubic_feet + cubic_yard = yd ** 3 = cu_yd @end @group USCSLengthSurvey - link = 0.66 survey_foot = li = survey_link - survey_foot = foot / 0.999998 = sft - rod = 16.5 survey_foot = rd = pole = perch - chain = 66 survey_foot - survey_mile = 5280 survey_foot - - acre = 43560 survey_foot ** 2 - square_rod = 1 rod ** 2 = sq_rod = sq_pole = sq_perch - - fathom = 6 survey_foot - us_statute_mile = 5280 survey_foot - league = 3 us_statute_mile - furlong = us_statute_mile / 8 + link = 1e-2 * chain = li = survey_link + survey_foot = 1200 / 3937 * meter = sft + fathom = 6 * survey_foot + rod = 16.5 * survey_foot = rd = pole = perch + chain = 4 * rod + furlong = 40 * rod = fur + cables_length = 120 * fathom + survey_mile = 5280 * survey_foot = smi = us_statute_mile + league = 3 * survey_mile + + square_rod = rod ** 2 = sq_rod = sq_pole = sq_perch + acre = 10 * chain ** 2 + square_survey_mile = survey_mile ** 2 = _ = section + square_league = league ** 2 + + acre_foot = acre * survey_foot = _ = acre_feet @end @group USCSDryVolume - dry_pint = 33.6003125 cubic_inch = dpi = US_dry_pint - dry_quart = 2 dry_pint = dqt = US_dry_quart - dry_gallon = 8 dry_pint = dgal = US_dry_gallon - peck = 16 dry_pint = pk - bushel = 64 dry_pint = bu - dry_barrel = 7065 cubic_inch = US_dry_barrel + dry_pint = bushel / 64 = dpi = US_dry_pint + dry_quart = bushel / 32 = dqt = US_dry_quart + dry_gallon = bushel / 8 = dgal = US_dry_gallon + peck = bushel / 4 = pk + bushel = 2150.42 cubic_inch = bu + dry_barrel = 7056 cubic_inch = _ = US_dry_barrel @end @group USCSLiquidVolume - minim = liquid_pint / 7680 - fluid_dram = liquid_pint / 128 = fldr = fluidram = US_fluid_dram - fluid_ounce = liquid_pint / 16 = floz = US_fluid_ounce = US_liquid_ounce - gill = liquid_pint / 4 = gi = liquid_gill = US_liquid_gill - - pint = 28.875 cubic_inch = pt = liquid_pint = US_pint - - quart = 2 liquid_pint = qt = liquid_quart = US_liquid_quart - gallon = 8 liquid_pint = gal = liquid_gallon = US_liquid_gallon + minim = pint / 7680 + fluid_dram = pint / 128 = fldr = fluidram = US_fluid_dram = US_liquid_dram + fluid_ounce = pint / 16 = floz = US_fluid_ounce = US_liquid_ounce + gill = pint / 4 = gi = liquid_gill = US_liquid_gill + pint = quart / 2 = pt = liquid_pint = US_pint + fifth = gallon / 5 = _ = US_liquid_fifth + quart = gallon / 4 = qt = liquid_quart = US_liquid_quart + gallon = 231 * cubic_inch = gal = liquid_gallon = US_liquid_gallon @end @group USCSVolumeOther - teaspoon = tablespoon / 3 = tsp - tablespoon = floz / 2 = tbsp = Tbsp = Tblsp = tblsp = tbs = Tbl + teaspoon = fluid_ounce / 6 = tsp + tablespoon = fluid_ounce / 2 = tbsp shot = 3 * tablespoon = jig = US_shot - cup = 8 fluid_ounce = cp = liquid_cup = US_liquid_cup + cup = pint / 2 = cp = liquid_cup = US_liquid_cup barrel = 31.5 * gallon = bbl oil_barrel = 42 * gallon = oil_bbl beer_barrel = 31 * gallon = beer_bbl @@ -381,65 +491,263 @@ stere = meter ** 3 @end @group Avoirdupois - grain = avdp_pound / 7000 = gr - drachm = pound / 256 = dr = avoirdupois_dram = avdp_dram = dram + dram = pound / 256 = dr = avoirdupois_dram = avdp_dram = drachm ounce = pound / 16 = oz = avoirdupois_ounce = avdp_ounce - pound = 453.59237 gram = lb = avoirdupois_pound = avdp_pound + pound = 7e3 * grain = lb = avoirdupois_pound = avdp_pound + stone = 14 * pound + quarter = 28 * stone + bag = 94 * pound + hundredweight = 100 * pound = cwt = short_hundredweight + long_hundredweight = 112 * pound + ton = 2e3 * pound = _ = short_ton + long_ton = 2240 * pound + slug = g_0 * pound * second ** 2 / foot + + force_ounce = g_0 * ounce = ozf = ounce_force + force_pound = g_0 * pound = lbf = pound_force + force_ton = g_0 * ton = _ = ton_force = force_short_ton = short_ton_force + force_long_ton = g_0 * long_ton = _ = long_ton_force + kip = 1e3 * force_pound + poundal = pound * foot / second ** 2 = pdl +@end - short_hunderdweight = 100 avoirdupois_pound = ch_cwt - long_hunderweight = 112 avoirdupois_pound = lg_cwt - short_ton = 2000 avoirdupois_pound - long_ton = 2240 avoirdupois_pound +@group AvoirdupoisUK using Avoirdupois + UK_hundredweight = long_hundredweight = UK_cwt + UK_ton = long_ton + UK_force_ton = force_long_ton = _ = UK_ton_force @end -@group Troy - pennyweight = 24 grain = dwt - troy_ounce = 480 grain = toz - troy_pound = 12 troy_ounce = tlb +@group AvoirdupoisUS using Avoirdupois + US_hundredweight = hundredweight = US_cwt + US_ton = ton + US_force_ton = force_ton = _ = US_ton_force @end -@group Apothecary - scruple = 20 grain - apothecary_dram = 3 scruple = ap_dr - apothecary_ounce = 8 apothecary_dram = ap_oz - apothecary_pound = 12 apothecary_ounce = ap_lb +@group Troy + pennyweight = 24 * grain = dwt + troy_ounce = 480 * grain = toz = ozt + troy_pound = 12 * troy_ounce = tlb = lbt @end -@group AvoirdupoisUK using Avoirdupois - stone = 14 pound - quarter = 28 stone - UK_hundredweight = long_hunderweight = UK_cwt - UK_ton = long_ton +@group Apothecary + scruple = 20 * grain + apothecary_dram = 3 * scruple = ap_dr + apothecary_ounce = 8 * apothecary_dram = ap_oz + apothecary_pound = 12 * apothecary_ounce = ap_lb @end -@group AvoirdupoisUS using Avoirdupois - US_hundredweight = short_hunderdweight = US_cwt - US_ton = short_ton = ton +@group ImperialVolume + imperial_minim = imperial_fluid_ounce / 480 + imperial_fluid_scruple = imperial_fluid_ounce / 24 + imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fldr = imperial_fluid_dram + imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce + imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill + imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup + imperial_pint = imperial_gallon / 8 = imperial_pt = UK_pint + imperial_quart = imperial_gallon / 4 = imperial_qt = UK_quart + imperial_gallon = 4.54609 * liter = imperial_gal = UK_gallon + imperial_peck = 2 * imperial_gallon = imperial_pk = UK_pk + imperial_bushel = 8 * imperial_gallon = imperial_bu = UK_bushel + imperial_barrel = 36 * imperial_gallon = imperial_bbl = UK_bbl @end @group Printer - # Length - pixel = [printing_unit] = dot = px = pel = picture_element + pica = inch / 6 = _ = printers_pica + point = pica / 12 = pp = printers_point = big_point = bp + didot = 1 / 2660 * m + cicero = 12 * didot + tex_point = inch / 72.27 + tex_pica = 12 * tex_point + tex_didot = 1238 / 1157 * tex_point + tex_cicero = 12 * tex_didot + scaled_point = tex_point / 65536 + css_pixel = inch / 96 = px + + pixel = [printing_unit] = _ = dot = pel = picture_element pixels_per_centimeter = pixel / cm = PPCM pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi bits_per_pixel = bit / pixel = bpp +@end + +@group Textile + tex = gram / kilometer = Tt + dtex = decitex + denier = gram / (9 * kilometer) = Td + jute = pound / (14400 * yard) = Tj + aberdeen = jute = Ta - point = yard / 216 / 12 = pp = printers_point - thou = yard / 36000 = th = mil - pica = yard / 216 = P̸ = printers_pica + number_english = 840 * yard / pound = Ne = NeC = ECC + number_meter = kilometer / kilogram = Nm @end -@group ImperialVolume - imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce - imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fluid_dram - imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill - imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup - imperial_pint = 568.26125 * milliliter = imperial_pt = UK_pint - imperial_quart = 2 * imperial_pint = imperial_qt = UK_quart - imperial_gallon = 8 * imperial_pint = imperial_gal = UK_gallon - imperial_peck = 16 * imperial_pint = imperial_pk = UK_pk - imperial_bushel = 64 * imperial_pint = imperial_bu = UK_bushel - imperial_barrel = 288 * imperial_pint = imperial_bbl = UK_bbl + +#### CGS ELECTROMAGNETIC UNITS #### + +# === Gaussian system of units === +@group Gaussian + franklin = erg ** 0.5 * centimeter ** 0.5 = Fr = statcoulomb = statC = esu + statvolt = erg / franklin = statV + statampere = franklin / second = statA + gauss = dyne / franklin = G + maxwell = gauss * centimeter ** 2 = Mx + oersted = dyne / maxwell = Oe = ørsted + statohm = statvolt / statampere = statΩ + statfarad = franklin / statvolt = statF + statmho = statampere / statvolt +@end +# Note this system is not commensurate with SI, as ε_0 and µ_0 disappear; +# some quantities with different dimensions in SI have the same +# dimensions in the Gaussian system (e.g. [Mx] = [Fr], but [Wb] != [C]), +# and therefore the conversion factors depend on the context (not in pint sense) +[gaussian_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[gaussian_current] = [gaussian_charge] / [time] +[gaussian_electric_potential] = [gaussian_charge] / [length] +[gaussian_electric_field] = [gaussian_electric_potential] / [length] +[gaussian_electric_displacement_field] = [gaussian_charge] / [area] +[gaussian_electric_flux] = [gaussian_charge] +[gaussian_electric_dipole] = [gaussian_charge] * [length] +[gaussian_electric_quadrupole] = [gaussian_charge] * [area] +[gaussian_magnetic_field] = [force] / [gaussian_charge] +[gaussian_magnetic_field_strength] = [gaussian_magnetic_field] +[gaussian_magnetic_flux] = [gaussian_magnetic_field] * [area] +[gaussian_magnetic_dipole] = [energy] / [gaussian_magnetic_field] +[gaussian_resistance] = [gaussian_electric_potential] / [gaussian_current] +[gaussian_resistivity] = [gaussian_resistance] * [length] +[gaussian_capacitance] = [gaussian_charge] / [gaussian_electric_potential] +[gaussian_inductance] = [gaussian_electric_potential] * [time] / [gaussian_current] +[gaussian_conductance] = [gaussian_current] / [gaussian_electric_potential] +@context Gaussian = Gau + [gaussian_charge] -> [charge]: value / k_C ** 0.5 + [charge] -> [gaussian_charge]: value * k_C ** 0.5 + [gaussian_current] -> [current]: value / k_C ** 0.5 + [current] -> [gaussian_current]: value * k_C ** 0.5 + [gaussian_electric_potential] -> [electric_potential]: value * k_C ** 0.5 + [electric_potential] -> [gaussian_electric_potential]: value / k_C ** 0.5 + [gaussian_electric_field] -> [electric_field]: value * k_C ** 0.5 + [electric_field] -> [gaussian_electric_field]: value / k_C ** 0.5 + [gaussian_electric_displacement_field] -> [electric_displacement_field]: value / (4 * π / ε_0) ** 0.5 + [electric_displacement_field] -> [gaussian_electric_displacement_field]: value * (4 * π / ε_0) ** 0.5 + [gaussian_electric_dipole] -> [electric_dipole]: value / k_C ** 0.5 + [electric_dipole] -> [gaussian_electric_dipole]: value * k_C ** 0.5 + [gaussian_electric_quadrupole] -> [electric_quadrupole]: value / k_C ** 0.5 + [electric_quadrupole] -> [gaussian_electric_quadrupole]: value * k_C ** 0.5 + [gaussian_magnetic_field] -> [magnetic_field]: value / (4 * π / µ_0) ** 0.5 + [magnetic_field] -> [gaussian_magnetic_field]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_flux] -> [magnetic_flux]: value / (4 * π / µ_0) ** 0.5 + [magnetic_flux] -> [gaussian_magnetic_flux]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π * µ_0) ** 0.5 + [magnetic_field_strength] -> [gaussian_magnetic_field_strength]: value * (4 * π * µ_0) ** 0.5 + [gaussian_magnetic_dipole] -> [magnetic_dipole]: value * (4 * π / µ_0) ** 0.5 + [magnetic_dipole] -> [gaussian_magnetic_dipole]: value / (4 * π / µ_0) ** 0.5 + [gaussian_resistance] -> [resistance]: value * k_C + [resistance] -> [gaussian_resistance]: value / k_C + [gaussian_resistivity] -> [resistivity]: value * k_C + [resistivity] -> [gaussian_resistivity]: value / k_C + [gaussian_capacitance] -> [capacitance]: value / k_C + [capacitance] -> [gaussian_capacitance]: value * k_C + [gaussian_inductance] -> [inductance]: value * k_C + [inductance] -> [gaussian_inductance]: value / k_C + [gaussian_conductance] -> [conductance]: value / k_C + [conductance] -> [gaussian_conductance]: value * k_C +@end + +# === ESU system of units === +# (where different from Gaussian) +# See note for Gaussian system too +@group ESU using Gaussian + statweber = statvolt * second = statWb + stattesla = statweber / centimeter ** 2 = statT + stathenry = statweber / statampere = statH +@end +[esu_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[esu_current] = [esu_charge] / [time] +[esu_electric_potential] = [esu_charge] / [length] +[esu_magnetic_flux] = [esu_electric_potential] * [time] +[esu_magnetic_field] = [esu_magnetic_flux] / [area] +[esu_magnetic_field_strength] = [esu_current] / [length] +[esu_magnetic_dipole] = [esu_current] * [area] +@context ESU = esu + [esu_magnetic_field] -> [magnetic_field]: value * k_C ** 0.5 + [magnetic_field] -> [esu_magnetic_field]: value / k_C ** 0.5 + [esu_magnetic_flux] -> [magnetic_flux]: value * k_C ** 0.5 + [magnetic_flux] -> [esu_magnetic_flux]: value / k_C ** 0.5 + [esu_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π / ε_0) ** 0.5 + [magnetic_field_strength] -> [esu_magnetic_field_strength]: value * (4 * π / ε_0) ** 0.5 + [esu_magnetic_dipole] -> [magnetic_dipole]: value / k_C ** 0.5 + [magnetic_dipole] -> [esu_magnetic_dipole]: value * k_C ** 0.5 +@end + + +#### CONVERSION CONTEXTS #### + +@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 + # allow wavenumber / kayser + [wavenumber] <-> [length]: 1 / value +@end + +@context boltzmann + [temperature] -> [energy]: boltzmann_constant * value + [energy] -> [temperature]: value / boltzmann_constant +@end + +@context energy + [energy] -> [energy] / [substance]: value * N_A + [energy] / [substance] -> [energy]: value / N_A + [energy] -> [mass]: value / c ** 2 + [mass] -> [energy]: value * c ** 2 +@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 + +@context textile + # Allow switching between Direct count system (i.e. tex) and + # Indirect count system (i.e. Ne, Nm) + [mass] / [length] <-> [length] / [mass]: 1 / value +@end + + +#### SYSTEMS OF UNITS #### + +@system SI + second + meter + kilogram + ampere + kelvin + mole + candela @end @system mks using international @@ -448,12 +756,30 @@ stere = meter ** 3 second @end -@system cgs using international +@system cgs using international, Gaussian, ESU centimeter gram second @end +@system atomic using international + # based on unit m_e, e, hbar, k_C, k + bohr: meter + electron_mass: gram + atomic_unit_of_time: second + atomic_unit_of_current: ampere + atomic_unit_of_temperature: kelvin +@end + +@system Planck using international + # based on unit c, gravitational_constant, hbar, k_C, k + planck_length: meter + planck_mass: gram + planck_time: second + planck_current: ampere + planck_temperature: kelvin +@end + @system imperial using ImperialVolume, USCSLengthInternational, AvoirdupoisUK yard pound From 9315c892e71a40cd9f9bc2ed5e566b4639868006 Mon Sep 17 00:00:00 2001 From: good597 Date: Tue, 6 Aug 2019 15:44:33 -0700 Subject: [PATCH 048/612] restore numpy formatting docs --- docs/tutorial.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 9e5d381a9..b10cc574a 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -292,6 +292,17 @@ Pint's physical quantities can be easily printed: >>> print('The magnitude is {0.magnitude} with units {0.units}'.format(accel)) The magnitude is 1.3 with units meter / second ** 2 +Pint supports float formatting for numpy arrays as well: + +.. doctest:: + + >>> accel = np.array([-1.1, 1e-6, 1.2505, 1.3]) * ureg['meter/second**2'] + >>> # float formatting numpy arrays + >>> print('The array is {:.2f}'.format(accel)) + The array is [-1.10 0.00 1.25 1.30] meter / second ** 2 + >>> # scientific form formatting with unit pretty printing + >>> print('The array is {:+.2E~P}'.format(accel)) + The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: From 4c387454eec4b5ff24a93006e227a341b2f4cd3b Mon Sep 17 00:00:00 2001 From: good597 Date: Tue, 6 Aug 2019 15:49:00 -0700 Subject: [PATCH 049/612] Added f-string documentation as suggested by @cpascual --- docs/tutorial.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b10cc574a..54638a8b9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -303,6 +303,22 @@ Pint supports float formatting for numpy arrays as well: >>> # scientific form formatting with unit pretty printing >>> print('The array is {:+.2E~P}'.format(accel)) The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² + +Pint also supports 'f-strings'_ from python>=3.6 : + +.. doctest:: + + >>> accel = 1.3 * ureg['meter/second**2'] + >>> print(f'The str is {accel}') + The str is 1.3 meter / second ** 2 + >>> print(f'The str is {accel:.3e}') + The str is 1.300e+00 meter / second ** 2 + >>> print(f'The str is {accel:~}') + The str is 1.3 m / s ** 2 + >>> print(f'The str is {accel:~.3e}') + The str is 1.300e+00 m / s ** 2 + >>> print(f'The str is {accel:~H}') + The str is 1.3 m/s² But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: @@ -421,4 +437,5 @@ also define the registry as the application registry:: .. _eval: http://docs.python.org/3/library/functions.html#eval .. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html .. _`Babel`: http://babel.pocoo.org/ -.. _'formatting syntax': https://docs.python.org/3/library/string.html#format-specification-mini-language \ No newline at end of file +.. _'formatting syntax': https://docs.python.org/3/library/string.html#format-specification-mini-language +.. _'f-strings': https://www.python.org/dev/peps/pep-0498/ \ No newline at end of file From ece16aa87f9e1e9b40f9118f6f0587361f85f881 Mon Sep 17 00:00:00 2001 From: xtreak Date: Sat, 29 Jun 2019 04:58:59 +0000 Subject: [PATCH 050/612] Use context manager for assertWarns and fix DeprecationWarning --- pint/testsuite/parameterized.py | 7 ++++++- pint/testsuite/test_quantity.py | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 9b920373a..7c4591574 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -32,6 +32,11 @@ import collections import unittest +try: + from collections.abc import Callable +except ImportError: + from collections import Callable + def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" def wrapper(cls): @@ -69,7 +74,7 @@ def __new__(meta, classname, bases, class_dict): new_class_dict = {} for attr_name, attr_value in list(class_dict.items()): - if isinstance(attr_value, collections.Callable) and hasattr(attr_value, 'param_names'): + if isinstance(attr_value, Callable) and hasattr(attr_value, 'param_names'): # print("Processing attr_name = %r; attr_value = %r" % ( # attr_name, attr_value)) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fdb246007..232eea2ca 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -383,7 +383,7 @@ def test_from_sequence(self): self.assertFalse(u_array_2.u == u_array_ref_reversed.u) u_array_3 = self.Q_.from_sequence(u_seq_reversed, units='g') - self.assertTrue(all(u_array_3 == u_array_ref_reversed)) + self.assertTrue(all(u_array_3 == u_array_ref_reversed)) self.assertTrue(u_array_3.u == u_array_ref_reversed.u) with self.assertRaises(ValueError): @@ -454,7 +454,8 @@ def test_limits_magnitudes(self): def test_nonnumeric_magnitudes(self): ureg = self.ureg x = "some string"*ureg.m - self.assertRaises(RuntimeError, self.compareQuantity_compact(x,x)) + with self.assertWarns(RuntimeWarning): + self.compareQuantity_compact(x,x) class TestQuantityBasicMath(QuantityTestCase): From 69ef39ed30b42a6b6044f3db4027f9972b1270f6 Mon Sep 17 00:00:00 2001 From: xtreak Date: Sat, 17 Aug 2019 13:06:34 +0000 Subject: [PATCH 051/612] Use assertWarns only in Python 3 --- pint/testsuite/test_quantity.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 232eea2ca..41c690381 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -454,8 +454,11 @@ def test_limits_magnitudes(self): def test_nonnumeric_magnitudes(self): ureg = self.ureg x = "some string"*ureg.m - with self.assertWarns(RuntimeWarning): - self.compareQuantity_compact(x,x) + if PYTHON3: + with self.assertWarns(RuntimeWarning): + self.compareQuantity_compact(x,x) + else: + self.assertRaises(RuntimeError, self.compareQuantity_compact(x,x)) class TestQuantityBasicMath(QuantityTestCase): From cfa7171ac82dcb59031635cb6645a973b01fff93 Mon Sep 17 00:00:00 2001 From: Christoph Buchner Date: Wed, 21 Aug 2019 12:01:38 +0200 Subject: [PATCH 052/612] Improve OffsetUnitCalculusError message. Closes #839. --- pint/errors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/errors.py b/pint/errors.py index 65ad56b79..e5bc72d93 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -109,6 +109,7 @@ def __init__(self, units1, units2='', extra_msg=''): def __str__(self): msg = ("Ambiguous operation with offset unit (%s)." % ', '.join(['%s' % u for u in [self.units1, self.units2] if u]) + + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance." + self.extra_msg) return msg.format(self.units1, self.units2) From 788e4736302faddf202bea18689c3162d128919e Mon Sep 17 00:00:00 2001 From: Andrew Savage Date: Thu, 22 Aug 2019 10:31:55 +0900 Subject: [PATCH 053/612] try fix travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a759926d6..6232204a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,8 +39,8 @@ before_install: - conda info -a # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda - - sudo rm -rf /dev/shm - - sudo ln -s /run/shm /dev/shm + #- sudo rm -rf /dev/shm + #- sudo ln -s /run/shm /dev/shm - export ENV_NAME=travis From 4913dff700eb031bdfd2d5994a693d48a4438674 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 13:53:13 +0100 Subject: [PATCH 054/612] Explained '_' symbols in documentation --- docs/defining.rst | 15 ++++++++++++--- pint/default_en.txt | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/defining.rst b/docs/defining.rst index 0a0214750..50c177b09 100644 --- a/docs/defining.rst +++ b/docs/defining.rst @@ -16,9 +16,16 @@ other units. For example this is how the minute and the hour are defined in minute = 60 * second = min It is quite straightforward, isn't it? We are saying that `minute` is -`60 seconds` and is also known as `min`. The first word is always the canonical -name. Next comes the definition (based on other units). Finally, a list of -aliases, separated by equal signs. +`60 seconds` and is also known as `min`. + +1. The first word is always the canonical name. +2. Next comes the definition (based on other units). +3. Next, optionally, there is the unit symbol. +4. Finally, again optionally, a list of aliases, separated by equal signs. + If one wants to specify aliases but not a symbol, the symbol should be + conventionally set to ``_``; e.g.:: + + millennium = 1e3 * year = _ = millennia The order in which units are defined does not matter, Pint will resolve the dependencies to define them in the right order. What is important is that if @@ -93,6 +100,8 @@ You can easily add units to the registry programmatically. Let's add a dog_year Note that we have used the name `dog_years` even though we have not defined the plural form as an alias. Pint takes care of that, so you don't have to. +Plural forms that aren't simply built by adding a 's' suffix to the singular form +should be explicitly stated as aliases (see for example ``millennia`` above). You can also add prefixes programmatically: diff --git a/pint/default_en.txt b/pint/default_en.txt index 55daf6b5e..0176761d1 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -3,6 +3,22 @@ # Language: english # :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. +# Definition syntax +# ================= +# = [= ] [= ] [ = ] [...] +# +# The canonical name and aliases should be expressed in singular form. +# Pint automatically deals with plurals built by adding 's' to the singular form; plural +# forms that don't follow this rule should be instead explicitly listed as aliases. +# +# If a unit has no symbol and one wants to define aliases, then the symbol should be +# conventionally set to _. +# +# Example: +# millennium = 1e3 * year = _ = millennia +# +# See also: https://pint.readthedocs.io/en/latest/defining.html + @defaults group = international system = mks From b7804ce7637bdf6a577db2f1b207c12f65c99641 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 14:22:14 +0100 Subject: [PATCH 055/612] Add issue number to already existing test --- pint/testsuite/test_issues.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index bb79699ce..6985a152b 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -668,11 +668,7 @@ def test_issue655a(self): self.assertEqual(velocity.check('[length] / [time]'), True) self.assertEqual(velocity.check('1 / [time] * [length]'), True) - def test_issue783(self): - ureg = UnitRegistry() - assert not ureg('g') == [] - - def test_issue(self): + def test_issue655b(self): import math try: from inspect import signature @@ -695,4 +691,6 @@ def pendulum_period(length, G=Q_(1, 'standard_gravity')): t = pendulum_period(l, moon_gravity) self.assertAlmostEqual(t, Q_('4.928936075204336 second')) - + def test_issue783(self): + ureg = UnitRegistry() + assert not ureg('g') == [] From d649f76f3fb39958b55dbdc70fbe3e1c84de96da Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 30 Aug 2019 10:57:30 -0300 Subject: [PATCH 056/612] Try to fix travis --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 033439fe6..844277492 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,8 +42,9 @@ before_install: - conda info -a # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda - - sudo rm -rf /dev/shm - - sudo ln -s /run/shm /dev/shm + # But broke travis 2019-08 + # - sudo rm -rf /dev/shm + # - sudo ln -s /run/shm /dev/shm - export ENV_NAME=travis From 4ff5a28d97d4a0c6f9d4b6d1592e8b79448102a4 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 15:15:32 +0100 Subject: [PATCH 057/612] Fix crash in copy.deepcopy(ParserHelper(1)) --- pint/systems.py | 3 +++ pint/util.py | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pint/systems.py b/pint/systems.py index 47b1ed1f1..e2e41258b 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -297,6 +297,9 @@ def __dir__(self): return list(self.members) def __getattr__(self, item): + if item == "__setstate__": + raise AttributeError(item) + u = getattr(self._REGISTRY, self.name + '_' + item, None) if u is not None: return u diff --git a/pint/util.py b/pint/util.py index 13fc14934..9add87a26 100644 --- a/pint/util.py +++ b/pint/util.py @@ -309,13 +309,6 @@ def __contains__(self, key): def __hash__(self): return self._hash - def __getstate__(self): - return {'_d': self._d, '_hash': self._hash} - - def __setstate__(self, state): - self._d = state['_d'] - self._hash = state['_hash'] - def __eq__(self, other): if isinstance(other, UnitsContainer): other = other._d From a9a97ba98167a6a20df874a14343d303a3cd2163 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 15:18:52 +0100 Subject: [PATCH 058/612] Many speedups in UnitsContainer and ParserHelper --- pint/util.py | 79 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/pint/util.py b/pint/util.py index 9add87a26..34fa73cfd 100644 --- a/pint/util.py +++ b/pint/util.py @@ -243,6 +243,9 @@ class udict(dict): def __missing__(self, key): return 0. + def copy(self): + return udict(self) + class UnitsContainer(Mapping): """The UnitsContainer stores the product of units and their respective @@ -264,7 +267,7 @@ def __init__(self, *args, **kwargs): raise TypeError('value must be a number, not {}'.format(type(value))) if not isinstance(value, float): d[key] = float(value) - self._hash = hash(frozenset(self._d.items())) + self._hash = None def copy(self): return self.__copy__() @@ -275,24 +278,28 @@ def add(self, key, value): if newval: new._d[key] = newval else: - del new._d[key] - + new._d.pop(key, None) + new._hash = None return new def remove(self, keys): """ Create a new UnitsContainer purged from given keys. """ - d = udict(self._d) - return UnitsContainer(((key, d[key]) for key in d if key not in keys)) + new = self.copy() + for k in keys: + new._d.pop(k, None) + new._hash = None + return new def rename(self, oldkey, newkey): """ Create a new UnitsContainer in which an entry has been renamed. """ - d = udict(self._d) - d[newkey] = d.pop(oldkey) - return UnitsContainer(d) + new = self.copy() + new._d[newkey] = new._d.pop(oldkey) + new._hash = None + return new def __iter__(self): return iter(self._d) @@ -307,12 +314,15 @@ def __contains__(self, key): return key in self._d def __hash__(self): + if self._hash is None: + self._hash = hash(frozenset(self._d.items())) return self._hash def __eq__(self, other): if isinstance(other, UnitsContainer): - other = other._d - elif isinstance(other, string_types): + # Not the same as hash(self); see ParserHelper.__hash__ and __eq__ + return UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other) + if isinstance(other, string_types): other = ParserHelper.from_string(other) other = other._d @@ -333,20 +343,25 @@ def format_babel(self, spec, **kwspec): return format_unit(self, spec, **kwspec) def __copy__(self): - return UnitsContainer(self._d) + # Skip expensive health checks performed by __init__ + out = object.__new__(self.__class__) + out._d = self._d.copy() + out._hash = self._hash + return out def __mul__(self, other): - d = udict(self._d) if not isinstance(other, self.__class__): err = 'Cannot multiply UnitsContainer by {}' raise TypeError(err.format(type(other))) + + new = self.copy() for key, value in other.items(): - d[key] += value - keys = [key for key, value in d.items() if value == 0] - for key in keys: - del d[key] + new._d[key] += value + if new._d[key] == 0: + del new._d[key] - return UnitsContainer(d) + new._hash = None + return new __rmul__ = __mul__ @@ -354,26 +369,26 @@ def __pow__(self, other): if not isinstance(other, NUMERIC_TYPES): err = 'Cannot power UnitsContainer by {}' raise TypeError(err.format(type(other))) - d = udict(self._d) - for key, value in d.items(): - d[key] *= other - return UnitsContainer(d) + + new = self.copy() + for key, value in new._d.items(): + new._d[key] *= other + new._hash = None + return new def __truediv__(self, other): if not isinstance(other, self.__class__): err = 'Cannot divide UnitsContainer by {}' raise TypeError(err.format(type(other))) - d = udict(self._d) - + new = self.copy() for key, value in other.items(): - d[key] -= value - - keys = [key for key, value in d.items() if value == 0] - for key in keys: - del d[key] + new._d[key] -= value + if new._d[key] == 0: + del new._d[key] - return UnitsContainer(d) + new._hash = None + return new def __rtruediv__(self, other): if not isinstance(other, self.__class__) and other != 1: @@ -461,7 +476,9 @@ def _from_string(cls, input_string): for key, value in ret.items())) def __copy__(self): - return ParserHelper(scale=self.scale, **self) + new = super(ParserHelper, self).__copy__() + new.scale = self.scale + return new def copy(self): return self.__copy__() @@ -470,7 +487,7 @@ def __hash__(self): if self.scale != 1.0: mess = 'Only scale 1.0 ParserHelper instance should be considered hashable' raise ValueError(mess) - return self._hash + return super(ParserHelper, self).__hash__() def __eq__(self, other): if isinstance(other, self.__class__): From 90c5aeee2047d0e32d4760a7abd48ad7c84cfb5a Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 15:32:35 +0100 Subject: [PATCH 059/612] BaseRegistry.__getattr__ to ignore double-underscore attributes --- pint/registry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 0129ba5d5..667b97e2e 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -195,8 +195,11 @@ def _parse_defaults(self, ifile): self._defaults[k.strip()] = v.strip() def __getattr__(self, item): - if item[0] == '_': - return super(BaseRegistry, self).__getattribute__(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('__'): + # Generate AttributeError + return object.__getattr__(self, item) return self.Unit(item) def __getitem__(self, item): From 9e14bb4a9044e1ef9aae327d4ca544db41bf084b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 15:51:41 +0100 Subject: [PATCH 060/612] Enable many tests in test_issues when numpy is not available --- pint/testsuite/test_issues.py | 448 ++++++++++++++++------------------ 1 file changed, 215 insertions(+), 233 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index bb79699ce..15762c1b3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -45,6 +45,80 @@ def test_issue29(self): self.assertEqual(t._units, UnitsContainer(milliwatt=1)) self.assertEqual(t.to('joule / second'), 4e-3 * ureg('W')) + @unittest.expectedFailure + @helpers.requires_numpy() + def test_issue37(self): + x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) + ureg = UnitRegistry() + q = ureg.meter * x + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + q = x * ureg.meter + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + + m = np.ma.masked_array(2 * np.ones(3,3)) + qq = q * m + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + qq = m * q + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + + @unittest.expectedFailure + @helpers.requires_numpy() + def test_issue39(self): + x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) + ureg = UnitRegistry() + q = ureg.meter * x + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + q = x * ureg.meter + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + + m = np.matrix(2 * np.ones(3,3)) + qq = q * m + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + qq = m * q + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + + @helpers.requires_numpy() + def test_issue44(self): + ureg = UnitRegistry() + x = 4. * ureg.dimensionless + np.sqrt(x) + self.assertQuantityAlmostEqual(np.sqrt([4.] * ureg.dimensionless), [2.] * ureg.dimensionless) + self.assertQuantityAlmostEqual(np.sqrt(4. * ureg.dimensionless), 2. * ureg.dimensionless) + + def test_issue45(self): + import math + ureg = UnitRegistry() + self.assertAlmostEqual(math.sqrt(4 * ureg.m/ureg.cm), math.sqrt(4 * 100)) + self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.) + + @helpers.requires_numpy() + def test_issue45b(self): + ureg = UnitRegistry() + self.assertAlmostEqual(np.sin([np.pi/2] * ureg.m / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless)) + self.assertAlmostEqual(np.sin([np.pi/2] * ureg.cm / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless * 0.01)) + + def test_issue50(self): + ureg = UnitRegistry() + Q_ = ureg.Quantity + self.assertEqual(Q_(100), 100 * ureg.dimensionless) + self.assertEqual(Q_('100'), 100 * ureg.dimensionless) + def test_issue52(self): u1 = UnitRegistry() u2 = UnitRegistry() @@ -86,6 +160,11 @@ def test_issue61_notNP(self): self.assertRaises(TypeError, Q_, value) self.assertRaises(TypeError, Q_, value, 'meter') + def test_issue62(self): + ureg = UnitRegistry() + m = ureg('m**0.5') + self.assertEqual(str(m.units), 'meter ** 0.5') + def test_issue66(self): ureg = UnitRegistry() self.assertEqual(ureg.get_dimensionality(UnitsContainer({'[temperature]': 1})), @@ -107,6 +186,46 @@ def test_issue69(self): q = ureg('m').to(ureg('in')) self.assertEqual(q, ureg('m').to('in')) + @helpers.requires_numpy() + def test_issue74(self): + ureg = UnitRegistry() + v1 = np.asarray([1., 2., 3.]) + v2 = np.asarray([3., 2., 1.]) + q1 = v1 * ureg.ms + q2 = v2 * ureg.ms + + np.testing.assert_array_equal(q1 < q2, v1 < v2) + np.testing.assert_array_equal(q1 > q2, v1 > v2) + + np.testing.assert_array_equal(q1 <= q2, v1 <= v2) + np.testing.assert_array_equal(q1 >= q2, v1 >= v2) + + q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s + v2s = q2s.to('ms').magnitude + + np.testing.assert_array_equal(q1 < q2s, v1 < v2s) + np.testing.assert_array_equal(q1 > q2s, v1 > v2s) + + np.testing.assert_array_equal(q1 <= q2s, v1 <= v2s) + np.testing.assert_array_equal(q1 >= q2s, v1 >= v2s) + + @helpers.requires_numpy() + def test_issue75(self): + ureg = UnitRegistry() + v1 = np.asarray([1., 2., 3.]) + v2 = np.asarray([3., 2., 1.]) + q1 = v1 * ureg.ms + q2 = v2 * ureg.ms + + np.testing.assert_array_equal(q1 == q2, v1 == v2) + np.testing.assert_array_equal(q1 != q2, v1 != v2) + + q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s + v2s = q2s.to('ms').magnitude + + np.testing.assert_array_equal(q1 == q2s, v1 == v2s) + np.testing.assert_array_equal(q1 != q2s, v1 != v2s) + @helpers.requires_uncertainties() def test_issue77(self): ureg = UnitRegistry() @@ -205,32 +324,36 @@ def test_issue93(self): self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - def test_issue523(self): + @helpers.requires_numpy_previous_than('1.10') + def test_issue94(self): ureg = UnitRegistry() - src, dst = UnitsContainer({'meter': 1}), UnitsContainer({'degF': 1}) - value = 10. - convert = self.ureg.convert - self.assertRaises(DimensionalityError, convert, value, src, dst) - self.assertRaises(DimensionalityError, convert, value, dst, src) + v1 = np.array([5, 5]) * ureg.meter + v2 = 0.1 * ureg.meter + v3 = np.array([5, 5]) * ureg.meter + v3 += v2 - def _test_issueXX(self): - ureg = UnitRegistry() - try: - ureg.convert(1, ureg.degC, ureg.kelvin * ureg.meter / ureg.nanometer) - except: - self.assertTrue(False, - 'Error while trying to convert {} to {}'.format(ureg.degC, ureg.kelvin * ureg.meter / ureg.nanometer)) + np.testing.assert_array_equal((v1 + v2).magnitude, np.array([5.1, 5.1])) + np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) - def test_issue121(self): - sh = (2, 1) + def test_issue104(self): ureg = UnitRegistry() - z, v = 0, 2. - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) + x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] + y = [ureg('1 meter')] * 3 + + def summer(values): + if not values: + return 0 + total = values[0] + for v in values[1:]: + total += v + + return total + + self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) + self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) def test_issue105(self): ureg = UnitRegistry() @@ -245,25 +368,61 @@ def test_issue105(self): self.assertRaises(AttributeError, func, 'METER') self.assertEqual(val, func('METER', False)) - def test_issue104(self): + def test_issue121(self): + sh = (2, 1) ureg = UnitRegistry() + z, v = 0, 2. + self.assertEqual(z + v * ureg.meter, v * ureg.meter) + self.assertEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertEqual(v * ureg.meter + z, v * ureg.meter) + self.assertEqual(v * ureg.meter - z, v * ureg.meter) - x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] - y = [ureg('1 meter')] * 3 + self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - def summer(values): - if not values: - return 0 - total = values[0] - for v in values[1:]: - total += v + @helpers.requires_numpy18() + def test_issue121b(self): + sh = (2, 1) + ureg = UnitRegistry() - return total + z, v = 0, 2. + self.assertEqual(z + v * ureg.meter, v * ureg.meter) + self.assertEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertEqual(v * ureg.meter + z, v * ureg.meter) + self.assertEqual(v * ureg.meter - z, v * ureg.meter) - self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) - self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) + self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) + + z, v = np.zeros(sh), 2. * np.ones(sh) + self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) + self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) + self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) + + z, v = np.zeros((3, 1)), 2. * np.ones(sh) + for x, y in ((z, v), + (z, v * ureg.meter), + (v * ureg.meter, z) + ): + try: + w = x + y + self.assertTrue(False, "ValueError not raised") + except ValueError: + pass + try: + w = x - y + self.assertTrue(False, "ValueError not raised") + except ValueError: + pass + + @helpers.requires_numpy() + def test_issue127(self): + q = [1., 2., 3., 4.] * self.ureg.meter + q[0] = np.nan + self.assertNotEqual(q[0], 1.) + self.assertTrue(math.isnan(q[0].magnitude)) + q[1] = float('NaN') + self.assertNotEqual(q[1], 2.) + self.assertTrue(math.isnan(q[1].magnitude)) def test_issue170(self): Q_ = UnitRegistry().Quantity @@ -316,193 +475,6 @@ def test_micro_creation(self): else: ureg.Quantity(2, 'µm') -@helpers.requires_numpy() -class TestIssuesNP(QuantityTestCase): - - FORCE_NDARRAY = False - - @unittest.expectedFailure - def test_issue37(self): - x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) - ureg = UnitRegistry() - q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - - m = np.ma.masked_array(2 * np.ones(3,3)) - qq = q * m - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - qq = m * q - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - - @unittest.expectedFailure - def test_issue39(self): - x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) - ureg = UnitRegistry() - q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - - m = np.matrix(2 * np.ones(3,3)) - qq = q * m - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - qq = m * q - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - - def test_issue44(self): - ureg = UnitRegistry() - x = 4. * ureg.dimensionless - np.sqrt(x) - self.assertQuantityAlmostEqual(np.sqrt([4.] * ureg.dimensionless), [2.] * ureg.dimensionless) - self.assertQuantityAlmostEqual(np.sqrt(4. * ureg.dimensionless), 2. * ureg.dimensionless) - - def test_issue45(self): - import math - ureg = UnitRegistry() - self.assertAlmostEqual(math.sqrt(4 * ureg.m/ureg.cm), math.sqrt(4 * 100)) - self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.) - - def test_issue45b(self): - ureg = UnitRegistry() - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.m / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless)) - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.cm / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless * 0.01)) - - def test_issue50(self): - ureg = UnitRegistry() - Q_ = ureg.Quantity - self.assertEqual(Q_(100), 100 * ureg.dimensionless) - self.assertEqual(Q_('100'), 100 * ureg.dimensionless) - - def test_issue62(self): - ureg = UnitRegistry() - m = ureg('m**0.5') - self.assertEqual(str(m.units), 'meter ** 0.5') - - def test_issue74(self): - ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) - q1 = v1 * ureg.ms - q2 = v2 * ureg.ms - - np.testing.assert_array_equal(q1 < q2, v1 < v2) - np.testing.assert_array_equal(q1 > q2, v1 > v2) - - np.testing.assert_array_equal(q1 <= q2, v1 <= v2) - np.testing.assert_array_equal(q1 >= q2, v1 >= v2) - - q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude - - np.testing.assert_array_equal(q1 < q2s, v1 < v2s) - np.testing.assert_array_equal(q1 > q2s, v1 > v2s) - - np.testing.assert_array_equal(q1 <= q2s, v1 <= v2s) - np.testing.assert_array_equal(q1 >= q2s, v1 >= v2s) - - def test_issue75(self): - ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) - q1 = v1 * ureg.ms - q2 = v2 * ureg.ms - - np.testing.assert_array_equal(q1 == q2, v1 == v2) - np.testing.assert_array_equal(q1 != q2, v1 != v2) - - q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude - - np.testing.assert_array_equal(q1 == q2s, v1 == v2s) - np.testing.assert_array_equal(q1 != q2s, v1 != v2s) - - def test_issue93(self): - ureg = UnitRegistry() - x = 5 * ureg.meter - self.assertIsInstance(x.magnitude, int) - y = 0.1 * ureg.meter - self.assertIsInstance(y.magnitude, float) - z = 5 * ureg.meter - self.assertIsInstance(z.magnitude, int) - z += y - self.assertIsInstance(z.magnitude, float) - - self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) - self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - - @helpers.requires_numpy_previous_than('1.10') - def test_issue94(self): - ureg = UnitRegistry() - v1 = np.array([5, 5]) * ureg.meter - v2 = 0.1 * ureg.meter - v3 = np.array([5, 5]) * ureg.meter - v3 += v2 - - np.testing.assert_array_equal((v1 + v2).magnitude, np.array([5.1, 5.1])) - np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) - - @helpers.requires_numpy18() - def test_issue121(self): - sh = (2, 1) - ureg = UnitRegistry() - - z, v = 0, 2. - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - - z, v = np.zeros(sh), 2. * np.ones(sh) - self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) - self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) - - z, v = np.zeros((3, 1)), 2. * np.ones(sh) - for x, y in ((z, v), - (z, v * ureg.meter), - (v * ureg.meter, z) - ): - try: - w = x + y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass - try: - w = x - y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass - - def test_issue127(self): - q = [1., 2., 3., 4.] * self.ureg.meter - q[0] = np.nan - self.assertNotEqual(q[0], 1.) - self.assertTrue(math.isnan(q[0].magnitude)) - q[1] = float('NaN') - self.assertNotEqual(q[1], 2.) - self.assertTrue(math.isnan(q[1].magnitude)) - def test_issue171_real_imag(self): qr = [1., 2., 3., 4.] * self.ureg.meter qi = [4., 3., 2., 1.] * self.ureg.meter @@ -510,12 +482,14 @@ def test_issue171_real_imag(self): self.assertQuantityEqual(q.real, qr) self.assertQuantityEqual(q.imag, qi) + @helpers.requires_numpy() def test_issue171_T(self): a = np.asarray([[1., 2., 3., 4.],[4., 3., 2., 1.]]) q1 = a * self.ureg.meter q2 = a.T * self.ureg.meter self.assertQuantityEqual(q1.T, q2) + @helpers.requires_numpy() def test_issue250(self): a = self.ureg.V b = self.ureg.mV @@ -553,11 +527,6 @@ def test_issue354_356_370(self): self.assertEqual('{0:~}'.format(1 * self.ureg('MiB')), '1 MiB') - def test_issue482(self): - q = self.ureg.Quantity(1, self.ureg.dimensionless) - qe = np.exp(q) - self.assertIsInstance(qe, self.ureg.Quantity) - def test_issue468(self): ureg = UnitRegistry() @@ -570,6 +539,13 @@ def f(x): z = x * y self.assertEquals(z, ureg.Quantity(1., 'meter * kilogram')) + @helpers.requires_numpy() + def test_issue482(self): + q = self.ureg.Quantity(1, self.ureg.dimensionless) + qe = np.exp(q) + self.assertIsInstance(qe, self.ureg.Quantity) + + @helpers.requires_numpy() def test_issue483(self): ureg = self.ureg a = np.asarray([1, 2, 3]) @@ -577,6 +553,14 @@ def test_issue483(self): p = (q ** q).m np.testing.assert_array_equal(p, a ** a) + def test_issue523(self): + ureg = UnitRegistry() + src, dst = UnitsContainer({'meter': 1}), UnitsContainer({'degF': 1}) + value = 10. + convert = self.ureg.convert + self.assertRaises(DimensionalityError, convert, value, src, dst) + self.assertRaises(DimensionalityError, convert, value, dst, src) + def test_issue532(self): ureg = self.ureg @@ -668,11 +652,7 @@ def test_issue655a(self): self.assertEqual(velocity.check('[length] / [time]'), True) self.assertEqual(velocity.check('1 / [time] * [length]'), True) - def test_issue783(self): - ureg = UnitRegistry() - assert not ureg('g') == [] - - def test_issue(self): + def test_issue655b(self): import math try: from inspect import signature @@ -695,4 +675,6 @@ def pendulum_period(length, G=Q_(1, 'standard_gravity')): t = pendulum_period(l, moon_gravity) self.assertAlmostEqual(t, Q_('4.928936075204336 second')) - + def test_issue783(self): + ureg = UnitRegistry() + assert not ureg('g') == [] From 3186f6c30b50ff6c1f78c6746b444cdde10f781e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 16:25:29 +0100 Subject: [PATCH 061/612] Successfully complete copy.deepcopy(UnitRegistry()) --- pint/registry.py | 9 +++------ pint/systems.py | 8 ++++---- pint/testsuite/test_issues.py | 21 +++++++++++++++++++++ pint/util.py | 10 ++++++++++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 667b97e2e..399b72770 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -47,7 +47,8 @@ from . import registry_helpers from .context import Context, ContextChain -from .util import (logger, pi_theorem, solve_dependencies, ParserHelper, +from .util import (getattr_maybe_raise, + logger, pi_theorem, solve_dependencies, ParserHelper, string_preprocessor, find_connected_nodes, find_shortest_path, UnitsContainer, _is_dim, to_units_container, SourceIterator) @@ -195,11 +196,7 @@ def _parse_defaults(self, ifile): self._defaults[k.strip()] = v.strip() def __getattr__(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('__'): - # Generate AttributeError - return object.__getattr__(self, item) + getattr_maybe_raise(self, item) return self.Unit(item) def __getitem__(self, item): diff --git a/pint/systems.py b/pint/systems.py index e2e41258b..e73428ecc 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -15,7 +15,7 @@ from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError, RedefinitionError -from .util import to_units_container, SharedRegistryObject, SourceIterator, logger +from .util import to_units_container, getattr_maybe_raise, SharedRegistryObject, SourceIterator, logger from .babel_names import _babel_systems from pint.compat import Loc @@ -232,6 +232,7 @@ def from_lines(cls, lines, define_func): return grp def __getattr__(self, item): + getattr_maybe_raise(self, item) return self._REGISTRY @@ -297,9 +298,7 @@ def __dir__(self): return list(self.members) def __getattr__(self, item): - if item == "__setstate__": - raise AttributeError(item) - + getattr_maybe_raise(self, item) u = getattr(self._REGISTRY, self.name + '_' + item, None) if u is not None: return u @@ -443,6 +442,7 @@ def __dir__(self): return list(self.d.keys()) def __getattr__(self, item): + getattr_maybe_raise(self, item) return self.d[item] diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6985a152b..df75b91f6 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -694,3 +694,24 @@ def pendulum_period(length, G=Q_(1, 'standard_gravity')): def test_issue783(self): ureg = UnitRegistry() assert not ureg('g') == [] + + def test_issue856(self): + ph1 = ParserHelper(scale=123) + ph2 = copy.deepcopy(ph1) + assert ph2.scale == ph1.scale + + ureg1 = UnitRegistry() + ureg2 = copy.deepcopy(ureg1) + # Very basic functionality test + assert ureg2('1 t').to('kg').magnitude == 1000 + + @unittest.expectedFailure + def test_issue856b(self): + # Test that, after a deepcopy(), the two UnitRegistries are + # independent from each other + ureg1 = UnitRegistry() + ureg2 = copy.deepcopy(ureg1) + ureg1.define('test123 = 123 kg') + ureg2.define('test123 = 456 kg') + assert ureg1('1 test123').to('kg').magnitude == 123 + assert ureg2('1 test123').to('kg').magnitude == 456 diff --git a/pint/util.py b/pint/util.py index 34fa73cfd..20d67698b 100644 --- a/pint/util.py +++ b/pint/util.py @@ -706,6 +706,16 @@ def __bytes__(self): return cls +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. + """ + # 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('__'): + object.__getattr__(self, item) + + class SourceIterator(object): """Iterator to facilitate reading the definition files. From 59d361263344a20d56d3f161a9984c9870998745 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 16:31:06 +0100 Subject: [PATCH 062/612] Remove spurious change --- pint/testsuite/test_issues.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index df75b91f6..2b6039e57 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -668,7 +668,11 @@ def test_issue655a(self): self.assertEqual(velocity.check('[length] / [time]'), True) self.assertEqual(velocity.check('1 / [time] * [length]'), True) - def test_issue655b(self): + def test_issue783(self): + ureg = UnitRegistry() + assert not ureg('g') == [] + + def test_issue(self): import math try: from inspect import signature @@ -691,10 +695,6 @@ def pendulum_period(length, G=Q_(1, 'standard_gravity')): t = pendulum_period(l, moon_gravity) self.assertAlmostEqual(t, Q_('4.928936075204336 second')) - def test_issue783(self): - ureg = UnitRegistry() - assert not ureg('g') == [] - def test_issue856(self): ph1 = ParserHelper(scale=123) ph2 = copy.deepcopy(ph1) From 2c19393da9204657a65ffed9589694f171594886 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 16:34:33 +0100 Subject: [PATCH 063/612] Missing decorator --- pint/testsuite/test_issues.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 15762c1b3..6e79601f4 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -475,6 +475,7 @@ def test_micro_creation(self): else: ureg.Quantity(2, 'µm') + @helpers.requires_numpy() def test_issue171_real_imag(self): qr = [1., 2., 3., 4.] * self.ureg.meter qi = [4., 3., 2., 1.] * self.ureg.meter From d3861d66375173d71321f4d41e0b1168edfeace7 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 16:56:21 +0100 Subject: [PATCH 064/612] Fix regression in Python 2.7 --- pint/unit.py | 2 +- pint/util.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pint/unit.py b/pint/unit.py index adb871c3f..79f304811 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -64,7 +64,7 @@ def __copy__(self): return ret def __deepcopy__(self, memo): - ret = self.__class__(copy.deepcopy(self._units)) + ret = self.__class__(copy.deepcopy(self._units, memo)) ret.__used = self.__used return ret diff --git a/pint/util.py b/pint/util.py index 20d67698b..1f34eeac9 100644 --- a/pint/util.py +++ b/pint/util.py @@ -318,6 +318,13 @@ def __hash__(self): self._hash = hash(frozenset(self._d.items())) return self._hash + # Only needed by Python 2.7 + def __getstate__(self): + return self._d, self._hash + + def __setstate__(self, state): + self._d, self._hash = state + def __eq__(self, other): if isinstance(other, UnitsContainer): # Not the same as hash(self); see ParserHelper.__hash__ and __eq__ @@ -489,6 +496,13 @@ def __hash__(self): raise ValueError(mess) return super(ParserHelper, self).__hash__() + # Only needed by Python 2.7 + def __getstate__(self): + return self._d, self._hash, self.scale + + def __setstate__(self, state): + self._d, self._hash, self.scale = state + def __eq__(self, other): if isinstance(other, self.__class__): return self.scale == other.scale and\ From e507c1d5932463a8364f00978426f5340939df5c Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 17:17:23 +0100 Subject: [PATCH 065/612] Merge from Master --- pint/testsuite/test_issues.py | 449 ++++++++++++++++------------------ 1 file changed, 217 insertions(+), 232 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 2b6039e57..f223843d6 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -45,6 +45,80 @@ def test_issue29(self): self.assertEqual(t._units, UnitsContainer(milliwatt=1)) self.assertEqual(t.to('joule / second'), 4e-3 * ureg('W')) + @unittest.expectedFailure + @helpers.requires_numpy() + def test_issue37(self): + x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) + ureg = UnitRegistry() + q = ureg.meter * x + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + q = x * ureg.meter + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + + m = np.ma.masked_array(2 * np.ones(3,3)) + qq = q * m + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + qq = m * q + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + + @unittest.expectedFailure + @helpers.requires_numpy() + def test_issue39(self): + x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) + ureg = UnitRegistry() + q = ureg.meter * x + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + q = x * ureg.meter + self.assertIsInstance(q, ureg.Quantity) + np.testing.assert_array_equal(q.magnitude, x) + self.assertEqual(q.units, ureg.meter.units) + + m = np.matrix(2 * np.ones(3,3)) + qq = q * m + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + qq = m * q + self.assertIsInstance(qq, ureg.Quantity) + np.testing.assert_array_equal(qq.magnitude, x * m) + self.assertEqual(qq.units, ureg.meter.units) + + @helpers.requires_numpy() + def test_issue44(self): + ureg = UnitRegistry() + x = 4. * ureg.dimensionless + np.sqrt(x) + self.assertQuantityAlmostEqual(np.sqrt([4.] * ureg.dimensionless), [2.] * ureg.dimensionless) + self.assertQuantityAlmostEqual(np.sqrt(4. * ureg.dimensionless), 2. * ureg.dimensionless) + + def test_issue45(self): + import math + ureg = UnitRegistry() + self.assertAlmostEqual(math.sqrt(4 * ureg.m/ureg.cm), math.sqrt(4 * 100)) + self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.) + + @helpers.requires_numpy() + def test_issue45b(self): + ureg = UnitRegistry() + self.assertAlmostEqual(np.sin([np.pi/2] * ureg.m / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless)) + self.assertAlmostEqual(np.sin([np.pi/2] * ureg.cm / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless * 0.01)) + + def test_issue50(self): + ureg = UnitRegistry() + Q_ = ureg.Quantity + self.assertEqual(Q_(100), 100 * ureg.dimensionless) + self.assertEqual(Q_('100'), 100 * ureg.dimensionless) + def test_issue52(self): u1 = UnitRegistry() u2 = UnitRegistry() @@ -86,6 +160,11 @@ def test_issue61_notNP(self): self.assertRaises(TypeError, Q_, value) self.assertRaises(TypeError, Q_, value, 'meter') + def test_issue62(self): + ureg = UnitRegistry() + m = ureg('m**0.5') + self.assertEqual(str(m.units), 'meter ** 0.5') + def test_issue66(self): ureg = UnitRegistry() self.assertEqual(ureg.get_dimensionality(UnitsContainer({'[temperature]': 1})), @@ -107,6 +186,46 @@ def test_issue69(self): q = ureg('m').to(ureg('in')) self.assertEqual(q, ureg('m').to('in')) + @helpers.requires_numpy() + def test_issue74(self): + ureg = UnitRegistry() + v1 = np.asarray([1., 2., 3.]) + v2 = np.asarray([3., 2., 1.]) + q1 = v1 * ureg.ms + q2 = v2 * ureg.ms + + np.testing.assert_array_equal(q1 < q2, v1 < v2) + np.testing.assert_array_equal(q1 > q2, v1 > v2) + + np.testing.assert_array_equal(q1 <= q2, v1 <= v2) + np.testing.assert_array_equal(q1 >= q2, v1 >= v2) + + q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s + v2s = q2s.to('ms').magnitude + + np.testing.assert_array_equal(q1 < q2s, v1 < v2s) + np.testing.assert_array_equal(q1 > q2s, v1 > v2s) + + np.testing.assert_array_equal(q1 <= q2s, v1 <= v2s) + np.testing.assert_array_equal(q1 >= q2s, v1 >= v2s) + + @helpers.requires_numpy() + def test_issue75(self): + ureg = UnitRegistry() + v1 = np.asarray([1., 2., 3.]) + v2 = np.asarray([3., 2., 1.]) + q1 = v1 * ureg.ms + q2 = v2 * ureg.ms + + np.testing.assert_array_equal(q1 == q2, v1 == v2) + np.testing.assert_array_equal(q1 != q2, v1 != v2) + + q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s + v2s = q2s.to('ms').magnitude + + np.testing.assert_array_equal(q1 == q2s, v1 == v2s) + np.testing.assert_array_equal(q1 != q2s, v1 != v2s) + @helpers.requires_uncertainties() def test_issue77(self): ureg = UnitRegistry() @@ -205,32 +324,36 @@ def test_issue93(self): self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - def test_issue523(self): + @helpers.requires_numpy_previous_than('1.10') + def test_issue94(self): ureg = UnitRegistry() - src, dst = UnitsContainer({'meter': 1}), UnitsContainer({'degF': 1}) - value = 10. - convert = self.ureg.convert - self.assertRaises(DimensionalityError, convert, value, src, dst) - self.assertRaises(DimensionalityError, convert, value, dst, src) + v1 = np.array([5, 5]) * ureg.meter + v2 = 0.1 * ureg.meter + v3 = np.array([5, 5]) * ureg.meter + v3 += v2 - def _test_issueXX(self): - ureg = UnitRegistry() - try: - ureg.convert(1, ureg.degC, ureg.kelvin * ureg.meter / ureg.nanometer) - except: - self.assertTrue(False, - 'Error while trying to convert {} to {}'.format(ureg.degC, ureg.kelvin * ureg.meter / ureg.nanometer)) + np.testing.assert_array_equal((v1 + v2).magnitude, np.array([5.1, 5.1])) + np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) - def test_issue121(self): - sh = (2, 1) + def test_issue104(self): ureg = UnitRegistry() - z, v = 0, 2. - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) + x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] + y = [ureg('1 meter')] * 3 + + def summer(values): + if not values: + return 0 + total = values[0] + for v in values[1:]: + total += v + + return total + + self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) + self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) def test_issue105(self): ureg = UnitRegistry() @@ -245,25 +368,61 @@ def test_issue105(self): self.assertRaises(AttributeError, func, 'METER') self.assertEqual(val, func('METER', False)) - def test_issue104(self): + def test_issue121(self): + sh = (2, 1) ureg = UnitRegistry() + z, v = 0, 2. + self.assertEqual(z + v * ureg.meter, v * ureg.meter) + self.assertEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertEqual(v * ureg.meter + z, v * ureg.meter) + self.assertEqual(v * ureg.meter - z, v * ureg.meter) - x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] - y = [ureg('1 meter')] * 3 + self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - def summer(values): - if not values: - return 0 - total = values[0] - for v in values[1:]: - total += v + @helpers.requires_numpy18() + def test_issue121b(self): + sh = (2, 1) + ureg = UnitRegistry() - return total + z, v = 0, 2. + self.assertEqual(z + v * ureg.meter, v * ureg.meter) + self.assertEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertEqual(v * ureg.meter + z, v * ureg.meter) + self.assertEqual(v * ureg.meter - z, v * ureg.meter) - self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) - self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) + self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) + + z, v = np.zeros(sh), 2. * np.ones(sh) + self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) + self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) + self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) + self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) + + z, v = np.zeros((3, 1)), 2. * np.ones(sh) + for x, y in ((z, v), + (z, v * ureg.meter), + (v * ureg.meter, z) + ): + try: + w = x + y + self.assertTrue(False, "ValueError not raised") + except ValueError: + pass + try: + w = x - y + self.assertTrue(False, "ValueError not raised") + except ValueError: + pass + + @helpers.requires_numpy() + def test_issue127(self): + q = [1., 2., 3., 4.] * self.ureg.meter + q[0] = np.nan + self.assertNotEqual(q[0], 1.) + self.assertTrue(math.isnan(q[0].magnitude)) + q[1] = float('NaN') + self.assertNotEqual(q[1], 2.) + self.assertTrue(math.isnan(q[1].magnitude)) def test_issue170(self): Q_ = UnitRegistry().Quantity @@ -316,193 +475,7 @@ def test_micro_creation(self): else: ureg.Quantity(2, 'µm') -@helpers.requires_numpy() -class TestIssuesNP(QuantityTestCase): - - FORCE_NDARRAY = False - - @unittest.expectedFailure - def test_issue37(self): - x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) - ureg = UnitRegistry() - q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - - m = np.ma.masked_array(2 * np.ones(3,3)) - qq = q * m - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - qq = m * q - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - - @unittest.expectedFailure - def test_issue39(self): - x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) - ureg = UnitRegistry() - q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) - np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) - - m = np.matrix(2 * np.ones(3,3)) - qq = q * m - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - qq = m * q - self.assertIsInstance(qq, ureg.Quantity) - np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) - - def test_issue44(self): - ureg = UnitRegistry() - x = 4. * ureg.dimensionless - np.sqrt(x) - self.assertQuantityAlmostEqual(np.sqrt([4.] * ureg.dimensionless), [2.] * ureg.dimensionless) - self.assertQuantityAlmostEqual(np.sqrt(4. * ureg.dimensionless), 2. * ureg.dimensionless) - - def test_issue45(self): - import math - ureg = UnitRegistry() - self.assertAlmostEqual(math.sqrt(4 * ureg.m/ureg.cm), math.sqrt(4 * 100)) - self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.) - - def test_issue45b(self): - ureg = UnitRegistry() - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.m / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless)) - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.cm / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless * 0.01)) - - def test_issue50(self): - ureg = UnitRegistry() - Q_ = ureg.Quantity - self.assertEqual(Q_(100), 100 * ureg.dimensionless) - self.assertEqual(Q_('100'), 100 * ureg.dimensionless) - - def test_issue62(self): - ureg = UnitRegistry() - m = ureg('m**0.5') - self.assertEqual(str(m.units), 'meter ** 0.5') - - def test_issue74(self): - ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) - q1 = v1 * ureg.ms - q2 = v2 * ureg.ms - - np.testing.assert_array_equal(q1 < q2, v1 < v2) - np.testing.assert_array_equal(q1 > q2, v1 > v2) - - np.testing.assert_array_equal(q1 <= q2, v1 <= v2) - np.testing.assert_array_equal(q1 >= q2, v1 >= v2) - - q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude - - np.testing.assert_array_equal(q1 < q2s, v1 < v2s) - np.testing.assert_array_equal(q1 > q2s, v1 > v2s) - - np.testing.assert_array_equal(q1 <= q2s, v1 <= v2s) - np.testing.assert_array_equal(q1 >= q2s, v1 >= v2s) - - def test_issue75(self): - ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) - q1 = v1 * ureg.ms - q2 = v2 * ureg.ms - - np.testing.assert_array_equal(q1 == q2, v1 == v2) - np.testing.assert_array_equal(q1 != q2, v1 != v2) - - q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude - - np.testing.assert_array_equal(q1 == q2s, v1 == v2s) - np.testing.assert_array_equal(q1 != q2s, v1 != v2s) - - def test_issue93(self): - ureg = UnitRegistry() - x = 5 * ureg.meter - self.assertIsInstance(x.magnitude, int) - y = 0.1 * ureg.meter - self.assertIsInstance(y.magnitude, float) - z = 5 * ureg.meter - self.assertIsInstance(z.magnitude, int) - z += y - self.assertIsInstance(z.magnitude, float) - - self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) - self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - - @helpers.requires_numpy_previous_than('1.10') - def test_issue94(self): - ureg = UnitRegistry() - v1 = np.array([5, 5]) * ureg.meter - v2 = 0.1 * ureg.meter - v3 = np.array([5, 5]) * ureg.meter - v3 += v2 - - np.testing.assert_array_equal((v1 + v2).magnitude, np.array([5.1, 5.1])) - np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) - - @helpers.requires_numpy18() - def test_issue121(self): - sh = (2, 1) - ureg = UnitRegistry() - - z, v = 0, 2. - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - - z, v = np.zeros(sh), 2. * np.ones(sh) - self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) - self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) - - z, v = np.zeros((3, 1)), 2. * np.ones(sh) - for x, y in ((z, v), - (z, v * ureg.meter), - (v * ureg.meter, z) - ): - try: - w = x + y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass - try: - w = x - y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass - - def test_issue127(self): - q = [1., 2., 3., 4.] * self.ureg.meter - q[0] = np.nan - self.assertNotEqual(q[0], 1.) - self.assertTrue(math.isnan(q[0].magnitude)) - q[1] = float('NaN') - self.assertNotEqual(q[1], 2.) - self.assertTrue(math.isnan(q[1].magnitude)) - + @helpers.requires_numpy() def test_issue171_real_imag(self): qr = [1., 2., 3., 4.] * self.ureg.meter qi = [4., 3., 2., 1.] * self.ureg.meter @@ -510,12 +483,14 @@ def test_issue171_real_imag(self): self.assertQuantityEqual(q.real, qr) self.assertQuantityEqual(q.imag, qi) + @helpers.requires_numpy() def test_issue171_T(self): a = np.asarray([[1., 2., 3., 4.],[4., 3., 2., 1.]]) q1 = a * self.ureg.meter q2 = a.T * self.ureg.meter self.assertQuantityEqual(q1.T, q2) + @helpers.requires_numpy() def test_issue250(self): a = self.ureg.V b = self.ureg.mV @@ -553,11 +528,6 @@ def test_issue354_356_370(self): self.assertEqual('{0:~}'.format(1 * self.ureg('MiB')), '1 MiB') - def test_issue482(self): - q = self.ureg.Quantity(1, self.ureg.dimensionless) - qe = np.exp(q) - self.assertIsInstance(qe, self.ureg.Quantity) - def test_issue468(self): ureg = UnitRegistry() @@ -570,6 +540,13 @@ def f(x): z = x * y self.assertEquals(z, ureg.Quantity(1., 'meter * kilogram')) + @helpers.requires_numpy() + def test_issue482(self): + q = self.ureg.Quantity(1, self.ureg.dimensionless) + qe = np.exp(q) + self.assertIsInstance(qe, self.ureg.Quantity) + + @helpers.requires_numpy() def test_issue483(self): ureg = self.ureg a = np.asarray([1, 2, 3]) @@ -577,6 +554,14 @@ def test_issue483(self): p = (q ** q).m np.testing.assert_array_equal(p, a ** a) + def test_issue523(self): + ureg = UnitRegistry() + src, dst = UnitsContainer({'meter': 1}), UnitsContainer({'degF': 1}) + value = 10. + convert = self.ureg.convert + self.assertRaises(DimensionalityError, convert, value, src, dst) + self.assertRaises(DimensionalityError, convert, value, dst, src) + def test_issue532(self): ureg = self.ureg @@ -668,11 +653,7 @@ def test_issue655a(self): self.assertEqual(velocity.check('[length] / [time]'), True) self.assertEqual(velocity.check('1 / [time] * [length]'), True) - def test_issue783(self): - ureg = UnitRegistry() - assert not ureg('g') == [] - - def test_issue(self): + def test_issue655b(self): import math try: from inspect import signature @@ -695,6 +676,10 @@ def pendulum_period(length, G=Q_(1, 'standard_gravity')): t = pendulum_period(l, moon_gravity) self.assertAlmostEqual(t, Q_('4.928936075204336 second')) + def test_issue783(self): + ureg = UnitRegistry() + assert not ureg('g') == [] + def test_issue856(self): ph1 = ParserHelper(scale=123) ph2 = copy.deepcopy(ph1) From fb888aea989ddea27bbe62fa5b73c2ab97b7e8ee Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 30 Aug 2019 17:43:25 +0100 Subject: [PATCH 066/612] Deep-copied UnitRegistries don't share state anymore --- pint/registry.py | 38 ++++++++++++++++++++++++----------- pint/testsuite/test_issues.py | 1 - pint/util.py | 2 +- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 399b72770..71e45a86a 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -32,6 +32,7 @@ from __future__ import division, unicode_literals, print_function, absolute_import +import copy import os import re import math @@ -116,15 +117,7 @@ class BaseRegistry(meta.with_metaclass(_Meta)): def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', auto_reduce_dimensions=False): self._register_parsers() - - from .unit import build_unit_class - self.Unit = build_unit_class(self) - - from .quantity import build_quantity_class - self.Quantity = build_quantity_class(self, force_ndarray) - - from .measurement import build_measurement_class - self.Measurement = build_measurement_class(self, force_ndarray) + self._init_dynamic_classes(force_ndarray) self._filename = filename @@ -169,6 +162,18 @@ def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', aut self._initialized = False + def _init_dynamic_classes(self, force_ndarray): + """Generate subclasses on the fly and attach them to self + """ + from .unit import build_unit_class + self.Unit = build_unit_class(self) + + from .quantity import build_quantity_class + self.Quantity = build_quantity_class(self, force_ndarray) + + from .measurement import build_measurement_class + self.Measurement = build_measurement_class(self, force_ndarray) + def _after_init(self): """This should be called after all __init__ """ @@ -195,6 +200,13 @@ def _parse_defaults(self, ifile): k, v = part.split('=') self._defaults[k.strip()] = v.strip() + def __deepcopy__(self, memo): + new = object.__new__(type(self)) + new.__dict__ = copy.deepcopy(self.__dict__, memo) + force_ndarray = self.Quantity.force_ndarray + new._init_dynamic_classes(force_ndarray) + return new + def __getattr__(self, item): getattr_maybe_raise(self, item) return self.Unit(item) @@ -1283,12 +1295,14 @@ def __init__(self, system=None, **kwargs): #: Map group name to group. #: :type: dict[ str | Group] self._groups = {} - self.Group = systems.build_group_class(self) self._groups['root'] = self.Group('root') - self.System = systems.build_system_class(self) - self._default_system = system + def _init_dynamic_classes(self, force_ndarray): + super(SystemRegistry, self)._init_dynamic_classes(force_ndarray) + self.Group = systems.build_group_class(self) + self.System = systems.build_system_class(self) + def _after_init(self): """After init function diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index f223843d6..6ca703586 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -690,7 +690,6 @@ def test_issue856(self): # Very basic functionality test assert ureg2('1 t').to('kg').magnitude == 1000 - @unittest.expectedFailure def test_issue856b(self): # Test that, after a deepcopy(), the two UnitRegistries are # independent from each other diff --git a/pint/util.py b/pint/util.py index 1f34eeac9..587517a68 100644 --- a/pint/util.py +++ b/pint/util.py @@ -727,7 +727,7 @@ 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('__'): - object.__getattr__(self, item) + raise AttributeError("%r object has no attribute %r" % (self, item)) class SourceIterator(object): From 0d3ae6c2ac0d52bdd49428e8431bc950ac713a33 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Sep 2019 18:02:00 +0100 Subject: [PATCH 067/612] New syntax to add aliases to already existing definitions --- docs/defining.rst | 32 +++++++++++++++++++++--- pint/default_en.txt | 40 +++++++++++++++++++++++++++--- pint/definitions.py | 18 ++++++++++++++ pint/registry.py | 14 ++++++++++- pint/testsuite/test_definitions.py | 8 +++++- pint/testsuite/test_unit.py | 11 ++++++++ 6 files changed, 115 insertions(+), 8 deletions(-) diff --git a/docs/defining.rst b/docs/defining.rst index 50c177b09..7aba5bbdf 100644 --- a/docs/defining.rst +++ b/docs/defining.rst @@ -75,12 +75,28 @@ unit, including non-metric ones (e.g. kiloinch is valid for Pint). This simplifies definitions files enormously without introducing major problems. Pint, like Python, believes that we are all consenting adults. +Derived dimensions are defined as follows:: + + [density] = [mass] / [volume] + +Note that primary dimensions don't need to be declared; they can be +defined for the first time as part of a unit definition. + +Finally, one may add aliases to an already existing unit definition:: + + @alias meter = metro = metr + +This is particularly useful when one wants to enrich definitions from defaults_en.txt +with new aliases from a custom file. It can also be used for translations (like in the +example above) as long as one is happy to have the localized units automatically +converted to English when they are parsed. + Programmatically ---------------- -You can easily add units to the registry programmatically. Let's add a dog_year -(sometimes written as dy) equivalent to 52 (human) days: +You can easily add units, dimensions, or aliases to the registry programmatically. +Let's add a dog_year (sometimes written as dy) equivalent to 52 (human) days: .. doctest:: @@ -111,4 +127,14 @@ You can also add prefixes programmatically: where the number indicates the multiplication factor. -.. warning:: Units and prefixes added programmatically are forgotten when the program ends. +Same for aliases and derived dimensions: + +.. doctest:: + + >>> ureg.define('@alias meter = metro = metr') + >>> ureg.define('[hypervolume] = [length ** 4]') + + +.. warning:: + Units, prefixes, aliases and dimensions added programmatically are forgotten when the + program ends. diff --git a/pint/default_en.txt b/pint/default_en.txt index fecc9e0c0..8adabe776 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -3,9 +3,11 @@ # Language: english # :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. -# Definition syntax -# ================= -# = [= ] [= ] [ = ] [...] +# Syntax +# ====== +# Units +# ----- +# = [= ] [= ] [ = ] [...] # # The canonical name and aliases should be expressed in singular form. # Pint automatically deals with plurals built by adding 's' to the singular form; plural @@ -17,6 +19,38 @@ # Example: # millennium = 1e3 * year = _ = millennia # +# +# Prefixes +# -------- +# - = [= ] [= ] [ = ] [...] +# +# Example: +# deca- = 1e+1 = da- = deka- +# +# +# Derived dimensions +# ------------------ +# [dimension name] = +# +# Example: +# [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. +# E.g. see below `meter = [length]` +# +# +# Additional aliases +# ------------------ +# @alias = [ = ] [...] +# +# Used to add aliases to already existing unit definitions. +# Particularly useful when one wants to enrich definitions +# from defaults_en.txt with custom aliases. +# +# Example: +# @alias meter = my_meter + # See also: https://pint.readthedocs.io/en/latest/defining.html @defaults diff --git a/pint/definitions.py b/pint/definitions.py index e510a1b10..0cd7ec571 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -52,6 +52,12 @@ def from_string(cls, definition): value.decode('utf-8') except UnicodeEncodeError: result.remove(value) + + # @alias name = alias1 = alias2 = ... + if name.startswith("@alias "): + name = name[len("@alias "):].lstrip() + return AliasDefinition(name, tuple(result)) + value, aliases = result[0], tuple([x for x in result[1:] if x != '']) symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) @@ -83,6 +89,9 @@ def has_symbol(self): def aliases(self): return self._aliases + def add_aliases(self, *alias): + self._aliases = self._aliases + alias + @property def converter(self): return self._converter @@ -166,3 +175,12 @@ def __init__(self, name, symbol, aliases, converter, super(DimensionDefinition, self).__init__(name, symbol, aliases, converter=None) + + +class AliasDefinition(Definition): + """Additional alias(es) for an already existing unit + """ + def __init__(self, name, aliases): + super(AliasDefinition, self).__init__( + name=name, symbol=None, aliases=aliases, converter=None + ) diff --git a/pint/registry.py b/pint/registry.py index 0129ba5d5..3c90dedd7 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -54,7 +54,7 @@ from .compat import tokenizer, string_types, meta from .definitions import (Definition, UnitDefinition, PrefixDefinition, - DimensionDefinition) + DimensionDefinition, AliasDefinition) from .converters import ScaleConverter from .errors import (DimensionalityError, UndefinedUnitError, DefinitionSyntaxError, RedefinitionError) @@ -263,6 +263,11 @@ def _define(self, definition): elif isinstance(definition, PrefixDefinition): d, di = self._prefixes, None + elif isinstance(definition, AliasDefinition): + d, di = self._units, self._units_casei + self._define_alias(definition, d, di) + return d[definition.name], d, di + else: raise TypeError('{} is not a valid definition.'.format(definition)) @@ -325,6 +330,13 @@ def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): if casei_unit_dict is not None: casei_unit_dict[key.lower()].add(key) + def _define_alias(self, definition, unit_dict, casei_unit_dict): + unit = unit_dict[definition.name] + unit.add_aliases(*definition.aliases) + for alias in unit.aliases: + unit_dict[alias] = unit + casei_unit_dict[alias.lower()].add(alias) + def _register_parser(self, prefix, parserfunc): """Register a loader for a given @ directive.. diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 3b73fd8e0..8a66a4e65 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -5,7 +5,7 @@ from pint.util import (UnitsContainer) from pint.converters import (ScaleConverter, OffsetConverter) from pint.definitions import (Definition, PrefixDefinition, UnitDefinition, - DimensionDefinition) + DimensionDefinition, AliasDefinition) from pint.testsuite import BaseTestCase @@ -88,3 +88,9 @@ def test_dimension_definition(self): x = Definition.from_string('[speed] = [length]/[time]') self.assertIsInstance(x, DimensionDefinition) self.assertEqual(x.reference, UnitsContainer({'[length]': 1, '[time]': -1})) + + def test_alias_definition(self): + x = Definition.from_string("@alias meter = metro = metr") + self.assertIsInstance(x, AliasDefinition) + self.assertEqual(x.name, "meter") + self.assertEqual(x.aliases, ("metro", "metr")) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 5ce35c51a..da67ce4f7 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -648,3 +648,14 @@ def test_to_and_from_offset_units(self, input_tuple, expected): if src != dst: self.assertQuantityAlmostEqual(convert(expected, dst, src), value, atol=0.001) + + def test_alias(self): + ureg = UnitRegistry() + # Against canonical name + ureg.define("@alias meter = metro = metr") + self.assertQuantityEqual(ureg("1 metro"), ureg("1 meter")) + # Against a previously defined alias + ureg.define("@alias octet = word") + self.assertQuantityEqual(ureg("1 word"), ureg("1 byte")) + # Against unknown name + self.assertRaises(KeyError, ureg.define, "@alias notexist = something") From a3fafa6f8ad2fdf79183ba2b71a987c980297f82 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Sep 2019 11:10:15 +0100 Subject: [PATCH 068/612] Board feet --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index fecc9e0c0..76ba26805 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -482,6 +482,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N peck = bushel / 4 = pk bushel = 2150.42 cubic_inch = bu dry_barrel = 7056 cubic_inch = _ = US_dry_barrel + board_foot = ft * ft * in = FBM = board_feet = BF = BDFT = super_foot = superficial_foot = super_feet = superficial_feet @end @group USCSLiquidVolume From 25cd87c9afc8333878bc2d6f71b66eb903f43e55 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Sep 2019 14:35:18 +0100 Subject: [PATCH 069/612] fix load_definitions; deduplicate --- pint/definitions.py | 1 + pint/registry.py | 2 +- pint/testsuite/test_unit.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 0cd7ec571..811a47187 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -90,6 +90,7 @@ def aliases(self): return self._aliases def add_aliases(self, *alias): + alias = tuple(a for a in alias if a not in self._aliases) self._aliases = self._aliases + alias @property diff --git a/pint/registry.py b/pint/registry.py index 3c90dedd7..1a24442dd 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -379,7 +379,7 @@ def load_definitions(self, file, is_resource=False): ifile = SourceIterator(file) for no, line in ifile: - if line and line[0] == '@': + if line.startswith('@') and not line.startswith('@alias'): if line.startswith('@import'): if is_resource: path = line[7:].strip() diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index da67ce4f7..9c6bf5196 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -650,12 +650,28 @@ def test_to_and_from_offset_units(self, input_tuple, expected): value, atol=0.001) def test_alias(self): - ureg = UnitRegistry() - # Against canonical name - ureg.define("@alias meter = metro = metr") - self.assertQuantityEqual(ureg("1 metro"), ureg("1 meter")) - # Against a previously defined alias - ureg.define("@alias octet = word") - self.assertQuantityEqual(ureg("1 word"), ureg("1 byte")) - # Against unknown name + # Use load_definitions + ureg = UnitRegistry([ + "canonical = [] = can = alias1 = alias2\n", + # overlapping aliases + "@alias canonical = alias2 = alias3\n", + # Against another alias + "@alias alias3 = alias4\n", + ]) + + # Use define + ureg.define("@alias canonical = alias5") + + # Test that new aliases work + # Test that pre-existing aliases and symbol are not eliminated + for a in ("can", "alias1", "alias2", "alias3", "alias4", "alias5"): + self.assertEqual(ureg.Unit(a), ureg.Unit("canonical")) + + # Test that aliases defined multiple times are not duplicated + self.assertEqual( + ureg._units["canonical"].aliases, + ("alias1", "alias2", "alias3", "alias4", "alias5") + ) + + # Define against unknown name self.assertRaises(KeyError, ureg.define, "@alias notexist = something") From 46ce8cb28f471a8ea916111edd86fbb45b3e5c5e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Sep 2019 17:16:39 +0100 Subject: [PATCH 070/612] Anonymous contexts --- docs/contexts.rst | 16 ++++++++++++++-- pint/context.py | 2 +- pint/registry.py | 2 ++ pint/testsuite/test_contexts.py | 24 ++++++++++++++++++------ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 66ec5e619..3fcbad00e 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -188,7 +188,19 @@ functions. For example: >>> ureg = pint.UnitRegistry() >>> c = pint.Context('ab') >>> c.add_transformation('[length]', '[time]', - ... lambda ureg, x: ureg.speed_of_light / x) + ... lambda ureg, x: x / ureg.speed_of_light) >>> c.add_transformation('[time]', '[length]', - ... lambda ureg, x: ureg.speed_of_light * x) + ... lambda ureg, x: x * ureg.speed_of_light) >>> ureg.add_context(c) + >>> ureg("1 s").to("km", "ab") + 299792.458 kilometer + +It is also possible to create anonymous contexts without invoking add_context: + + >>> c = pint.Context() + ... + >>> ureg("1 s").to("km", c) + 299792.458 kilometer + >>> with ureg.context(c): + ... ureg("1 s").to("km") + 299792.458 kilometer diff --git a/pint/context.py b/pint/context.py index f492bceb4..6bd733584 100644 --- a/pint/context.py +++ b/pint/context.py @@ -62,7 +62,7 @@ class Context(object): """ - def __init__(self, name, aliases=(), defaults=None): + def __init__(self, name=None, aliases=(), defaults=None): self.name = name self.aliases = aliases diff --git a/pint/registry.py b/pint/registry.py index 1a24442dd..e5bba655e 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1071,6 +1071,8 @@ def add_context(self, context): Notice that this method will NOT enable the context. Use `enable_contexts`. """ + if not context.name: + raise ValueError("Can't add unnamed context to registry") if context.name in self._contexts: logger.warning('The name %s was already registered for another context.', context.name) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 22805dbfd..33eafc1eb 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -269,7 +269,6 @@ def test_unknown_nested_context(self): self.assertFalse(ureg._active_ctx) self.assertFalse(ureg._active_ctx.graph) - def test_one_context(self): ureg = UnitRegistry() @@ -288,7 +287,6 @@ def test_one_context(self): self.assertRaises(ValueError, q.to, 'Hz') self.assertEqual(ureg.get_compatible_units(q), meter_units) - def test_multiple_context(self): ureg = UnitRegistry() @@ -308,7 +306,6 @@ def test_multiple_context(self): self.assertRaises(ValueError, q.to, 'Hz') self.assertEqual(ureg.get_compatible_units(q), meter_units) - def test_nested_context(self): ureg = UnitRegistry() @@ -385,7 +382,6 @@ def test_enable_context_with_arg(self): self.assertRaises(TypeError, q.to, 'Hz') ureg.disable_contexts(1) - def test_context_with_arg_def(self): ureg = UnitRegistry() @@ -421,7 +417,6 @@ def test_context_with_arg_def(self): self.assertEqual(q.to('Hz'), s / 2) self.assertRaises(ValueError, q.to, 'Hz') - def test_context_with_sharedarg_def(self): ureg = UnitRegistry() @@ -462,6 +457,24 @@ def test_context_with_sharedarg_def(self): with ureg.context('lc', n=6): self.assertEqual(q.to('Hz'), s / 6) + def test_anonymous_context(self): + ureg = UnitRegistry() + c = Context() + c.add_transformation( + '[length]', '[time]', + lambda ureg, x: (x / ureg.speed_of_light) + ) + expect = ureg("1 m") / ureg.speed_of_light + + out = ureg("1 m").to("s", c) + self.assertQuantityEqual(out, expect) + + with ureg.context(c): + out = ureg("1 m").to("s") + self.assertQuantityEqual(out, expect) + + self.assertRaises(ValueError, ureg.add_context, c) + def _test_ctx(self, ctx): ureg = UnitRegistry() q = 500 * ureg.meter @@ -629,7 +642,6 @@ def test_spectroscopy(self): # assertAlmostEqualRelError converts second to first self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) - for a, b in itertools.product(eq, eq): self.assertQuantityAlmostEqual(a.to(b.units, 'sp'), b, rtol=0.01) From 5709efefaa0152cc4ae534632a864ba479634a27 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Sep 2019 17:26:30 +0100 Subject: [PATCH 071/612] test for enable_contexts --- docs/contexts.rst | 3 --- pint/testsuite/test_contexts.py | 14 ++++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 3fcbad00e..7401b86ac 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -201,6 +201,3 @@ It is also possible to create anonymous contexts without invoking add_context: ... >>> ureg("1 s").to("km", c) 299792.458 kilometer - >>> with ureg.context(c): - ... ureg("1 s").to("km") - 299792.458 kilometer diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 33eafc1eb..006280ff3 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -464,16 +464,22 @@ def test_anonymous_context(self): '[length]', '[time]', lambda ureg, x: (x / ureg.speed_of_light) ) - expect = ureg("1 m") / ureg.speed_of_light + self.assertRaises(ValueError, ureg.add_context, c) - out = ureg("1 m").to("s", c) + x = ureg("1 m") + expect = x / ureg.speed_of_light + out = x.to("s", c) self.assertQuantityEqual(out, expect) with ureg.context(c): - out = ureg("1 m").to("s") + out = x.to("s") self.assertQuantityEqual(out, expect) - self.assertRaises(ValueError, ureg.add_context, c) + ureg.enable_contexts(c) + out = x.to("s") + self.assertQuantityEqual(out, expect) + ureg.disable_contexts(1) + self.assertRaises(errors.DimensionalityError, x.to, "s") def _test_ctx(self, ctx): ureg = UnitRegistry() From ef42423ee2134413c38eebfcdab0565c2060a7a0 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 6 Sep 2019 09:30:21 +0100 Subject: [PATCH 072/612] Tests for multiple anonymous contexts --- pint/testsuite/test_contexts.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 006280ff3..7766aa7df 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -462,25 +462,39 @@ def test_anonymous_context(self): c = Context() c.add_transformation( '[length]', '[time]', - lambda ureg, x: (x / ureg.speed_of_light) + lambda ureg, x: x / ureg("5 cm/s") ) self.assertRaises(ValueError, ureg.add_context, c) - x = ureg("1 m") - expect = x / ureg.speed_of_light - out = x.to("s", c) - self.assertQuantityEqual(out, expect) + x = ureg("10 cm") + expect = ureg("2 s") + self.assertQuantityEqual(x.to("s", c), expect) with ureg.context(c): - out = x.to("s") - self.assertQuantityEqual(out, expect) + self.assertQuantityEqual(x.to("s"), expect) ureg.enable_contexts(c) - out = x.to("s") - self.assertQuantityEqual(out, expect) + self.assertQuantityEqual(x.to("s"), expect) ureg.disable_contexts(1) self.assertRaises(errors.DimensionalityError, x.to, "s") + # Multiple anonymous contexts + c2 = Context() + c2.add_transformation( + '[length]', '[time]', + lambda ureg, x: x / ureg("10 cm/s") + ) + c2.add_transformation( + '[mass]', '[time]', + lambda ureg, x: x / ureg("10 kg/s") + ) + with ureg.context(c2, c): + self.assertQuantityEqual(x.to("s"), expect) + # Transformations only in c2 are still working even if c takes priority + self.assertQuantityEqual(ureg("100 kg").to("s"), ureg("10 s")) + with ureg.context(c, c2): + self.assertQuantityEqual(x.to("s"), ureg("1 s")) + def _test_ctx(self, ctx): ureg = UnitRegistry() q = 500 * ureg.meter From f6a6a292d73edd86b82fdc562bb045b6b3e5ae8c Mon Sep 17 00:00:00 2001 From: Damien Cupif Date: Fri, 6 Sep 2019 10:35:22 +0200 Subject: [PATCH 073/612] fix(registry): shallow copy context transformations before mutating This commit aims to avoid potential problems while mutating context transformations. Since we modify the transformations dictionary structure while iterating over it, we may fail to iterate over all entries. Explanation: "Iterating views while adding or deleting entries in the dictionary may raise a `RuntimeError` or fail to iterate over all entries" https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects --- pint/registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 0129ba5d5..78dbf8d89 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1102,7 +1102,8 @@ def enable_contexts(self, *names_or_contexts, **kwargs): for ctx in ctxs: if getattr(ctx, '_checked', False): continue - for (src, dst), func in ctx.funcs.items(): + funcs_copy = dict(ctx.funcs) + for (src, dst), func in funcs_copy.items(): src_ = self._get_dimensionality(src) dst_ = self._get_dimensionality(dst) if src != src_ or dst != dst_: From b8cddd86e2c1ead9a6603090ce408055fa854ae5 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 11 Sep 2019 09:40:46 +0100 Subject: [PATCH 074/612] Partially revert "Many speedups in UnitsContainer and ParserHelper" --- pint/util.py | 66 +++++++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/pint/util.py b/pint/util.py index 587517a68..f5282a300 100644 --- a/pint/util.py +++ b/pint/util.py @@ -286,20 +286,16 @@ def remove(self, keys): """ Create a new UnitsContainer purged from given keys. """ - new = self.copy() - for k in keys: - new._d.pop(k, None) - new._hash = None - return new + d = udict(self._d) + return UnitsContainer(((key, d[key]) for key in d if key not in keys)) def rename(self, oldkey, newkey): """ Create a new UnitsContainer in which an entry has been renamed. """ - new = self.copy() - new._d[newkey] = new._d.pop(oldkey) - new._hash = None - return new + d = udict(self._d) + d[newkey] = d.pop(oldkey) + return UnitsContainer(d) def __iter__(self): return iter(self._d) @@ -327,9 +323,8 @@ def __setstate__(self, state): def __eq__(self, other): if isinstance(other, UnitsContainer): - # Not the same as hash(self); see ParserHelper.__hash__ and __eq__ - return UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other) - if isinstance(other, string_types): + other = other._d + elif isinstance(other, string_types): other = ParserHelper.from_string(other) other = other._d @@ -350,25 +345,20 @@ def format_babel(self, spec, **kwspec): return format_unit(self, spec, **kwspec) def __copy__(self): - # Skip expensive health checks performed by __init__ - out = object.__new__(self.__class__) - out._d = self._d.copy() - out._hash = self._hash - return out + return UnitsContainer(self._d) def __mul__(self, other): + d = udict(self._d) if not isinstance(other, self.__class__): err = 'Cannot multiply UnitsContainer by {}' raise TypeError(err.format(type(other))) - - new = self.copy() for key, value in other.items(): - new._d[key] += value - if new._d[key] == 0: - del new._d[key] + d[key] += value + keys = [key for key, value in d.items() if value == 0] + for key in keys: + del d[key] - new._hash = None - return new + return UnitsContainer(d) __rmul__ = __mul__ @@ -376,26 +366,26 @@ def __pow__(self, other): if not isinstance(other, NUMERIC_TYPES): err = 'Cannot power UnitsContainer by {}' raise TypeError(err.format(type(other))) - - new = self.copy() - for key, value in new._d.items(): - new._d[key] *= other - new._hash = None - return new + d = udict(self._d) + for key, value in d.items(): + d[key] *= other + return UnitsContainer(d) def __truediv__(self, other): if not isinstance(other, self.__class__): err = 'Cannot divide UnitsContainer by {}' raise TypeError(err.format(type(other))) - new = self.copy() + d = udict(self._d) + for key, value in other.items(): - new._d[key] -= value - if new._d[key] == 0: - del new._d[key] + d[key] -= value - new._hash = None - return new + keys = [key for key, value in d.items() if value == 0] + for key in keys: + del d[key] + + return UnitsContainer(d) def __rtruediv__(self, other): if not isinstance(other, self.__class__) and other != 1: @@ -483,9 +473,7 @@ def _from_string(cls, input_string): for key, value in ret.items())) def __copy__(self): - new = super(ParserHelper, self).__copy__() - new.scale = self.scale - return new + return ParserHelper(scale=self.scale, **self) def copy(self): return self.__copy__() From 7cd8e1ce906e0ea02eca9ff54559f8f1caa4baa1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 11 Sep 2019 10:55:17 +0100 Subject: [PATCH 075/612] Identical hashes do not guarantee equality --- pint/testsuite/test_issues.py | 20 +++++++++- pint/util.py | 74 ++++++++++++++++++++++------------- 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6ca703586..275ed338b 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -11,7 +11,7 @@ from pint.unit import UnitsContainer from pint.util import ParserHelper -from pint.compat import np, long_type +from pint.compat import np from pint.errors import UndefinedUnitError, DimensionalityError from pint.testsuite import QuantityTestCase, helpers @@ -699,3 +699,21 @@ def test_issue856b(self): ureg2.define('test123 = 456 kg') assert ureg1('1 test123').to('kg').magnitude == 123 assert ureg2('1 test123').to('kg').magnitude == 456 + + def test_issue876(self): + # Same hash must not imply equality. + + # As an implementation detail of CPython, hash(-1) == hash(-2) This test is + # useless in potential alternative Python implementations where hash(-1) != + # hash(-2); one would need to find hash collisions specific for each + # implementation + + a = UnitsContainer({"[mass]": -1}) + b = UnitsContainer({"[mass]": -2}) + c = UnitsContainer({"[mass]": -3}) + + # Guarantee working on alternative Python implementations + assert (hash(-1) == hash(-2)) == (hash(a) == hash(b)) + assert (hash(-1) == hash(-3)) == (hash(a) == hash(c)) + assert a != b + assert a != c diff --git a/pint/util.py b/pint/util.py index f5282a300..d8ebcace0 100644 --- a/pint/util.py +++ b/pint/util.py @@ -286,16 +286,20 @@ def remove(self, keys): """ Create a new UnitsContainer purged from given keys. """ - d = udict(self._d) - return UnitsContainer(((key, d[key]) for key in d if key not in keys)) + new = self.copy() + for k in keys: + new._d.pop(k, None) + new._hash = None + return new def rename(self, oldkey, newkey): """ Create a new UnitsContainer in which an entry has been renamed. """ - d = udict(self._d) - d[newkey] = d.pop(oldkey) - return UnitsContainer(d) + new = self.copy() + new._d[newkey] = new._d.pop(oldkey) + new._hash = None + return new def __iter__(self): return iter(self._d) @@ -323,7 +327,14 @@ def __setstate__(self, state): def __eq__(self, other): if isinstance(other, UnitsContainer): + out = UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other) + # Different hashes guarantee that the actual contents are different, but + # identical hashes give no guarantee of equality + # e.g. in CPython, hash(-1) == hash(-2) + if not out: + return False other = other._d + elif isinstance(other, string_types): other = ParserHelper.from_string(other) other = other._d @@ -345,20 +356,25 @@ def format_babel(self, spec, **kwspec): return format_unit(self, spec, **kwspec) def __copy__(self): - return UnitsContainer(self._d) + # Skip expensive health checks performed by __init__ + out = object.__new__(self.__class__) + out._d = self._d.copy() + out._hash = self._hash + return out def __mul__(self, other): - d = udict(self._d) if not isinstance(other, self.__class__): err = 'Cannot multiply UnitsContainer by {}' raise TypeError(err.format(type(other))) + + new = self.copy() for key, value in other.items(): - d[key] += value - keys = [key for key, value in d.items() if value == 0] - for key in keys: - del d[key] + new._d[key] += value + if new._d[key] == 0: + del new._d[key] - return UnitsContainer(d) + new._hash = None + return new __rmul__ = __mul__ @@ -366,26 +382,26 @@ def __pow__(self, other): if not isinstance(other, NUMERIC_TYPES): err = 'Cannot power UnitsContainer by {}' raise TypeError(err.format(type(other))) - d = udict(self._d) - for key, value in d.items(): - d[key] *= other - return UnitsContainer(d) + + new = self.copy() + for key, value in new._d.items(): + new._d[key] *= other + new._hash = None + return new def __truediv__(self, other): if not isinstance(other, self.__class__): err = 'Cannot divide UnitsContainer by {}' raise TypeError(err.format(type(other))) - d = udict(self._d) - + new = self.copy() for key, value in other.items(): - d[key] -= value - - keys = [key for key, value in d.items() if value == 0] - for key in keys: - del d[key] + new._d[key] -= value + if new._d[key] == 0: + del new._d[key] - return UnitsContainer(d) + new._hash = None + return new def __rtruediv__(self, other): if not isinstance(other, self.__class__) and other != 1: @@ -473,7 +489,9 @@ def _from_string(cls, input_string): for key, value in ret.items())) def __copy__(self): - return ParserHelper(scale=self.scale, **self) + new = super(ParserHelper, self).__copy__() + new.scale = self.scale + return new def copy(self): return self.__copy__() @@ -492,9 +510,11 @@ def __setstate__(self, state): self._d, self._hash, self.scale = state def __eq__(self, other): - if isinstance(other, self.__class__): - return self.scale == other.scale and\ + if isinstance(other, ParserHelper): + return ( + self.scale == other.scale and super(ParserHelper, self).__eq__(other) + ) elif isinstance(other, string_types): return self == ParserHelper.from_string(other) elif isinstance(other, Number): From 0a84c83b4b75d2811ed2ab44bb8f5648b183e2bf Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 11 Sep 2019 10:59:59 +0100 Subject: [PATCH 076/612] Minor comments tweak --- pint/testsuite/test_issues.py | 6 +++--- pint/util.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 275ed338b..e031fe5c3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -703,9 +703,9 @@ def test_issue856b(self): def test_issue876(self): # Same hash must not imply equality. - # As an implementation detail of CPython, hash(-1) == hash(-2) This test is - # useless in potential alternative Python implementations where hash(-1) != - # hash(-2); one would need to find hash collisions specific for each + # As an implementation detail of CPython, hash(-1) == hash(-2). + # This test is useless in potential alternative Python implementations where + # hash(-1) != hash(-2); one would need to find hash collisions specific for each # implementation a = UnitsContainer({"[mass]": -1}) diff --git a/pint/util.py b/pint/util.py index d8ebcace0..d89b9579a 100644 --- a/pint/util.py +++ b/pint/util.py @@ -327,11 +327,12 @@ def __setstate__(self, state): def __eq__(self, other): if isinstance(other, UnitsContainer): - out = UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other) + # UnitsContainer.__hash__(self) is not the same as hash(self); see + # ParserHelper.__hash__ and __eq__. # Different hashes guarantee that the actual contents are different, but - # identical hashes give no guarantee of equality + # identical hashes give no guarantee of equality. # e.g. in CPython, hash(-1) == hash(-2) - if not out: + if UnitsContainer.__hash__(self) != UnitsContainer.__hash__(other): return False other = other._d From 61df17ab2db74bf84ffec0c3e1f8dfe53425c25a Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 15:37:59 +0100 Subject: [PATCH 077/612] global pint.Quantity and pint.Unit - incomplete --- pint/__init__.py | 25 +++++++++++++++++++++---- pint/quantity.py | 33 +++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 08ac13241..52c4ac956 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -16,9 +16,15 @@ import pkg_resources from .formatting import formatter -from .registry import (UnitRegistry, LazyRegistry) -from .errors import (DimensionalityError, OffsetUnitCalculusError, - UndefinedUnitError, UnitStrippedWarning) +from .registry import UnitRegistry, LazyRegistry +from .quantity import Quantity +from .unit import Unit +from .errors import ( + DimensionalityError, + OffsetUnitCalculusError, + UndefinedUnitError, + UnitStrippedWarning +) from .util import pi_theorem, logger from .context import Context @@ -83,7 +89,8 @@ def _build_unit(units): def set_application_registry(registry): - """Set the application registry which is used for unpickling operations. + """Set the application registry, which is used for unpickling operations + and when invoking pint.Quantity or pint.Unit directly. :param registry: a UnitRegistry instance. """ @@ -93,6 +100,16 @@ def set_application_registry(registry): _APP_REGISTRY = registry +def get_application_registry(): + """Return the application registry. If :func:`set_application_registry` was never + invoked, return a registry built using :file:`defaults_en.txt` embedded in the pint + package. + + :param registry: a UnitRegistry instance. + """ + return _APP_REGISTRY + + def test(): """Run all tests. diff --git a/pint/quantity.py b/pint/quantity.py index 3373552c7..9c927a5f1 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -31,6 +31,7 @@ fix_str_conversions) from pint.compat import Loc + def _eq(first, second, check_all): """Comparison of scalars and arrays """ @@ -64,6 +65,7 @@ def wrapped(self, *args, **kwargs): return result return wrapped + def check_implemented(f): def wrapped(self, *args, **kwargs): other=args[0] @@ -94,7 +96,7 @@ def printoptions(*args, **kwargs): @fix_str_conversions -class _Quantity(PrettyIPython, SharedRegistryObject): +class Quantity(PrettyIPython, SharedRegistryObject): """Implements a class to describe a physical quantity: the product of a numerical value and a unit of measurement. @@ -107,6 +109,16 @@ class _Quantity(PrettyIPython, SharedRegistryObject): #: Default formatting string. default_format = '' + @property + def _REGISTRY(self): + from . import _APP_REGISTRY + + return _APP_REGISTRY + + @property + def force_ndarray(self): + return self._REGISTRY.force_ndarray + def __reduce__(self): from . import _build_quantity return _build_quantity, (self.magnitude, self._units) @@ -117,7 +129,12 @@ def __new__(cls, value, units=None): if value == '': raise ValueError('Expression to parse as Quantity cannot ' 'be an empty string.') - inst = cls._REGISTRY.parse_expression(value) + _REGISTRY = cls._REGISTRY + if isinstance(_REGISTRY, property): + # Base class, not subclassed with build_*_class + from . import _APP_REGISTRY as _REGISTRY + + inst = _REGISTRY.parse_expression(value) return cls.__new__(cls, inst) elif isinstance(value, cls): inst = copy.copy(value) @@ -134,7 +151,7 @@ def __new__(cls, value, units=None): inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = inst._REGISTRY.parse_units(units)._units elif isinstance(units, SharedRegistryObject): - if isinstance(units, _Quantity) and units.magnitude != 1: + if isinstance(units, Quantity) and units.magnitude != 1: inst = copy.copy(units) logger.warning('Creating new Quantity using a non unity ' 'Quantity as units.') @@ -1737,13 +1754,9 @@ def to_timedelta(self): return datetime.timedelta(microseconds=self.to('microseconds').magnitude) +def build_quantity_class(registry): -def build_quantity_class(registry, force_ndarray=False): - - class Quantity(_Quantity): - pass - - Quantity._REGISTRY = registry - Quantity.force_ndarray = force_ndarray + class Quantity(Quantity): + _REGISTRY = registry return Quantity From 90c531f9f248d3339aa7f661ef0271460352901b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 17:14:06 +0100 Subject: [PATCH 078/612] Unit and Quantity in root module --- pint/__init__.py | 20 ++++++++++++++++++++ pint/quantity.py | 16 ++++++---------- pint/registry.py | 16 ++++++++-------- pint/systems.py | 16 ++++++++-------- pint/testsuite/__init__.py | 10 ++++------ pint/testsuite/test_numpy.py | 2 +- pint/testsuite/test_util.py | 5 +++-- pint/unit.py | 14 ++++++++------ pint/util.py | 10 +++++++++- 9 files changed, 67 insertions(+), 42 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 52c4ac956..de1402535 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -117,3 +117,23 @@ def test(): """ from .testsuite import run return run() + + +# Enumerate all user-facing objects +# Hint to intersphinx that, when building objects.inv, these objects must be registered +# under the top-level module and not in their original submodules +__all__ = ( + 'Context', + 'Quantity', + 'Unit', + 'UnitRegistry', + + 'DimensionalityError', + 'OffsetUnitCalculusError', + 'UndefinedUnitError', + 'UnitStrippedWarning', + + 'get_application_registry', + 'set_application_registry', + '__version__', +) diff --git a/pint/quantity.py b/pint/quantity.py index 9c927a5f1..b0c5014d9 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -109,12 +109,6 @@ class Quantity(PrettyIPython, SharedRegistryObject): #: Default formatting string. default_format = '' - @property - def _REGISTRY(self): - from . import _APP_REGISTRY - - return _APP_REGISTRY - @property def force_ndarray(self): return self._REGISTRY.force_ndarray @@ -266,7 +260,7 @@ def __format__(self, spec): def _repr_pretty_(self, p, cycle): if cycle: - super(_Quantity, self)._repr_pretty_(p, cycle) + super(Quantity, self)._repr_pretty_(p, cycle) else: p.pretty(self.magnitude) p.text(" ") @@ -1214,7 +1208,7 @@ def __neg__(self): def __eq__(self, other): # We compare to the base class of Quantity because # each Quantity class is unique. - if not isinstance(other, _Quantity): + if not isinstance(other, Quantity): if _eq(other, 0, True): # Handle the special case in which we compare to zero # (or an array of zeros) @@ -1754,9 +1748,11 @@ def to_timedelta(self): return datetime.timedelta(microseconds=self.to('microseconds').magnitude) -def build_quantity_class(registry): +_Quantity = Quantity + - class Quantity(Quantity): +def build_quantity_class(registry): + class Quantity(_Quantity): _REGISTRY = registry return Quantity diff --git a/pint/registry.py b/pint/registry.py index a832aec9c..cfe870f51 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -117,9 +117,10 @@ class BaseRegistry(meta.with_metaclass(_Meta)): def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', auto_reduce_dimensions=False): self._register_parsers() - self._init_dynamic_classes(force_ndarray) + self._init_dynamic_classes() self._filename = filename + self.force_ndarray = force_ndarray #: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore' self._on_redefinition = on_redefinition @@ -162,17 +163,17 @@ def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', aut self._initialized = False - def _init_dynamic_classes(self, force_ndarray): + def _init_dynamic_classes(self): """Generate subclasses on the fly and attach them to self """ from .unit import build_unit_class self.Unit = build_unit_class(self) from .quantity import build_quantity_class - self.Quantity = build_quantity_class(self, force_ndarray) + self.Quantity = build_quantity_class(self) from .measurement import build_measurement_class - self.Measurement = build_measurement_class(self, force_ndarray) + self.Measurement = build_measurement_class(self) def _after_init(self): """This should be called after all __init__ @@ -203,8 +204,7 @@ def _parse_defaults(self, ifile): def __deepcopy__(self, memo): new = object.__new__(type(self)) new.__dict__ = copy.deepcopy(self.__dict__, memo) - force_ndarray = self.Quantity.force_ndarray - new._init_dynamic_classes(force_ndarray) + new._init_dynamic_classes() return new def __getattr__(self, item): @@ -1312,8 +1312,8 @@ def __init__(self, system=None, **kwargs): self._groups['root'] = self.Group('root') self._default_system = system - def _init_dynamic_classes(self, force_ndarray): - super(SystemRegistry, self)._init_dynamic_classes(force_ndarray) + def _init_dynamic_classes(self): + super(SystemRegistry, self)._init_dynamic_classes() self.Group = systems.build_group_class(self) self.System = systems.build_system_class(self) diff --git a/pint/systems.py b/pint/systems.py index e73428ecc..913896bc9 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -20,7 +20,7 @@ from pint.compat import Loc -class _Group(SharedRegistryObject): +class Group(SharedRegistryObject): """A group is a set of units. Units can be added directly or by including other groups. @@ -236,7 +236,7 @@ def __getattr__(self, item): return self._REGISTRY -class _System(SharedRegistryObject): +class System(SharedRegistryObject): """A system is a Group plus a set of base units. Members are computed dynamically, that is if a unit is added to a group X @@ -446,19 +446,19 @@ def __getattr__(self, item): return self.d[item] -def build_group_class(registry): +_Group = Group +_System = System + +def build_group_class(registry): class Group(_Group): - pass + _REGISTRY = registry - Group._REGISTRY = registry return Group def build_system_class(registry): - class System(_System): - pass + _REGISTRY = registry - System._REGISTRY = registry return System diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 4b02bc8e5..2f505447b 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -5,15 +5,13 @@ import doctest import logging import os -import sys import unittest from contextlib import contextmanager from pint.compat import ndarray, np -from pint import logger, UnitRegistry -from pint.quantity import _Quantity +from pint import logger, UnitRegistry, Quantity from pint.testsuite.helpers import PintOutputChecker from logging.handlers import BufferingHandler @@ -79,15 +77,15 @@ def setUpClass(cls): cls.U_ = cls.ureg.Unit def _get_comparable_magnitudes(self, first, second, msg): - if isinstance(first, _Quantity) and isinstance(second, _Quantity): + if isinstance(first, Quantity) and isinstance(second, Quantity): second = second.to(first) self.assertEqual(first.units, second.units, msg=msg + ' Units are not equal.') m1, m2 = first.magnitude, second.magnitude - elif isinstance(first, _Quantity): + elif isinstance(first, Quantity): self.assertTrue(first.dimensionless, msg=msg + ' The first is not dimensionless.') first = first.to('') m1, m2 = first.magnitude, second - elif isinstance(second, _Quantity): + elif isinstance(second, Quantity): self.assertTrue(second.dimensionless, msg=msg + ' The second is not dimensionless.') second = second.to('') m1, m2 = first, second.magnitude diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 360b29b53..5053daed9 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -458,7 +458,7 @@ def test_exponentiation_array_exp_2(self): self.assertRaises(DimensionalityError, op.pow, arr_cp, q_cp) # ..not for op.ipow ! # q_cp is treated as if it is an array. The units are ignored. - # _Quantity.__ipow__ is never called + # Quantity.__ipow__ is never called arr_cp = copy.copy(arr) q_cp = copy.copy(q) self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 915fc6648..5de4216d9 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -134,8 +134,9 @@ def test_quantity_conversion(self): UnitsContainer(m=1)) def test_unit_conversion(self): - from pint.unit import _Unit - self.assertEqual(to_units_container(_Unit(UnitsContainer(m=1))), + from pint import Unit + + self.assertEqual(to_units_container(Unit(UnitsContainer(m=1))), UnitsContainer(m=1)) def test_dict_conversion(self): diff --git a/pint/unit.py b/pint/unit.py index 79f304811..d03479bec 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -24,7 +24,7 @@ @fix_str_conversions -class _Unit(PrettyIPython, SharedRegistryObject): +class Unit(PrettyIPython, SharedRegistryObject): """Implements a class to describe a unit supporting math operations. :type units: UnitsContainer, str, Unit or Quantity. @@ -44,7 +44,7 @@ def __new__(cls, units): inst._units = units elif isinstance(units, string_types): inst._units = inst._REGISTRY.parse_units(units)._units - elif isinstance(units, _Unit): + elif isinstance(units, Unit): inst._units = units._units else: raise TypeError('units must be of type str, Unit or ' @@ -210,7 +210,7 @@ def compare(self, other, op): if isinstance(other, NUMERIC_TYPES): return self_q.compare(other, op) - elif isinstance(other, (_Unit, UnitsContainer, dict)): + elif isinstance(other, (Unit, UnitsContainer, dict)): return self_q.compare(self._REGISTRY.Quantity(1, other), op) else: return NotImplemented @@ -288,10 +288,12 @@ def m_from(self, value, strict=True, name='value'): """ return self.from_(value, strict=strict, name=name).magnitude -def build_unit_class(registry): +_Unit = Unit + + +def build_unit_class(registry): class Unit(_Unit): - pass + _REGISTRY = registry - Unit._REGISTRY = registry return Unit diff --git a/pint/util.py b/pint/util.py index d89b9579a..875024b98 100644 --- a/pint/util.py +++ b/pint/util.py @@ -639,10 +639,18 @@ def _is_dim(name): class SharedRegistryObject(object): """Base class for object keeping a refrence to the registree. - Such object are for now _Quantity and _Unit, in a number of places it is + Such object are for now Quantity and Unit, in a number of places it is that an object from this class has a '_units' attribute. """ + @property + def _REGISTRY(self): + """Return the global application registry. This property is overridden by + ``UnitRegistry.__init__`` by creating subclasses on the fly. + """ + from . import _APP_REGISTRY + + return _APP_REGISTRY def _check(self, other): """Check if the other object use a registry and if so that it is the From 1f11a7de12f503b0c14fe6e9158a232cd630f273 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 17:14:42 +0100 Subject: [PATCH 079/612] Sphinx fixes --- docs/developers_reference.rst | 4 ++-- pint/compat/tokenize.py | 5 +++-- pint/converters.py | 2 +- pint/definitions.py | 2 +- pint/errors.py | 2 +- pint/matplotlib.py | 2 +- pint/quantity.py | 24 ++++++++++++------------ pint/registry.py | 6 ++++-- pint/registry_helpers.py | 4 ++-- pint/systems.py | 5 ++--- pint/util.py | 2 +- 11 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst index 9e318bbcf..0f598707d 100644 --- a/docs/developers_reference.rst +++ b/docs/developers_reference.rst @@ -65,7 +65,7 @@ Pint .. automodule:: pint.testsuite.helpers :members: -.. automodule:: pint.testsuite.parametrized +.. automodule:: pint.testsuite.parameterized :members: .. automodule:: pint.testsuite.test_babel @@ -92,7 +92,7 @@ Pint .. automodule:: pint.testsuite.test_issues :members: -.. automodule:: pint.testsuite.test_measurements +.. automodule:: pint.testsuite.test_measurement :members: .. automodule:: pint.testsuite.test_numpy diff --git a/pint/compat/tokenize.py b/pint/compat/tokenize.py index 316622418..8d28b4f8e 100644 --- a/pint/compat/tokenize.py +++ b/pint/compat/tokenize.py @@ -1,7 +1,7 @@ """Tokenization help for Python programs. tokenize(readline) is a generator that breaks a stream of bytes into -Python tokens. It decodes the bytes according to PEP-0263 for +Python tokens. It decodes the bytes according to PEP-0263 for determining source file encoding. It accepts a readline-like method which is called repeatedly to get the @@ -462,7 +462,8 @@ def tokenize(readline): must be a callable object which provides the same interface as the readline() method of built-in file objects. Each call to the function should return one line of input as bytes. Alternately, readline - can be a callable function terminating with StopIteration: + can be a callable function terminating with StopIteration:: + readline = open(myfile, 'rb').__next__ # Example of alternate readline The generator produces 5-tuples with these members: the token type; the diff --git a/pint/converters.py b/pint/converters.py index 41e89af59..6dbf4b4a0 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ pint.converters - ~~~~~~~~~ + ~~~~~~~~~~~~~~~ Functions and classes related to unit conversions. diff --git a/pint/definitions.py b/pint/definitions.py index 811a47187..428aeaeca 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ pint.definitions - ~~~~~~~~~ + ~~~~~~~~~~~~~~~~ Functions and classes related to unit definitions. diff --git a/pint/errors.py b/pint/errors.py index e5bc72d93..700d79cbf 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ pint.errors - ~~~~~~~~~ + ~~~~~~~~~~~ Functions and classes related to unit definitions and conversions. diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 0d28d433d..5dc69422f 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ pint.matplotlib - ~~~~~~~~~ + ~~~~~~~~~~~~~~~ Functions and classes related to working with Matplotlib's support for plotting with units. diff --git a/pint/quantity.py b/pint/quantity.py index b0c5014d9..9a9e6b175 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -100,10 +100,10 @@ class Quantity(PrettyIPython, SharedRegistryObject): """Implements a class to describe a physical quantity: the product of a numerical value and a unit of measurement. - :param value: value of the physical quantity to be created. - :type value: str, Quantity or any numeric type. - :param units: units of the physical quantity to be created. - :type units: UnitsContainer, str or Quantity. + :param value: value of the physical quantity to be created + :type value: str, Quantity or any numeric type + :param units: units of the physical quantity to be created + :type units: UnitsContainer, str or Quantity """ #: Default formatting string. @@ -308,7 +308,7 @@ def m_as(self, units): def units(self): """Quantity's units. Long form for `u` - :rtype: UnitContainer + :rtype: UnitsContainer """ return self._REGISTRY.Unit(self._units) @@ -316,7 +316,7 @@ def units(self): def u(self): """Quantity's units. Short form for `units` - :rtype: UnitContainer + :rtype: UnitsContainer """ return self._REGISTRY.Unit(self._units) @@ -360,10 +360,10 @@ def from_list(cls, quant_list, units=None): If units is not specified and list is empty, the unit cannot be determined and a ValueError is raised. - :param quant_list: list of Quantities + :param quant_list: list of Quantity :type quant_list: list of Quantity - :param units: units of the physical quantity to be created. - :type units: UnitsContainer, str or Quantity. + :param units: units of the physical quantity to be created + :type units: UnitsContainer, str or Quantity """ return cls.from_sequence(quant_list, units=units) @@ -375,10 +375,10 @@ def from_sequence(cls, seq, units=None): If units is not specified and sequence is empty, the unit cannot be determined and a ValueError is raised. - :param seq: sequence of Quantities + :param seq: sequence of Quantity :type seq: sequence of Quantity - :param units: units of the physical quantity to be created. - :type units: UnitsContainer, str or Quantity. + :param units: units of the physical quantity to be created + :type units: UnitsContainer, str or Quantity """ len_seq = len(seq) diff --git a/pint/registry.py b/pint/registry.py index cfe870f51..c97d83380 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -12,11 +12,13 @@ - NonMultiplicativeRegistry: Conversion between non multiplicative (offset) units. (e.g. Temperature) + * Inherits from BaseRegistry - ContextRegisty: Conversion between units with different dimenstions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) + * Inherits from BaseRegistry - SystemRegistry: Group unit and changing of base units. @@ -93,7 +95,7 @@ class BaseRegistry(meta.with_metaclass(_Meta)): :param filename: path of the units definition file to load or line iterable object. Empty to load the default definition file. None to leave the UnitRegistry empty. - :type filename: str | None + :type filename: str or None :param force_ndarray: convert any input, scalar or not to a numpy.ndarray. :param on_redefinition: action to take in case a unit is redefined. 'warn', 'raise', 'ignore' @@ -234,7 +236,7 @@ def define(self, definition): """Add unit to the registry. :param definition: a dimension, unit or prefix definition. - :type definition: str | Definition + :type definition: str or Definition """ if isinstance(definition, string_types): diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index ed993ea55..1f0f170fd 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -220,8 +220,8 @@ def check(ureg, *args): :param ureg: a UnitRegistry instance. :param args: iterable of input units. :return: the wrapped function. - :raises: - :class:`DimensionalityError` if the parameters don't match dimensions + :raises DimensionalityError: + if the parameters don't match dimensions """ dimensions = [ureg.get_dimensionality(dim) if dim is not None else None for dim in args] diff --git a/pint/systems.py b/pint/systems.py index 913896bc9..3c5666018 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -30,14 +30,13 @@ class Group(SharedRegistryObject): The group belongs to one Registry. - It can be specified in the definition file as: + It can be specified in the definition file as:: @group [using , ..., ] ... @end - """ #: Regex to match the header parts of a definition. @@ -244,7 +243,7 @@ class System(SharedRegistryObject): The System belongs to one Registry. - It can be specified in the definition file as: + It can be specified in the definition file as:: @system [using , ..., ] diff --git a/pint/util.py b/pint/util.py index 875024b98..1afaae49b 100644 --- a/pint/util.py +++ b/pint/util.py @@ -637,7 +637,7 @@ def _is_dim(name): class SharedRegistryObject(object): - """Base class for object keeping a refrence to the registree. + """Base class for object keeping a reference to the registree. Such object are for now Quantity and Unit, in a number of places it is that an object from this class has a '_units' attribute. From 30a270ad3590e794b86103e01ccdba880a0bae43 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 17:30:23 +0100 Subject: [PATCH 080/612] Sphinx fixes --- docs/contexts.rst | 3 ++- docs/developers_reference.rst | 3 +++ docs/getting.rst | 3 ++- pint/quantity.py | 32 ++++++++++++++++---------------- pint/registry.py | 4 ++-- pint/registry_helpers.py | 2 +- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 7401b86ac..6e2c4cc14 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -138,7 +138,8 @@ context and the parameters that you wish to set. 398.496240602 -This decorator can be combined with **wraps** or **check** decorators described in `wrapping`_ +This decorator can be combined with **wraps** or **check** decorators described in +:doc:`wrapping`. Defining contexts in a file diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst index 0f598707d..0ba667974 100644 --- a/docs/developers_reference.rst +++ b/docs/developers_reference.rst @@ -5,6 +5,9 @@ Developer reference Pint ==== +.. automodule:: pint + :members: + .. automodule:: pint.babel_names :members: diff --git a/docs/getting.rst b/docs/getting.rst index 75b4bae7f..939b4b49d 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -19,7 +19,8 @@ That's all! You can check that Pint is correctly installed by starting up python .. note:: If you have an old system installation of Python and you don't want to mess with it, you can try `Anaconda CE`_. It is a free Python distribution by Continuum Analytics that includes many scientific packages. To install pint - from the conda-forge channel instead of through pip use: + from the conda-forge channel instead of through pip use:: + $ conda install -c conda-forge pint You can check the installation with the following command: diff --git a/pint/quantity.py b/pint/quantity.py index 9a9e6b175..aedd16dc9 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -101,9 +101,9 @@ class Quantity(PrettyIPython, SharedRegistryObject): the product of a numerical value and a unit of measurement. :param value: value of the physical quantity to be created - :type value: str, Quantity or any numeric type + :type value: str, pint.Quantity or any numeric type :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or Quantity + :type units: UnitsContainer, str or pint.Quantity """ #: Default formatting string. @@ -300,7 +300,7 @@ def m_as(self, units): """Quantity's magnitude expressed in particular units. :param units: destination units - :type units: Quantity, str or dict + :type units: pint.Quantity, str or dict """ return self.to(units).magnitude @@ -360,10 +360,10 @@ def from_list(cls, quant_list, units=None): If units is not specified and list is empty, the unit cannot be determined and a ValueError is raised. - :param quant_list: list of Quantity - :type quant_list: list of Quantity + :param quant_list: list of pint.Quantity + :type quant_list: list of pint.Quantity :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or Quantity + :type units: UnitsContainer, str or pint.Quantity """ return cls.from_sequence(quant_list, units=units) @@ -375,10 +375,10 @@ def from_sequence(cls, seq, units=None): If units is not specified and sequence is empty, the unit cannot be determined and a ValueError is raised. - :param seq: sequence of Quantity - :type seq: sequence of Quantity + :param seq: sequence of pint.Quantity + :type seq: sequence of pint.Quantity :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or Quantity + :type units: UnitsContainer, str or pint.Quantity """ len_seq = len(seq) @@ -430,7 +430,7 @@ def ito(self, other=None, *contexts, **ctx_kwargs): """Inplace rescale to different units. :param other: destination units. - :type other: Quantity, str or dict + :type other: pint.Quantity, str or dict """ other = to_units_container(other, self._REGISTRY) @@ -444,7 +444,7 @@ def to(self, other=None, *contexts, **ctx_kwargs): """Return Quantity rescaled to different units. :param other: destination units. - :type other: Quantity, str or dict + :type other: pint.Quantity, str or dict """ other = to_units_container(other, self._REGISTRY) @@ -619,7 +619,7 @@ def _iadd_sub(self, other, op): """Perform addition or subtraction operation in-place and return the result. :param other: object to be added to / subtracted from self - :type other: Quantity or any type accepted by :func:`_to_magnitude` + :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` :param op: operator function (e.g. operator.add, operator.isub) :type op: function """ @@ -718,7 +718,7 @@ def _add_sub(self, other, op): """Perform addition or subtraction operation and return the result. :param other: object to be added to / subtracted from self - :type other: Quantity or any type accepted by :func:`_to_magnitude` + :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` :param op: operator function (e.g. operator.add, operator.isub) :type op: function """ @@ -852,7 +852,7 @@ def _imul_div(self, other, magnitude_op, units_op=None): result. :param other: object to be multiplied/divided with self - :type other: Quantity or any type accepted by :func:`_to_magnitude` + :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` :param magnitude_op: operator function to perform on the magnitudes (e.g. operator.mul) :type magnitude_op: function @@ -910,7 +910,7 @@ def _mul_div(self, other, magnitude_op, units_op=None): """Perform multiplication or division operation and return the result. :param other: object to be multiplied/divided with self - :type other: Quantity or any type accepted by :func:`_to_magnitude` + :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` :param magnitude_op: operator function to perform on the magnitudes (e.g. operator.mul) :type magnitude_op: function @@ -1726,7 +1726,7 @@ def _has_compatible_delta(self, unit): def _ok_for_muldiv(self, no_offset_units=None): """Checks if Quantity object can be multiplied or divided - :q: quantity object that is checked + :q: pint.Quantity object that is checked :no_offset_units: number of offset units in q """ is_ok = True diff --git a/pint/registry.py b/pint/registry.py index c97d83380..4324c7d54 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -719,9 +719,9 @@ def convert(self, value, src, dst, inplace=False): :param value: value :param src: source units. - :type src: Quantity or str + :type src: pint.Quantity or str :param dst: destination units. - :type dst: Quantity or str + :type dst: pint.Quantity or str :return: converted value """ diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 1f0f170fd..74f16d3d8 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -220,7 +220,7 @@ def check(ureg, *args): :param ureg: a UnitRegistry instance. :param args: iterable of input units. :return: the wrapped function. - :raises DimensionalityError: + :raises pint.DimensionalityError: if the parameters don't match dimensions """ dimensions = [ureg.get_dimensionality(dim) if dim is not None else None for dim in args] From 945c1dd72efcd60ea8938f2ed8ae7b729cfb4022 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 17:42:35 +0100 Subject: [PATCH 081/612] Sphinx fixes --- docs/contexts.rst | 2 +- docs/nonmult.rst | 6 +++--- docs/numpy.rst | 4 ++-- docs/serialization.rst | 21 ++++++++++++++++++--- docs/tutorial.rst | 4 ++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 6e2c4cc14..0b2e273bf 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -17,7 +17,7 @@ raise an error if your do this directly: >>> q.to('Hz') Traceback (most recent call last): ... - pint.errors.DimensionalityError: Cannot convert from 'nanometer' ([length]) to 'hertz' (1 / [time]) + DimensionalityError: Cannot convert from 'nanometer' ([length]) to 'hertz' (1 / [time]) You probably want to use the relation `frequency = speed_of_light / wavelength`: diff --git a/docs/nonmult.rst b/docs/nonmult.rst index 6d83da96d..4d792c1f4 100644 --- a/docs/nonmult.rst +++ b/docs/nonmult.rst @@ -79,7 +79,7 @@ If you want to add a quantity with absolute unit to one with offset unit, like h >>> Q_(10., ureg.degC) + heating_rate * Q_(30, ureg.min) Traceback (most recent call last): ... - pint.errors.OffsetUnitCalculusError: Ambiguous operation with offset unit (degC, kelvin). + OffsetUnitCalculusError: Ambiguous operation with offset unit (degC, kelvin). you have to avoid the ambiguity by either converting the offset unit to the absolute unit before addition @@ -123,7 +123,7 @@ to be explicitly created: >>> home = 25.4 * ureg.degC Traceback (most recent call last): ... - pint.errors.OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). + OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). >>> Q_(25.4, ureg.degC) @@ -157,7 +157,7 @@ You can change the behaviour at any time: >>> 1/T Traceback (most recent call last): ... - pint.errors.OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). + OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). The parser knows about *delta* units and uses them when a temperature unit is found in a multiplicative context. For example, here: diff --git a/docs/numpy.rst b/docs/numpy.rst index 9c351b1b3..8e7a36338 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -51,7 +51,7 @@ All usual Pint methods can be used with this quantity. For example: >>> legs1.to('joule') Traceback (most recent call last): ... - pint.errors.DimensionalityError: Cannot convert from 'meter' ([length]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) + DimensionalityError: Cannot convert from 'meter' ([length]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) NumPy functions are supported by Pint. For example if we define: @@ -96,7 +96,7 @@ results in an error: >>> np.arccos(legs2) Traceback (most recent call last): ... - pint.errors.DimensionalityError: Cannot convert from 'centimeter' ([length]) to 'dimensionless' (dimensionless) + DimensionalityError: Cannot convert from 'centimeter' ([length]) to 'dimensionless' (dimensionless) Support diff --git a/docs/serialization.rst b/docs/serialization.rst index 3e4cf9e64..fe09f9d8e 100644 --- a/docs/serialization.rst +++ b/docs/serialization.rst @@ -51,9 +51,24 @@ UnitRegistry dependent. In certain cases, you want a binary representation of the data. Python's standard algorithm for serialization is called Pickle_. Pint quantities implement the magic `__reduce__` method and therefore can be *Pickled* and *Unpickled*. However, you have to bear in mind, that -the **DEFAULT_REGISTRY** is used for unpickling and this might be different from the one -that was used during pickling. If you want to have control over the deserialization, the -best way is to create a tuple with the magnitude and the units: +the **application registry** is used for unpickling and this might be different from the one +that was used during pickling. + +By default, the application registry is one initialized with :file:`defaults_en.txt`; in +other words, the same as what you get when creating a :class:`pint.UnitRegistry` without +arguments and without adding any definitions afterwards. + +If your application is fine just using :file:`defaults_en.txt`, you don't need to worry +further. + +If your application needs a single, global registry with custom definitions, you must +make sure that it is registered using :func:`pint.set_application_registry` before +unpickling anything. You may use :func:`pint.get_application_registry` to get the +current instance of the application registry. + +Finally, if you need multiple custom registries, it's impossible to correctly unpickle +:class:`pint.Quantity` or :class:`pint.Unit` objects.The best way is to create a tuple +with the magnitude and the units: .. doctest:: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 54638a8b9..2894c0b65 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -89,7 +89,7 @@ If you ask Pint to perform an invalid conversion: >>> speed.to(ureg.joule) Traceback (most recent call last): ... - pint.errors.DimensionalityError: Cannot convert from 'inch / minute' ([length] / [time]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) + DimensionalityError: Cannot convert from 'inch / minute' ([length] / [time]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) Sometimes, the magnitude of the quantity will be very large or very small. The method 'to_compact' can adjust the units to make the quantity more @@ -170,7 +170,7 @@ If you try to use a unit which is not in the registry: >>> speed = 23 * ureg.snail_speed Traceback (most recent call last): ... - pint.errors.UndefinedUnitError: 'snail_speed' is not defined in the unit registry + UndefinedUnitError: 'snail_speed' is not defined in the unit registry You can add your own units to the registry or build your own list. More info on that :ref:`defining` From 91a321b93d80e2afce2ff83e686a82ded2729d50 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Sep 2019 17:58:56 +0100 Subject: [PATCH 082/612] Unit tests cleanup --- pint/testsuite/test_infer_base_unit.py | 8 ++------ pint/testsuite/test_numpy.py | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pint/testsuite/test_infer_base_unit.py b/pint/testsuite/test_infer_base_unit.py index 885f7ed93..12507b62d 100644 --- a/pint/testsuite/test_infer_base_unit.py +++ b/pint/testsuite/test_infer_base_unit.py @@ -1,11 +1,7 @@ -from pint import UnitRegistry, set_application_registry +from pint import Quantity as Q from pint.testsuite import QuantityTestCase from pint.util import infer_base_unit -ureg = UnitRegistry() -set_application_registry(ureg) -Q = ureg.Quantity - class TestInferBaseUnit(QuantityTestCase): def test_infer_base_unit(self): @@ -29,4 +25,4 @@ def test_volts(self): r = Q(1, 'V') * Q(1, 'mV') / Q(1, 'kV') b = infer_base_unit(r) self.assertEqual(b, Q(1, 'V').units) - self.assertQuantityAlmostEqual(r, Q(1, 'uV')) \ No newline at end of file + self.assertQuantityAlmostEqual(r, Q(1, 'uV')) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 5053daed9..398bb7f11 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -6,7 +6,7 @@ import operator as op import unittest -from pint import DimensionalityError, set_application_registry +from pint import DimensionalityError from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.test_umath import TestUFuncs @@ -247,13 +247,13 @@ def test_reversible_op(self): def test_pickle(self): import pickle - set_application_registry(self.ureg) + def pickle_test(q): pq = pickle.loads(pickle.dumps(q)) np.testing.assert_array_equal(q.magnitude, pq.magnitude) self.assertEqual(q.units, pq.units) - pickle_test([10,20]*self.ureg.m) + pickle_test([10, 20]*self.ureg.m) def test_equal(self): x = self.q.magnitude From 19e46f3d7b8ac12dd02a485c23a4fd98cb451a5a Mon Sep 17 00:00:00 2001 From: Shiri Avni Date: Fri, 13 Sep 2019 21:16:02 +0300 Subject: [PATCH 083/612] Update README --- README | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README b/README index 9a0812de6..3e3d3dc9a 100644 --- a/README +++ b/README @@ -57,6 +57,11 @@ Documentation Full documentation is available at http://pint.readthedocs.org/ +GUI Website +----------- + +This [website](www.dimensionalanalysis.org) wraps Pint's "dimensional analysis" methods to provide a GUI. + Design principles ----------------- From 7c30e4a4e469ea7e8e30b75269da3b5cb5661662 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 11:31:58 +0100 Subject: [PATCH 084/612] Unit tests for global Quantity, Unit, and UnitRegistry --- pint/__init__.py | 7 +--- pint/testsuite/test_errors.py | 3 +- pint/testsuite/test_global.py | 73 +++++++++++++++++++++++++++++++++++ pint/testsuite/test_issues.py | 3 +- 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 pint/testsuite/test_global.py diff --git a/pint/__init__.py b/pint/__init__.py index de1402535..65de33b5e 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -58,8 +58,6 @@ def _build_quantity(value, units): """ from .unit import UnitsContainer - global _APP_REGISTRY - # Prefixed units are defined within the registry # on parsing (which does not happen here). # We need to make sure that this happens before using. @@ -76,8 +74,6 @@ def _build_unit(units): """ from .unit import UnitsContainer - global _APP_REGISTRY - # Prefixed units are defined within the registry # on parsing (which does not happen here). # We need to make sure that this happens before using. @@ -94,7 +90,8 @@ def set_application_registry(registry): :param registry: a UnitRegistry instance. """ - assert isinstance(registry, UnitRegistry) + if not isinstance(registry, (LazyRegistry, UnitRegistry)): + raise TypeError("Expected UnitRegistry; got %s" % type(registry)) global _APP_REGISTRY logger.debug('Changing app registry from %r to %r.', _APP_REGISTRY, registry) _APP_REGISTRY = registry diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index 42f2ea513..b6537c160 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -2,9 +2,10 @@ from __future__ import division, unicode_literals, print_function, absolute_import -from pint.errors import DimensionalityError, UndefinedUnitError +from pint import DimensionalityError, UndefinedUnitError from pint.testsuite import BaseTestCase + class TestErrors(BaseTestCase): def test_errors(self): diff --git a/pint/testsuite/test_global.py b/pint/testsuite/test_global.py new file mode 100644 index 000000000..47ad4b8ac --- /dev/null +++ b/pint/testsuite/test_global.py @@ -0,0 +1,73 @@ +"""Tests for global UnitRegistry, Unit, and Quantity +""" +import pickle + +from pint import ( + Quantity, + UndefinedUnitError, + Unit, + UnitRegistry, + get_application_registry, + set_application_registry +) + +from pint.testsuite import BaseTestCase + + +class TestDefaultGlobal(BaseTestCase): + def test_unit(self): + u = Unit("kg") + self.assertEqual(str(u), "kilogram") + u = pickle.loads(pickle.dumps(u)) + self.assertEqual(str(u), "kilogram") + + def test_quantity(self): + q = Quantity("123 kg") + self.assertEqual(str(q.units), "kilogram") + self.assertEqual(q.to("t").magnitude, 0.123) + q = pickle.loads(pickle.dumps(q)) + self.assertEqual(str(q.units), "kilogram") + self.assertEqual(q.to("t").magnitude, 0.123) + + def test_get_application_registry(self): + ureg = get_application_registry() + u = ureg.Unit("kg") + self.assertEqual(str(u), "kilogram") + + def test_pickle_crash(self): + ureg = UnitRegistry(None) + ureg.define("foo = []") + q = ureg.Quantity(123, "foo") + b = pickle.dumps(q) + self.assertRaises(UndefinedUnitError, pickle.loads, b) + + +class TestCustomGlobal(BaseTestCase): + def setUp(self): + super(TestCustomGlobal, self).setUp() + ureg = UnitRegistry(None) + ureg.define("foo = []") + ureg.define("bar = foo / 2") + + self.ureg = ureg + self.ureg_bak = get_application_registry() + set_application_registry(ureg) + assert get_application_registry() is ureg + + def tearDown(self): + super(TestCustomGlobal, self).tearDown() + set_application_registry(self.ureg_bak) + + def test_unit(self): + u = Unit("foo") + self.assertEqual(str(u), "foo") + u = pickle.loads(pickle.dumps(u)) + self.assertEqual(str(u), "foo") + + def test_quantity(self): + q = Quantity("123 foo") + self.assertEqual(str(q.units), "foo") + self.assertEqual(q.to("bar").magnitude, 246) + q = pickle.loads(pickle.dumps(q)) + self.assertEqual(str(q.units), "foo") + self.assertEqual(q.to("bar").magnitude, 246) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index e031fe5c3..eea527b2c 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -7,12 +7,11 @@ import unittest import sys -from pint import UnitRegistry +from pint import DimensionalityError, UndefinedUnitError, UnitRegistry from pint.unit import UnitsContainer from pint.util import ParserHelper from pint.compat import np -from pint.errors import UndefinedUnitError, DimensionalityError from pint.testsuite import QuantityTestCase, helpers From 4084ca13b38eef8e54a75ca8be1c91fc896b0f2b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 11:35:01 +0100 Subject: [PATCH 085/612] Rename file --- pint/testsuite/{test_global.py => test_application_registry.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pint/testsuite/{test_global.py => test_application_registry.py} (100%) diff --git a/pint/testsuite/test_global.py b/pint/testsuite/test_application_registry.py similarity index 100% rename from pint/testsuite/test_global.py rename to pint/testsuite/test_application_registry.py From f05a96f1a1d9c419356f71d462c857dc36332a25 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 11:35:32 +0100 Subject: [PATCH 086/612] Rename file --- pint/testsuite/test_application_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index 47ad4b8ac..616a9f663 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -14,7 +14,7 @@ from pint.testsuite import BaseTestCase -class TestDefaultGlobal(BaseTestCase): +class TestDefaultApplicationRegistry(BaseTestCase): def test_unit(self): u = Unit("kg") self.assertEqual(str(u), "kilogram") @@ -42,9 +42,9 @@ def test_pickle_crash(self): self.assertRaises(UndefinedUnitError, pickle.loads, b) -class TestCustomGlobal(BaseTestCase): +class TestCustomApplicationRegistry(BaseTestCase): def setUp(self): - super(TestCustomGlobal, self).setUp() + super(TestCustomApplicationRegistry, self).setUp() ureg = UnitRegistry(None) ureg.define("foo = []") ureg.define("bar = foo / 2") @@ -55,7 +55,7 @@ def setUp(self): assert get_application_registry() is ureg def tearDown(self): - super(TestCustomGlobal, self).tearDown() + super(TestCustomApplicationRegistry, self).tearDown() set_application_registry(self.ureg_bak) def test_unit(self): From 479287c0c284423b27a5ad6ce5ff189ac9921207 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 11:47:36 +0100 Subject: [PATCH 087/612] Fix uncertainties --- pint/measurement.py | 8 +++----- pint/quantity.py | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pint/measurement.py b/pint/measurement.py index 82ca5a447..deab99cea 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -127,19 +127,17 @@ def __format__(self, spec): return pars.format(mag) + space + format(self.units, spec) -def build_measurement_class(registry, force_ndarray=False): +def build_measurement_class(registry): if ufloat is None: class Measurement(object): + _REGISTRY = registry def __init__(self, *args): raise RuntimeError("Pint requires the 'uncertainties' package to create a Measurement object.") else: class Measurement(_Measurement, registry.Quantity): - pass - - Measurement._REGISTRY = registry - Measurement.force_ndarray = force_ndarray + _REGISTRY = registry return Measurement diff --git a/pint/quantity.py b/pint/quantity.py index aedd16dc9..25e1c0269 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -123,12 +123,12 @@ def __new__(cls, value, units=None): if value == '': raise ValueError('Expression to parse as Quantity cannot ' 'be an empty string.') - _REGISTRY = cls._REGISTRY - if isinstance(_REGISTRY, property): + ureg = cls._REGISTRY + if isinstance(ureg, property): # Base class, not subclassed with build_*_class - from . import _APP_REGISTRY as _REGISTRY + from . import _APP_REGISTRY as ureg - inst = _REGISTRY.parse_expression(value) + inst = ureg.parse_expression(value) return cls.__new__(cls, inst) elif isinstance(value, cls): inst = copy.copy(value) From 961fc8554512660bb0c3a8bf13088e7b83a3c57b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 12:59:01 +0100 Subject: [PATCH 088/612] global pint.Measurement object; fix bug where a Measurement would be upcast to Quantity on pickling --- pint/__init__.py | 45 ++++----------------- pint/measurement.py | 28 ++++++++----- pint/quantity.py | 24 +++++++++-- pint/testsuite/test_application_registry.py | 34 ++++++++++++++++ pint/unit.py | 15 ++++++- 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 65de33b5e..293381777 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -15,19 +15,21 @@ import pkg_resources -from .formatting import formatter -from .registry import UnitRegistry, LazyRegistry -from .quantity import Quantity -from .unit import Unit + +from .context import Context from .errors import ( DimensionalityError, OffsetUnitCalculusError, UndefinedUnitError, UnitStrippedWarning ) +from .formatting import formatter +from .measurement import Measurement +from .quantity import Quantity +from .registry import UnitRegistry, LazyRegistry +from .unit import Unit from .util import pi_theorem, logger -from .context import Context import sys try: @@ -52,38 +54,6 @@ _APP_REGISTRY = _DEFAULT_REGISTRY -def _build_quantity(value, units): - """Build Quantity using the Application registry. - Used only for unpickling operations. - """ - from .unit import UnitsContainer - - # Prefixed units are defined within the registry - # on parsing (which does not happen here). - # We need to make sure that this happens before using. - if isinstance(units, UnitsContainer): - for name in units.keys(): - _APP_REGISTRY.parse_units(name) - - return _APP_REGISTRY.Quantity(value, units) - - -def _build_unit(units): - """Build Unit using the Application registry. - Used only for unpickling operations. - """ - from .unit import UnitsContainer - - # Prefixed units are defined within the registry - # on parsing (which does not happen here). - # We need to make sure that this happens before using. - if isinstance(units, UnitsContainer): - for name in units.keys(): - _APP_REGISTRY.parse_units(name) - - return _APP_REGISTRY.Unit(units) - - def set_application_registry(registry): """Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly. @@ -121,6 +91,7 @@ def test(): # under the top-level module and not in their original submodules __all__ = ( 'Context', + 'Measurement', 'Quantity', 'Unit', 'UnitRegistry', diff --git a/pint/measurement.py b/pint/measurement.py index deab99cea..ba727f8d8 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -11,11 +11,12 @@ from .compat import ufloat from .formatting import _FORMATS, siunitx_format_unit +from .quantity import Quantity MISSING = object() -class _Measurement(object): +class Measurement(Quantity): """Implements a class to describe a quantity with uncertainty. :param value: The most likely value of the measurement. @@ -29,28 +30,28 @@ def __new__(cls, value, error, units=MISSING): try: value, units = value.magnitude, value.units except AttributeError: - #if called with two arguments and the first looks like a ufloat + # if called with two arguments and the first looks like a ufloat # then assume the second argument is the units, keep value intact - if hasattr(value,"nominal_value"): + if hasattr(value, "nominal_value"): units = error - error = MISSING #used for check below + error = MISSING # used for check below else: units = '' try: error = error.to(units).magnitude except AttributeError: pass - + if error is MISSING: mag = value elif error < 0: raise ValueError('The magnitude of the error cannot be negative'.format(value, error)) else: - mag = ufloat(value,error) - - inst = super(_Measurement, cls).__new__(cls, mag, units) + mag = ufloat(value, error) + + inst = super(Measurement, cls).__new__(cls, mag, units) return inst - + @property def value(self): return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units) @@ -63,6 +64,10 @@ def error(self): def rel(self): return float(abs(self.magnitude.std_dev / self.magnitude.nominal_value)) + def __reduce__(self): + # See Quantity.__reduce__ + return Measurement._expand, (self.magnitude, self._units) + def __repr__(self): return "".format(self.magnitude.nominal_value, self.magnitude.std_dev, @@ -127,6 +132,9 @@ def __format__(self, spec): return pars.format(mag) + space + format(self.units, spec) +_Measurement = Measurement + + def build_measurement_class(registry): if ufloat is None: @@ -137,7 +145,7 @@ def __init__(self, *args): raise RuntimeError("Pint requires the 'uncertainties' package to create a Measurement object.") else: - class Measurement(_Measurement, registry.Quantity): + class Measurement(_Measurement): _REGISTRY = registry return Measurement diff --git a/pint/quantity.py b/pint/quantity.py index 25e1c0269..c476e3322 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -114,8 +114,26 @@ def force_ndarray(self): return self._REGISTRY.force_ndarray def __reduce__(self): - from . import _build_quantity - return _build_quantity, (self.magnitude, self._units) + """Allow pickling quantities. Since UnitRegistries are not pickled, upon + unpickling the new object is always attached to the application registry. + """ + # Note: type(self) would be a mistake as subclasses built by + # build_quantity_class can't be pickled + return Quantity._expand, (self.magnitude, self._units) + + @classmethod + def _expand(cls, magnitude, units): + """Rebuild object upon unpickling. + All units must exist in the application registry. + + :param cls: + Quantity or Measurement + """ + from . import _APP_REGISTRY + + for name in units: + _APP_REGISTRY.parse_units(name) + return cls(magnitude, units) def __new__(cls, value, units=None): if units is None: @@ -492,7 +510,6 @@ def to_base_units(self): return self.__class__(magnitude, other) - def ito_reduced_units(self): """Return Quantity scaled in place to reduced units, i.e. one unit per dimension. This will not reduce compound units (intentionally), nor @@ -528,7 +545,6 @@ def to_reduced_units(self): newq.ito_reduced_units() return newq - def to_compact(self, unit=None): """Return Quantity rescaled to compact, human-readable units. diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index 616a9f663..c495e0d33 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -3,6 +3,7 @@ import pickle from pint import ( + Measurement, Quantity, UndefinedUnitError, Unit, @@ -12,6 +13,7 @@ ) from pint.testsuite import BaseTestCase +from pint.testsuite.helpers import requires_uncertainties class TestDefaultApplicationRegistry(BaseTestCase): @@ -29,6 +31,17 @@ def test_quantity(self): self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) + @requires_uncertainties() + def test_measurement(self): + m = Measurement(Quantity(123, 'kg'), Quantity(15, 'kg')) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 15) + self.assertEqual(str(m.units), "kilogram") + m = pickle.loads(pickle.dumps(m)) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 15) + self.assertEqual(str(m.units), "kilogram") + def test_get_application_registry(self): ureg = get_application_registry() u = ureg.Unit("kg") @@ -40,6 +53,16 @@ def test_pickle_crash(self): q = ureg.Quantity(123, "foo") b = pickle.dumps(q) self.assertRaises(UndefinedUnitError, pickle.loads, b) + b = pickle.dumps(q.units) + self.assertRaises(UndefinedUnitError, pickle.loads, b) + + @requires_uncertainties() + def test_pickle_crash_measurement(self): + ureg = UnitRegistry(None) + ureg.define("foo = []") + m = ureg.Quantity(123, "foo").plus_minus(10) + b = pickle.dumps(m) + self.assertRaises(UndefinedUnitError, pickle.loads, b) class TestCustomApplicationRegistry(BaseTestCase): @@ -71,3 +94,14 @@ def test_quantity(self): q = pickle.loads(pickle.dumps(q)) self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) + + @requires_uncertainties() + def test_measurement(self): + m = Measurement(Quantity(123, 'foo'), Quantity(10, 'bar')) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 5) + self.assertEqual(str(m.units), "foo") + m = pickle.loads(pickle.dumps(m)) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 5) + self.assertEqual(str(m.units), "foo") diff --git a/pint/unit.py b/pint/unit.py index d03479bec..a386b1c1c 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -35,8 +35,19 @@ class Unit(PrettyIPython, SharedRegistryObject): default_format = '' def __reduce__(self): - from . import _build_unit - return _build_unit, (self._units, ) + # See notes in Quantity.__reduce__ + return Unit._expand, (self._units, ) + + @staticmethod + def _expand(units): + """Rebuild object upon unpickling. + All units must exist in the application registry. + """ + from . import _APP_REGISTRY + + for name in units: + _APP_REGISTRY.parse_units(name) + return Unit(units) def __new__(cls, units): inst = object.__new__(cls) From 6673f02a05a6a8e20497ac003d4a3876e7a5163b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 13:12:53 +0100 Subject: [PATCH 089/612] sphinx fixes --- pint/measurement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/measurement.py b/pint/measurement.py index ba727f8d8..dc212aa34 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -19,10 +19,10 @@ class Measurement(Quantity): """Implements a class to describe a quantity with uncertainty. - :param value: The most likely value of the measurement. - :type value: Quantity or Number - :param error: The error or uncertainty of the measurement. - :type error: Quantity or Number + :param value: The expected value of the measurement + :type value: pint.Quantity or any numeric type + :param error: The error or uncertainty of the measurement + :type error: pint.Quantity or any numeric type """ def __new__(cls, value, error, units=MISSING): From d5c0f6d3b35ae8b8c54a54a7be0221c677610ba0 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Sep 2019 13:30:39 +0100 Subject: [PATCH 090/612] Python <3.5 can't pickle class methods --- pint/__init__.py | 20 ++++++++++++++++++++ pint/measurement.py | 6 ++++-- pint/quantity.py | 18 +++--------------- pint/unit.py | 13 ++----------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 293381777..0189f086f 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -54,6 +54,26 @@ _APP_REGISTRY = _DEFAULT_REGISTRY +def _unpickle(cls, *args): + """Rebuild object upon unpickling. + All units must exist in the application registry. + + :param cls: + Quantity, Magnitude, or Unit + """ + from .unit import UnitsContainer + + for arg in args: + # Prefixed units are defined within the registry + # on parsing (which does not happen here). + # We need to make sure that this happens before using. + if isinstance(arg, UnitsContainer): + for name in arg: + _APP_REGISTRY.parse_units(name) + + return cls(*args) + + def set_application_registry(registry): """Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly. diff --git a/pint/measurement.py b/pint/measurement.py index dc212aa34..3fc40e173 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -65,8 +65,10 @@ def rel(self): return float(abs(self.magnitude.std_dev / self.magnitude.nominal_value)) def __reduce__(self): - # See Quantity.__reduce__ - return Measurement._expand, (self.magnitude, self._units) + # See notes in Quantity.__reduce__ + from . import _unpickle + + return _unpickle, (Measurement, self.magnitude, self._units) def __repr__(self): return "".format(self.magnitude.nominal_value, diff --git a/pint/quantity.py b/pint/quantity.py index c476e3322..d7fae9ecb 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -117,23 +117,11 @@ def __reduce__(self): """Allow pickling quantities. Since UnitRegistries are not pickled, upon unpickling the new object is always attached to the application registry. """ + from . import _unpickle + # Note: type(self) would be a mistake as subclasses built by # build_quantity_class can't be pickled - return Quantity._expand, (self.magnitude, self._units) - - @classmethod - def _expand(cls, magnitude, units): - """Rebuild object upon unpickling. - All units must exist in the application registry. - - :param cls: - Quantity or Measurement - """ - from . import _APP_REGISTRY - - for name in units: - _APP_REGISTRY.parse_units(name) - return cls(magnitude, units) + return _unpickle, (Quantity, self.magnitude, self._units) def __new__(cls, value, units=None): if units is None: diff --git a/pint/unit.py b/pint/unit.py index a386b1c1c..65f2e5f88 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -36,18 +36,9 @@ class Unit(PrettyIPython, SharedRegistryObject): def __reduce__(self): # See notes in Quantity.__reduce__ - return Unit._expand, (self._units, ) + from . import _unpickle - @staticmethod - def _expand(units): - """Rebuild object upon unpickling. - All units must exist in the application registry. - """ - from . import _APP_REGISTRY - - for name in units: - _APP_REGISTRY.parse_units(name) - return Unit(units) + return _unpickle, (Unit, self._units) def __new__(cls, units): inst = object.__new__(cls) From e70945dda2bb549487e65f9d88f0c07f1a0d7590 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 17:49:09 -0500 Subject: [PATCH 091/612] add NumPy label to ndarray methods in Support section --- docs/numpy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/numpy.rst b/docs/numpy.rst index 9c351b1b3..f310b76a7 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -109,7 +109,7 @@ The following ufuncs_ can be applied to a Quantity object: - **Comparison functions**: greater, greater_equal, less, less_equal, not_equal, equal - **Floating functions**: isreal,iscomplex, isfinite, isinf, isnan, signbit, copysign, nextafter, modf, ldexp, frexp, fmod, floor, ceil, trunc -And the following `ndarrays methods`_ and functions: +And the following `NumPy ndarray methods`_ and functions: - sum, fill, reshape, transpose, flatten, ravel, squeeze, take, put, repeat, sort, argsort, diagonal, compress, nonzero, searchsorted, max, argmax, min, argmin, ptp, clip, round, trace, cumsum, mean, var, std, prod, cumprod, conj, conjugate, flatten From 28f9af217366cb3a9e78acc036523fa504c98b22 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 17:52:38 -0500 Subject: [PATCH 092/612] fix typo in Comments section for NumPy support --- docs/numpy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/numpy.rst b/docs/numpy.rst index f310b76a7..e03188f19 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -142,7 +142,7 @@ memory and CPU cycles. On top of this, all `ufuncs` are implemented in the after the calculation and before returning the value. To our knowledge, there is no way to signal back to NumPy that our code will take care of the calculation. For this reason the calculation is actually done twice: -first in the original ndarray and then in then in the one that has been +first in the original ndarray and then in the one that has been converted to the right units. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude. From c15c8c51d85a8f09601dc9c5985f6901b0b01ae1 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:01:30 -0500 Subject: [PATCH 093/612] fix typo in Specifying Relations section of wrapping --- docs/wrapping.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 84cfc8a55..2e3754b3a 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -183,8 +183,8 @@ arguments: Specifying relations between arguments -------------------------------------- -In certain cases the actual units but just their relation. This is done using string -starting with the equal sign `=`: +In certain cases, you may not be concerned with the actual units and only care about the unit relationships among arguments. +This is done using a string starting with the equal sign `=`: .. doctest:: From 5ea97f92938e7f5b97a4465fdc544ad511067f24 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:34:40 -0500 Subject: [PATCH 094/612] change relationships to relations in previous commit --- docs/wrapping.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 2e3754b3a..14e07d75c 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -183,7 +183,7 @@ arguments: Specifying relations between arguments -------------------------------------- -In certain cases, you may not be concerned with the actual units and only care about the unit relationships among arguments. +In certain cases, you may not be concerned with the actual units and only care about the unit relations among arguments. This is done using a string starting with the equal sign `=`: .. doctest:: From 3225996c19ee74ae05b955914c2f20ea3280b6cf Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:35:55 -0500 Subject: [PATCH 095/612] fix typo in plotting --- docs/plotting.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plotting.rst b/docs/plotting.rst index 70f5306b6..a008d4559 100644 --- a/docs/plotting.rst +++ b/docs/plotting.rst @@ -27,7 +27,7 @@ This support can also be disabled with: >>> ureg.setup_matplotlib(False) -This allows then plotting quantities with different units: +This allows plotting quantities with different units: .. plot:: :include-source: true From b4ff404bf61cc50d585f4e52208ba99fd2ca4e10 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:37:47 -0500 Subject: [PATCH 096/612] fix typo your to you in contexts --- docs/contexts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 7401b86ac..956da0441 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -7,7 +7,7 @@ If you work frequently on certain topics, you will probably find the need to convert between dimensions based on some pre-established (physical) relationships. For example, in spectroscopy you need to transform from wavelength to frequency. These are incompatible units and therefore Pint will -raise an error if your do this directly: +raise an error if you do this directly: .. doctest:: From f0b023daf1840941744ae97fbc78e4e4395fb970 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:45:30 -0500 Subject: [PATCH 097/612] add whitespace for readability in previous commit to wrapping --- docs/wrapping.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 14e07d75c..7b56dccd6 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -184,6 +184,7 @@ Specifying relations between arguments -------------------------------------- In certain cases, you may not be concerned with the actual units and only care about the unit relations among arguments. + This is done using a string starting with the equal sign `=`: .. doctest:: From 014915eee4fcc6f107790e3fb01512767407cc47 Mon Sep 17 00:00:00 2001 From: loganthomas Date: Wed, 2 Oct 2019 18:47:32 -0500 Subject: [PATCH 098/612] update link ref for new NumPy ndarray methods name --- docs/numpy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/numpy.rst b/docs/numpy.rst index e03188f19..6df800480 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -151,4 +151,4 @@ might want to convert the objects first and then use directly the magnitude. .. _`NumPy ndarray`: http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html .. _ufuncs: http://docs.scipy.org/doc/numpy/reference/ufuncs.html -.. _`ndarrays methods`: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods +.. _`NumPy ndarray methods`: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods From a3de3cfe71a729d032a7f760cf334227a873b70f Mon Sep 17 00:00:00 2001 From: Ryan Clary Date: Thu, 3 Oct 2019 11:40:04 -0700 Subject: [PATCH 099/612] Allow for user defined units formatting --- pint/formatting.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index c71437acc..efd68f76d 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -187,14 +187,13 @@ def formatter(items, as_ratio=True, single_denominator=False, # http://docs.python.org/2/library/string.html#format-specification-mini-language # We also add uS for uncertainties. _BASIC_TYPES = frozenset('bcdeEfFgGnosxX%uS') -_KNOWN_TYPES = frozenset(list(_FORMATS.keys()) + ['~']) def _parse_spec(spec): result = '' for ch in reversed(spec): if ch == '~' or ch in _BASIC_TYPES: continue - elif ch in _KNOWN_TYPES: + elif ch in list(_FORMATS.keys()) + ['~']: if result: raise ValueError("expected ':' after format specifier") else: @@ -272,7 +271,7 @@ def _tothe(power): def remove_custom_flags(spec): - for flag in _KNOWN_TYPES: + for flag in list(_FORMATS.keys()) + ['~']: if flag: spec = spec.replace(flag, '') return spec From 539be9fb7ec274ccd7efdaa98320665ee3eac495 Mon Sep 17 00:00:00 2001 From: Shiri Avni Date: Thu, 10 Oct 2019 09:43:30 +0200 Subject: [PATCH 100/612] Update README.rst Updated to include reference to www.dimensionalanalysis.org. --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 1f7dfc024..c696e1921 100644 --- a/README.rst +++ b/README.rst @@ -86,6 +86,11 @@ Documentation Full documentation is available at http://pint.readthedocs.org/ +GUI Website +----------- + +This [website](www.dimensionalanalysis.org) wraps Pint's "dimensional analysis" methods to provide a GUI. + Design principles ----------------- From 3a2c4b4af98f1160f78d3ce3ec3da62f624c6a1e Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 16 Oct 2019 18:14:27 -0500 Subject: [PATCH 101/612] Reimplement _Quantity.__iter__ to return an iterator Props to @crusaderky for the suggested fix. Adds tests verifying iter gives elements as Quantities with same units and using np.iterable(). --- pint/quantity.py | 22 ++++++++++++---------- pint/testsuite/test_numpy.py | 4 ++++ pint/testsuite/test_quantity.py | 13 +++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 3373552c7..01d759234 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -148,22 +148,24 @@ def __new__(cls, value, units=None): inst.__used = False inst.__handling = None - # Only instances where the magnitude is iterable should have __iter__() - if hasattr(inst._magnitude,"__iter__"): - inst.__iter__ = cls._iter + return inst - def _iter(self): - """ - Will be become __iter__() for instances with iterable magnitudes - """ - # # Allow exception to propagate in case of non-iterable magnitude - it_mag = iter(self.magnitude) - return iter((self.__class__(mag, self._units) for mag in it_mag)) @property def debug_used(self): return self.__used + def __iter__(self): + # Make sure that, if self.magnitude is not iterable, we raise TypeError as soon as one + # calls iter(self) without waiting for the first element to be drawn from the iterator + it_magnitude = iter(self.magnitude) + + def it_outer(): + for element in it_magnitude: + yield self.__class__(element, self._units) + + return it_outer() + def __copy__(self): ret = self.__class__(copy.copy(self._magnitude), self._units) ret.__used = self.__used diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 360b29b53..200115cb5 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -235,6 +235,10 @@ def test_iterator(self): for q, v in zip(self.q.flatten(), [1, 2, 3, 4]): self.assertEqual(q, v * self.ureg.m) + def test_iterable(self): + self.assertTrue(np.iterable(self.q)) + self.assertFalse(np.iterable(1 * self.ureg.m)) + def test_reversible_op(self): """ """ diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 41c690381..e5fc076f0 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -392,6 +392,19 @@ def test_from_sequence(self): u_array_5 = self.Q_.from_list(u_seq) self.assertTrue(all(u_array_5 == u_array_ref)) + @helpers.requires_numpy() + def test_iter(self): + # Verify that iteration gives element as Quantity with same units + x = self.Q_([0, 1, 2, 3], 'm') + self.assertQuantityEqual(next(iter(x)), self.Q_(0, 'm')) + + def test_notiter(self): + # Verify that iter() crashes immediately, without needing to draw any + # element from it, if the magnitude isn't iterable + x = self.Q_(1, 'm') + with self.assertRaises(TypeError): + iter(x) + class TestQuantityToCompact(QuantityTestCase): From b2edc86e435ef868e92f7f1316522604e35a313b Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 28 Oct 2019 11:14:27 -0500 Subject: [PATCH 102/612] Patch matplotlib.py for non-iterable arguments --- pint/matplotlib.py | 6 ++++-- pint/testsuite/test_util.py | 21 ++++++++++++++++++++- pint/util.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 0d28d433d..5b51cf953 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -13,6 +13,8 @@ import matplotlib.units +from .util import iterable, sized + class PintAxisInfo(matplotlib.units.AxisInfo): """Support default axis and tick labeling and default limits.""" @@ -31,7 +33,7 @@ def __init__(self, registry): def convert(self, value, unit, axis): """Convert :`Quantity` instances for matplotlib to use.""" - if hasattr(value,"__iter__"): + if iterable(value): return [self._convert_value(v, unit, axis) for v in value] else: return self._convert_value(value, unit, axis) @@ -51,7 +53,7 @@ def axisinfo(unit, axis): @staticmethod def default_units(x, axis): """Get the default unit to use for the given combination of unit and axis.""" - if hasattr(x,"__iter__") and len(x) > 0: + if iterable(x) and sized(x): return getattr(x[0], 'units', None) return getattr(x, 'units', None) diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 915fc6648..da6c02e8c 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -9,7 +9,7 @@ from pint.testsuite import BaseTestCase, QuantityTestCase from pint.util import (string_preprocessor, find_shortest_path, matrix_to_string, transpose, tokenizer, find_connected_nodes, ParserHelper, - UnitsContainer, to_units_container) + UnitsContainer, to_units_container, iterable, sized) class TestUnitsContainer(QuantityTestCase): @@ -333,3 +333,22 @@ def test_matrix_to_string(self): def test_transpose(self): self.assertEqual(transpose([[1, 2], [3, 4]]), [[1, 3], [2, 4]]) + + +class TestOtherUtils(BaseTestCase): + + def test_iterable(self): + + # Test with list, string, generator, and scalar + self.assertTrue(iterable([0, 1, 2, 3])) + self.assertTrue(iterable('test')) + self.assertTrue(iterable((i for i in range(5)))) + self.assertFalse(iterable(0)) + + def test_sized(self): + + # Test with list, string, generator, and scalar + self.assertTrue(sized([0, 1, 2, 3])) + self.assertTrue(sized('test')) + self.assertFalse(sized((i for i in range(5)))) + self.assertFalse(sized(0)) diff --git a/pint/util.py b/pint/util.py index d89b9579a..5480b6821 100644 --- a/pint/util.py +++ b/pint/util.py @@ -810,3 +810,32 @@ def __next__(self): return lineno, line next = __next__ + + +def iterable(y): + """Check whether or not an object can be iterated over. + + Vendored from numpy under the terms of the BSD 3-Clause License. (Copyright + (c) 2005-2019, NumPy Developers.) + + :param value: Input object. + :param type: object + """ + try: + iter(y) + except TypeError: + return False + return True + + +def sized(y): + """Check whether or not an object has a defined length. + + :param value: Input object. + :param type: object + """ + try: + len(y) + except TypeError: + return False + return True From 1eabe645ff2c16cdec1c0a9991f21c246615d593 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 31 Oct 2019 11:21:57 +0000 Subject: [PATCH 103/612] Global Quantity/Pint/Measurement to capture the application registry at init time --- pint/quantity.py | 14 +- pint/testsuite/test_application_registry.py | 154 ++++++++++++++++++-- pint/unit.py | 2 +- pint/util.py | 17 ++- 4 files changed, 155 insertions(+), 32 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 3c18b22f4..7428cab4c 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -129,25 +129,21 @@ def __new__(cls, value, units=None): if value == '': raise ValueError('Expression to parse as Quantity cannot ' 'be an empty string.') - ureg = cls._REGISTRY - if isinstance(ureg, property): - # Base class, not subclassed with build_*_class - from . import _APP_REGISTRY as ureg - + ureg = SharedRegistryObject.__new__(cls)._REGISTRY inst = ureg.parse_expression(value) return cls.__new__(cls, inst) elif isinstance(value, cls): inst = copy.copy(value) else: - inst = object.__new__(cls) + inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = UnitsContainer() elif isinstance(units, (UnitsContainer, UnitDefinition)): - inst = object.__new__(cls) + inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = units elif isinstance(units, string_types): - inst = object.__new__(cls) + inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = inst._REGISTRY.parse_units(units)._units elif isinstance(units, SharedRegistryObject): @@ -156,7 +152,7 @@ def __new__(cls, value, units=None): logger.warning('Creating new Quantity using a non unity ' 'Quantity as units.') else: - inst = object.__new__(cls) + inst = SharedRegistryObject.__new__(cls) inst._units = units._units inst._magnitude = _to_magnitude(value, inst.force_ndarray) else: diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index c495e0d33..f80162c8b 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -9,7 +9,7 @@ Unit, UnitRegistry, get_application_registry, - set_application_registry + set_application_registry, ) from pint.testsuite import BaseTestCase @@ -23,7 +23,7 @@ def test_unit(self): u = pickle.loads(pickle.dumps(u)) self.assertEqual(str(u), "kilogram") - def test_quantity(self): + def test_quantity_1arg(self): q = Quantity("123 kg") self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) @@ -31,9 +31,28 @@ def test_quantity(self): self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) + def test_quantity_2args(self): + q = Quantity(123, "kg") + self.assertEqual(str(q.units), "kilogram") + self.assertEqual(q.to("t").magnitude, 0.123) + q = pickle.loads(pickle.dumps(q)) + self.assertEqual(str(q.units), "kilogram") + self.assertEqual(q.to("t").magnitude, 0.123) + + @requires_uncertainties() + def test_measurement_2args(self): + m = Measurement(Quantity(123, "kg"), Quantity(15, "kg")) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 15) + self.assertEqual(str(m.units), "kilogram") + m = pickle.loads(pickle.dumps(m)) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 15) + self.assertEqual(str(m.units), "kilogram") + @requires_uncertainties() - def test_measurement(self): - m = Measurement(Quantity(123, 'kg'), Quantity(15, 'kg')) + def test_measurement_3args(self): + m = Measurement(123, 15, "kg") self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 15) self.assertEqual(str(m.units), "kilogram") @@ -68,14 +87,12 @@ def test_pickle_crash_measurement(self): class TestCustomApplicationRegistry(BaseTestCase): def setUp(self): super(TestCustomApplicationRegistry, self).setUp() - ureg = UnitRegistry(None) - ureg.define("foo = []") - ureg.define("bar = foo / 2") - - self.ureg = ureg self.ureg_bak = get_application_registry() - set_application_registry(ureg) - assert get_application_registry() is ureg + self.ureg = UnitRegistry(None) + self.ureg.define("foo = []") + self.ureg.define("bar = foo / 2") + set_application_registry(self.ureg) + assert get_application_registry() is self.ureg def tearDown(self): super(TestCustomApplicationRegistry, self).tearDown() @@ -87,7 +104,7 @@ def test_unit(self): u = pickle.loads(pickle.dumps(u)) self.assertEqual(str(u), "foo") - def test_quantity(self): + def test_quantity_1arg(self): q = Quantity("123 foo") self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) @@ -95,9 +112,17 @@ def test_quantity(self): self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) + def test_quantity_2args(self): + q = Quantity(123, "foo") + self.assertEqual(str(q.units), "foo") + self.assertEqual(q.to("bar").magnitude, 246) + q = pickle.loads(pickle.dumps(q)) + self.assertEqual(str(q.units), "foo") + self.assertEqual(q.to("bar").magnitude, 246) + @requires_uncertainties() - def test_measurement(self): - m = Measurement(Quantity(123, 'foo'), Quantity(10, 'bar')) + def test_measurement_2args(self): + m = Measurement(Quantity(123, "foo"), Quantity(10, "bar")) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") @@ -105,3 +130,104 @@ def test_measurement(self): self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") + + @requires_uncertainties() + def test_measurement_3args(self): + m = Measurement(123, 5, "foo") + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 5) + self.assertEqual(str(m.units), "foo") + m = pickle.loads(pickle.dumps(m)) + self.assertEqual(m.value.magnitude, 123) + self.assertEqual(m.error.magnitude, 5) + self.assertEqual(str(m.units), "foo") + + +class TestSwapApplicationRegistry(BaseTestCase): + """Test that the constructors of Quantity, Unit, and Measurement capture + the registry that is set as the application registry at creation time + """ + + def setUp(self): + super(TestSwapApplicationRegistry, self).setUp() + self.ureg_bak = get_application_registry() + self.ureg1 = UnitRegistry(None) + self.ureg1.define("foo = [dim1]") + self.ureg1.define("bar = foo / 2") + self.ureg2 = UnitRegistry(None) + self.ureg2.define("foo = [dim2]") + self.ureg2.define("bar = foo / 3") + + def tearDown(self): + set_application_registry(self.ureg_bak) + + def test_quantity_1arg(self): + set_application_registry(self.ureg1) + q1 = Quantity("1 foo") + set_application_registry(self.ureg2) + q2 = Quantity("1 foo") + q3 = pickle.loads(pickle.dumps(q1)) + assert q1.dimensionality == {"[dim1]": 1} + assert q2.dimensionality == {"[dim2]": 1} + assert q3.dimensionality == {"[dim2]": 1} + assert q1.to("bar").magnitude == 2 + assert q2.to("bar").magnitude == 3 + assert q3.to("bar").magnitude == 3 + + def test_quantity_2args(self): + set_application_registry(self.ureg1) + q1 = Quantity(1, "foo") + set_application_registry(self.ureg2) + q2 = Quantity(1, "foo") + q3 = pickle.loads(pickle.dumps(q1)) + assert q1.dimensionality == {"[dim1]": 1} + assert q2.dimensionality == {"[dim2]": 1} + assert q3.dimensionality == {"[dim2]": 1} + assert q1.to("bar").magnitude == 2 + assert q2.to("bar").magnitude == 3 + assert q3.to("bar").magnitude == 3 + + def test_unit(self): + set_application_registry(self.ureg1) + u1 = Unit("bar") + set_application_registry(self.ureg2) + u2 = Unit("bar") + u3 = pickle.loads(pickle.dumps(u1)) + assert u1.dimensionality == {"[dim1]": 1} + assert u2.dimensionality == {"[dim2]": 1} + assert u3.dimensionality == {"[dim2]": 1} + + @requires_uncertainties() + def test_measurement_2args(self): + set_application_registry(self.ureg1) + m1 = Measurement(Quantity(10, "foo"), Quantity(1, "foo")) + set_application_registry(self.ureg2) + m2 = Measurement(Quantity(10, "foo"), Quantity(1, "foo")) + m3 = pickle.loads(pickle.dumps(m1)) + + assert m1.dimensionality == {"[dim1]": 1} + assert m2.dimensionality == {"[dim2]": 1} + assert m3.dimensionality == {"[dim2]": 1} + self.assertEqual(m1.to("bar").value.magnitude, 20) + self.assertEqual(m2.to("bar").value.magnitude, 30) + self.assertEqual(m3.to("bar").value.magnitude, 30) + self.assertEqual(m1.to("bar").error.magnitude, 2) + self.assertEqual(m2.to("bar").error.magnitude, 3) + self.assertEqual(m3.to("bar").error.magnitude, 3) + + @requires_uncertainties() + def test_measurement_3args(self): + set_application_registry(self.ureg1) + m1 = Measurement(10, 1, "foo") + set_application_registry(self.ureg2) + m2 = Measurement(10, 1, "foo") + m3 = pickle.loads(pickle.dumps(m1)) + + assert m1.dimensionality == {"[dim1]": 1} + assert m2.dimensionality == {"[dim2]": 1} + self.assertEqual(m1.to("bar").value.magnitude, 20) + self.assertEqual(m2.to("bar").value.magnitude, 30) + self.assertEqual(m3.to("bar").value.magnitude, 30) + self.assertEqual(m1.to("bar").error.magnitude, 2) + self.assertEqual(m2.to("bar").error.magnitude, 3) + self.assertEqual(m3.to("bar").error.magnitude, 3) diff --git a/pint/unit.py b/pint/unit.py index 65f2e5f88..7328a35d9 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -41,7 +41,7 @@ def __reduce__(self): return _unpickle, (Unit, self._units) def __new__(cls, units): - inst = object.__new__(cls) + inst = SharedRegistryObject.__new__(cls) if isinstance(units, (UnitsContainer, UnitDefinition)): inst._units = units elif isinstance(units, string_types): diff --git a/pint/util.py b/pint/util.py index a6c51f00d..e447264ee 100644 --- a/pint/util.py +++ b/pint/util.py @@ -643,14 +643,15 @@ class SharedRegistryObject(object): that an object from this class has a '_units' attribute. """ - @property - def _REGISTRY(self): - """Return the global application registry. This property is overridden by - ``UnitRegistry.__init__`` by creating subclasses on the fly. - """ - from . import _APP_REGISTRY - - return _APP_REGISTRY + def __new__(cls, *args, **kwargs): + inst = object.__new__(cls) + if not hasattr(cls, "_REGISTRY"): + # Base class, not subclasses dynamically by + # UnitRegistry._init_dynamic_classes + from . import _APP_REGISTRY + + inst._REGISTRY = _APP_REGISTRY + return inst def _check(self, other): """Check if the other object use a registry and if so that it is the From 435d1932409d64d9244e9f65bb0ccaf93e524df1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 31 Oct 2019 11:25:41 +0000 Subject: [PATCH 104/612] No need for Unit to use __new__ --- pint/unit.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pint/unit.py b/pint/unit.py index 7328a35d9..24c9e0b87 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -40,21 +40,20 @@ def __reduce__(self): return _unpickle, (Unit, self._units) - def __new__(cls, units): - inst = SharedRegistryObject.__new__(cls) + def __init__(self, units): + super(Unit, self).__init__() if isinstance(units, (UnitsContainer, UnitDefinition)): - inst._units = units + self._units = units elif isinstance(units, string_types): - inst._units = inst._REGISTRY.parse_units(units)._units + self._units = self._REGISTRY.parse_units(units)._units elif isinstance(units, Unit): - inst._units = units._units + self._units = units._units else: raise TypeError('units must be of type str, Unit or ' 'UnitsContainer; not {}.'.format(type(units))) - inst.__used = False - inst.__handling = None - return inst + self.__used = False + self.__handling = None @property def debug_used(self): From 7e0556375357d7fa7b9d621d4a4590690b9fc86f Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 2 Dec 2019 16:34:56 +0100 Subject: [PATCH 105/612] mark all regexp / tex strings to raw strings --- pint/context.py | 4 ++-- pint/formatting.py | 2 +- pint/systems.py | 4 ++-- pint/testsuite/helpers.py | 12 ++++++------ pint/testsuite/test_measurement.py | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pint/context.py b/pint/context.py index 6bd733584..256cacb7f 100644 --- a/pint/context.py +++ b/pint/context.py @@ -22,10 +22,10 @@ from .errors import DefinitionSyntaxError #: Regex to match the header parts of a context. -_header_re = re.compile('@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*') +_header_re = re.compile(r'@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*') #: Regex to match variable names in an equation. -_varname_re = re.compile('[A-Za-z_][A-Za-z0-9_]*') +_varname_re = re.compile(r'[A-Za-z_][A-Za-z0-9_]*') def _expression_to_function(eq): diff --git a/pint/formatting.py b/pint/formatting.py index efd68f76d..50eaae972 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -16,7 +16,7 @@ from .babel_names import _babel_units, _babel_lengths from pint.compat import babel_units, Loc, string_types -__JOIN_REG_EXP = re.compile("\{\d*\}") +__JOIN_REG_EXP = re.compile(r"\{\d*\}") def _join(fmt, iterable): diff --git a/pint/systems.py b/pint/systems.py index 3c5666018..676cf49c2 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -40,7 +40,7 @@ class Group(SharedRegistryObject): """ #: Regex to match the header parts of a definition. - _header_re = re.compile('@group\s+(?P\w+)\s*(using\s(?P.*))*') + _header_re = re.compile(r'@group\s+(?P\w+)\s*(using\s(?P.*))*') def __init__(self, name): """ @@ -263,7 +263,7 @@ class System(SharedRegistryObject): """ #: Regex to match the header parts of a context. - _header_re = re.compile('@system\s+(?P\w+)\s*(using\s(?P.*))*') + _header_re = re.compile(r'@system\s+(?P\w+)\s*(using\s(?P.*))*') def __init__(self, name): """ diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 1f58d212c..4b6956c88 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -51,14 +51,14 @@ def requires_python3(): return unittest.skipUnless(PYTHON3, 'Requires Python 3.X.') -_number_re = '([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' -_q_re = re.compile('%s)' % _number_re + - '\s*,\s*' + "'(?P.*)'" + '\s*' + '\)>') +_number_re = r'([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' +_q_re = re.compile(r'%s)' % _number_re + + r'\s*,\s*' + r"'(?P.*)'" + r'\s*' + r'\)>') -_sq_re = re.compile('\s*' + '(?P%s)' % _number_re + - '\s' + "(?P.*)") +_sq_re = re.compile(r'\s*' + r'(?P%s)' % _number_re + + r'\s' + r"(?P.*)") -_unit_re = re.compile('') +_unit_re = re.compile(r'') class PintOutputChecker(doctest.OutputChecker): diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index eff666afb..dab8227af 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -57,7 +57,7 @@ def test_format(self): self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}') self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second2') self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') - self.assertEqual('{0:.1fLx}'.format(m), '\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') + self.assertEqual('{0:.1fLx}'.format(m), r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') def test_format_paru(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') @@ -77,8 +77,8 @@ def test_format_u(self): self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}') self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second2') self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') - self.assertEqual('{0:.3uLx}'.format(m), '\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') - self.assertEqual('{0:.1uLx}'.format(m), '\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') + self.assertEqual('{0:.3uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') + self.assertEqual('{0:.1uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') def test_format_percu(self): self.test_format_perce() From 2f2c88b385250fdb31580bd559a7697d7bcb319c Mon Sep 17 00:00:00 2001 From: "Martin K. Scherer" Date: Mon, 2 Dec 2019 17:14:55 +0100 Subject: [PATCH 106/612] Show proper location of UnitStrippedWarning Users are interested where in their code they triggered the warning and not the original underlying implementation throwing the warning. By setting the stacklevel accordingly, users see the line of their code, which strips the warning. --- pint/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 7428cab4c..f093d08db 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1452,7 +1452,7 @@ def __getattr__(self, item): # Attributes starting with `__array_` are common attributes of NumPy ndarray. # They are requested by numpy functions. if item.startswith('__array_'): - warnings.warn("The unit of the quantity is stripped.", UnitStrippedWarning) + warnings.warn("The unit of the quantity is stripped.", UnitStrippedWarning, stacklevel=2) if isinstance(self._magnitude, ndarray): return getattr(self._magnitude, item) else: From 64701ef5b6268ce92e120151d248909fc8b81d1b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 11:26:56 +0000 Subject: [PATCH 107/612] Apply NEP-29. Drop uncertainties < 3.0. Add support for Python 3.7 and 3.8. --- .travis.yml | 47 +++++++++++++++++++++++++++--------------- docs/getting.rst | 2 +- docs/index.rst | 5 +---- docs/serialization.rst | 2 +- setup.py | 13 ++++++------ 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index 844277492..35ddb766a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,21 +7,35 @@ branches: - trying.tmp env: - # Should pandas tests be removed or replaced wih import checks? - #- UNCERTAINTIES="N" PYTHON="3.6" NUMPY_VERSION=1.14 PANDAS=1 - - UNCERTAINTIES="N" PYTHON="3.3" NUMPY_VERSION=1.9.2 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.4" NUMPY_VERSION=1.11.2 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.5" NUMPY_VERSION=1.11.2 PANDAS=0 - - UNCERTAINTIES="Y" PYTHON="3.5" NUMPY_VERSION=1.11.2 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.6" NUMPY_VERSION=1.11.2 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="2.7" NUMPY_VERSION=0 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.5" NUMPY_VERSION=0 PANDAS=0 - # Test with the latest numpy version - - UNCERTAINTIES="N" PYTHON="2.7" NUMPY_VERSION=1.14 PANDAS=0 - #- UNCERTAINTIES="N" PYTHON="3.4" NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.5" NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES="Y" PYTHON="3.5" NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES="N" PYTHON="3.6" NUMPY_VERSION=1.14 PANDAS=0 + # This project adheres to NEP-29 + # https://numpy.org/neps/nep-0029-deprecation_policy.html + + # Refer to https://docs.scipy.org/doc/numpy/release.html for + # min/max Python version supported by numpy + # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py + # for min/max Python versions supported by uncertainties + # At the moment of writing, uncertainties doesn't officially support Python 3.8 + + # Without numpy, without uncertainties + - UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=0 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.7 NUMPY_VERSION=0 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.8 NUMPY_VERSION=0 PANDAS=0 + # Without numpy, min version of uncertainties + - UNCERTAINTIES=3.0 PYTHON=3.6 NUMPY_VERSION=0 PANDAS=0 + - UNCERTAINTIES=3.0 PYTHON=3.7 NUMPY_VERSION=0 PANDAS=0 + # Min version of numpy, without uncertainties + - UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.7 NUMPY_VERSION=1.14 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.8 NUMPY_VERSION=1.17 PANDAS=0 + # Min version of numpy, min version of uncertainties + - UNCERTAINTIES=3.0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=0 + - UNCERTAINTIES=3.0 PYTHON=3.7 NUMPY_VERSION=1.14 PANDAS=0 + # Latest versions of numpy and uncertainties (where different from the minimum ones) + - UNCERTAINTIES=3.1 PYTHON=3.6 NUMPY_VERSION=1.17 PANDAS=0 + - UNCERTAINTIES=3.1 PYTHON=3.7 NUMPY_VERSION=1.17 PANDAS=0 + + # TODO: Should pandas tests be removed or replaced wih import checks? + #- UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=1 before_install: - sudo apt-get update @@ -51,9 +65,8 @@ before_install: install: - conda create --yes -n $ENV_NAME python=$PYTHON pip - source activate $ENV_NAME - - if [ $UNCERTAINTIES == 'Y' ]; then pip install 'uncertainties==2.4.7.1'; fi + - if [ $UNCERTAINTIES != '0' ]; then pip install uncertainties==$UNCERTAINTIES; fi - if [ $NUMPY_VERSION != '0' ]; then conda install --yes numpy==$NUMPY_VERSION; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' && $NUMPY_VERSION == 1.11.2 && $UNCERTAINTIES == "Y" ]]; then pip install babel serialize pyyaml; fi # this is superslow but suck it up until updates to pandas are made - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - pip install coveralls diff --git a/docs/getting.rst b/docs/getting.rst index 939b4b49d..db3b2ebf9 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -3,7 +3,7 @@ Installation ============ -Pint has no dependencies except Python_ itself. In runs on Python 2.7 and 3.3+. +Pint has no dependencies except Python_ itself. In runs on Python 3.6+. You can install it (or upgrade to the latest version) using pip_:: diff --git a/docs/index.rst b/docs/index.rst index b6eac5dba..bb76a7249 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 2.7 and 3.3+ with no other +It has a complete test coverage. It runs in Python 3.6+ with no other dependency. It is licensed under BSD. @@ -99,9 +99,6 @@ points, like positions on a map or absolute temperature scales. **Dependency free**: it depends only on Python and its standard library. -**Python 2 and 3**: a single codebase that runs unchanged in Python 2.7+ and -Python 3.3+. - **Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. diff --git a/docs/serialization.rst b/docs/serialization.rst index fe09f9d8e..1ebd24552 100644 --- a/docs/serialization.rst +++ b/docs/serialization.rst @@ -109,7 +109,7 @@ Using the serialize_ package you can load and read from multiple formats: .. _Pickle: http://docs.python.org/3/library/pickle.html .. _json: http://docs.python.org/3/library/json.html .. _yaml: http://pyyaml.org/ -.. _shelve: http://docs.python.org/3.4/library/shelve.html +.. _shelve: http://docs.python.org/3.6/library/shelve.html .. _hdf5: http://www.h5py.org/ .. _PyTables: http://www.pytables.org .. _dill: https://pypi.python.org/pypi/dill diff --git a/setup.py b/setup.py index a6d83d6fc..478177c31 100644 --- a/setup.py +++ b/setup.py @@ -56,14 +56,13 @@ def read(filename): 'Programming Language :: Python', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], + python_requires='>=3.6', extras_require={ - ':python_version == "2.7"': [ - 'funcsigs', - ], + 'numpy': ['numpy >= 1.14'], + 'uncertainties': ['uncertainties >= 3.0'], }, ) From 93df6c460178b741048a3f5c3a621b487de9ea5d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 11:32:37 +0000 Subject: [PATCH 108/612] Remove Python 2.7 specific code --- .travis.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35ddb766a..9954df204 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,19 +39,12 @@ env: before_install: - sudo apt-get update - - if [[ "$PYTHON" == "2.7" ]]; then - wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; - else - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - fi + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r - conda config --set always_yes yes --set changeps1 no - conda update -q conda - - if [[ "$PYTHON" != "2.7" ]]; then - conda config --set restore_free_channel yes; - fi # Useful for debugging any issues with conda - conda info -a From 6fb06116f0f2a4fe0aadd4ceec8a503a9178237a Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 11:47:22 +0000 Subject: [PATCH 109/612] Unbound max versions --- .travis.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9954df204..2c54716da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,29 +17,29 @@ env: # At the moment of writing, uncertainties doesn't officially support Python 3.8 # Without numpy, without uncertainties - - UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=0 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.7 NUMPY_VERSION=0 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.8 NUMPY_VERSION=0 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.6 NUMPY=0 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.7 NUMPY=0 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.8 NUMPY=0 PANDAS=0 # Without numpy, min version of uncertainties - - UNCERTAINTIES=3.0 PYTHON=3.6 NUMPY_VERSION=0 PANDAS=0 - - UNCERTAINTIES=3.0 PYTHON=3.7 NUMPY_VERSION=0 PANDAS=0 + - UNCERTAINTIES=3.0.1 PYTHON=3.6 NUMPY=0 PANDAS=0 + - UNCERTAINTIES=3.0.1 PYTHON=3.7 NUMPY=0 PANDAS=0 # Min version of numpy, without uncertainties - - UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.7 NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.8 NUMPY_VERSION=1.17 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.6 NUMPY=1.14 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.7 NUMPY=1.14 PANDAS=0 + - UNCERTAINTIES=0 PYTHON=3.8 NUMPY=1.17 PANDAS=0 # Min version of numpy, min version of uncertainties - - UNCERTAINTIES=3.0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=0 - - UNCERTAINTIES=3.0 PYTHON=3.7 NUMPY_VERSION=1.14 PANDAS=0 + - UNCERTAINTIES=3.0.1 PYTHON=3.6 NUMPY=1.14 PANDAS=0 + - UNCERTAINTIES=3.0.1 PYTHON=3.7 NUMPY=1.14 PANDAS=0 # Latest versions of numpy and uncertainties (where different from the minimum ones) - - UNCERTAINTIES=3.1 PYTHON=3.6 NUMPY_VERSION=1.17 PANDAS=0 - - UNCERTAINTIES=3.1 PYTHON=3.7 NUMPY_VERSION=1.17 PANDAS=0 + - UNCERTAINTIES=999 PYTHON=3.6 NUMPY=999 PANDAS=0 + - UNCERTAINTIES=999 PYTHON=3.7 NUMPY=999 PANDAS=0 # TODO: Should pandas tests be removed or replaced wih import checks? - #- UNCERTAINTIES=0 PYTHON=3.6 NUMPY_VERSION=1.14 PANDAS=1 + #- UNCERTAINTIES=0 PYTHON=3.6 NUMPY=1.14 PANDAS=1 before_install: - sudo apt-get update - - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r @@ -58,8 +58,10 @@ before_install: install: - conda create --yes -n $ENV_NAME python=$PYTHON pip - source activate $ENV_NAME - - if [ $UNCERTAINTIES != '0' ]; then pip install uncertainties==$UNCERTAINTIES; fi - - if [ $NUMPY_VERSION != '0' ]; then conda install --yes numpy==$NUMPY_VERSION; fi + - if [[ $UNCERTAINTIES != 0 && $UNCERTAINTIES < 999 ]]; then pip install uncertainties==$UNCERTAINTIES; fi + - if [[ $UNCERTAINTIES == 999 ]]; then pip install uncertainties; fi + - if [[ $NUMPY != 0 && $NUMPY < 999 ]; then conda install --yes numpy==$NUMPY; fi + - if [[ $NUMPY == "latest" ]; then conda install --yes numpy; fi # this is superslow but suck it up until updates to pandas are made - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - pip install coveralls From 3f67922b953e3167f5c2417503347f9155881698 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 11:50:48 +0000 Subject: [PATCH 110/612] uncertainties on Python 3.8 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2c54716da..b489892b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,6 @@ env: # min/max Python version supported by numpy # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py # for min/max Python versions supported by uncertainties - # At the moment of writing, uncertainties doesn't officially support Python 3.8 # Without numpy, without uncertainties - UNCERTAINTIES=0 PYTHON=3.6 NUMPY=0 PANDAS=0 @@ -33,6 +32,9 @@ env: # Latest versions of numpy and uncertainties (where different from the minimum ones) - UNCERTAINTIES=999 PYTHON=3.6 NUMPY=999 PANDAS=0 - UNCERTAINTIES=999 PYTHON=3.7 NUMPY=999 PANDAS=0 + # At the moment of writing, uncertainties doesn't officially support Python 3.8 + # We're testing here at our own risk + - UNCERTAINTIES=999 PYTHON=3.8 NUMPY=999 PANDAS=0 # TODO: Should pandas tests be removed or replaced wih import checks? #- UNCERTAINTIES=0 PYTHON=3.6 NUMPY=1.14 PANDAS=1 From e38205401554efc31800405698f405351d570574 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 11:56:29 +0000 Subject: [PATCH 111/612] Bugfix --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b489892b3..b700eb065 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,8 +62,8 @@ install: - source activate $ENV_NAME - if [[ $UNCERTAINTIES != 0 && $UNCERTAINTIES < 999 ]]; then pip install uncertainties==$UNCERTAINTIES; fi - if [[ $UNCERTAINTIES == 999 ]]; then pip install uncertainties; fi - - if [[ $NUMPY != 0 && $NUMPY < 999 ]; then conda install --yes numpy==$NUMPY; fi - - if [[ $NUMPY == "latest" ]; then conda install --yes numpy; fi + - if [[ $NUMPY != 0 && $NUMPY < 999 ]]; then conda install --yes numpy==$NUMPY; fi + - if [[ $NUMPY == 999 ]]; then conda install --yes numpy; fi # this is superslow but suck it up until updates to pandas are made - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - pip install coveralls From d7ddc1de4d13497471f0aee5d909b1637b4476c3 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 13:25:00 +0000 Subject: [PATCH 112/612] Remove unneeded files --- .travis-exclude.yml | 7 --- .travis-full.yml | 106 -------------------------------------------- 2 files changed, 113 deletions(-) delete mode 100644 .travis-exclude.yml delete mode 100644 .travis-full.yml diff --git a/.travis-exclude.yml b/.travis-exclude.yml deleted file mode 100644 index 372418dc7..000000000 --- a/.travis-exclude.yml +++ /dev/null @@ -1,7 +0,0 @@ -# This file is used to autogenerate the travis exclude matrix. -rules: - "3.5": "NUMPY_VERSION>=1.9.2" - "3.4": "NUMPY_VERSION>=1.8.1" - "3.3": "NUMPY_VERSION>=1.7.0,<=1.9.2" - "2.7": "NUMPY_VERSION>1.5.1" - diff --git a/.travis-full.yml b/.travis-full.yml deleted file mode 100644 index 9e9776f56..000000000 --- a/.travis-full.yml +++ /dev/null @@ -1,106 +0,0 @@ -language: python - -python: - - "2.7" - - "3.3" - - "3.4" - - "3.5" -env: - - UNCERTAINTIES="N" NUMPY_VERSION=0 - - UNCERTAINTIES="N" NUMPY_VERSION=1.6.2 - - UNCERTAINTIES="N" NUMPY_VERSION=1.7.1 - - UNCERTAINTIES="N" NUMPY_VERSION=1.8.2 - - UNCERTAINTIES="N" NUMPY_VERSION=1.9.3 - - UNCERTAINTIES="N" NUMPY_VERSION=1.10.4 - - UNCERTAINTIES="Y" NUMPY_VERSION=0 - - UNCERTAINTIES="Y" NUMPY_VERSION=1.6.2 - - UNCERTAINTIES="Y" NUMPY_VERSION=1.7.1 - - UNCERTAINTIES="Y" NUMPY_VERSION=1.8.2 - - UNCERTAINTIES="Y" NUMPY_VERSION=1.9.3 - - UNCERTAINTIES="Y" NUMPY_VERSION=1.10.4 - -branches: - only: - - master - - develop - -before_install: - - sudo apt-get update - - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; - else - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - fi - - bash miniconda.sh -b -p $HOME/miniconda - - export PATH="$HOME/miniconda/bin:$PATH" - - hash -r - - conda config --set always_yes yes --set changeps1 no - - conda update -q conda - # Useful for debugging any issues with conda - - conda info -a - - # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda - - sudo rm -rf /dev/shm - - sudo ln -s /run/shm /dev/shm - - - export ENV_NAME=travis - -install: - - conda create -c mwcraig --yes -n $ENV_NAME python=$TRAVIS_PYTHON_VERSION pip - - source activate $ENV_NAME - - if [ $UNCERTAINTIES == 'Y' ]; then pip install 'uncertainties==2.4.7.1'; fi - - if [ $NUMPY_VERSION != '0' ]; then conda install -c mwcraig --yes numpy==$NUMPY_VERSION; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' && $NUMPY_VERSION == 1.10.4 && $UNCERTAINTIES == "Y" ]]; then pip install serialize pyyaml; fi - - pip install coveralls - -script: - - python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" setup.py test - - coverage combine - - coverage report -m - -after_success: - - coveralls --verbose - -matrix: - exclude: -# Do not edit after this line - - python: "3.5" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.8.2 - - python: "3.5" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.6.2 - - python: "3.5" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.8.2 - - python: "3.5" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.6.2 - - python: "3.5" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.7.1 - - python: "3.5" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.7.1 - - python: "3.4" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.6.2 - - python: "3.4" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.6.2 - - python: "3.4" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.7.1 - - python: "3.4" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.7.1 - - python: "3.3" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.6.2 - - python: "3.3" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.9.3 - - python: "3.3" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.10.4 - - python: "3.3" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.6.2 - - python: "3.3" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.9.3 - - python: "3.3" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.10.4 - - python: "2.6" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.9.3 - - python: "2.6" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.10.4 - - python: "2.6" - env: UNCERTAINTIES="Y" NUMPY_VERSION=1.9.3 - - python: "2.6" - env: UNCERTAINTIES="N" NUMPY_VERSION=1.10.4 From 9ab86896f73bc8ce492be863bc5961a1a06a496b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 13:44:47 +0000 Subject: [PATCH 113/612] Simplify travis CI --- .travis.yml | 57 +++++++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/.travis.yml b/.travis.yml index b700eb065..ea25ab36c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,29 +15,22 @@ env: # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py # for min/max Python versions supported by uncertainties - # Without numpy, without uncertainties - - UNCERTAINTIES=0 PYTHON=3.6 NUMPY=0 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.7 NUMPY=0 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.8 NUMPY=0 PANDAS=0 - # Without numpy, min version of uncertainties - - UNCERTAINTIES=3.0.1 PYTHON=3.6 NUMPY=0 PANDAS=0 - - UNCERTAINTIES=3.0.1 PYTHON=3.7 NUMPY=0 PANDAS=0 - # Min version of numpy, without uncertainties - - UNCERTAINTIES=0 PYTHON=3.6 NUMPY=1.14 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.7 NUMPY=1.14 PANDAS=0 - - UNCERTAINTIES=0 PYTHON=3.8 NUMPY=1.17 PANDAS=0 - # Min version of numpy, min version of uncertainties - - UNCERTAINTIES=3.0.1 PYTHON=3.6 NUMPY=1.14 PANDAS=0 - - UNCERTAINTIES=3.0.1 PYTHON=3.7 NUMPY=1.14 PANDAS=0 - # Latest versions of numpy and uncertainties (where different from the minimum ones) - - UNCERTAINTIES=999 PYTHON=3.6 NUMPY=999 PANDAS=0 - - UNCERTAINTIES=999 PYTHON=3.7 NUMPY=999 PANDAS=0 - # At the moment of writing, uncertainties doesn't officially support Python 3.8 - # We're testing here at our own risk - - UNCERTAINTIES=999 PYTHON=3.8 NUMPY=999 PANDAS=0 + - PKGS="python=3.6" + - PKGS="python=3.7" + - PKGS="python=3.8" + - PKGS="python=3.6 uncertainties=3.0" + - PKGS="python=3.7 uncertainties=3.0" + - PKGS="python=3.6 numpy=1.14" + - PKGS="python=3.7 numpy=1.14" + - PKGS="python=3.8 numpy=1.17" + - PKGS="python=3.6 numpy=1.14 uncertainties=3.0" + - PKGS="python=3.7 numpy=1.14 uncertainties=3.0" + - PKGS="python=3.6 numpy uncertainties" + - PKGS="python=3.7 numpy uncertainties" + - PKGS="python=3.8 numpy uncertainties" - # TODO: Should pandas tests be removed or replaced wih import checks? - #- UNCERTAINTIES=0 PYTHON=3.6 NUMPY=1.14 PANDAS=1 + # TODO: pandas tests + # - PKGS="python=3.7 numpy pandas uncertainties pandas" before_install: - sudo apt-get update @@ -46,6 +39,7 @@ before_install: - export PATH="$HOME/miniconda/bin:$PATH" - hash -r - conda config --set always_yes yes --set changeps1 no + - conda config --add channels conda-forge - conda update -q conda # Useful for debugging any issues with conda - conda info -a @@ -55,25 +49,20 @@ before_install: # - sudo rm -rf /dev/shm # - sudo ln -s /run/shm /dev/shm - - export ENV_NAME=travis - install: - - conda create --yes -n $ENV_NAME python=$PYTHON pip - - source activate $ENV_NAME - - if [[ $UNCERTAINTIES != 0 && $UNCERTAINTIES < 999 ]]; then pip install uncertainties==$UNCERTAINTIES; fi - - if [[ $UNCERTAINTIES == 999 ]]; then pip install uncertainties; fi - - if [[ $NUMPY != 0 && $NUMPY < 999 ]]; then conda install --yes numpy==$NUMPY; fi - - if [[ $NUMPY == 999 ]]; then conda install --yes numpy; fi + - conda create -n travis $PKGS coveralls + - source activate travis + - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi # this is superslow but suck it up until updates to pandas are made - - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - - pip install coveralls + # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi + - conda list script: # if we're doing the pandas tests and hence have pytest available, we can # simply use it to run all the tests - - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi + # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi # test notebooks too if pandas available - - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi + # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == '0' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi - coverage combine - coverage report -m From 2a3b1135f55b6b9bb8de739b16ff02f66274ad4b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 15:00:04 +0000 Subject: [PATCH 114/612] Code review --- AUTHORS | 1 + pint/context.py | 10 +++----- pint/registry.py | 64 ++++++++++++++++++++++++------------------------ 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6bb635827..21ba865f8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,6 +20,7 @@ Other contributors, listed alphabetically, are: * Felix Hummel * Francisco Couzo * Giel van Schijndel +* Guido Imperiale * Ignacio Fdez. Galván * James Rowe * Jim Turner diff --git a/pint/context.py b/pint/context.py index 3f082bc2a..bce99daef 100644 --- a/pint/context.py +++ b/pint/context.py @@ -89,7 +89,7 @@ def from_context(cls, context, **defaults): newdef = dict(context.defaults, **defaults) c = cls(context.name, context.aliases, newdef) c.funcs = context.funcs - for edge in context.funcs.keys(): + for edge in context.funcs: c.relation_to_context[edge] = c return c return context @@ -192,16 +192,14 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) - def __hash__(self): - return hash(self.name) class ContextChain(ChainMap): """A specialized ChainMap for contexts that simplifies finding rules to transform from one dimension to another. """ - def __init__(self, *args, **kwargs): - super(ContextChain, self).__init__(*args, **kwargs) + def __init__(self): + super(ContextChain, self).__init__() self._graph = None self._contexts = () @@ -212,7 +210,7 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts = tuple(contexts) + self._contexts + self._contexts = contexts + self._contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None diff --git a/pint/registry.py b/pint/registry.py index e3eafbb56..59df79394 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -79,21 +79,24 @@ def __call__(self, *args, **kwargs): obj._after_init() return obj -class RegistryCache(): - """Cache to speed up unit registries""" + +class RegistryCache(object): + """Cache to speed up unit registries + """ def __init__(self): #: Maps dimensionality (UnitsContainer) to Units (str) - self.dimensional_equivalents = dict() + self.dimensional_equivalents = {} #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) - self.root_units = dict() + self.root_units = {} #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) - self.dimensionality = dict() + self.dimensionality = {} #: Cache the unit name associated to user input. ('mV' -> 'millivolt') - self.parse_unit = dict() + self.parse_unit = {} + class BaseRegistry(meta.with_metaclass(_Meta)): """Base class for all registries. @@ -466,10 +469,8 @@ def _build_cache(self): dimensionality[uc] = di if not prefixed: - if di not in dimensional_equivalents: - dimensional_equivalents[di] = set() - - dimensional_equivalents[di].add(self._units[base_name]._name) + dimeq_set = dimensional_equivalents.setdefault(di, set()) + dimeq_set.add(self._units[base_name]._name) except Exception as e: logger.warning('Could not resolve {0}: {1!r}'.format(unit_name, e)) @@ -566,8 +567,10 @@ def _get_dimensionality(self, input_units): cache = self._cache.dimensionality - if input_units in cache: + try: return cache[input_units] + except KeyError: + pass accumulator = defaultdict(float) self._get_dimensionality_recurse(input_units, 1.0, accumulator) @@ -653,11 +656,13 @@ def _get_root_units(self, input_units, check_nonmult=True): if not input_units: return 1., UnitsContainer() - cache = self._cache.root_units - # The cache is only done for check_nonmult=True - if check_nonmult and input_units in cache: - return cache[input_units] + cache = self._cache.root_units + if check_nonmult: + try: + return cache[input_units] + except KeyError: + pass accumulators = [1., defaultdict(float)] self._get_root_units_recurse(input_units, 1.0, accumulators) @@ -722,10 +727,7 @@ def _get_compatible_units(self, input_units, group_or_system): return frozenset() src_dim = self._get_dimensionality(input_units) - - ret = self._cache.dimensional_equivalents[src_dim] - - return ret + return self._cache.dimensional_equivalents[src_dim] def convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. @@ -829,16 +831,15 @@ def parse_units(self, input_string, as_delta=None): units = self._parse_units(input_string, as_delta) return self.Unit(units) - def _parse_units(self, input_string, as_delta=None): + def _parse_units(self, input_string, as_delta=True): """ """ - if as_delta is None: - as_delta = True - cache = self._cache.parse_unit - - if as_delta and input_string in cache: - return cache[input_string] + if as_delta: + try: + return cache[input_string] + except KeyError: + pass if not input_string: return UnitsContainer() @@ -1083,7 +1084,7 @@ def __init__(self, **kwargs): self._active_ctx = ContextChain() #: Map context chain to cache - self._caches = { self._active_ctx: self._cache } + self._caches = {self._active_ctx: self._cache} def _register_parsers(self): super(ContextRegistry, self)._register_parsers() @@ -1130,12 +1131,11 @@ def remove_context(self, name_or_alias): def _build_cache(self): """""" - cache = self._caches.get(self._active_ctx) - if cache is None: - super()._build_cache() + try: + self._cache = self._caches[self._active_ctx] + except KeyError: + super(ContextRegistry, self)._build_cache() self._caches[self._active_ctx] = self._cache - else: - self._cache = cache def enable_contexts(self, *names_or_contexts, **kwargs): """Enable contexts provided by name or by object. From f1d75430eacd1f5bca490033711a8f3baeb6a47e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 15:46:18 +0000 Subject: [PATCH 115/612] Fix failing tests --- pint/registry.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 59df79394..7d9fdbbcb 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -441,12 +441,11 @@ def _build_cache(self): """Build a cache of dimensionality and base units. """ self._cache = RegistryCache() - root_units = self._cache.root_units - dimensionality = self._cache.dimensionality - dimensional_equivalents = self._cache.dimensional_equivalents - deps = dict((name, set(definition.reference.keys() if definition.reference else {})) - for name, definition in self._units.items()) + deps = { + name: set(definition.reference.keys()) if definition.reference else set() + for name, definition in self._units.items() + } for unit_names in solve_dependencies(deps): for unit_name in unit_names: @@ -465,11 +464,13 @@ def _build_cache(self): bu = self._get_root_units(uc) di = self._get_dimensionality(uc) - root_units[uc] = bu - dimensionality[uc] = di + self._cache.root_units[uc] = bu + self._cache.dimensionality[uc] = di if not prefixed: - dimeq_set = dimensional_equivalents.setdefault(di, set()) + dimeq_set = self._cache.dimensional_equivalents.setdefault( + di, set() + ) dimeq_set.add(self._units[base_name]._name) except Exception as e: @@ -1075,16 +1076,14 @@ class ContextRegistry(BaseRegistry): """ def __init__(self, **kwargs): - super(ContextRegistry, self).__init__(**kwargs) - #: Map context name (string) or abbreviation to context. self._contexts = {} - #: Stores active contexts. self._active_ctx = ContextChain() - #: Map context chain to cache - self._caches = {self._active_ctx: self._cache} + self._caches = {} + + super(ContextRegistry, self).__init__(**kwargs) def _register_parsers(self): super(ContextRegistry, self)._register_parsers() @@ -1130,7 +1129,6 @@ def remove_context(self, name_or_alias): return context def _build_cache(self): - """""" try: self._cache = self._caches[self._active_ctx] except KeyError: From 906b7fab656210d3d00b27aebb0af3134e8f4831 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 16:24:55 +0000 Subject: [PATCH 116/612] Fix last bug --- pint/context.py | 6 +++--- pint/registry.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pint/context.py b/pint/context.py index bce99daef..ea06d9e14 100644 --- a/pint/context.py +++ b/pint/context.py @@ -201,7 +201,7 @@ class ContextChain(ChainMap): def __init__(self): super(ContextChain, self).__init__() self._graph = None - self._contexts = () + self._contexts = [] def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. @@ -210,7 +210,7 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts = contexts + self._contexts + self._contexts.insert(0, contexts) self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None @@ -246,4 +246,4 @@ def transform(self, src, dst, registry, value): return self[(src, dst)].transform(src, dst, registry, value) def __hash__(self): - return hash(self._contexts) + return hash(tuple(self._contexts)) diff --git a/pint/registry.py b/pint/registry.py index 7d9fdbbcb..b9ff71c10 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -80,7 +80,7 @@ def __call__(self, *args, **kwargs): return obj -class RegistryCache(object): +class RegistryCache: """Cache to speed up unit registries """ @@ -1083,7 +1083,7 @@ def __init__(self, **kwargs): #: Map context chain to cache self._caches = {} - super(ContextRegistry, self).__init__(**kwargs) + super().__init__(**kwargs) def _register_parsers(self): super(ContextRegistry, self)._register_parsers() @@ -1132,7 +1132,7 @@ def _build_cache(self): try: self._cache = self._caches[self._active_ctx] except KeyError: - super(ContextRegistry, self)._build_cache() + super()._build_cache() self._caches[self._active_ctx] = self._cache def enable_contexts(self, *names_or_contexts, **kwargs): From 693113465b2a43b2026091b9c10e66a2de05df9c Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 16:32:46 +0000 Subject: [PATCH 117/612] Trivial cleanup --- pint/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/context.py b/pint/context.py index ea06d9e14..3e43eabcc 100644 --- a/pint/context.py +++ b/pint/context.py @@ -199,7 +199,7 @@ class ContextChain(ChainMap): """ def __init__(self): - super(ContextChain, self).__init__() + super().__init__() self._graph = None self._contexts = [] From 31a01bf568ec55b9ecd2862ff9daf9dbae703170 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:29:46 +0000 Subject: [PATCH 118/612] Remove 2.7 support from codebase --- bench/bench.py | 33 +++--- pint/__init__.py | 2 - pint/babel_names.py | 2 +- pint/compat/__init__.py | 47 +------- pint/compat/chainmap.py | 155 ------------------------- pint/compat/lrucache.py | 177 ----------------------------- pint/compat/meta.py | 25 ---- pint/compat/tokenize.py | 56 ++++----- pint/context.py | 10 +- pint/converters.py | 4 +- pint/definitions.py | 20 +--- pint/errors.py | 6 +- pint/formatting.py | 6 +- pint/matplotlib.py | 1 - pint/measurement.py | 4 +- pint/pint_eval.py | 2 +- pint/quantity.py | 15 +-- pint/registry.py | 37 +++--- pint/registry_helpers.py | 13 +-- pint/systems.py | 12 +- pint/testsuite/__init__.py | 2 - pint/testsuite/helpers.py | 13 +-- pint/testsuite/parameterized.py | 2 +- pint/testsuite/test_babel.py | 2 +- pint/testsuite/test_contexts.py | 2 - pint/testsuite/test_converters.py | 2 +- pint/testsuite/test_definitions.py | 3 +- pint/testsuite/test_errors.py | 2 - pint/testsuite/test_formatter.py | 3 - pint/testsuite/test_issues.py | 44 +------ pint/testsuite/test_measurement.py | 2 - pint/testsuite/test_numpy.py | 2 - pint/testsuite/test_pint_eval.py | 1 - pint/testsuite/test_pitheorem.py | 3 - pint/testsuite/test_quantity.py | 36 +++--- pint/testsuite/test_systems.py | 4 - pint/testsuite/test_umath.py | 2 - pint/testsuite/test_unit.py | 14 +-- pint/testsuite/test_util.py | 2 - pint/unit.py | 9 +- pint/util.py | 78 ++++++------- 41 files changed, 164 insertions(+), 691 deletions(-) delete mode 100644 pint/compat/chainmap.py delete mode 100644 pint/compat/lrucache.py delete mode 100644 pint/compat/meta.py diff --git a/bench/bench.py b/bench/bench.py index 9063d453b..2d2576430 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -1,7 +1,3 @@ - - -from __future__ import division, unicode_literals, print_function, absolute_import - import fnmatch import os import copy @@ -23,11 +19,11 @@ def time_stmt(stmt='pass', setup='pass', number=0, repeat=3): if not number: # determine number so that 0.2 <= total time < 2.0 for i in range(1, 10): - number = 10**i + number = 10 ** i try: x = t.timeit(number) - except: + except Exception: print(t.print_exc()) return float('NaN') @@ -36,7 +32,7 @@ def time_stmt(stmt='pass', setup='pass', number=0, repeat=3): try: r = t.repeat(repeat, number) - except: + except Exception: print(t.print_exc()) return float('NaN') @@ -92,23 +88,28 @@ def time_file(filename, name='', setup='', number=0, repeat=3): def recursive_glob(rootdir='.', pattern='*'): - return [os.path.join(looproot, filename) - for looproot, _, filenames in os.walk(rootdir) - for filename in filenames - if fnmatch.fnmatch(filename, pattern)] + return [ + os.path.join(looproot, filename) + for looproot, _, filenames in os.walk(rootdir) + for filename in filenames + if fnmatch.fnmatch(filename, pattern) + ] + def main(filenames=None): if not filenames: - filenames = recursive_glob('.', 'bench_*.yaml') - elif isinstance(filenames, basestring): - filenames = [filenames, ] + filenames = recursive_glob(".", "bench_*.yaml") + elif isinstance(filenames, str): + filenames = [filenames] for filename in filenames: print(filename) print('-' * len(filename)) print() for task_name, value in time_file(filename): - print('%.2e %s' % (value, task_name)) + print(f"{value:.2e} {task_name}") print() -main() + +if __name__ == "__main__": + main() diff --git a/pint/__init__.py b/pint/__init__.py index 0189f086f..55486b22b 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -11,8 +11,6 @@ :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import pkg_resources diff --git a/pint/babel_names.py b/pint/babel_names.py index 05b550980..25037ae93 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -138,7 +138,7 @@ ) if not HAS_PROPER_BABEL: - _babel_units = dict() + _babel_units = {} _babel_systems = dict( mks='metric', diff --git a/pint/compat/__init__.py b/pint/compat/__init__.py index 1fc438d27..255dc9c1d 100644 --- a/pint/compat/__init__.py +++ b/pint/compat/__init__.py @@ -9,10 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - -import sys - from io import BytesIO from numbers import Number from decimal import Decimal @@ -21,7 +17,6 @@ ENCODING_TOKEN = tokenize.ENCODING -PYTHON3 = sys.version >= '3' def tokenizer(input_string): for tokinfo in tokenize.tokenize(BytesIO(input_string.encode('utf-8')).readline): @@ -30,42 +25,6 @@ def tokenizer(input_string): yield tokinfo -if PYTHON3: - string_types = str - - def u(x): - return x - - maketrans = str.maketrans - - long_type = int -else: - string_types = basestring - - import codecs - - def u(x): - return codecs.unicode_escape_decode(x)[0] - - maketrans = lambda f, t: dict((ord(a), b) for a, b in zip(f, t)) - - long_type = long - -try: - from collections import Chainmap -except ImportError: - from .chainmap import ChainMap - -try: - from functools import lru_cache -except ImportError: - from .lrucache import lru_cache - -try: - from itertools import zip_longest -except ImportError: - from itertools import izip_longest as zip_longest - try: import numpy as np from numpy import ndarray @@ -77,7 +36,7 @@ def u(x): def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, string_types) and value == '': + elif isinstance(value, str) and value == '': raise ValueError('Quantity magnitude cannot be an empty string.') elif isinstance(value, (list, tuple)): return np.asarray(value) @@ -89,7 +48,7 @@ def _to_magnitude(value, force_ndarray=False): np = None - class ndarray(object): + class ndarray: pass HAS_NUMPY = False @@ -99,7 +58,7 @@ class ndarray(object): def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, string_types) and value == '': + elif isinstance(value, str) and value == '': raise ValueError('Quantity magnitude cannot be an empty string.') elif isinstance(value, (list, tuple)): raise TypeError('lists and tuples are valid magnitudes for ' diff --git a/pint/compat/chainmap.py b/pint/compat/chainmap.py deleted file mode 100644 index 8a0ad1433..000000000 --- a/pint/compat/chainmap.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -""" - pint.compat.chainmap - ~~~~~~~~~~~~~~~~~~~~ - - Taken from the Python 3.3 source code. - - :copyright: 2013, PSF - :license: PSF License -""" - -from __future__ import division, unicode_literals, print_function, absolute_import - -import sys - -if sys.version_info < (3, 3): - from collections import MutableMapping -else: - from collections.abc import MutableMapping - -if sys.version_info < (3, 0): - from thread import get_ident -else: - from threading import get_ident - - -def _recursive_repr(fillvalue='...'): - 'Decorator to make a repr function return fillvalue for a recursive call' - - def decorating_function(user_function): - repr_running = set() - - def wrapper(self): - key = id(self), get_ident() - if key in repr_running: - return fillvalue - repr_running.add(key) - try: - result = user_function(self) - finally: - repr_running.discard(key) - return result - - # Can't use functools.wraps() here because of bootstrap issues - wrapper.__module__ = getattr(user_function, '__module__') - wrapper.__doc__ = getattr(user_function, '__doc__') - wrapper.__name__ = getattr(user_function, '__name__') - wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) - return wrapper - - return decorating_function - - -class ChainMap(MutableMapping): - ''' A ChainMap groups multiple dicts (or other mappings) together - to create a single, updateable view. - - The underlying mappings are stored in a list. That list is public and can - accessed or updated using the *maps* attribute. There is no other state. - - Lookups search the underlying mappings successively until a key is found. - In contrast, writes, updates, and deletions only operate on the first - mapping. - - ''' - - def __init__(self, *maps): - '''Initialize a ChainMap by setting *maps* to the given mappings. - If no mappings are provided, a single empty dictionary is used. - - ''' - self.maps = list(maps) or [{}] # always at least one map - - def __missing__(self, key): - raise KeyError(key) - - def __getitem__(self, key): - for mapping in self.maps: - try: - return mapping[key] # can't use 'key in mapping' with defaultdict - except KeyError: - pass - return self.__missing__(key) # support subclasses that define __missing__ - - def get(self, key, default=None): - return self[key] if key in self else default - - def __len__(self): - return len(set().union(*self.maps)) # reuses stored hash values if possible - - def __iter__(self): - return iter(set().union(*self.maps)) - - def __contains__(self, key): - return any(key in m for m in self.maps) - - def __bool__(self): - return any(self.maps) - - @_recursive_repr() - def __repr__(self): - return '{0.__class__.__name__}({1})'.format( - self, ', '.join(map(repr, self.maps))) - - @classmethod - def fromkeys(cls, iterable, *args): - 'Create a ChainMap with a single dict created from the iterable.' - return cls(dict.fromkeys(iterable, *args)) - - def copy(self): - 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' - return self.__class__(self.maps[0].copy(), *self.maps[1:]) - - __copy__ = copy - - def new_child(self, m=None): # like Django's _Context.push() - ''' - New ChainMap with a new map followed by all previous maps. If no - map is provided, an empty dict is used. - ''' - if m is None: - m = {} - return self.__class__(m, *self.maps) - - @property - def parents(self): # like Django's _Context.pop() - 'New ChainMap from maps[1:].' - return self.__class__(*self.maps[1:]) - - def __setitem__(self, key, value): - self.maps[0][key] = value - - def __delitem__(self, key): - try: - del self.maps[0][key] - except KeyError: - raise KeyError('Key not found in the first mapping: {!r}'.format(key)) - - def popitem(self): - 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' - try: - return self.maps[0].popitem() - except KeyError: - raise KeyError('No keys found in the first mapping.') - - def pop(self, key, *args): - 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' - try: - return self.maps[0].pop(key, *args) - except KeyError: - raise KeyError('Key not found in the first mapping: {!r}'.format(key)) - - def clear(self): - 'Clear maps[0], leaving maps[1:] intact.' - self.maps[0].clear() diff --git a/pint/compat/lrucache.py b/pint/compat/lrucache.py deleted file mode 100644 index 868b59867..000000000 --- a/pint/compat/lrucache.py +++ /dev/null @@ -1,177 +0,0 @@ -# -*- coding: utf-8 -*- -""" - pint.compat.lrucache - ~~~~~~~~~~~~~~~~~~~~ - - LRU (least recently used) cache backport. - - From https://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/ - - :copyright: 2004, Raymond Hettinger, - :license: MIT License -""" - -from collections import namedtuple -from functools import update_wrapper -from threading import RLock - -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) - -class _HashedSeq(list): - __slots__ = 'hashvalue' - - def __init__(self, tup, hash=hash): - self[:] = tup - self.hashvalue = hash(tup) - - def __hash__(self): - return self.hashvalue - -def _make_key(args, kwds, typed, - kwd_mark = (object(),), - fasttypes = set((int, str, frozenset, type(None))), - sorted=sorted, tuple=tuple, type=type, len=len): - 'Make a cache key from optionally typed positional and keyword arguments' - key = args - if kwds: - sorted_items = sorted(kwds.items()) - key += kwd_mark - for item in sorted_items: - key += item - if typed: - key += tuple(type(v) for v in args) - if kwds: - key += tuple(type(v) for k, v in sorted_items) - elif len(key) == 1 and type(key[0]) in fasttypes: - return key[0] - return _HashedSeq(key) - -def lru_cache(maxsize=100, typed=False): - """Least-recently-used cache decorator. - - If *maxsize* is set to None, the LRU features are disabled and the cache - can grow without bound. - - If *typed* is True, arguments of different types will be cached separately. - For example, f(3.0) and f(3) will be treated as distinct calls with - distinct results. - - Arguments to the cached function must be hashable. - - View the cache statistics named tuple (hits, misses, maxsize, currsize) with - f.cache_info(). Clear the cache and statistics with f.cache_clear(). - Access the underlying function with f.__wrapped__. - - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - - """ - - # Users should only access the lru_cache through its public API: - # cache_info, cache_clear, and f.__wrapped__ - # The internals of the lru_cache are encapsulated for thread safety and - # to allow the implementation to change (including a possible C version). - - def decorating_function(user_function): - - cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields - make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields - - if maxsize == 0: - - def wrapper(*args, **kwds): - # no caching, just do a statistics update after a successful call - result = user_function(*args, **kwds) - stats[MISSES] += 1 - return result - - elif maxsize is None: - - def wrapper(*args, **kwds): - # simple caching without ordering or size limit - key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel - if result is not root: - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - cache[key] = result - stats[MISSES] += 1 - return result - - else: - - def wrapper(*args, **kwds): - # size limited caching that tracks accesses by recency - key = make_key(args, kwds, typed) if kwds or typed else args - with lock: - link = cache_get(key) - if link is not None: - # record recent use of the key by moving it to the front of the list - root, = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - with lock: - root, = nonlocal_root - if key in cache: - # getting here means that this same key was added to the - # cache while the lock was released. since the link - # update is already done, we need only return the - # computed result and update the count of misses. - pass - elif _len(cache) >= maxsize: - # use the old root to store the new key and result - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - # empty the oldest link and make it the new root - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - oldvalue = root[RESULT] - root[KEY] = root[RESULT] = None - # now update the cache dictionary for the new links - del cache[oldkey] - cache[key] = oldroot - else: - # put result in a new link at the front of the list - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - return result - - def cache_info(): - """Report cache statistics""" - with lock: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) - - def cache_clear(): - """Clear the cache and cache statistics""" - with lock: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return update_wrapper(wrapper, user_function) - - return decorating_function diff --git a/pint/compat/meta.py b/pint/compat/meta.py deleted file mode 100644 index 00368701d..000000000 --- a/pint/compat/meta.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -""" - pint.compat.meta - ~~~~~~~~~~~~~~~~ - - Compatibility layer. - - :copyright: 2016 by Pint Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. -""" - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - - # Taken from six - - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) \ No newline at end of file diff --git a/pint/compat/tokenize.py b/pint/compat/tokenize.py index 8d28b4f8e..a285a6b36 100644 --- a/pint/compat/tokenize.py +++ b/pint/compat/tokenize.py @@ -31,29 +31,11 @@ from io import TextIOWrapper from itertools import chain import re -import sys from token import * -try: - reASCII = re.ASCII -except: - reASCII = 0 - -try: - unicode - _name_re = re.compile(r"\w*$", re.UNICODE) - def isidentifier(s): - if s[0] in '0123456789': - return False - return bool(_name_re.match(s)) -except NameError: - def isidentifier(s): - return s.isidentifier() - - -cookie_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)', reASCII) -blank_re = re.compile(br'^[ \t\f]*(?:[#\r\n]|$)', reASCII) +cookie_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)', re.ASCII) +blank_re = re.compile(br'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) COMMENT = N_TOKENS tok_name[COMMENT] = 'COMMENT' @@ -108,6 +90,7 @@ def isidentifier(s): '@': AT } + class TokenInfo(collections.namedtuple('TokenInfo', 'type string start end line')): def __repr__(self): annotated_type = '%d (%s)' % (self.type, tok_name[self.type]) @@ -121,9 +104,18 @@ def exact_type(self): else: return self.type -def group(*choices): return '(' + '|'.join(choices) + ')' -def any(*choices): return group(*choices) + '*' -def maybe(*choices): return group(*choices) + '?' + +def group(*choices): + return '(' + '|'.join(choices) + ')' + + +def any(*choices): + return group(*choices) + '*' + + +def maybe(*choices): + return group(*choices) + '?' + # Note: we use unicode matching for names ("\w") but ascii matching for # number literals. @@ -182,9 +174,11 @@ def maybe(*choices): return group(*choices) + '?' PseudoExtras = group(r'\\\r?\n|\Z', Comment, Triple) PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name) + def _compile(expr): return re.compile(expr, re.UNICODE) + endpats = {"'": Single, '"': Double, "'''": Single3, '"""': Double3, "r'''": Single3, 'r"""': Double3, @@ -230,9 +224,13 @@ def _compile(expr): tabsize = 8 -class TokenError(Exception): pass -class StopTokenizing(Exception): pass +class TokenError(Exception): + pass + + +class StopTokenizing(Exception): + pass class Untokenizer: @@ -351,6 +349,7 @@ def _get_normal_name(orig_enc): return "iso-8859-1" return orig_enc + def detect_encoding(readline): """ The detect_encoding() function is used to detect the encoding that should @@ -375,6 +374,7 @@ def detect_encoding(readline): bom_found = False encoding = None default = 'utf-8' + def read_or_stop(): try: return readline() @@ -486,7 +486,7 @@ def tokenize(readline): try: return _tokenize(chain(consumed, rl_gen, empty).__next__, encoding) except AttributeError: - return _tokenize(chain(consumed, rl_gen, empty).next, encoding) + return _tokenize(chain(consumed, rl_gen, empty).__next__, encoding) def _tokenize(readline, encoding): @@ -621,7 +621,7 @@ def _tokenize(readline, encoding): break else: # ordinary string yield TokenInfo(STRING, token, spos, epos, line) - elif isidentifier(initial): # ordinary name + elif initial.isidentifier(): # ordinary name yield TokenInfo(NAME, token, spos, epos, line) elif initial == '\\': # continued stmt continued = 1 @@ -636,6 +636,6 @@ def _tokenize(readline, encoding): (lnum, pos), (lnum, pos+1), line) pos += 1 - for indent in indents[1:]: # pop remaining indent levels + for _ in indents[1:]: # pop remaining indent levels yield TokenInfo(DEDENT, '', (lnum, 0), (lnum, 0), '') yield TokenInfo(ENDMARKER, '', (lnum, 0), (lnum, 0), '') diff --git a/pint/context.py b/pint/context.py index 6bd733584..d4d27b98e 100644 --- a/pint/context.py +++ b/pint/context.py @@ -9,15 +9,11 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - - import re -from collections import defaultdict import weakref +from collections import ChainMap, defaultdict -from .compat import ChainMap -from .util import (ParserHelper, UnitsContainer, string_types, +from .util import (ParserHelper, UnitsContainer, to_units_container, SourceIterator) from .errors import DefinitionSyntaxError @@ -34,7 +30,7 @@ def func(ureg, value, **kwargs): return func -class Context(object): +class Context: """A specialized container that defines transformation functions from one dimension to another. Each Dimension are specified using a UnitsContainer. Simple transformation are given with a function taking a single parameter. diff --git a/pint/converters.py b/pint/converters.py index 6dbf4b4a0..62572f8ad 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -8,11 +8,9 @@ :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import (division, unicode_literals, print_function, - absolute_import) -class Converter(object): +class Converter: """Base class for value converters. """ diff --git a/pint/definitions.py b/pint/definitions.py index 428aeaeca..f180f7c16 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -9,16 +9,11 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import (division, unicode_literals, print_function, - absolute_import) -import sys - from .converters import ScaleConverter, OffsetConverter from .util import UnitsContainer, _is_dim, ParserHelper -from .compat import string_types -class Definition(object): +class Definition: """Base class for definitions. :param name: name. @@ -45,13 +40,6 @@ def from_string(cls, definition): name = name.strip() result = [res.strip() for res in definition.split('=')] - # For python 2.7, remove unsupported unicode character - if (sys.version_info < (3, 0)): - for value in result: - try: - value.decode('utf-8') - except UnicodeEncodeError: - result.remove(value) # @alias name = alias1 = alias2 = ... if name.startswith("@alias "): @@ -106,7 +94,7 @@ class PrefixDefinition(Definition): """ def __init__(self, name, symbol, aliases, converter): - if isinstance(converter, string_types): + if isinstance(converter, str): converter = ScaleConverter(eval(converter)) aliases = tuple(alias.strip('-') for alias in aliases) if symbol: @@ -126,7 +114,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base - if isinstance(converter, string_types): + if isinstance(converter, str): if ';' in converter: [converter, modifiers] = converter.split(';', 2) modifiers = dict((key.strip(), eval(value)) for key, value in @@ -162,7 +150,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base - if isinstance(converter, string_types): + if isinstance(converter, str): converter = ParserHelper.from_string(converter) if not converter: self.is_base = True diff --git a/pint/errors.py b/pint/errors.py index 700d79cbf..1939dce87 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -8,10 +8,6 @@ :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import (division, unicode_literals, print_function, - absolute_import) - -from .compat import string_types class DefinitionSyntaxError(ValueError): @@ -60,7 +56,7 @@ def __init__(self, unit_names): def __str__(self): mess = "'{}' is not defined in the unit registry" mess_plural = "'{}' are not defined in the unit registry" - if isinstance(self.unit_names, string_types): + if isinstance(self.unit_names, str): return mess.format(self.unit_names) elif isinstance(self.unit_names, (list, tuple))\ and len(self.unit_names) == 1: diff --git a/pint/formatting.py b/pint/formatting.py index efd68f76d..24a44d50c 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -9,12 +9,10 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - import re from .babel_names import _babel_units, _babel_lengths -from pint.compat import babel_units, Loc, string_types +from .compat import Loc __JOIN_REG_EXP = re.compile("\{\d*\}") @@ -291,7 +289,7 @@ def matrix_to_latex(matrix, fmtfun=lambda x: format(x, '.2f')): def ndarray_to_latex_parts(ndarr, fmtfun=lambda x: format(x, '.2f'), dim=()): - if isinstance(fmtfun, string_types): + if isinstance(fmtfun, str): fmt = fmtfun fmtfun = lambda x: format(x, fmt) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 4c8af6883..67ffef8e8 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -9,7 +9,6 @@ :copyright: 2017 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import absolute_import import matplotlib.units diff --git a/pint/measurement.py b/pint/measurement.py index 3fc40e173..63f874467 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -7,8 +7,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - from .compat import ufloat from .formatting import _FORMATS, siunitx_format_unit from .quantity import Quantity @@ -140,7 +138,7 @@ def __format__(self, spec): def build_measurement_class(registry): if ufloat is None: - class Measurement(object): + class Measurement: _REGISTRY = registry def __init__(self, *args): diff --git a/pint/pint_eval.py b/pint/pint_eval.py index 751e49677..b198a403b 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -44,7 +44,7 @@ } -class EvalTreeNode(object): +class EvalTreeNode: def __init__(self, left, operator=None, right=None): """ diff --git a/pint/quantity.py b/pint/quantity.py index f093d08db..9fe1b4542 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -7,8 +7,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - import contextlib import copy import datetime @@ -25,7 +23,7 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import string_types, ndarray, np, _to_magnitude, long_type +from .compat import ndarray, np, _to_magnitude from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, fix_str_conversions) @@ -125,7 +123,7 @@ def __reduce__(self): def __new__(cls, value, units=None): if units is None: - if isinstance(value, string_types): + if isinstance(value, str): if value == '': raise ValueError('Expression to parse as Quantity cannot ' 'be an empty string.') @@ -142,7 +140,7 @@ def __new__(cls, value, units=None): inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = units - elif isinstance(units, string_types): + elif isinstance(units, str): inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude(value, inst.force_ndarray) inst._units = inst._REGISTRY.parse_units(units)._units @@ -577,7 +575,7 @@ def to_compact(self, unit=None): magnitude = q_base.magnitude units = list(q_base._units.items()) - units_numerator = list(filter(lambda a: a[1]>0, units)) + units_numerator = [a for a in units if a[1] > 0] if len(units_numerator) > 0: unit_str, unit_power = units_numerator[0] @@ -602,11 +600,6 @@ def __int__(self): return int(self._convert_magnitude_not_inplace(UnitsContainer())) raise DimensionalityError(self._units, 'dimensionless') - def __long__(self): - if self.dimensionless: - return long_type(self._convert_magnitude_not_inplace(UnitsContainer())) - raise DimensionalityError(self._units, 'dimensionless') - def __float__(self): if self.dimensionless: return float(self._convert_magnitude_not_inplace(UnitsContainer())) diff --git a/pint/registry.py b/pint/registry.py index 4324c7d54..f6be01810 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -32,8 +32,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - import copy import os import re @@ -56,7 +54,7 @@ find_shortest_path, UnitsContainer, _is_dim, to_units_container, SourceIterator) -from .compat import tokenizer, string_types, meta +from .compat import tokenizer from .definitions import (Definition, UnitDefinition, PrefixDefinition, DimensionDefinition, AliasDefinition) from .converters import ScaleConverter @@ -69,18 +67,18 @@ _BLOCK_RE = re.compile(r' |\(') -class _Meta(type): +class RegistryMeta(type): """This is just to call after_init at the right time instead of asking the developer to do it when subclassing. """ def __call__(self, *args, **kwargs): - obj = super(_Meta, self).__call__(*args, **kwargs) + obj = super().__call__(*args, **kwargs) obj._after_init() return obj -class BaseRegistry(meta.with_metaclass(_Meta)): +class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. Capabilities: @@ -239,7 +237,7 @@ def define(self, definition): :type definition: str or Definition """ - if isinstance(definition, string_types): + if isinstance(definition, str): for line in definition.split('\n'): self._define(Definition.from_string(line)) else: @@ -301,8 +299,9 @@ def _define(self, definition): d_aliases = tuple('Δ' + alias for alias in definition.aliases) + \ tuple('delta_' + alias for alias in definition.aliases) - d_reference = UnitsContainer(dict((ref, value) - for ref, value in definition.reference.items())) + d_reference = UnitsContainer( + {ref: value for ref, value in definition.reference.items()} + ) d_def = UnitDefinition(d_name, d_symbol, d_aliases, ScaleConverter(definition.converter.scale), @@ -359,7 +358,7 @@ def _register_parser(self, prefix, parserfunc): :type parserfunc: SourceIterator -> None """ if self._parsers is None: - self._parsers = dict() + self._parsers = {} if prefix and prefix[0] == '@': self._parsers[prefix] = parserfunc @@ -374,7 +373,7 @@ def load_definitions(self, file, is_resource=False): and therefore should be loaded from the package. """ # Permit both filenames and line-iterables - if isinstance(file, string_types): + if isinstance(file, str): try: if is_resource: with closing(pkg_resources.resource_stream(__name__, file)) as fp: @@ -564,8 +563,7 @@ def _get_dimensionality(self, input_units): if '[]' in accumulator: del accumulator['[]'] - dims = UnitsContainer(dict((k, v) for k, v in accumulator.items() - if v != 0.0)) + dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0.0}) self._dimensionality_cache[input_units] = dims @@ -650,12 +648,11 @@ def _get_root_units(self, input_units, check_nonmult=True): self._get_root_units_recurse(input_units, 1.0, accumulators) factor = accumulators[0] - units = UnitsContainer(dict((k, v) for k, v in accumulators[1].items() - if v != 0.)) + units = UnitsContainer({k: v for k, v in accumulators[1].items() if v != 0}) # Check if any of the final units is non multiplicative and return None instead. if check_nonmult: - for unit in units.keys(): + for unit in units: if not self._units[unit].converter.is_multiplicative: return None, units @@ -1122,8 +1119,10 @@ def enable_contexts(self, *names_or_contexts, **kwargs): kwargs = dict(self._active_ctx.defaults, **kwargs) # For each name, we first find the corresponding context - ctxs = list((self._contexts[name] if isinstance(name, string_types) else name) - for name in names_or_contexts) + ctxs = [ + self._contexts[name] if isinstance(name, str) else name + for name in names_or_contexts + ] # Check if the contexts have been checked first, if not we make sure # that dimensions are expressed in terms of base dimensions. @@ -1550,7 +1549,7 @@ def setup_matplotlib(self, enable=True): check = registry_helpers.check -class LazyRegistry(object): +class LazyRegistry: def __init__(self, args=None, kwargs=None): self.__dict__['params'] = args or (), kwargs or {} diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 74f16d3d8..7733c49d2 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -10,17 +10,12 @@ """ import functools +from inspect import signature +from itertools import zip_longest -from .compat import string_types, zip_longest from .errors import DimensionalityError from .util import to_units_container, UnitsContainer -try: - from inspect import signature -except ImportError: - # Python2 does not have the inspect library. Import the backport. - from funcsigs import signature - def _replace_units(original_units, values_by_name): """Convert a unit compatible type to a UnitsContainer. @@ -42,7 +37,7 @@ def _to_units_container(a, registry=None): Return a tuple with the unit container and a boolean indicating if it was a reference. """ - if isinstance(a, string_types) and '=' in a: + if isinstance(a, str) and '=' in a: return to_units_container(a.split('=', 1)[1]), True return to_units_container(a, registry), False @@ -130,6 +125,7 @@ def _converter(ureg, values, strict): return _converter + def _apply_defaults(func, args, kwargs): """Apply default keyword arguments. @@ -145,6 +141,7 @@ def _apply_defaults(func, args, kwargs): args = [bound_arguments.arguments[key] for key in sig.parameters.keys()] return args, {} + def wraps(ureg, ret, args, strict=True): """Wraps a function to become pint-aware. diff --git a/pint/systems.py b/pint/systems.py index 3c5666018..3daf52763 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - import re from .definitions import Definition, UnitDefinition @@ -401,9 +399,11 @@ def from_lines(cls, lines, get_root_func): # Here we invert the equation, in other words # we write old units in terms new unit and expansion - new_unit_dict = dict((new_unit, -1./value) - for new_unit, value in new_unit_expanded.items() - if new_unit != old_unit) + new_unit_dict = { + new_unit: -1./value + for new_unit, value in new_unit_expanded.items() + if new_unit != old_unit + } new_unit_dict[new_unit] = 1 / new_unit_expanded[old_unit] base_unit_names[old_unit] = new_unit_dict @@ -432,7 +432,7 @@ def from_lines(cls, lines, get_root_func): return system -class Lister(object): +class Lister: def __init__(self, d): self.d = d diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 2f505447b..b46d86f37 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import doctest import logging import os diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 1f58d212c..bf638bdf4 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - - import doctest from distutils.version import StrictVersion import re import unittest -from pint.compat import HAS_NUMPY, HAS_PROPER_BABEL, HAS_UNCERTAINTIES, NUMPY_VER, PYTHON3 +from ..compat import HAS_NUMPY, HAS_PROPER_BABEL, HAS_UNCERTAINTIES, NUMPY_VER def requires_numpy18(): @@ -43,14 +40,6 @@ def requires_not_uncertainties(): return unittest.skipIf(HAS_UNCERTAINTIES, 'Requires Uncertainties is not installed.') -def requires_python2(): - return unittest.skipIf(PYTHON3, 'Requires Python 2.X.') - - -def requires_python3(): - return unittest.skipUnless(PYTHON3, 'Requires Python 3.X.') - - _number_re = '([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' _q_re = re.compile('%s)' % _number_re + '\s*,\s*' + "'(?P.*)'" + '\s*' + '\)>') diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 7c4591574..37575fd8f 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -121,7 +121,7 @@ def new_method(self): return new_method @add_metaclass(ParameterizedTestCaseMetaClass) -class ParameterizedTestMixin(object): +class ParameterizedTestMixin: @classmethod def parameterize(cls, param_names, data, func_name_format='{func_name}_{case_num:05d}'): diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 019497766..d9f7433fa 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import from pint.testsuite import helpers, BaseTestCase from pint import UnitRegistry import os + class TestBabel(BaseTestCase): @helpers.requires_proper_babel() diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 7766aa7df..10c5037e0 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import itertools from collections import defaultdict diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 962242ee9..30a7eaae3 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import import itertools @@ -7,6 +6,7 @@ from pint.converters import (ScaleConverter, OffsetConverter, Converter) from pint.testsuite import helpers, BaseTestCase + class TestConverter(BaseTestCase): def test_converter(self): diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 8a66a4e65..0181f5ad9 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - from pint.util import (UnitsContainer) from pint.converters import (ScaleConverter, OffsetConverter) from pint.definitions import (Definition, PrefixDefinition, UnitDefinition, @@ -9,6 +7,7 @@ from pint.testsuite import BaseTestCase + class TestDefinition(BaseTestCase): def test_invalid(self): diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index b6537c160..015886166 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - from pint import DimensionalityError, UndefinedUnitError from pint.testsuite import BaseTestCase diff --git a/pint/testsuite/test_formatter.py b/pint/testsuite/test_formatter.py index 9f2015bf0..b471a1ce0 100644 --- a/pint/testsuite/test_formatter.py +++ b/pint/testsuite/test_formatter.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - from pint import formatting as fmt from pint.testsuite import QuantityTestCase @@ -14,7 +12,6 @@ def test_join(self): self.assertEqual(fmt._join('*', '1 2 3'.split()), '1*2*3') self.assertEqual(fmt._join('{0}*{1}', '1 2 3'.split()), '1*2*3') - def test_formatter(self): self.assertEqual(fmt.formatter(dict().items()), '') self.assertEqual(fmt.formatter(dict(meter=1).items()), 'meter') diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index eea527b2c..7cc0f9c16 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - -import math import copy +import math import unittest import sys @@ -430,49 +428,17 @@ def test_issue170(self): self.assertEqual(iq, 10) self.assertIsInstance(iq, int) - @helpers.requires_python2() - def test_issue170b(self): - Q_ = UnitRegistry().Quantity - q = Q_('1 kHz')/Q_('100 Hz') - iq = long(q) - self.assertEqual(iq, long(10)) - self.assertIsInstance(iq, long) - def test_angstrom_creation(self): ureg = UnitRegistry() - try: - # Check if the install supports unicode, travis python27 seems to - # support it... - if (sys.version_info < (3, 0)): - 'Å'.decode('utf-8') - except UnicodeEncodeError: - self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'Å') - else: - ureg.Quantity(2, 'Å') + ureg.Quantity(2, 'Å') def test_alternative_angstrom_definition(self): ureg = UnitRegistry() - try: - # Check if the install supports unicode, travis python27 seems to - # support it... - if (sys.version_info < (3, 0)): - 'Å'.decode('utf-8') - except UnicodeEncodeError: - self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, '\u212B') - else: - ureg.Quantity(2, '\u212B') + ureg.Quantity(2, '\u212B') def test_micro_creation(self): ureg = UnitRegistry() - try: - # Check if the install supports unicode, travis python27 seems to - # support it... - if (sys.version_info < (3, 0)): - 'µ'.decode('utf-8') - except UnicodeEncodeError: - self.assertRaises(UndefinedUnitError, ureg.Quantity, 2, 'µm') - else: - ureg.Quantity(2, 'µm') + ureg.Quantity(2, 'µm') @helpers.requires_numpy() def test_issue171_real_imag(self): @@ -537,7 +503,7 @@ def f(x): x = ureg.Quantity(1., 'meter') y = f(x) z = x * y - self.assertEquals(z, ureg.Quantity(1., 'meter * kilogram')) + self.assertEqual(z, ureg.Quantity(1., 'meter * kilogram')) @helpers.requires_numpy() def test_issue482(self): diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index eff666afb..389cd20e7 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - from pint.testsuite import QuantityTestCase, helpers diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 996195c26..f821315ef 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import copy import operator as op import unittest diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index c01975f5b..1f6131039 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import import unittest from pint.compat import tokenizer diff --git a/pint/testsuite/test_pitheorem.py b/pint/testsuite/test_pitheorem.py index 47f379012..ce2026fca 100644 --- a/pint/testsuite/test_pitheorem.py +++ b/pint/testsuite/test_pitheorem.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import itertools from pint import pi_theorem - from pint.testsuite import QuantityTestCase diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index e5fc076f0..e15923db8 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import copy import datetime import math import operator as op -import warnings from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry from pint.unit import UnitsContainer -from pint.compat import string_types, PYTHON3, np +from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase @@ -191,7 +188,7 @@ def test_exponent_formatting(self): def test_ipython(self): alltext = [] - class Pretty(object): + class Pretty: @staticmethod def text(text): alltext.append(text) @@ -467,22 +464,20 @@ def test_limits_magnitudes(self): def test_nonnumeric_magnitudes(self): ureg = self.ureg x = "some string"*ureg.m - if PYTHON3: - with self.assertWarns(RuntimeWarning): - self.compareQuantity_compact(x,x) - else: - self.assertRaises(RuntimeError, self.compareQuantity_compact(x,x)) + with self.assertWarns(RuntimeWarning): + self.compareQuantity_compact(x, x) + class TestQuantityBasicMath(QuantityTestCase): FORCE_NDARRAY = False def _test_inplace(self, operator, value1, value2, expected_result, unit=None): - if isinstance(value1, string_types): + if isinstance(value1, str): value1 = self.Q_(value1) - if isinstance(value2, string_types): + if isinstance(value2, str): value2 = self.Q_(value2) - if isinstance(expected_result, string_types): + if isinstance(expected_result, str): expected_result = self.Q_(expected_result) if not unit is None: @@ -502,11 +497,11 @@ def _test_inplace(self, operator, value1, value2, expected_result, unit=None): self.assertEqual(id2, id(value2)) def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None): - if isinstance(value1, string_types): + if isinstance(value1, str): value1 = self.Q_(value1) - if isinstance(value2, string_types): + if isinstance(value2, str): value2 = self.Q_(value2) - if isinstance(expected_result, string_types): + if isinstance(expected_result, str): expected_result = self.Q_(expected_result) if not unit is None: @@ -616,9 +611,9 @@ def _test_quantity_ifloordiv(self, unit, func): func(op.ifloordiv, '10*meter', '4.2*inch', 93, unit) def _test_quantity_divmod_one(self, a, b): - if isinstance(a, string_types): + if isinstance(a, str): a = self.Q_(a) - if isinstance(b, string_types): + if isinstance(b, str): b = self.Q_(b) q, r = divmod(a, b) @@ -685,9 +680,8 @@ def test_quantity_abs_round(self): x = self.Q_(-4.2, 'meter') y = self.Q_(4.2, 'meter') - # In Python 3+ round of x is delegated to x.__round__, instead of round(x.__float__) - # and therefore it can be properly implemented by Pint - for fun in (abs, op.pos, op.neg) + (round, ) if PYTHON3 else (): + + for fun in (abs, round, op.pos, op.neg): zx = self.Q_(fun(x.magnitude), 'meter') zy = self.Q_(fun(y.magnitude), 'meter') rx = fun(x) diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index 734bb736a..ba5c9e441 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - - from pint import UnitRegistry - from pint.testsuite import QuantityTestCase diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index 589aaa44a..d4fa9c6ed 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - from pint.compat import np from pint.testsuite import QuantityTestCase, helpers diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 9c6bf5196..8bbe54c33 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import copy import math -from pint.registry import (UnitRegistry, LazyRegistry) -from pint.util import (UnitsContainer, ParserHelper) from pint import DimensionalityError, UndefinedUnitError -from pint.compat import u, np, string_types +from pint.compat import np +from pint.registry import (UnitRegistry, LazyRegistry) from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase +from pint.util import (UnitsContainer, ParserHelper) class TestUnit(QuantityTestCase): @@ -66,7 +64,7 @@ def test_unit_default_formatting(self): def test_ipython(self): alltext = [] - class Pretty(object): + class Pretty: @staticmethod def text(text): alltext.append(text) @@ -231,7 +229,7 @@ def test_parse_complex(self): def test_str_errors(self): self.assertEqual(str(UndefinedUnitError('rabbits')), "'{0!s}' is not defined in the unit registry".format('rabbits')) self.assertEqual(str(UndefinedUnitError(('rabbits', 'horses'))), "'{0!s}' are not defined in the unit registry".format(('rabbits', 'horses'))) - self.assertEqual(u(str(DimensionalityError('meter', 'second'))), + self.assertEqual(str(DimensionalityError('meter', 'second')), "Cannot convert from 'meter' to 'second'") self.assertEqual(str(DimensionalityError('meter', 'second', 'length', 'time')), "Cannot convert from 'meter' (length) to 'second' (time)") @@ -638,7 +636,7 @@ def test_to_and_from_offset_units(self, input_tuple, expected): src, dst = UnitsContainer(src), UnitsContainer(dst) value = 10. convert = self.ureg.convert - if isinstance(expected, string_types): + if isinstance(expected, str): self.assertRaises(DimensionalityError, convert, value, src, dst) if src != dst: self.assertRaises(DimensionalityError, convert, value, dst, src) diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 30d0bf976..b10b04775 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import - import collections import copy import operator as op diff --git a/pint/unit.py b/pint/unit.py index 24c9e0b87..5d194a528 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - import copy import operator from numbers import Number @@ -18,7 +16,7 @@ from .util import ( PrettyIPython, UnitsContainer, SharedRegistryObject, fix_str_conversions) -from .compat import string_types, NUMERIC_TYPES, long_type +from .compat import NUMERIC_TYPES from .formatting import siunitx_format_unit from .definitions import UnitDefinition @@ -44,7 +42,7 @@ def __init__(self, units): super(Unit, self).__init__() if isinstance(units, (UnitsContainer, UnitDefinition)): self._units = units - elif isinstance(units, string_types): + elif isinstance(units, str): self._units = self._REGISTRY.parse_units(units)._units elif isinstance(units, Unit): self._units = units._units @@ -224,9 +222,6 @@ def compare(self, other, op): def __int__(self): return int(self._REGISTRY.Quantity(1, self._units)) - def __long__(self): - return long_type(self._REGISTRY.Quantity(1, self._units)) - def __float__(self): return float(self._REGISTRY.Quantity(1, self._units)) diff --git a/pint/util.py b/pint/util.py index e447264ee..ec3170d62 100644 --- a/pint/util.py +++ b/pint/util.py @@ -9,29 +9,22 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import division, unicode_literals, print_function, absolute_import - -from decimal import Decimal import locale +import logging +from logging import NullHandler import sys import re import operator +from collections.abc import Mapping +from decimal import Decimal from numbers import Number from fractions import Fraction +from functools import lru_cache -try: - from collections.abc import Mapping -except ImportError: - from collections import Mapping - -from logging import NullHandler +from token import NAME, NUMBER -import logging -from token import STRING, NAME, OP, NUMBER -from tokenize import untokenize - -from .compat import string_types, tokenizer, lru_cache, maketrans, NUMERIC_TYPES -from .formatting import format_unit,siunitx_format_unit +from .compat import tokenizer, NUMERIC_TYPES +from .formatting import format_unit from .pint_eval import build_eval_tree from .errors import DefinitionSyntaxError @@ -144,7 +137,7 @@ def pi_theorem(quantities, registry=None): getdim = registry.get_dimensionality for name, value in quantities.items(): - if isinstance(value, string_types): + if isinstance(value, str): value = ParserHelper.from_string(value) if isinstance(value, dict): dims = getdim(UnitsContainer(value)) @@ -194,15 +187,18 @@ def solve_dependencies(dependencies): r = [] while d: # values not in keys (items without dep) - t = set(i for v in d.values() for i in v) - set(d.keys()) + t = {i for v in d.values() for i in v} - d.keys() # and keys without value (items without dep) t.update(k for k, v in d.items() if not v) # can be done right away if not t: - raise ValueError('Cyclic dependencies exist among these items: {}'.format(', '.join(repr(x) for x in d.items()))) + raise ValueError( + 'Cyclic dependencies exist among these items: {}' + .format(', '.join(repr(x) for x in d.items())) + ) r.append(t) # and cleaned up - d = dict(((k, v - t) for k, v in d.items() if v)) + d = {k: v - t for k, v in d.items() if v} return r @@ -261,7 +257,7 @@ def __init__(self, *args, **kwargs): d = udict(*args, **kwargs) self._d = d for key, value in d.items(): - if not isinstance(key, string_types): + if not isinstance(key, str): raise TypeError('key must be a str, not {}'.format(type(key))) if not isinstance(value, Number): raise TypeError('value must be a number, not {}'.format(type(value))) @@ -318,10 +314,6 @@ def __hash__(self): self._hash = hash(frozenset(self._d.items())) return self._hash - # Only needed by Python 2.7 - def __getstate__(self): - return self._d, self._hash - def __setstate__(self, state): self._d, self._hash = state @@ -336,7 +328,7 @@ def __eq__(self, other): return False other = other._d - elif isinstance(other, string_types): + elif isinstance(other, str): other = ParserHelper.from_string(other) other = other._d @@ -485,9 +477,13 @@ def _from_string(cls, input_string): if not reps: return ret - return ParserHelper(ret.scale, - dict((key.replace('__obra__', '[').replace('__cbra__', ']'), value) - for key, value in ret.items())) + return ParserHelper( + ret.scale, + { + key.replace('__obra__', '[').replace('__cbra__', ']'): value + for key, value in ret.items() + } + ) def __copy__(self): new = super(ParserHelper, self).__copy__() @@ -503,10 +499,6 @@ def __hash__(self): raise ValueError(mess) return super(ParserHelper, self).__hash__() - # Only needed by Python 2.7 - def __getstate__(self): - return self._d, self._hash, self.scale - def __setstate__(self, state): self._d, self._hash, self.scale = state @@ -516,7 +508,7 @@ def __eq__(self, other): self.scale == other.scale and super(ParserHelper, self).__eq__(other) ) - elif isinstance(other, string_types): + elif isinstance(other, str): return self == ParserHelper.from_string(other) elif isinstance(other, Number): return self.scale == other and not len(self._d) @@ -546,7 +538,7 @@ def __repr__(self): return ''.format(self.scale, tmp) def __mul__(self, other): - if isinstance(other, string_types): + if isinstance(other, str): new = self.add(other, 1) elif isinstance(other, Number): new = self.copy() @@ -567,7 +559,7 @@ def __pow__(self, other): return self.__class__(self.scale**other, d) def __truediv__(self, other): - if isinstance(other, string_types): + if isinstance(other, str): new = self.add(other, -1) elif isinstance(other, Number): new = self.copy() @@ -583,7 +575,7 @@ def __truediv__(self, other): def __rtruediv__(self, other): new = self.__pow__(-1) - if isinstance(other, string_types): + if isinstance(other, str): new = new.add(other, 1) elif isinstance(other, Number): new.scale *= other @@ -609,7 +601,7 @@ def __rtruediv__(self, other): #: Compiles the regex and replace {} by a regex that matches an identifier. _subs_re = [(re.compile(a.format(r"[_a-zA-Z][_a-zA-Z0-9]*")), b) for a, b in _subs_re] -_pretty_table = maketrans('⁰¹²³⁴⁵⁶⁷⁸⁹·⁻', '0123456789*-') +_pretty_table = str.maketrans('⁰¹²³⁴⁵⁶⁷⁸⁹·⁻', '0123456789*-') _pretty_exp_re = re.compile(r"⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+(?:\.[⁰¹²³⁴⁵⁶⁷⁸⁹]*)?") @@ -636,7 +628,7 @@ def _is_dim(name): return name[0] == '[' and name[-1] == ']' -class SharedRegistryObject(object): +class SharedRegistryObject: """Base class for object keeping a reference to the registree. Such object are for now Quantity and Unit, in a number of places it is @@ -673,7 +665,7 @@ def _check(self, other): return False -class PrettyIPython(object): +class PrettyIPython: """Mixin to add pretty-printers for IPython""" def _repr_html_(self): @@ -704,7 +696,7 @@ def to_units_container(unit_like, registry=None): return unit_like elif SharedRegistryObject in mro: return unit_like._units - elif string_types in mro: + elif str in mro: if registry: return registry._parse_units(unit_like) else: @@ -722,7 +714,9 @@ def infer_base_unit(q): _, base_unit, __ = completely_parsed_unit d[base_unit] += power - return UnitsContainer(dict((k, v) for k, v in d.items() if v != 0)) # remove values that resulted in a power of 0 + + # remove values that resulted in a power of 0 + return UnitsContainer({k: v for k, v in d.items() if v != 0}) def fix_str_conversions(cls): @@ -748,7 +742,7 @@ def getattr_maybe_raise(self, item): raise AttributeError("%r object has no attribute %r" % (self, item)) -class SourceIterator(object): +class SourceIterator: """Iterator to facilitate reading the definition files. Accepts any sequence (like a list of lines, a file or another SourceIterator) From 8f8192919f22baab71e7d2b869a752e7fa72a02d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:40:56 +0000 Subject: [PATCH 119/612] Remove 2.7 support from codebase --- pint/quantity.py | 15 ++++++++------- pint/unit.py | 8 ++------ pint/util.py | 6 +----- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 9fe1b4542..cece445db 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -7,16 +7,16 @@ :license: BSD, see LICENSE for more details. """ +import bisect import contextlib import copy import datetime -import math -import operator import functools -import bisect -import warnings +import math import numbers +import operator import re +import warnings from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts) @@ -167,8 +167,9 @@ def debug_used(self): return self.__used def __iter__(self): - # Make sure that, if self.magnitude is not iterable, we raise TypeError as soon as one - # calls iter(self) without waiting for the first element to be drawn from the iterator + # Make sure that, if self.magnitude is not iterable, we raise TypeError as soon + # as one calls iter(self) without waiting for the first element to be drawn from + # the iterator it_magnitude = iter(self.magnitude) def it_outer(): @@ -192,7 +193,7 @@ def __str__(self): return format(self) def __repr__(self): - return "".format(self._magnitude, self._units) + return f"" def __hash__(self): self_base = self.to_base_units() diff --git a/pint/unit.py b/pint/unit.py index 5d194a528..a18ed8127 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -76,12 +76,8 @@ def __repr__(self): def __format__(self, spec): spec = spec or self.default_format # special cases - if 'Lx' in spec: # the LaTeX siunitx code - opts = '' - ustr = siunitx_format_unit(self) - ret = r'\si[%s]{%s}'%( opts, ustr ) - return ret - + if 'Lx' in spec: # the LaTeX siunitx code + return r'\si[]{%s}' % siunitx_format_unit(self) if '~' in spec: if not self._units: diff --git a/pint/util.py b/pint/util.py index ec3170d62..6ef174c9c 100644 --- a/pint/util.py +++ b/pint/util.py @@ -12,7 +12,6 @@ import locale import logging from logging import NullHandler -import sys import re import operator from collections.abc import Mapping @@ -725,10 +724,7 @@ def __bytes__(self): return self.__unicode__().encode(locale.getpreferredencoding()) cls.__unicode__ = __unicode__ = cls.__str__ cls.__bytes__ = __bytes__ - if sys.version_info[0] == 2: - cls.__str__ = __bytes__ - else: - cls.__str__ = __unicode__ + cls.__str__ = __unicode__ return cls From 3fdea6bdd05df7e9aa7ba6b266f484bd92dd5310 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:43:56 +0000 Subject: [PATCH 120/612] Fix regression --- pint/util.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pint/util.py b/pint/util.py index 6ef174c9c..bfe79b950 100644 --- a/pint/util.py +++ b/pint/util.py @@ -313,6 +313,10 @@ def __hash__(self): self._hash = hash(frozenset(self._d.items())) return self._hash + # Only needed by Python 2.7 + def __getstate__(self): + return self._d, self._hash + def __setstate__(self, state): self._d, self._hash = state @@ -498,6 +502,10 @@ def __hash__(self): raise ValueError(mess) return super(ParserHelper, self).__hash__() + # Only needed by Python 2.7 + def __getstate__(self): + return self._d, self._hash, self.scale + def __setstate__(self, state): self._d, self._hash, self.scale = state From 69b24e6cc6c05ce1f9e938339c13af8534b3d8c0 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:46:51 +0000 Subject: [PATCH 121/612] More cleanup --- pint/quantity.py | 8 +++++--- pint/unit.py | 8 +++++--- pint/util.py | 14 +------------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index cece445db..55a2fcff0 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -12,6 +12,7 @@ import copy import datetime import functools +import locale import math import numbers import operator @@ -25,8 +26,7 @@ from .definitions import UnitDefinition from .compat import ndarray, np, _to_magnitude from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, - to_units_container, infer_base_unit, - fix_str_conversions) + to_units_container, infer_base_unit) from pint.compat import Loc @@ -93,7 +93,6 @@ def printoptions(*args, **kwargs): np.set_printoptions(**opts) -@fix_str_conversions class Quantity(PrettyIPython, SharedRegistryObject): """Implements a class to describe a physical quantity: the product of a numerical value and a unit of measurement. @@ -192,6 +191,9 @@ def __deepcopy__(self, memo): def __str__(self): return format(self) + def __bytes__(self): + return str(self).encode(locale.getpreferredencoding()) + def __repr__(self): return f"" diff --git a/pint/unit.py b/pint/unit.py index a18ed8127..6d8b03625 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -10,18 +10,17 @@ """ import copy +import locale import operator from numbers import Number -from .util import ( - PrettyIPython, UnitsContainer, SharedRegistryObject, fix_str_conversions) +from .util import PrettyIPython, UnitsContainer, SharedRegistryObject from .compat import NUMERIC_TYPES from .formatting import siunitx_format_unit from .definitions import UnitDefinition -@fix_str_conversions class Unit(PrettyIPython, SharedRegistryObject): """Implements a class to describe a unit supporting math operations. @@ -70,6 +69,9 @@ def __deepcopy__(self, memo): def __str__(self): return format(self) + def __bytes__(self): + return str(self).encode(locale.getpreferredencoding()) + def __repr__(self): return "".format(self._units) diff --git a/pint/util.py b/pint/util.py index bfe79b950..0ab29aff9 100644 --- a/pint/util.py +++ b/pint/util.py @@ -9,17 +9,15 @@ :license: BSD, see LICENSE for more details. """ -import locale import logging from logging import NullHandler -import re import operator +import re from collections.abc import Mapping from decimal import Decimal from numbers import Number from fractions import Fraction from functools import lru_cache - from token import NAME, NUMBER from .compat import tokenizer, NUMERIC_TYPES @@ -726,16 +724,6 @@ def infer_base_unit(q): return UnitsContainer({k: v for k, v in d.items() if v != 0}) -def fix_str_conversions(cls): - """Enable python2/3 compatible behaviour for __str__.""" - def __bytes__(self): - return self.__unicode__().encode(locale.getpreferredencoding()) - cls.__unicode__ = __unicode__ = cls.__str__ - cls.__bytes__ = __bytes__ - cls.__str__ = __unicode__ - return cls - - 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. From acd7fd4b69225859430310926439e383def74f34 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:51:16 +0000 Subject: [PATCH 122/612] Python 3 super() --- pint/context.py | 2 +- pint/definitions.py | 12 ++---- pint/errors.py | 10 ++--- pint/matplotlib.py | 4 +- pint/measurement.py | 2 +- pint/quantity.py | 2 +- pint/registry.py | 46 +++++++++++---------- pint/testsuite/helpers.py | 2 +- pint/testsuite/test_application_registry.py | 6 +-- pint/testsuite/test_unit.py | 2 +- pint/unit.py | 2 +- pint/util.py | 10 ++--- 12 files changed, 49 insertions(+), 51 deletions(-) diff --git a/pint/context.py b/pint/context.py index d4d27b98e..815711535 100644 --- a/pint/context.py +++ b/pint/context.py @@ -195,7 +195,7 @@ class ContextChain(ChainMap): """ def __init__(self, *args, **kwargs): - super(ContextChain, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._graph = None self._contexts = [] diff --git a/pint/definitions.py b/pint/definitions.py index f180f7c16..aefaf81c5 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -99,8 +99,7 @@ def __init__(self, name, symbol, aliases, converter): aliases = tuple(alias.strip('-') for alias in aliases) if symbol: symbol = symbol.strip('-') - super(PrefixDefinition, self).__init__(name, symbol, aliases, - converter) + super().__init__(name, symbol, aliases, converter) class UnitDefinition(Definition): @@ -139,7 +138,7 @@ def __init__(self, name, symbol, aliases, converter, else: converter = ScaleConverter(converter.scale) - super(UnitDefinition, self).__init__(name, symbol, aliases, converter) + super().__init__(name, symbol, aliases, converter) class DimensionDefinition(Definition): @@ -162,14 +161,11 @@ def __init__(self, name, symbol, aliases, converter, 'to dimensions.') self.reference = UnitsContainer(converter) - super(DimensionDefinition, self).__init__(name, symbol, aliases, - converter=None) + super().__init__(name, symbol, aliases, converter=None) class AliasDefinition(Definition): """Additional alias(es) for an already existing unit """ def __init__(self, name, aliases): - super(AliasDefinition, self).__init__( - name=name, symbol=None, aliases=aliases, converter=None - ) + super().__init__(name=name, symbol=None, aliases=aliases, converter=None) diff --git a/pint/errors.py b/pint/errors.py index 1939dce87..79aae363c 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -15,7 +15,7 @@ class DefinitionSyntaxError(ValueError): """ def __init__(self, msg, filename=None, lineno=None): - super(DefinitionSyntaxError, self).__init__() + super().__init__() self.msg = msg self.filename = None self.lineno = None @@ -30,7 +30,7 @@ class RedefinitionError(ValueError): """ def __init__(self, name, definition_type): - super(RedefinitionError, self).__init__() + super().__init__() self.name = name self.definition_type = definition_type self.filename = None @@ -50,7 +50,7 @@ class UndefinedUnitError(AttributeError): """ def __init__(self, unit_names): - super(UndefinedUnitError, self).__init__() + super().__init__() self.unit_names = unit_names def __str__(self): @@ -73,7 +73,7 @@ class DimensionalityError(ValueError): """ def __init__(self, units1, units2, dim1=None, dim2=None, extra_msg=''): - super(DimensionalityError, self).__init__() + super().__init__() self.units1 = units1 self.units2 = units2 self.dim1 = dim1 @@ -97,7 +97,7 @@ class OffsetUnitCalculusError(ValueError): """Raised on ambiguous operations with offset units. """ def __init__(self, units1, units2='', extra_msg=''): - super(ValueError, self).__init__() + super().__init__() self.units1 = units1 self.units2 = units2 self.extra_msg = extra_msg diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 67ffef8e8..49aad2090 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -20,14 +20,14 @@ class PintAxisInfo(matplotlib.units.AxisInfo): def __init__(self, units): """Set the default label to the pretty-print of the unit.""" - super(PintAxisInfo, self).__init__(label='{:P}'.format(units)) + super().__init__(label='{:P}'.format(units)) class PintConverter(matplotlib.units.ConversionInterface): """Implement support for pint within matplotlib's unit conversion framework.""" def __init__(self, registry): - super(PintConverter, self).__init__() + super().__init__() self._reg = registry def convert(self, value, unit, axis): diff --git a/pint/measurement.py b/pint/measurement.py index 63f874467..55ed4b5ec 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -47,7 +47,7 @@ def __new__(cls, value, error, units=MISSING): else: mag = ufloat(value, error) - inst = super(Measurement, cls).__new__(cls, mag, units) + inst = super().__new__(cls, mag, units) return inst @property diff --git a/pint/quantity.py b/pint/quantity.py index 55a2fcff0..d7108ba6a 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -265,7 +265,7 @@ def __format__(self, spec): def _repr_pretty_(self, p, cycle): if cycle: - super(Quantity, self)._repr_pretty_(p, cycle) + super()._repr_pretty_(p, cycle) else: p.pretty(self.magnitude) p.text(" ") diff --git a/pint/registry.py b/pint/registry.py index f6be01810..8d2fc1bf4 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -903,7 +903,7 @@ class NonMultiplicativeRegistry(BaseRegistry): """ def __init__(self, default_as_delta=True, autoconvert_offset_to_baseunit=False, **kwargs): - super(NonMultiplicativeRegistry, self).__init__(**kwargs) + super().__init__(**kwargs) #: When performing a multiplication of units, interpret #: non-multiplicative units as their *delta* counterparts. @@ -919,7 +919,7 @@ def _parse_units(self, input_string, as_delta=None): if as_delta is None: as_delta = self.default_as_delta - return super(NonMultiplicativeRegistry, self)._parse_units(input_string, as_delta) + return super()._parse_units(input_string, as_delta) def _define(self, definition): """Add unit to the registry. @@ -933,7 +933,7 @@ def _define(self, definition): :rtype: Definition, dict, dict """ - definition, d, di = super(NonMultiplicativeRegistry, self)._define(definition) + definition, d, di = super()._define(definition) # define additional units for units with an offset if getattr(definition.converter, "offset", 0.0) != 0.0: @@ -1012,7 +1012,7 @@ def _convert(self, value, src, dst, inplace=False): raise DimensionalityError(src, dst, extra_msg=' - In destination units, %s ' % ex) if not (src_offset_unit or dst_offset_unit): - return super(NonMultiplicativeRegistry, self)._convert(value, src, dst, inplace) + return super()._convert(value, src, dst, inplace) src_dim = self._get_dimensionality(src) dst_dim = self._get_dimensionality(dst) @@ -1032,7 +1032,7 @@ def _convert(self, value, src, dst, inplace=False): dst = dst.remove([dst_offset_unit]) # Convert non multiplicative units to the dst. - value = super(NonMultiplicativeRegistry, self)._convert(value, src, dst, inplace, False) + value = super()._convert(value, src, dst, inplace, False) # Finally convert to offset units specified in destination if dst_offset_unit: @@ -1056,7 +1056,7 @@ class ContextRegistry(BaseRegistry): """ def __init__(self, **kwargs): - super(ContextRegistry, self).__init__(**kwargs) + super().__init__(**kwargs) #: Map context name (string) or abbreviation to context. self._contexts = {} @@ -1065,7 +1065,7 @@ def __init__(self, **kwargs): self._active_ctx = ContextChain() def _register_parsers(self): - super(ContextRegistry, self)._register_parsers() + super()._register_parsers() self._register_parser('@context', self._parse_context) def _parse_context(self, ifile): @@ -1263,7 +1263,7 @@ def _convert(self, value, src, dst, inplace=False): value, src = src._magnitude, src._units - return super(ContextRegistry, self)._convert(value, src, dst, inplace) + return super()._convert(value, src, dst, inplace) def _get_compatible_units(self, input_units, group_or_system): """ @@ -1271,7 +1271,7 @@ def _get_compatible_units(self, input_units, group_or_system): src_dim = self._get_dimensionality(input_units) - ret = super(ContextRegistry, self)._get_compatible_units(input_units, group_or_system) + ret = super()._get_compatible_units(input_units, group_or_system) if self._active_ctx: nodes = find_connected_nodes(self._active_ctx.graph, src_dim) @@ -1298,7 +1298,7 @@ class SystemRegistry(BaseRegistry): """ def __init__(self, system=None, **kwargs): - super(SystemRegistry, self).__init__(**kwargs) + super().__init__(**kwargs) #: Map system name to system. #: :type: dict[ str | System] @@ -1314,7 +1314,7 @@ def __init__(self, system=None, **kwargs): self._default_system = system def _init_dynamic_classes(self): - super(SystemRegistry, self)._init_dynamic_classes() + super()._init_dynamic_classes() self.Group = systems.build_group_class(self) self.System = systems.build_system_class(self) @@ -1325,7 +1325,7 @@ def _after_init(self): Add all orphan units to it. Set default system. """ - super(SystemRegistry, self)._after_init() + super()._after_init() #: Copy units not defined in any group to the default group if 'group' in self._defaults: @@ -1345,7 +1345,7 @@ def _after_init(self): self._default_system = self._default_system or self._defaults.get('system', None) def _register_parsers(self): - super(SystemRegistry, self)._register_parsers() + super()._register_parsers() self._register_parser('@group', self._parse_group) self._register_parser('@system', self._parse_system) @@ -1408,7 +1408,7 @@ def _define(self, definition): # In addition to the what is done by the BaseRegistry, # this adds all units to the `root` group. - definition, d, di = super(SystemRegistry, self)._define(definition) + definition, d, di = super()._define(definition) if isinstance(definition, UnitDefinition): # We add all units to the root group @@ -1483,7 +1483,7 @@ def _get_compatible_units(self, input_units, group_or_system): if group_or_system is None: group_or_system = self._default_system - ret = super(SystemRegistry, self)._get_compatible_units(input_units, group_or_system) + ret = super()._get_compatible_units(input_units, group_or_system) if group_or_system: if group_or_system in self._systems: @@ -1520,12 +1520,14 @@ def __init__(self, filename='', force_ndarray=False, default_as_delta=True, on_redefinition='warn', system=None, auto_reduce_dimensions=False): - super(UnitRegistry, self).__init__(filename=filename, force_ndarray=force_ndarray, - on_redefinition=on_redefinition, - default_as_delta=default_as_delta, - autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, - system=system, - auto_reduce_dimensions=auto_reduce_dimensions) + super().__init__( + filename=filename, force_ndarray=force_ndarray, + on_redefinition=on_redefinition, + default_as_delta=default_as_delta, + autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, + system=system, + auto_reduce_dimensions=auto_reduce_dimensions + ) def pi_theorem(self, quantities): """Builds dimensionless quantities using the Buckingham π theorem @@ -1569,7 +1571,7 @@ def __getattr__(self, item): def __setattr__(self, key, value): if key == '__class__': - super(LazyRegistry, self).__setattr__(key, value) + super().__setattr__(key, value) else: self.__init() setattr(self, key, value) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index bf638bdf4..8902e90bd 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -53,7 +53,7 @@ def requires_not_uncertainties(): class PintOutputChecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): - check = super(PintOutputChecker, self).check_output(want, got, optionflags) + check = super().check_output(want, got, optionflags) if check: return check diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index f80162c8b..b0f77ebc1 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -86,7 +86,7 @@ def test_pickle_crash_measurement(self): class TestCustomApplicationRegistry(BaseTestCase): def setUp(self): - super(TestCustomApplicationRegistry, self).setUp() + super().setUp() self.ureg_bak = get_application_registry() self.ureg = UnitRegistry(None) self.ureg.define("foo = []") @@ -95,7 +95,7 @@ def setUp(self): assert get_application_registry() is self.ureg def tearDown(self): - super(TestCustomApplicationRegistry, self).tearDown() + super().tearDown() set_application_registry(self.ureg_bak) def test_unit(self): @@ -149,7 +149,7 @@ class TestSwapApplicationRegistry(BaseTestCase): """ def setUp(self): - super(TestSwapApplicationRegistry, self).setUp() + super().setUp() self.ureg_bak = get_application_registry() self.ureg1 = UnitRegistry(None) self.ureg1.define("foo = [dim1]") diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 8bbe54c33..2026c9a39 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -515,7 +515,7 @@ class TestCompatibleUnits(QuantityTestCase): FORCE_NDARRAY = False def setUp(self): - super(TestCompatibleUnits, self).setUp() + super().setUp() self.ureg = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) self.Q_ = self.ureg.Quantity self.U_ = self.ureg.Unit diff --git a/pint/unit.py b/pint/unit.py index 6d8b03625..eda92d6fa 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -38,7 +38,7 @@ def __reduce__(self): return _unpickle, (Unit, self._units) def __init__(self, units): - super(Unit, self).__init__() + super().__init__() if isinstance(units, (UnitsContainer, UnitDefinition)): self._units = units elif isinstance(units, str): diff --git a/pint/util.py b/pint/util.py index 0ab29aff9..b5adb6a04 100644 --- a/pint/util.py +++ b/pint/util.py @@ -421,7 +421,7 @@ class ParserHelper(UnitsContainer): __slots__ = ('scale', ) def __init__(self, scale=1, *args, **kwargs): - super(ParserHelper, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.scale = scale @classmethod @@ -487,7 +487,7 @@ def _from_string(cls, input_string): ) def __copy__(self): - new = super(ParserHelper, self).__copy__() + new = super().__copy__() new.scale = self.scale return new @@ -498,7 +498,7 @@ def __hash__(self): if self.scale != 1.0: mess = 'Only scale 1.0 ParserHelper instance should be considered hashable' raise ValueError(mess) - return super(ParserHelper, self).__hash__() + return super().__hash__() # Only needed by Python 2.7 def __getstate__(self): @@ -511,14 +511,14 @@ def __eq__(self, other): if isinstance(other, ParserHelper): return ( self.scale == other.scale and - super(ParserHelper, self).__eq__(other) + super().__eq__(other) ) elif isinstance(other, str): return self == ParserHelper.from_string(other) elif isinstance(other, Number): return self.scale == other and not len(self._d) else: - return self.scale == 1. and super(ParserHelper, self).__eq__(other) + return self.scale == 1. and super().__eq__(other) def operate(self, items, op=operator.iadd, cleanup=True): d = udict(self._d) From 91b8ad1fa3153c387450119af97b074472e63c04 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:52:31 +0000 Subject: [PATCH 123/612] Remove Python 2 pickle idiosyncrasies --- pint/util.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pint/util.py b/pint/util.py index b5adb6a04..d5ec5a292 100644 --- a/pint/util.py +++ b/pint/util.py @@ -311,13 +311,6 @@ def __hash__(self): self._hash = hash(frozenset(self._d.items())) return self._hash - # Only needed by Python 2.7 - def __getstate__(self): - return self._d, self._hash - - def __setstate__(self, state): - self._d, self._hash = state - def __eq__(self, other): if isinstance(other, UnitsContainer): # UnitsContainer.__hash__(self) is not the same as hash(self); see @@ -500,13 +493,6 @@ def __hash__(self): raise ValueError(mess) return super().__hash__() - # Only needed by Python 2.7 - def __getstate__(self): - return self._d, self._hash, self.scale - - def __setstate__(self, state): - self._d, self._hash, self.scale = state - def __eq__(self, other): if isinstance(other, ParserHelper): return ( From 75eed2f1e8061582e77834b208911009ec2ba172 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 17:58:17 +0000 Subject: [PATCH 124/612] Auto stash before merge of "py27_cleanup" and "upstream/master" --- pint/pint_eval.py | 2 -- pint/testsuite/test_issues.py | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/pint/pint_eval.py b/pint/pint_eval.py index b198a403b..d00cc8a39 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from decimal import Decimal -import math import operator import token as tokenlib diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 7cc0f9c16..8c39cb3bf 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -3,9 +3,9 @@ import copy import math import unittest -import sys +from inspect import signature -from pint import DimensionalityError, UndefinedUnitError, UnitRegistry +from pint import DimensionalityError, UnitRegistry from pint.unit import UnitsContainer from pint.util import ParserHelper @@ -538,12 +538,6 @@ def f(x): self.assertRaises(DimensionalityError, f, ureg.Quantity(1, 'm')) def test_issue625a(self): - try: - from inspect import signature - except ImportError: - # Python2 does not have the inspect library. Import the backport. - from funcsigs import signature - ureg = UnitRegistry() Q_ = ureg.Quantity from math import sqrt From 9ed5cd28357fd0e3148971261a8a871738d2d415 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 18:04:15 +0000 Subject: [PATCH 125/612] Clean up imports --- docs/conf.py | 1 - pint/testsuite/parameterized.py | 7 ++----- pint/testsuite/test_issues.py | 24 +++--------------------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6d8b8470a..5afe625ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os import pkg_resources import datetime diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 37575fd8f..c5336a737 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -28,14 +28,11 @@ # def test_eval(self, input, expected_output): # self.assertEqual(eval(input), expected_output) +from collections.abc import Callable from functools import wraps -import collections + import unittest -try: - from collections.abc import Callable -except ImportError: - from collections import Callable def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 8c39cb3bf..0254550e3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -3,7 +3,6 @@ import copy import math import unittest -from inspect import signature from pint import DimensionalityError, UnitRegistry from pint.unit import UnitsContainer @@ -564,12 +563,6 @@ def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2')): self.assertAlmostEqual(t2, Q_(3.508232077228117, 's')) def test_issue625b(self): - try: - from inspect import signature - except ImportError: - # Python2 does not have the inspect library. Import the backport. - from funcsigs import signature - ureg = UnitRegistry() Q_ = ureg.Quantity @@ -585,13 +578,7 @@ def get_displacement(time, rate=Q_(1, 'm/s')): d2 = get_displacement(Q_(2, 's'), Q_(1, 'deg/s')) self.assertAlmostEqual(d2, Q_(2,' deg')) - def test_issue625c(self): - try: - from inspect import signature - except ImportError: - # Python2 does not have the inspect library. Import the backport. - from funcsigs import signature - + def test_issue625c(self): u = UnitRegistry() @u.wraps('=A*B*C', ('=A', '=B', '=C')) @@ -613,19 +600,14 @@ def test_issue655a(self): self.assertEqual(velocity.check('1 / [time] * [length]'), True) def test_issue655b(self): - import math - try: - from inspect import signature - except ImportError: - # Python2 does not have the inspect library. Import the backport - from funcsigs import signature - ureg = UnitRegistry() Q_ = ureg.Quantity + @ureg.check('[length]', '[length]/[time]^2') def pendulum_period(length, G=Q_(1, 'standard_gravity')): print(length) return (2*math.pi*(length/G)**.5).to('s') + l = 1 * ureg.m # Assume earth gravity t = pendulum_period(l) From dd01f63762684363997fa1b7acc715e20124f310 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 18:04:33 +0000 Subject: [PATCH 126/612] Remove obsolete tox --- .gitignore | 1 - README | 4 +--- README.rst | 4 +--- setup.cfg | 1 - setup.py | 2 +- tox.ini | 59 ------------------------------------------------------ 6 files changed, 3 insertions(+), 68 deletions(-) delete mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index d5d342322..6a25db6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ docs/_build/ build/ dist/ MANIFEST -.tox *pytest_cache* # WebDAV file system cache files diff --git a/README b/README index 3e3d3dc9a..e2d43b644 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ and constants. Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 2.7 and 3.3+ +It has a complete test coverage. It runs in Python 3.6+ with no other dependency. It is licensed under BSD. It is extremely easy and natural to use: @@ -87,7 +87,5 @@ Pint to scratch my own itches. - Dependency free: it depends only on Python and its standard library. -- Python 2 and 3: A single codebase that runs unchanged in Python 2.7 and Python 3.3+. - - Advanced NumPy support: While NumPy is not a requirement for Pint, when available ndarray methods and ufuncs can be used in Quantity objects. diff --git a/README.rst b/README.rst index c696e1921..673e38d6e 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,7 @@ and constants. Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 2.7 and 3.3+ +It has a complete test coverage. It runs in Python 3.6+ with no other dependency. It is licensed under BSD. It is extremely easy and natural to use: @@ -116,7 +116,5 @@ Pint to scratch my own itches. - Dependency free: it depends only on Python and its standard library. -- Python 2 and 3: A single codebase that runs unchanged in Python 2.7 and Python 3.3+. - - Advanced NumPy support: While NumPy is not a requirement for Pint, when available ndarray methods and ufuncs can be used in Quantity objects. diff --git a/setup.cfg b/setup.cfg index 5586ef4ba..6499fe786 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,6 @@ [check-manifest] ignore = .travis.yml - tox.ini [bdist_wheel] universal = 1 diff --git a/setup.py b/setup.py index 478177c31..4fab6b104 100644 --- a/setup.py +++ b/setup.py @@ -65,4 +65,4 @@ def read(filename): 'numpy': ['numpy >= 1.14'], 'uncertainties': ['uncertainties >= 3.0'], }, - ) +) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 4efc61b18..000000000 --- a/tox.ini +++ /dev/null @@ -1,59 +0,0 @@ -[tox] -envlist = py27, py33, py34, numpy27, numpy33, numpy34, - py27u,py33u,py34u,numpy27u,numpy33u,numpy34u - -[testenv] -deps = coverage -commands = coverage run -p --source=pint setup.py test - -setenv= - COVERAGE_FILE=.coverage.{envname} - - -[testenv:numpy27] -basepython=python2.7 -deps = numpy - coverage - -[testenv:numpy33] -basepython=python3.3 -deps = numpy - coverage - -[testenv:numpy34] -basepython=python3.4 -deps = numpy - coverage - -[testenv:py27u] -basepython=python2.7 -deps = uncertainties - coverage - -[testenv:py33u] -basepython=python3.3 -deps = uncertainties - coverage - -[testenv:py34u] -basepython=python3.4 -deps = uncertainties - coverage - -[testenv:numpy27u] -basepython=python2.7 -deps = numpy - uncertainties - coverage - -[testenv:numpy33u] -basepython=python3.3 -deps = numpy - uncertainties - coverage - -[testenv:numpy34u] -basepython=python3.4 -deps = numpy - uncertainties - coverage From 3c151c288e9f76011d173005c2f1c0e6b6474a11 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 3 Dec 2019 18:09:39 +0000 Subject: [PATCH 127/612] Empty forgotten files --- CHANGES_DEV | 5 ----- MANIFEST.in | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 CHANGES_DEV diff --git a/CHANGES_DEV b/CHANGES_DEV deleted file mode 100644 index 76a9deb22..000000000 --- a/CHANGES_DEV +++ /dev/null @@ -1,5 +0,0 @@ - - - -0.7 (unreleased) ----------------- diff --git a/MANIFEST.in b/MANIFEST.in index 4e37c21ca..e8e05bbdc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README AUTHORS CHANGES LICENSE CHANGES_DEV README.rst BADGES.rst +include README AUTHORS CHANGES LICENSE README.rst BADGES.rst recursive-include pint * recursive-include docs * recursive-include bench * From 718c18d810bfb966563989346623e02b7919e4ae Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 11:52:46 +0000 Subject: [PATCH 128/612] Exceptions overhaul --- pint/__init__.py | 4 ++ pint/context.py | 11 ++--- pint/errors.py | 69 +++++++++++++++--------------- pint/quantity.py | 69 +++++++++++++++++++++--------- pint/registry.py | 8 +++- pint/testsuite/test_contexts.py | 54 +++++++++++------------ pint/testsuite/test_errors.py | 2 +- pint/testsuite/test_measurement.py | 10 +++-- pint/testsuite/test_numpy.py | 53 +++++++++++++---------- pint/testsuite/test_umath.py | 29 ++++++------- pint/testsuite/test_unit.py | 26 +++++------ 11 files changed, 191 insertions(+), 144 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 0189f086f..67d9f62ab 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -18,8 +18,10 @@ from .context import Context from .errors import ( + DefinitionSyntaxError, DimensionalityError, OffsetUnitCalculusError, + RedefinitionError, UndefinedUnitError, UnitStrippedWarning ) @@ -116,8 +118,10 @@ def test(): 'Unit', 'UnitRegistry', + 'DefinitionSyntaxError', 'DimensionalityError', 'OffsetUnitCalculusError', + 'RedefinitionError', 'UndefinedUnitError', 'UnitStrippedWarning', diff --git a/pint/context.py b/pint/context.py index 6bd733584..3a3031628 100644 --- a/pint/context.py +++ b/pint/context.py @@ -119,14 +119,15 @@ def to_num(val): return val.real return val - _txt = defaults + txt = defaults try: defaults = (part.split('=') for part in defaults.strip('()').split(',')) - defaults = dict((str(k).strip(), to_num(v)) - for k, v in defaults) + defaults = {str(k).strip(): to_num(v) for k, v in defaults} except (ValueError, TypeError): - raise DefinitionSyntaxError("Could not parse Context definition defaults: '%s'" % _txt, - lineno=lineno) + raise DefinitionSyntaxError( + f"Could not parse Context definition defaults: '{txt}'", + lineno=lineno + ) ctx = cls(name, aliases, defaults) else: diff --git a/pint/errors.py b/pint/errors.py index 700d79cbf..1b3730645 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -8,25 +8,19 @@ :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ -from __future__ import (division, unicode_literals, print_function, - absolute_import) -from .compat import string_types - -class DefinitionSyntaxError(ValueError): +class DefinitionSyntaxError(SyntaxError): """Raised when a textual definition has a syntax error. """ def __init__(self, msg, filename=None, lineno=None): - super(DefinitionSyntaxError, self).__init__() - self.msg = msg + super().__init__(msg) self.filename = None self.lineno = None def __str__(self): - mess = "While opening {}, in line {}: " - return mess.format(self.filename, self.lineno) + self.msg + return f"While opening {self.filename}, in line {self.lineno}: {self.args[0]}" class RedefinitionError(ValueError): @@ -34,18 +28,16 @@ class RedefinitionError(ValueError): """ def __init__(self, name, definition_type): - super(RedefinitionError, self).__init__() + super().__init__() self.name = name self.definition_type = definition_type self.filename = None self.lineno = None def __str__(self): - msg = "cannot redefine '{}' ({})".format(self.name, - self.definition_type) + msg = f"Cannot redefine '{self.name}' ({self.definition_type})" if self.filename: - mess = "While opening {}, in line {}: " - return mess.format(self.filename, self.lineno) + msg + return f"While opening {self.filename}, in line {self.lineno}: {msg}" return msg @@ -54,16 +46,15 @@ class UndefinedUnitError(AttributeError): """ def __init__(self, unit_names): - super(UndefinedUnitError, self).__init__() + super().__init__() self.unit_names = unit_names def __str__(self): mess = "'{}' is not defined in the unit registry" mess_plural = "'{}' are not defined in the unit registry" - if isinstance(self.unit_names, string_types): + if isinstance(self.unit_names, str): return mess.format(self.unit_names) - elif isinstance(self.unit_names, (list, tuple))\ - and len(self.unit_names) == 1: + elif isinstance(self.unit_names, (list, tuple)) and len(self.unit_names) == 1: return mess.format(self.unit_names[0]) elif isinstance(self.unit_names, set) and len(self.unit_names) == 1: uname = list(self.unit_names)[0] @@ -72,12 +63,16 @@ def __str__(self): return mess_plural.format(self.unit_names) -class DimensionalityError(ValueError): +class PintTypeError(TypeError): + pass + + +class DimensionalityError(PintTypeError): """Raised when trying to convert between incompatible units. """ - def __init__(self, units1, units2, dim1=None, dim2=None, extra_msg=''): - super(DimensionalityError, self).__init__() + def __init__(self, units1, units2, dim1=None, dim2=None, *, extra_msg=""): + super().__init__() self.units1 = units1 self.units2 = units2 self.dim1 = dim1 @@ -86,31 +81,35 @@ def __init__(self, units1, units2, dim1=None, dim2=None, extra_msg=''): def __str__(self): if self.dim1 or self.dim2: - dim1 = ' ({})'.format(self.dim1) - dim2 = ' ({})'.format(self.dim2) + dim1 = f" ({self.dim1})" + dim2 = f" ({self.dim2})" else: - dim1 = '' - dim2 = '' - - msg = "Cannot convert from '{}'{} to '{}'{}" + self.extra_msg + dim1 = "" + dim2 = "" - return msg.format(self.units1, dim1, self.units2, dim2) + return ( + f"Cannot convert from '{self.units1}'{dim1} to " + f"'{self.units2}'{dim2}{self.extra_msg}" + ) -class OffsetUnitCalculusError(ValueError): +class OffsetUnitCalculusError(PintTypeError): """Raised on ambiguous operations with offset units. """ - def __init__(self, units1, units2='', extra_msg=''): - super(ValueError, self).__init__() + + def __init__(self, units1, units2="", *, extra_msg=""): + super().__init__() self.units1 = units1 self.units2 = units2 self.extra_msg = extra_msg def __str__(self): - msg = ("Ambiguous operation with offset unit (%s)." % - ', '.join(['%s' % u for u in [self.units1, self.units2] if u]) - + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance." - + self.extra_msg) + msg = ( + "Ambiguous operation with offset unit (%s)." + % ", ".join(["%s" % u for u in [self.units1, self.units2] if u]) + + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance." + + self.extra_msg + ) return msg.format(self.units1, self.units2) diff --git a/pint/quantity.py b/pint/quantity.py index f093d08db..fcaac078c 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -22,7 +22,7 @@ from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts) -from .errors import (DimensionalityError, OffsetUnitCalculusError, +from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition from .compat import string_types, ndarray, np, _to_magnitude, long_type @@ -629,8 +629,11 @@ def _iadd_sub(self, other, op): # other not from same Registry or not a Quantity try: other_magnitude = _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented + if _eq(other, 0, True): # If the other value is 0 (but not Quantity 0) # do the operation without checking units. @@ -880,6 +883,8 @@ def _imul_div(self, other, magnitude_op, units_op=None): getattr(other, 'units', '')) try: other_magnitude = _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented self._magnitude = magnitude_op(self._magnitude, other_magnitude) @@ -938,6 +943,8 @@ def _mul_div(self, other, magnitude_op, units_op=None): getattr(other, 'units', '')) try: other_magnitude = _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented @@ -991,6 +998,8 @@ def __truediv__(self, other): def __rtruediv__(self, other): try: other_magnitude = _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented @@ -1084,7 +1093,9 @@ def __ipow__(self, other): return self.__pow__(other) try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented else: @@ -1105,9 +1116,12 @@ def __ipow__(self, other): self._magnitude **= other return self elif np.size(other) > 1: - raise DimensionalityError(self._units, 'dimensionless', - extra_msg='Quantity array exponents are only allowed ' - 'if the base is dimensionless') + raise DimensionalityError( + self._units, + 'dimensionless', + extra_msg=". Quantity array exponents are only allowed if the " + "base is dimensionless" + ) if other == 1: return self @@ -1134,7 +1148,9 @@ def __ipow__(self, other): @check_implemented def __pow__(self, other): try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented else: @@ -1153,9 +1169,12 @@ def __pow__(self, other): else: return self.__class__(self.m ** other) elif np.size(other) > 1: - raise DimensionalityError(self._units, 'dimensionless', - extra_msg='Quantity array exponents are only allowed ' - 'if the base is dimensionless') + raise DimensionalityError( + self._units, + 'dimensionless', + extra_msg=". Quantity array exponents are only allowed if the " + "base is dimensionless" + ) new_self = self if other == 1: @@ -1182,7 +1201,9 @@ def __pow__(self, other): @check_implemented def __rpow__(self, other): try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray) + except PintTypeError: + raise except TypeError: return NotImplemented else: @@ -1475,8 +1496,9 @@ def __getattr__(self, item): def __getitem__(self, key): try: - value = self._magnitude[key] - return self.__class__(value, self._units) + return type(self)(self._magnitude[key], self._units) + except PintTypeError: + raise except TypeError: raise TypeError("Neither Quantity object nor its magnitude ({})" "supports indexing".format(self._magnitude)) @@ -1486,7 +1508,7 @@ def __setitem__(self, key, value): if math.isnan(value): self._magnitude[key] = value return - except (TypeError, DimensionalityError): + except TypeError: pass try: @@ -1497,17 +1519,24 @@ def __setitem__(self, key, value): if isinstance(factor, self.__class__): if not factor.dimensionless: - raise DimensionalityError(value, self.units, - extra_msg='. Assign a quantity with the same dimensionality or ' - 'access the magnitude directly as ' - '`obj.magnitude[%s] = %s`' % (key, value)) + raise DimensionalityError( + value, + self.units, + extra_msg=". Assign a quantity with the same dimensionality " + "or access the magnitude directly as " + f"`obj.magnitude[{key}] = {value}`." + ) self._magnitude[key] = factor.magnitude else: self._magnitude[key] = factor - except TypeError: - raise TypeError("Neither Quantity object nor its magnitude ({})" - "supports indexing".format(self._magnitude)) + except PintTypeError: + raise + except TypeError as exc: + raise TypeError( + f"Neither Quantity object nor its magnitude ({self._magnitude}) " + "supports indexing" + ) from exc def tolist(self): units = self._units diff --git a/pint/registry.py b/pint/registry.py index 4324c7d54..97de8e6b4 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1007,12 +1007,16 @@ def _convert(self, value, src, dst, inplace=False): try: src_offset_unit = self._validate_and_extract(src) except ValueError as ex: - raise DimensionalityError(src, dst, extra_msg=' - In source units, %s ' % ex) + raise DimensionalityError( + src, dst, extra_msg=f" - In source units, {ex}" + ) try: dst_offset_unit = self._validate_and_extract(dst) except ValueError as ex: - raise DimensionalityError(src, dst, extra_msg=' - In destination units, %s ' % ex) + raise DimensionalityError( + src, dst, extra_msg=f" - In destination units, {ex}" + ) if not (src_offset_unit or dst_offset_unit): return super(NonMultiplicativeRegistry, self)._convert(value, src, dst, inplace) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 7766aa7df..c88d1636e 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -5,7 +5,7 @@ import itertools from collections import defaultdict -from pint import UnitRegistry, errors +from pint import UnitRegistry, DefinitionSyntaxError, DimensionalityError from pint.context import Context from pint.util import UnitsContainer from pint.testsuite import QuantityTestCase @@ -280,11 +280,11 @@ def test_one_context(self): meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertEqual(q.to('Hz'), s) self.assertEqual(ureg.get_compatible_units(q), meter_units | hertz_units) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') self.assertEqual(ureg.get_compatible_units(q), meter_units) def test_multiple_context(self): @@ -299,11 +299,11 @@ def test_multiple_context(self): hertz_units = ureg.get_compatible_units(ureg.hertz) ampere_units = ureg.get_compatible_units(ureg.ampere) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc', 'ab'): self.assertEqual(q.to('Hz'), s) self.assertEqual(ureg.get_compatible_units(q), meter_units | hertz_units | ampere_units) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') self.assertEqual(ureg.get_compatible_units(q), meter_units) def test_nested_context(self): @@ -314,7 +314,7 @@ def test_nested_context(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to('Hz') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): @@ -322,10 +322,10 @@ def test_nested_context(self): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertEqual(q.to('Hz'), s) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') def test_context_with_arg(self): @@ -336,7 +336,7 @@ def test_context_with_arg(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to('Hz') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc', n=1): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): @@ -344,10 +344,10 @@ def test_context_with_arg(self): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc', n=1): self.assertEqual(q.to('Hz'), s) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertRaises(TypeError, q.to, 'Hz') @@ -361,7 +361,7 @@ def test_enable_context_with_arg(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to('Hz') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') ureg.enable_contexts('lc', n=1) self.assertEqual(q.to('Hz'), s) ureg.enable_contexts('ab') @@ -371,11 +371,11 @@ def test_enable_context_with_arg(self): ureg.disable_contexts(1) ureg.enable_contexts('ab') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') ureg.enable_contexts('lc', n=1) self.assertEqual(q.to('Hz'), s) ureg.disable_contexts(1) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') ureg.disable_contexts(1) ureg.enable_contexts('lc') @@ -391,7 +391,7 @@ def test_context_with_arg_def(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to('Hz') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): @@ -399,12 +399,12 @@ def test_context_with_arg_def(self): self.assertEqual(q.to('Hz'), s) with ureg.context('ab'): - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc'): self.assertEqual(q.to('Hz'), s) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc', n=2): self.assertEqual(q.to('Hz'), s / 2) with ureg.context('ab'): @@ -412,10 +412,10 @@ def test_context_with_arg_def(self): self.assertEqual(q.to('Hz'), s / 2) with ureg.context('ab'): - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') with ureg.context('lc', n=2): self.assertEqual(q.to('Hz'), s / 2) - self.assertRaises(ValueError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, 'Hz') def test_context_with_sharedarg_def(self): @@ -476,7 +476,7 @@ def test_anonymous_context(self): ureg.enable_contexts(c) self.assertQuantityEqual(x.to("s"), expect) ureg.disable_contexts(1) - self.assertRaises(errors.DimensionalityError, x.to, "s") + self.assertRaises(DimensionalityError, x.to, "s") # Multiple anonymous contexts c2 = Context() @@ -521,7 +521,7 @@ def test_parse_invalid(self): '[length] = 1 / [time]: c / value', '1 / [time] = [length]: c / value'] - self.assertRaises(ValueError, Context.from_lines, s) + self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) def test_parse_simple(self): @@ -592,7 +592,7 @@ def test_parse_parameterized(self): c = Context.from_lines(s) self.assertEqual(c.defaults, {'n': 1}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) s = ['@context(n=1, bla=2) longcontextname', @@ -600,12 +600,12 @@ def test_parse_parameterized(self): c = Context.from_lines(s) self.assertEqual(c.defaults, {'n': 1, 'bla': 2}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) # If the variable is not present in the definition, then raise an error s = ['@context(n=1) longcontextname', '[length] <-> 1 / [time]: c / value'] - self.assertRaises(ValueError, Context.from_lines, s) + self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) def test_warnings(self): @@ -668,7 +668,7 @@ def test_spectroscopy(self): def test_textile(self): ureg = self.ureg qty_direct = 1.331 * ureg.tex - with self.assertRaises(errors.DimensionalityError): + with self.assertRaises(DimensionalityError): qty_indirect = qty_direct.to('Nm') with ureg.context('textile'): @@ -693,7 +693,7 @@ def test_decorator(self): def f(wl): return wl.to('terahertz') - self.assertRaises(errors.DimensionalityError, f, a) + self.assertRaises(DimensionalityError, f, a) @ureg.with_context('sp') def g(wl): diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index b6537c160..df053b5e4 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -16,5 +16,5 @@ def test_errors(self): self.assertEqual(str(UndefinedUnitError(set(x))), msg) msg = "Cannot convert from 'a' (c) to 'b' (d)msg" - ex = DimensionalityError('a', 'b', 'c', 'd', 'msg') + ex = DimensionalityError('a', 'b', 'c', 'd', extra_msg='msg') self.assertEqual(str(ex), msg) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index eff666afb..1cb8f3332 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -2,6 +2,7 @@ from __future__ import division, unicode_literals, print_function, absolute_import +from pint import DimensionalityError from pint.testsuite import QuantityTestCase, helpers @@ -104,9 +105,12 @@ def test_raise_build(self): o = self.Q_(.1, 'm') M_ = self.ureg.Measurement - self.assertRaises(ValueError, M_, v, o) - self.assertRaises(ValueError, v.plus_minus, o) - self.assertRaises(ValueError, v.plus_minus, u, True) + with self.assertRaises(DimensionalityError): + M_(v, o) + with self.assertRaises(DimensionalityError): + v.plus_minus(o) + with self.assertRaises(ValueError): + v.plus_minus(u, relative=True) def test_propagate_linear(self): diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 996195c26..48c80b053 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -81,8 +81,10 @@ def test_put(self): self.assertQuantityEqual(q, [0.001, 2., 0.002, 4.]*self.ureg.m/self.ureg.mm) q = [1., 2., 3., 4.] * self.ureg.m - self.assertRaises(ValueError, q.put, [0, 2], [4., 6.] * self.ureg.J) - self.assertRaises(ValueError, q.put, [0, 2], [4., 6.]) + with self.assertRaises(DimensionalityError): + q.put([0, 2], [4., 6.] * self.ureg.J) + with self.assertRaises(DimensionalityError): + q.put([0, 2], [4., 6.]) def test_repeat(self): self.assertQuantityEqual(self.q.repeat(2), [1,1,2,2,3,3,4,4]*self.ureg.m) @@ -111,7 +113,7 @@ def test_searchsorted(self): np.testing.assert_array_equal(q.searchsorted([1.5, 2.5] * self.ureg.m), [1, 2]) q = self.q.flatten() - self.assertRaises(ValueError, q.searchsorted, [1.5, 2.5]) + self.assertRaises(DimensionalityError, q.searchsorted, [1.5, 2.5]) def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" @@ -151,8 +153,8 @@ def test_clip(self): self.q.clip(min=2*self.ureg.m, max=3*self.ureg.m), [[2, 2], [3, 3]] * self.ureg.m ) - self.assertRaises(ValueError, self.q.clip, self.ureg.J) - self.assertRaises(ValueError, self.q.clip, 1) + self.assertRaises(DimensionalityError, self.q.clip, self.ureg.J) + self.assertRaises(DimensionalityError, self.q.clip, 1) def test_round(self): q = [1, 1.33, 5.67, 22] * self.ureg.m @@ -179,7 +181,7 @@ def test_prod(self): self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) def test_cumprod(self): - self.assertRaises(ValueError, self.q.cumprod) + self.assertRaises(DimensionalityError, self.q.cumprod) self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) @helpers.requires_numpy_previous_than('1.10') @@ -202,34 +204,39 @@ def test_getitem(self): self.assertEqual(self.q[1,1], 4*self.ureg.m) def test_setitem(self): - self.assertRaises(ValueError, self.q.__setitem__, (0,0), 1) - self.assertRaises(ValueError, self.q.__setitem__, (0,0), 1*self.ureg.J) - self.assertRaises(ValueError, self.q.__setitem__, 0, 1) - self.assertRaises(ValueError, self.q.__setitem__, 0, np.ndarray([1, 2])) - self.assertRaises(ValueError, self.q.__setitem__, 0, 1*self.ureg.J) + with self.assertRaises(TypeError): + self.q[0, 0] = 1 + with self.assertRaises(DimensionalityError): + self.q[0, 0] = 1 * self.ureg.J + with self.assertRaises(DimensionalityError): + self.q[0] = 1 + with self.assertRaises(DimensionalityError): + self.q[0] = np.ndarray([1, 2]) + with self.assertRaises(DimensionalityError): + self.q[0] = 1 * self.ureg.J q = self.q.copy() - q[0] = 1*self.ureg.m - self.assertQuantityEqual(q, [[1,1],[3,4]]*self.ureg.m) + q[0] = 1 * self.ureg.m + self.assertQuantityEqual(q, [[1, 1], [3, 4]] * self.ureg.m) q = self.q.copy() - q.__setitem__(Ellipsis, 1*self.ureg.m) - self.assertQuantityEqual(q, [[1,1],[1,1]]*self.ureg.m) + q[...] = 1 * self.ureg.m + self.assertQuantityEqual(q, [[1, 1], [1, 1]] * self.ureg.m) q = self.q.copy() - q[:] = 1*self.ureg.m - self.assertQuantityEqual(q, [[1,1],[1,1]]*self.ureg.m) + q[:] = 1 * self.ureg.m + self.assertQuantityEqual(q, [[1, 1], [1, 1]] * self.ureg.m) # check and see that dimensionless num bers work correctly - q = [0,1,2,3]*self.ureg.dimensionless + q = [0, 1, 2, 3] * self.ureg.dimensionless q[0] = 1 - self.assertQuantityEqual(q, np.asarray([1,1,2,3])) + self.assertQuantityEqual(q, np.asarray([1, 1, 2, 3])) q[0] = self.ureg.m/self.ureg.mm - self.assertQuantityEqual(q, np.asarray([1000, 1,2,3])) + self.assertQuantityEqual(q, np.asarray([1000, 1, 2, 3])) - q = [0.,1.,2.,3.] * self.ureg.m / self.ureg.mm - q[0] = 1. - self.assertQuantityEqual(q, [0.001,1,2,3]*self.ureg.m / self.ureg.mm) + q = [0., 1., 2., 3.] * self.ureg.m / self.ureg.mm + q[0] = 1.0 + self.assertQuantityEqual(q, [0.001, 1, 2, 3] * self.ureg.m / self.ureg.mm) def test_iterator(self): for q, v in zip(self.q.flatten(), [1, 2, 3, 4]): diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index 589aaa44a..9715c56fe 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -2,6 +2,7 @@ from __future__ import division, unicode_literals, print_function, absolute_import +from pint import DimensionalityError from pint.compat import np from pint.testsuite import QuantityTestCase, helpers @@ -40,15 +41,6 @@ def qm(self): def qi(self): return np.asarray([1 + 1j, 2 + 2j, 3 + 3j, 4 + 4j]) * self.ureg.m - def assertRaisesMsg(self, msg, ExcType, func, *args, **kwargs): - try: - func(*args, **kwargs) - self.assertFalse(True, msg='Exception {} not raised {}'.format(ExcType, msg)) - except ExcType as e: - pass - except Exception as e: - self.assertFalse(True, msg='{} not raised but {}\n{}'.format(ExcType, e, msg)) - def _test1(self, func, ok_with, raise_with=(), output_units='same', results=None, rtol=1e-6): """Test function that takes a single argument and returns Quantity. @@ -85,8 +77,10 @@ def _test1(self, func, ok_with, raise_with=(), output_units='same', results=Non self.assertQuantityAlmostEqual(qm, res, rtol=rtol, msg=err_msg) for x1 in raise_with: - self.assertRaisesMsg('At {} with {}'.format(func.__name__, x1), - ValueError, func, x1) + with self.assertRaises( + DimensionalityError, msg=f'At {func.__name__} with {x1}' + ): + func(x1) def _testn(self, func, ok_with, raise_with=(), results=None): """Test function that takes a single argument and returns and ndarray (not a Quantity) @@ -138,8 +132,8 @@ def _test1_2o(self, func, ok_with, raise_with=(), output_units=('same', 'same'), self.assertQuantityAlmostEqual(qm, re, rtol=rtol, msg=err_msg) for x1 in raise_with: - self.assertRaisesMsg('At {} with {}'.format(func.__name__, x1), - ValueError, func, x1) + with self.assertRaises(ValueError, msg=f'At {func.__name__} with {x1}'): + func(x1) def _test2(self, func, x1, ok_with, raise_with=(), output_units='same', rtol=1e-6, convert2=True): """Test function that takes two arguments and return a Quantity. @@ -185,8 +179,10 @@ def _test2(self, func, x1, ok_with, raise_with=(), output_units='same', rtol=1e- self.assertQuantityAlmostEqual(qm, res, rtol=rtol, msg=err_msg) for x2 in raise_with: - self.assertRaisesMsg('At {} with {} and {}'.format(func.__name__, x1, x2), - ValueError, func, x1, x2) + with self.assertRaises( + DimensionalityError, msg=f'At {func.__name__} with {x1} and {x2}' + ): + func(x1, x2) def _testn2(self, func, x1, ok_with, raise_with=()): """Test function that takes two arguments and return a ndarray. @@ -473,7 +469,8 @@ def test_arctan2(self): def test_hypot(self): self.assertTrue(np.hypot(3. * self.ureg.m, 4. * self.ureg.m) == 5. * self.ureg.m) self.assertTrue(np.hypot(3. * self.ureg.m, 400. * self.ureg.cm) == 5. * self.ureg.m) - self.assertRaises(ValueError, np.hypot, 1. * self.ureg.m, 2. * self.ureg.J) + with self.assertRaises(DimensionalityError): + np.hypot(1. * self.ureg.m, 2. * self.ureg.J) def test_sinh(self): self._test1(np.sinh, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 9c6bf5196..838e163f3 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -5,9 +5,11 @@ import copy import math -from pint.registry import (UnitRegistry, LazyRegistry) -from pint.util import (UnitsContainer, ParserHelper) -from pint import DimensionalityError, UndefinedUnitError +from pint import ( + DefinitionSyntaxError, DimensionalityError, RedefinitionError, UndefinedUnitError +) +from pint.registry import UnitRegistry, LazyRegistry +from pint.util import UnitsContainer, ParserHelper from pint.compat import u, np, string_types from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase @@ -174,7 +176,7 @@ def setup(self): def test_base(self): ureg = UnitRegistry(None) ureg.define('meter = [length]') - self.assertRaises(ValueError, ureg.define, 'meter = [length]') + self.assertRaises(DefinitionSyntaxError, ureg.define, 'meter = [length]') self.assertRaises(TypeError, ureg.define, list()) x = ureg.define('degC = kelvin; offset: 273.15') @@ -319,19 +321,19 @@ def func(x): self.assertRaises(ValueError, f1, 3.) self.assertEqual(f1(3. * ureg.centimeter), 0.03) self.assertEqual(f1(3. * ureg.meter), 3.) - self.assertRaises(ValueError, f1, 3 * ureg.second) + self.assertRaises(DimensionalityError, f1, 3 * ureg.second) f1b = ureg.wraps(None, [ureg.meter, ])(func) self.assertRaises(ValueError, f1b, 3.) self.assertEqual(f1b(3. * ureg.centimeter), 0.03) self.assertEqual(f1b(3. * ureg.meter), 3.) - self.assertRaises(ValueError, f1b, 3 * ureg.second) + self.assertRaises(DimensionalityError, f1b, 3 * ureg.second) f1 = ureg.wraps(None, 'meter')(func) self.assertRaises(ValueError, f1, 3.) self.assertEqual(f1(3. * ureg.centimeter), 0.03) self.assertEqual(f1(3. * ureg.meter), 3.) - self.assertRaises(ValueError, f1, 3 * ureg.second) + self.assertRaises(DimensionalityError, f1, 3 * ureg.second) f2 = ureg.wraps('centimeter', ['meter', ])(func) self.assertRaises(ValueError, f2, 3.) @@ -591,14 +593,14 @@ def test_lazy(self): def test_redefinition(self): d = self.ureg.define - self.assertRaises(ValueError, d, 'meter = [time]') - self.assertRaises(ValueError, d, 'kilo- = 1000') - self.assertRaises(ValueError, d, '[speed] = [length]') + self.assertRaises(RedefinitionError, d, 'meter = [time]') + self.assertRaises(RedefinitionError, d, 'kilo- = 1000') + self.assertRaises(RedefinitionError, d, '[speed] = [length]') # aliases self.assertIn('inch', self.ureg._units) - self.assertRaises(ValueError, d, 'bla = 3.2 meter = inch') - self.assertRaises(ValueError, d, 'myk- = 1000 = kilo-') + self.assertRaises(RedefinitionError, d, 'bla = 3.2 meter = inch') + self.assertRaises(RedefinitionError, d, 'myk- = 1000 = kilo-') class TestConvertWithOffset(QuantityTestCase, ParameterizedTestCase): From d0a123f85a026d3b55ffdc28fafdf7aa17f3ac04 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 11:55:44 +0000 Subject: [PATCH 129/612] Specific test for issue --- pint/testsuite/test_issues.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index eea527b2c..d5e5eff47 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -5,6 +5,7 @@ import math import copy import unittest +import pprint import sys from pint import DimensionalityError, UndefinedUnitError, UnitRegistry @@ -716,3 +717,13 @@ def test_issue876(self): assert (hash(-1) == hash(-3)) == (hash(a) == hash(c)) assert a != b assert a != c + + def test_issue912(self): + """pprint.pformat() invokes sorted() on large sets and frozensets and graciously + handles TypeError, but not generic Exceptions. This test will fail if + pint.DimensionalityError stops being a subclass of TypeError. + """ + ureg = UnitRegistry() + meter_units = ureg.get_compatible_units(ureg.meter) + hertz_units = ureg.get_compatible_units(ureg.hertz) + pprint.pformat(meter_units | hertz_units) From ec76aa1614bc05a793ca7cba354f834d42f97fdc Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 3 Dec 2019 16:45:00 -0600 Subject: [PATCH 130/612] Implement registry-based string preprocessing as list of callables --- pint/registry.py | 17 ++++++++++++++--- pint/testsuite/test_unit.py | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 4324c7d54..23b110855 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -101,6 +101,8 @@ class BaseRegistry(meta.with_metaclass(_Meta)): 'warn', 'raise', 'ignore' :type on_redefinition: str :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. + :param preprocessors: list of callables which are iteratively ran on any input expression + or unit string """ #: Map context prefix to function @@ -116,13 +118,15 @@ class BaseRegistry(meta.with_metaclass(_Meta)): 'parse_unit_name', 'parse_units', 'parse_expression', 'convert'] - def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', auto_reduce_dimensions=False): + def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', + auto_reduce_dimensions=False, preprocessors=None): self._register_parsers() self._init_dynamic_classes() self._filename = filename self.force_ndarray = force_ndarray + self.preprocessors = preprocessors or [] #: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore' self._on_redefinition = on_redefinition @@ -813,6 +817,8 @@ def parse_units(self, input_string, as_delta=None): :class:`pint.UndefinedUnitError` if a unit is not in the registry :class:`ValueError` if the expression is invalid. """ + for p in self.preprocessors: + input_string = p(input_string) units = self._parse_units(input_string, as_delta) return self.Unit(units) @@ -881,6 +887,8 @@ def parse_expression(self, input_string, case_sensitive=True, use_decimal=False, if not input_string: return self.Quantity(1) + for p in self.preprocessors: + input_string = p(input_string) input_string = string_preprocessor(input_string) gen = tokenizer(input_string) @@ -1514,19 +1522,22 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): 'warn', 'raise', 'ignore' :type on_redefinition: str :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. + :param preprocessors: list of callables which are iteratively ran on any input expression + or unit string """ def __init__(self, filename='', force_ndarray=False, default_as_delta=True, autoconvert_offset_to_baseunit=False, on_redefinition='warn', system=None, - auto_reduce_dimensions=False): + auto_reduce_dimensions=False, preprocessors=None): super(UnitRegistry, self).__init__(filename=filename, force_ndarray=force_ndarray, on_redefinition=on_redefinition, default_as_delta=default_as_delta, autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, system=system, - auto_reduce_dimensions=auto_reduce_dimensions) + auto_reduce_dimensions=auto_reduce_dimensions, + preprocessors=preprocessors) def pi_theorem(self, quantities): """Builds dimensionless quantities using the Buckingham π theorem diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 9c6bf5196..087962743 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -3,7 +3,9 @@ from __future__ import division, unicode_literals, print_function, absolute_import import copy +import functools import math +import re from pint.registry import (UnitRegistry, LazyRegistry) from pint.util import (UnitsContainer, ParserHelper) @@ -274,6 +276,28 @@ def test_as_delta(self): self.assertEqual(parse('kelvin*meter', as_delta=True), UnitsContainer(kelvin=1, meter=1)) self.assertEqual(parse('kelvin*meter', as_delta=False), UnitsContainer(kelvin=1, meter=1)) + def test_parse_expression_with_preprocessor(self): + # Add parsing of UDUNITS-style power + self.ureg.preprocessors.append(functools.partial( + re.sub, r'(?<=[A-Za-z])(?![A-Za-z])(? Date: Wed, 4 Dec 2019 12:47:44 +0000 Subject: [PATCH 131/612] Thorough str tests for all exceptions --- pint/context.py | 4 +- pint/errors.py | 75 +++++++++++++++----------------- pint/registry.py | 2 +- pint/systems.py | 7 ++- pint/testsuite/test_errors.py | 82 ++++++++++++++++++++++++++++++++--- pint/testsuite/test_unit.py | 8 ---- 6 files changed, 121 insertions(+), 57 deletions(-) diff --git a/pint/context.py b/pint/context.py index 3a3031628..7b48cfe6a 100644 --- a/pint/context.py +++ b/pint/context.py @@ -165,7 +165,9 @@ def to_num(val): if defaults: missing_pars = set(defaults.keys()).difference(set(names)) if missing_pars: - raise DefinitionSyntaxError('Context parameters {} not found in any equation.'.format(missing_pars)) + raise DefinitionSyntaxError( + f'Context parameters {missing_pars} not found in any equation' + ) return ctx diff --git a/pint/errors.py b/pint/errors.py index 1b3730645..83c974027 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -10,57 +10,62 @@ """ -class DefinitionSyntaxError(SyntaxError): +class FilenameMixin: + def __init__(self, filename=None, lineno=None): + self.filename = filename + self.lineno = lineno + + def __str__(self): + if self.filename and self.lineno is not None: + return f"While opening {self.filename}, in line {self.lineno}: " + elif self.filename: + return f"While opening {self.filename}: " + elif self.lineno is not None: + return f"In line {self.lineno}: " + else: + return "" + + +class DefinitionSyntaxError(SyntaxError, FilenameMixin): """Raised when a textual definition has a syntax error. """ - def __init__(self, msg, filename=None, lineno=None): - super().__init__(msg) - self.filename = None - self.lineno = None + def __init__(self, msg, *, filename=None, lineno=None): + SyntaxError.__init__(self, msg) + FilenameMixin.__init__(self, filename, lineno) def __str__(self): - return f"While opening {self.filename}, in line {self.lineno}: {self.args[0]}" + return f"{FilenameMixin.__str__(self)}{self.args[0]}" -class RedefinitionError(ValueError): +class RedefinitionError(ValueError, FilenameMixin): """Raised when a unit or prefix is redefined. """ - def __init__(self, name, definition_type): - super().__init__() + def __init__(self, name, definition_type, filename=None, lineno=None): + ValueError().__init__(self) + FilenameMixin.__init__(self, filename, lineno) self.name = name self.definition_type = definition_type - self.filename = None - self.lineno = None def __str__(self): msg = f"Cannot redefine '{self.name}' ({self.definition_type})" - if self.filename: - return f"While opening {self.filename}, in line {self.lineno}: {msg}" - return msg + return FilenameMixin.__str__(self) + msg class UndefinedUnitError(AttributeError): """Raised when the units are not defined in the unit registry. """ - def __init__(self, unit_names): - super().__init__() - self.unit_names = unit_names + def __init__(self, *unit_names): + if len(unit_names) == 1 and not isinstance(unit_names[0], str): + unit_names = unit_names[0] + super().__init__(*unit_names) def __str__(self): - mess = "'{}' is not defined in the unit registry" - mess_plural = "'{}' are not defined in the unit registry" - if isinstance(self.unit_names, str): - return mess.format(self.unit_names) - elif isinstance(self.unit_names, (list, tuple)) and len(self.unit_names) == 1: - return mess.format(self.unit_names[0]) - elif isinstance(self.unit_names, set) and len(self.unit_names) == 1: - uname = list(self.unit_names)[0] - return mess.format(uname) - else: - return mess_plural.format(self.unit_names) + if len(self.args) == 1: + return f"'{self.args[0]}' is not defined in the unit registry" + return f"{self.args} are not defined in the unit registry" class PintTypeError(TypeError): @@ -71,7 +76,7 @@ class DimensionalityError(PintTypeError): """Raised when trying to convert between incompatible units. """ - def __init__(self, units1, units2, dim1=None, dim2=None, *, extra_msg=""): + def __init__(self, units1, units2, dim1="", dim2="", *, extra_msg=""): super().__init__() self.units1 = units1 self.units2 = units2 @@ -97,20 +102,12 @@ class OffsetUnitCalculusError(PintTypeError): """Raised on ambiguous operations with offset units. """ - def __init__(self, units1, units2="", *, extra_msg=""): - super().__init__() - self.units1 = units1 - self.units2 = units2 - self.extra_msg = extra_msg - def __str__(self): - msg = ( + return ( "Ambiguous operation with offset unit (%s)." - % ", ".join(["%s" % u for u in [self.units1, self.units2] if u]) + % ", ".join(str(u) for u in self.args) + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance." - + self.extra_msg ) - return msg.format(self.units1, self.units2) class UnitStrippedWarning(UserWarning): diff --git a/pint/registry.py b/pint/registry.py index 97de8e6b4..edc516877 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1080,7 +1080,7 @@ def _parse_context(self, ifile): self.add_context(Context.from_lines(ifile.block_iter(), self.get_dimensionality)) except KeyError as e: - raise DefinitionSyntaxError('unknown dimension {} in context'.format(str(e))) + raise DefinitionSyntaxError(f'unknown dimension {e} in context') def add_context(self, context): """Add a context object to the registry. diff --git a/pint/systems.py b/pint/systems.py index 3c5666018..9e965a7cc 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -207,8 +207,11 @@ def from_lines(cls, lines, define_func): # Is a definition definition = Definition.from_string(line) if not isinstance(definition, UnitDefinition): - raise DefinitionSyntaxError('Only UnitDefinition are valid inside _used_groups, ' - 'not %s' % type(definition), lineno=lineno) + raise DefinitionSyntaxError( + 'Only UnitDefinition are valid inside _used_groups, not ' + + str(definition), + lineno=lineno + ) try: define_func(definition) diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index df053b5e4..32bb433ef 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -2,19 +2,89 @@ from __future__ import division, unicode_literals, print_function, absolute_import -from pint import DimensionalityError, UndefinedUnitError +from pint import ( + DefinitionSyntaxError, + DimensionalityError, + Quantity, + OffsetUnitCalculusError, + RedefinitionError, + UndefinedUnitError, +) from pint.testsuite import BaseTestCase class TestErrors(BaseTestCase): + def test_definition_syntax_error(self): + ex = DefinitionSyntaxError("foo") + self.assertEqual(str(ex), "foo") - def test_errors(self): - x = ('meter', ) + # filename and lineno can be attached after init + ex.filename = "a.txt" + ex.lineno = 123 + self.assertEqual(str(ex), "While opening a.txt, in line 123: foo") + + ex = DefinitionSyntaxError("foo", lineno=123) + self.assertEqual(str(ex), "In line 123: foo") + + ex = DefinitionSyntaxError("foo", filename="a.txt") + self.assertEqual(str(ex), "While opening a.txt: foo") + + ex = DefinitionSyntaxError("foo", filename="a.txt", lineno=123) + self.assertEqual(str(ex), "While opening a.txt, in line 123: foo") + + def test_redefinition_error(self): + ex = RedefinitionError("foo", "bar") + self.assertEqual(str(ex), "Cannot redefine 'foo' (bar)") + + # filename and lineno can be attached after init + ex.filename = "a.txt" + ex.lineno = 123 + self.assertEqual( + str(ex), "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" + ) + + ex = RedefinitionError("foo", "bar", lineno=123) + self.assertEqual(str(ex), "In line 123: Cannot redefine 'foo' (bar)") + + ex = RedefinitionError("foo", "bar", filename="a.txt") + self.assertEqual(str(ex), "While opening a.txt: Cannot redefine 'foo' (bar)") + + ex = RedefinitionError("foo", "bar", filename="a.txt", lineno=123) + self.assertEqual( + str(ex), "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" + ) + + def test_undefined_unit_error(self): + x = ("meter",) msg = "'meter' is not defined in the unit registry" self.assertEqual(str(UndefinedUnitError(x)), msg) self.assertEqual(str(UndefinedUnitError(list(x))), msg) self.assertEqual(str(UndefinedUnitError(set(x))), msg) - msg = "Cannot convert from 'a' (c) to 'b' (d)msg" - ex = DimensionalityError('a', 'b', 'c', 'd', extra_msg='msg') - self.assertEqual(str(ex), msg) + def test_undefined_unit_error_multi(self): + x = ("meter", "kg") + msg = "('meter', 'kg') are not defined in the unit registry" + self.assertEqual(str(UndefinedUnitError(x)), msg) + self.assertEqual(str(UndefinedUnitError(list(x))), msg) + + def test_dimensionality_error(self): + ex = DimensionalityError("a", "b") + self.assertEqual(str(ex), "Cannot convert from 'a' to 'b'") + ex = DimensionalityError("a", "b", "c") + self.assertEqual(str(ex), "Cannot convert from 'a' (c) to 'b' ()") + ex = DimensionalityError("a", "b", "c", "d", extra_msg=": msg") + self.assertEqual(str(ex), "Cannot convert from 'a' (c) to 'b' (d): msg") + + def test_offset_unit_calculus_error(self): + ex = OffsetUnitCalculusError(Quantity("1 kg")._units) + self.assertEqual( + str(ex), + "Ambiguous operation with offset unit (kilogram). See " + "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.", + ) + ex = OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units) + self.assertEqual( + str(ex), + "Ambiguous operation with offset unit (kilogram, second). See " + "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.", + ) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 838e163f3..89bee797e 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -230,14 +230,6 @@ def test_parse_complex(self): self.assertEqual(self.ureg.parse_expression('kilometre'), self.Q_(1, UnitsContainer(kilometer=1.))) self.assertEqual(self.ureg.parse_expression('kilometres'), self.Q_(1, UnitsContainer(kilometer=1.))) - def test_str_errors(self): - self.assertEqual(str(UndefinedUnitError('rabbits')), "'{0!s}' is not defined in the unit registry".format('rabbits')) - self.assertEqual(str(UndefinedUnitError(('rabbits', 'horses'))), "'{0!s}' are not defined in the unit registry".format(('rabbits', 'horses'))) - self.assertEqual(u(str(DimensionalityError('meter', 'second'))), - "Cannot convert from 'meter' to 'second'") - self.assertEqual(str(DimensionalityError('meter', 'second', 'length', 'time')), - "Cannot convert from 'meter' (length) to 'second' (time)") - def test_parse_mul_div(self): self.assertEqual(self.ureg.parse_expression('meter*meter'), self.Q_(1, UnitsContainer(meter=2.))) self.assertEqual(self.ureg.parse_expression('meter**2'), self.Q_(1, UnitsContainer(meter=2.))) From bc4eaf121fc68dc142d10f54515886d2f2c5dffb Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 14:39:53 +0000 Subject: [PATCH 132/612] pickle exceptions --- pint/errors.py | 67 ++++++++++++++++++++--------------- pint/testsuite/test_errors.py | 32 ++++++++++++++++- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/pint/errors.py b/pint/errors.py index 83c974027..82db18ea4 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -10,47 +10,55 @@ """ -class FilenameMixin: - def __init__(self, filename=None, lineno=None): - self.filename = filename - self.lineno = lineno - - def __str__(self): - if self.filename and self.lineno is not None: - return f"While opening {self.filename}, in line {self.lineno}: " - elif self.filename: - return f"While opening {self.filename}: " - elif self.lineno is not None: - return f"In line {self.lineno}: " - else: - return "" - - -class DefinitionSyntaxError(SyntaxError, FilenameMixin): +def _file_prefix(filename=None, lineno=None): + if filename and lineno is not None: + return f"While opening {filename}, in line {lineno}: " + elif filename: + return f"While opening {filename}: " + elif lineno is not None: + return f"In line {lineno}: " + else: + return "" + + +class DefinitionSyntaxError(SyntaxError): """Raised when a textual definition has a syntax error. """ def __init__(self, msg, *, filename=None, lineno=None): - SyntaxError.__init__(self, msg) - FilenameMixin.__init__(self, filename, lineno) + super().__init__(msg) + self.filename = filename + self.lineno = lineno def __str__(self): - return f"{FilenameMixin.__str__(self)}{self.args[0]}" + return _file_prefix(self.filename, self.lineno) + str(self.args[0]) + + @property + def __dict__(self): + # For some reason, SyntaxError.__dict__ is always empty. + # There are no __slots__ either. This messes up pickling and deepcopy, as well + # as any other Python library that expects sane behaviour. + return {"filename": self.filename, "lineno": self.lineno} + def __reduce__(self): + return DefinitionSyntaxError, self.args, self.__dict__ -class RedefinitionError(ValueError, FilenameMixin): + +class RedefinitionError(ValueError): """Raised when a unit or prefix is redefined. """ - def __init__(self, name, definition_type, filename=None, lineno=None): - ValueError().__init__(self) - FilenameMixin.__init__(self, filename, lineno) - self.name = name - self.definition_type = definition_type + def __init__(self, name, definition_type, *, filename=None, lineno=None): + super().__init__(name, definition_type) + self.filename = filename + self.lineno = lineno def __str__(self): - msg = f"Cannot redefine '{self.name}' ({self.definition_type})" - return FilenameMixin.__str__(self) + msg + msg = f"Cannot redefine '{self.args[0]}' ({self.args[1]})" + return _file_prefix(self.filename, self.lineno) + msg + + def __reduce__(self): + return RedefinitionError, self.args, self.__dict__ class UndefinedUnitError(AttributeError): @@ -97,6 +105,9 @@ def __str__(self): f"'{self.units2}'{dim2}{self.extra_msg}" ) + def __reduce__(self): + return TypeError.__new__, (DimensionalityError,), self.__dict__ + class OffsetUnitCalculusError(PintTypeError): """Raised on ambiguous operations with offset units. diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index 32bb433ef..be0cb56f3 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import division, unicode_literals, print_function, absolute_import +import pickle from pint import ( DefinitionSyntaxError, @@ -9,6 +9,7 @@ OffsetUnitCalculusError, RedefinitionError, UndefinedUnitError, + UnitRegistry, ) from pint.testsuite import BaseTestCase @@ -88,3 +89,32 @@ def test_offset_unit_calculus_error(self): "Ambiguous operation with offset unit (kilogram, second). See " "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.", ) + + def test_pickle_definition_syntax_error(self): + # OffsetUnitCalculusError raised from a custom ureg must be pickleable even if + # the ureg is not the application ureg + # pickled + ureg = UnitRegistry(filename=None) + ureg.define("foo = [bar]") + ureg.define("bar = 2 foo") + pik = pickle.dumps(ureg.Quantity("1 foo")) + with self.assertRaises(UndefinedUnitError): + pickle.loads(pik) + q1 = ureg.Quantity("1 foo") + q2 = ureg.Quantity("1 bar") + + for ex in [ + DefinitionSyntaxError("foo", filename="a.txt", lineno=123), + RedefinitionError("foo", "bar"), + UndefinedUnitError("meter"), + DimensionalityError("a", "b", "c", "d", extra_msg=": msg"), + OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units), + OffsetUnitCalculusError(q1._units, q2._units), + ]: + with self.subTest(etype=type(ex)): + # assert False, ex.__reduce__() + ex2 = pickle.loads(pickle.dumps(ex)) + assert type(ex) is type(ex2) + self.assertEqual(ex.args, ex2.args) + self.assertEqual(ex.__dict__, ex2.__dict__) + self.assertEqual(str(ex), str(ex2)) From 2f85679c7facfbdbf0a7b5b92fb4e2fe1284958e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 14:55:22 +0000 Subject: [PATCH 133/612] Fix tests regression --- pint/registry.py | 4 +++- pint/testsuite/test_unit.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index edc516877..a8f636892 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -269,7 +269,9 @@ def _define(self, definition): for dimension in definition.reference.keys(): if dimension in self._dimensions: if dimension != '[]': - raise DefinitionSyntaxError('only one unit per dimension can be a base unit.') + raise DefinitionSyntaxError( + 'Only one unit per dimension can be a base unit' + ) continue self.define(DimensionDefinition(dimension, '', (), None, is_base=True)) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 89bee797e..c7e012aa3 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -585,7 +585,8 @@ def test_lazy(self): def test_redefinition(self): d = self.ureg.define - self.assertRaises(RedefinitionError, d, 'meter = [time]') + self.assertRaises(DefinitionSyntaxError, d, 'meter = [time]') + self.assertRaises(RedefinitionError, d, 'meter = [newdim]') self.assertRaises(RedefinitionError, d, 'kilo- = 1000') self.assertRaises(RedefinitionError, d, '[speed] = [length]') From 2ba63cc7b6bf7e5e34df40437dde393383fbbd8e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 15:42:18 +0000 Subject: [PATCH 134/612] CPython caches the output of __hash__ - don't implement it on mutable objects --- pint/context.py | 11 ++++++++--- pint/registry.py | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pint/context.py b/pint/context.py index 3e43eabcc..04ab29757 100644 --- a/pint/context.py +++ b/pint/context.py @@ -224,7 +224,7 @@ def remove_contexts(self, n): @property def defaults(self): if self: - return list(self.maps[0].values())[0].defaults + return next(iter(self.maps[0].values())).defaults return {} @property @@ -245,5 +245,10 @@ def transform(self, src, dst, registry, value): """ return self[(src, dst)].transform(src, dst, registry, value) - def __hash__(self): - return hash(tuple(self._contexts)) + def context_ids(self): + """Hashable unique identifier of the current contents of the context chain. This + is not implemented as ``__hash__`` as doing so on a mutable object can provoke + unpredictable behaviour, as interpreter-level optimizations can cache the output + of ``__hash__``. + """ + return tuple(id(ctx) for ctx in self._contexts) diff --git a/pint/registry.py b/pint/registry.py index b9ff71c10..62a40d654 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1129,11 +1129,12 @@ def remove_context(self, name_or_alias): return context def _build_cache(self): + key = self._active_ctx.context_ids() try: - self._cache = self._caches[self._active_ctx] + self._cache = self._caches[key] except KeyError: super()._build_cache() - self._caches[self._active_ctx] = self._cache + self._caches[key] = self._cache def enable_contexts(self, *names_or_contexts, **kwargs): """Enable contexts provided by name or by object. From 983b9a2e09db1214bca469ad69689da2bb36888b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 16:14:11 +0000 Subject: [PATCH 135/612] Remove tokenize backport --- pint/babel_names.py | 4 +- pint/compat/__init__.py | 29 +- pint/compat/tokenize.py | 641 ----------------------------------- pint/testsuite/helpers.py | 6 +- pint/testsuite/test_babel.py | 2 +- 5 files changed, 11 insertions(+), 671 deletions(-) delete mode 100644 pint/compat/tokenize.py diff --git a/pint/babel_names.py b/pint/babel_names.py index 25037ae93..b68e378ea 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -7,7 +7,7 @@ :license: BSD, see LICENSE for more details. """ -from pint.compat import HAS_PROPER_BABEL +from pint.compat import HAS_BABEL _babel_units = dict( standard_gravity='acceleration-g-force', @@ -137,7 +137,7 @@ radian='angle-radian', ) -if not HAS_PROPER_BABEL: +if not HAS_BABEL: _babel_units = {} _babel_systems = dict( diff --git a/pint/compat/__init__.py b/pint/compat/__init__.py index 255dc9c1d..6c3221af0 100644 --- a/pint/compat/__init__.py +++ b/pint/compat/__init__.py @@ -8,19 +8,15 @@ :copyright: 2013 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ - +import tokenize from io import BytesIO from numbers import Number from decimal import Decimal -from . import tokenize - -ENCODING_TOKEN = tokenize.ENCODING - def tokenizer(input_string): for tokinfo in tokenize.tokenize(BytesIO(input_string.encode('utf-8')).readline): - if tokinfo.type == ENCODING_TOKEN: + if tokinfo.type == tokenize.ENCODING: continue yield tokinfo @@ -75,24 +71,9 @@ def _to_magnitude(value, force_ndarray=False): try: from babel import Locale as Loc from babel import units as babel_units - HAS_BABEL = True - HAS_PROPER_BABEL = hasattr(babel_units, 'format_unit') + HAS_BABEL = hasattr(babel_units, 'format_unit') except ImportError: - HAS_PROPER_BABEL = HAS_BABEL = False + HAS_BABEL = False -if not HAS_PROPER_BABEL: +if not HAS_BABEL: Loc = babel_units = None - -try: - import pandas as pd - HAS_PANDAS = True - # pin Pandas version for now - HAS_PROPER_PANDAS = pd.__version__.startswith("0.24.0.dev0+625.gbdb7a16") -except ImportError: - HAS_PROPER_PANDAS = HAS_PANDAS = False - -try: - import pytest - HAS_PYTEST = True -except ImportError: - HAS_PYTEST = False diff --git a/pint/compat/tokenize.py b/pint/compat/tokenize.py deleted file mode 100644 index a285a6b36..000000000 --- a/pint/compat/tokenize.py +++ /dev/null @@ -1,641 +0,0 @@ -"""Tokenization help for Python programs. - -tokenize(readline) is a generator that breaks a stream of bytes into -Python tokens. It decodes the bytes according to PEP-0263 for -determining source file encoding. - -It accepts a readline-like method which is called repeatedly to get the -next line of input (or b"" for EOF). It generates 5-tuples with these -members: - - the token type (see token.py) - the token (a string) - the starting (row, column) indices of the token (a 2-tuple of ints) - the ending (row, column) indices of the token (a 2-tuple of ints) - the original line (string) - -It is designed to match the working of the Python tokenizer exactly, except -that it produces COMMENT tokens for comments and gives type OP for all -operators. Additionally, all token lists start with an ENCODING token -which tells you which encoding was used to decode the bytes stream. -""" - -__author__ = 'Ka-Ping Yee ' -__credits__ = ('GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, ' - 'Skip Montanaro, Raymond Hettinger, Trent Nelson, ' - 'Michael Foord') - -from codecs import lookup, BOM_UTF8 -import collections -import io -from io import TextIOWrapper -from itertools import chain -import re -from token import * - - -cookie_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)', re.ASCII) -blank_re = re.compile(br'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII) - -COMMENT = N_TOKENS -tok_name[COMMENT] = 'COMMENT' -NL = N_TOKENS + 1 -tok_name[NL] = 'NL' -ENCODING = N_TOKENS + 2 -tok_name[ENCODING] = 'ENCODING' -N_TOKENS += 3 -EXACT_TOKEN_TYPES = { - '(': LPAR, - ')': RPAR, - '[': LSQB, - ']': RSQB, - ':': COLON, - ',': COMMA, - ';': SEMI, - '+': PLUS, - '-': MINUS, - '*': STAR, - '/': SLASH, - '|': VBAR, - '&': AMPER, - '<': LESS, - '>': GREATER, - '=': EQUAL, - '.': DOT, - '%': PERCENT, - '{': LBRACE, - '}': RBRACE, - '==': EQEQUAL, - '!=': NOTEQUAL, - '<=': LESSEQUAL, - '>=': GREATEREQUAL, - '~': TILDE, - '^': CIRCUMFLEX, - '<<': LEFTSHIFT, - '>>': RIGHTSHIFT, - '**': DOUBLESTAR, - '+=': PLUSEQUAL, - '-=': MINEQUAL, - '*=': STAREQUAL, - '/=': SLASHEQUAL, - '%=': PERCENTEQUAL, - '&=': AMPEREQUAL, - '|=': VBAREQUAL, - '^=': CIRCUMFLEXEQUAL, - '<<=': LEFTSHIFTEQUAL, - '>>=': RIGHTSHIFTEQUAL, - '**=': DOUBLESTAREQUAL, - '//': DOUBLESLASH, - '//=': DOUBLESLASHEQUAL, - '@': AT -} - - -class TokenInfo(collections.namedtuple('TokenInfo', 'type string start end line')): - def __repr__(self): - annotated_type = '%d (%s)' % (self.type, tok_name[self.type]) - return ('TokenInfo(type=%s, string=%r, start=%r, end=%r, line=%r)' % - self._replace(type=annotated_type)) - - @property - def exact_type(self): - if self.type == OP and self.string in EXACT_TOKEN_TYPES: - return EXACT_TOKEN_TYPES[self.string] - else: - return self.type - - -def group(*choices): - return '(' + '|'.join(choices) + ')' - - -def any(*choices): - return group(*choices) + '*' - - -def maybe(*choices): - return group(*choices) + '?' - - -# Note: we use unicode matching for names ("\w") but ascii matching for -# number literals. -Whitespace = r'[ \f\t]*' -Comment = r'#[^\r\n]*' -Ignore = Whitespace + any(r'\\\r?\n' + Whitespace) + maybe(Comment) -Name = r'\w+' - -Hexnumber = r'0[xX][0-9a-fA-F]+' -Binnumber = r'0[bB][01]+' -Octnumber = r'0[oO][0-7]+' -Decnumber = r'(?:0+|[1-9][0-9]*)' -Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber) -Exponent = r'[eE][-+]?[0-9]+' -Pointfloat = group(r'[0-9]+\.[0-9]*', r'\.[0-9]+') + maybe(Exponent) -Expfloat = r'[0-9]+' + Exponent -Floatnumber = group(Pointfloat, Expfloat) -Imagnumber = group(r'[0-9]+[jJ]', Floatnumber + r'[jJ]') -Number = group(Imagnumber, Floatnumber, Intnumber) - -StringPrefix = r'(?:[bB][rR]?|[rR][bB]?|[uU])?' - -# Tail end of ' string. -Single = r"[^'\\]*(?:\\.[^'\\]*)*'" -# Tail end of " string. -Double = r'[^"\\]*(?:\\.[^"\\]*)*"' -# Tail end of ''' string. -Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''" -# Tail end of """ string. -Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' -Triple = group(StringPrefix + "'''", StringPrefix + '"""') -# Single-line ' or " string. -String = group(StringPrefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*'", - StringPrefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*"') - -# Because of leftmost-then-longest match semantics, be sure to put the -# longest operators first (e.g., if = came before ==, == would get -# recognized as two instances of =). -Operator = group(r"\*\*=?", r">>=?", r"<<=?", r"!=", - r"//=?", r"->", - r"[+\-*/%&|^=<>]=?", - r"~") - -Bracket = '[][(){}]' -Special = group(r'\r?\n', r'\.\.\.', r'[:;.,@]') -Funny = group(Operator, Bracket, Special) - -PlainToken = group(Number, Funny, String, Name) -Token = Ignore + PlainToken - -# First (or only) line of ' or " string. -ContStr = group(StringPrefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*" + - group("'", r'\\\r?\n'), - StringPrefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + - group('"', r'\\\r?\n')) -PseudoExtras = group(r'\\\r?\n|\Z', Comment, Triple) -PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name) - - -def _compile(expr): - return re.compile(expr, re.UNICODE) - - -endpats = {"'": Single, '"': Double, - "'''": Single3, '"""': Double3, - "r'''": Single3, 'r"""': Double3, - "b'''": Single3, 'b"""': Double3, - "R'''": Single3, 'R"""': Double3, - "B'''": Single3, 'B"""': Double3, - "br'''": Single3, 'br"""': Double3, - "bR'''": Single3, 'bR"""': Double3, - "Br'''": Single3, 'Br"""': Double3, - "BR'''": Single3, 'BR"""': Double3, - "rb'''": Single3, 'rb"""': Double3, - "Rb'''": Single3, 'Rb"""': Double3, - "rB'''": Single3, 'rB"""': Double3, - "RB'''": Single3, 'RB"""': Double3, - "u'''": Single3, 'u"""': Double3, - "R'''": Single3, 'R"""': Double3, - "U'''": Single3, 'U"""': Double3, - 'r': None, 'R': None, 'b': None, 'B': None, - 'u': None, 'U': None} - -triple_quoted = {} -for t in ("'''", '"""', - "r'''", 'r"""', "R'''", 'R"""', - "b'''", 'b"""', "B'''", 'B"""', - "br'''", 'br"""', "Br'''", 'Br"""', - "bR'''", 'bR"""', "BR'''", 'BR"""', - "rb'''", 'rb"""', "rB'''", 'rB"""', - "Rb'''", 'Rb"""', "RB'''", 'RB"""', - "u'''", 'u"""', "U'''", 'U"""', - ): - triple_quoted[t] = t -single_quoted = {} -for t in ("'", '"', - "r'", 'r"', "R'", 'R"', - "b'", 'b"', "B'", 'B"', - "br'", 'br"', "Br'", 'Br"', - "bR'", 'bR"', "BR'", 'BR"' , - "rb'", 'rb"', "rB'", 'rB"', - "Rb'", 'Rb"', "RB'", 'RB"' , - "u'", 'u"', "U'", 'U"', - ): - single_quoted[t] = t - -tabsize = 8 - - -class TokenError(Exception): - pass - - -class StopTokenizing(Exception): - pass - - -class Untokenizer: - - def __init__(self): - self.tokens = [] - self.prev_row = 1 - self.prev_col = 0 - self.encoding = None - - def add_whitespace(self, start): - row, col = start - if row < self.prev_row or row == self.prev_row and col < self.prev_col: - raise ValueError("start ({},{}) precedes previous end ({},{})" - .format(row, col, self.prev_row, self.prev_col)) - row_offset = row - self.prev_row - if row_offset: - self.tokens.append("\\\n" * row_offset) - self.prev_col = 0 - col_offset = col - self.prev_col - if col_offset: - self.tokens.append(" " * col_offset) - - def untokenize(self, iterable): - it = iter(iterable) - for t in it: - if len(t) == 2: - self.compat(t, it) - break - tok_type, token, start, end, line = t - if tok_type == ENCODING: - self.encoding = token - continue - if tok_type == ENDMARKER: - break - self.add_whitespace(start) - self.tokens.append(token) - self.prev_row, self.prev_col = end - if tok_type in (NEWLINE, NL): - self.prev_row += 1 - self.prev_col = 0 - return "".join(self.tokens) - - def compat(self, token, iterable): - indents = [] - toks_append = self.tokens.append - startline = token[0] in (NEWLINE, NL) - prevstring = False - - for tok in chain([token], iterable): - toknum, tokval = tok[:2] - if toknum == ENCODING: - self.encoding = tokval - continue - - if toknum in (NAME, NUMBER): - tokval += ' ' - - # Insert a space between two consecutive strings - if toknum == STRING: - if prevstring: - tokval = ' ' + tokval - prevstring = True - else: - prevstring = False - - if toknum == INDENT: - indents.append(tokval) - continue - elif toknum == DEDENT: - indents.pop() - continue - elif toknum in (NEWLINE, NL): - startline = True - elif startline and indents: - toks_append(indents[-1]) - startline = False - toks_append(tokval) - - -def untokenize(iterable): - """Transform tokens back into Python source code. - It returns a bytes object, encoded using the ENCODING - token, which is the first token sequence output by tokenize. - - Each element returned by the iterable must be a token sequence - with at least two elements, a token number and token value. If - only two tokens are passed, the resulting output is poor. - - Round-trip invariant for full input: - Untokenized source will match input source exactly - - Round-trip invariant for limited intput: - # Output bytes will tokenize the back to the input - t1 = [tok[:2] for tok in tokenize(f.readline)] - newcode = untokenize(t1) - readline = BytesIO(newcode).readline - t2 = [tok[:2] for tok in tokenize(readline)] - assert t1 == t2 - """ - ut = Untokenizer() - out = ut.untokenize(iterable) - if ut.encoding is not None: - out = out.encode(ut.encoding) - return out - - -def _get_normal_name(orig_enc): - """Imitates get_normal_name in tokenizer.c.""" - # Only care about the first 12 characters. - enc = orig_enc[:12].lower().replace("_", "-") - if enc == "utf-8" or enc.startswith("utf-8-"): - return "utf-8" - if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or \ - enc.startswith(("latin-1-", "iso-8859-1-", "iso-latin-1-")): - return "iso-8859-1" - return orig_enc - - -def detect_encoding(readline): - """ - The detect_encoding() function is used to detect the encoding that should - be used to decode a Python source file. It requires one argument, readline, - in the same way as the tokenize() generator. - - It will call readline a maximum of twice, and return the encoding used - (as a string) and a list of any lines (left as bytes) it has read in. - - It detects the encoding from the presence of a utf-8 bom or an encoding - cookie as specified in pep-0263. If both a bom and a cookie are present, - but disagree, a SyntaxError will be raised. If the encoding cookie is an - invalid charset, raise a SyntaxError. Note that if a utf-8 bom is found, - 'utf-8-sig' is returned. - - If no encoding is specified, then the default of 'utf-8' will be returned. - """ - try: - filename = readline.__self__.name - except AttributeError: - filename = None - bom_found = False - encoding = None - default = 'utf-8' - - def read_or_stop(): - try: - return readline() - except StopIteration: - return b'' - - def find_cookie(line): - try: - # Decode as UTF-8. Either the line is an encoding declaration, - # in which case it should be pure ASCII, or it must be UTF-8 - # per default encoding. - line_string = line.decode('utf-8') - except UnicodeDecodeError: - msg = "invalid or missing encoding declaration" - if filename is not None: - msg = '{} for {!r}'.format(msg, filename) - raise SyntaxError(msg) - - match = cookie_re.match(line_string) - if not match: - return None - encoding = _get_normal_name(match.group(1)) - try: - codec = lookup(encoding) - except LookupError: - # This behaviour mimics the Python interpreter - if filename is None: - msg = "unknown encoding: " + encoding - else: - msg = "unknown encoding for {!r}: {}".format(filename, - encoding) - raise SyntaxError(msg) - - if bom_found: - if encoding != 'utf-8': - # This behaviour mimics the Python interpreter - if filename is None: - msg = 'encoding problem: utf-8' - else: - msg = 'encoding problem for {!r}: utf-8'.format(filename) - raise SyntaxError(msg) - encoding += '-sig' - return encoding - - first = read_or_stop() - if first.startswith(BOM_UTF8): - bom_found = True - first = first[3:] - default = 'utf-8-sig' - if not first: - return default, [] - - encoding = find_cookie(first) - if encoding: - return encoding, [first] - if not blank_re.match(first): - return default, [first] - - second = read_or_stop() - if not second: - return default, [first] - - encoding = find_cookie(second) - if encoding: - return encoding, [first, second] - - return default, [first, second] - - -def open(filename): - """Open a file in read only mode using the encoding detected by - detect_encoding(). - """ - buffer = io.open(filename, 'rb') - encoding, lines = detect_encoding(buffer.readline) - buffer.seek(0) - text = TextIOWrapper(buffer, encoding, line_buffering=True) - text.mode = 'r' - return text - - -def tokenize(readline): - """ - The tokenize() generator requires one argment, readline, which - must be a callable object which provides the same interface as the - readline() method of built-in file objects. Each call to the function - should return one line of input as bytes. Alternately, readline - can be a callable function terminating with StopIteration:: - - readline = open(myfile, 'rb').__next__ # Example of alternate readline - - The generator produces 5-tuples with these members: the token type; the - token string; a 2-tuple (srow, scol) of ints specifying the row and - column where the token begins in the source; a 2-tuple (erow, ecol) of - ints specifying the row and column where the token ends in the source; - and the line on which the token was found. The line passed is the - logical line; continuation lines are included. - - The first token sequence will always be an ENCODING token - which tells you which encoding was used to decode the bytes stream. - """ - # This import is here to avoid problems when the itertools module is not - # built yet and tokenize is imported. - from itertools import chain, repeat - encoding, consumed = detect_encoding(readline) - rl_gen = iter(readline, b"") - empty = repeat(b"") - - try: - return _tokenize(chain(consumed, rl_gen, empty).__next__, encoding) - except AttributeError: - return _tokenize(chain(consumed, rl_gen, empty).__next__, encoding) - - -def _tokenize(readline, encoding): - lnum = parenlev = continued = 0 - numchars = '0123456789' - contstr, needcont = '', 0 - contline = None - indents = [0] - - if encoding is not None: - if encoding == "utf-8-sig": - # BOM will already have been stripped. - encoding = "utf-8" - yield TokenInfo(ENCODING, encoding, (0, 0), (0, 0), '') - while True: # loop over lines in stream - try: - line = readline() - except StopIteration: - line = b'' - - if encoding is not None: - line = line.decode(encoding) - lnum += 1 - pos, max = 0, len(line) - - if contstr: # continued string - if not line: - raise TokenError("EOF in multi-line string", strstart) - endmatch = endprog.match(line) - if endmatch: - pos = end = endmatch.end(0) - yield TokenInfo(STRING, contstr + line[:end], - strstart, (lnum, end), contline + line) - contstr, needcont = '', 0 - contline = None - elif needcont and line[-2:] != '\\\n' and line[-3:] != '\\\r\n': - yield TokenInfo(ERRORTOKEN, contstr + line, - strstart, (lnum, len(line)), contline) - contstr = '' - contline = None - continue - else: - contstr = contstr + line - contline = contline + line - continue - - elif parenlev == 0 and not continued: # new statement - if not line: break - column = 0 - while pos < max: # measure leading whitespace - if line[pos] == ' ': - column += 1 - elif line[pos] == '\t': - column = (column//tabsize + 1)*tabsize - elif line[pos] == '\f': - column = 0 - else: - break - pos += 1 - if pos == max: - break - - if line[pos] in '#\r\n': # skip comments or blank lines - if line[pos] == '#': - comment_token = line[pos:].rstrip('\r\n') - nl_pos = pos + len(comment_token) - yield TokenInfo(COMMENT, comment_token, - (lnum, pos), (lnum, pos + len(comment_token)), line) - yield TokenInfo(NL, line[nl_pos:], - (lnum, nl_pos), (lnum, len(line)), line) - else: - yield TokenInfo((NL, COMMENT)[line[pos] == '#'], line[pos:], - (lnum, pos), (lnum, len(line)), line) - continue - - if column > indents[-1]: # count indents or dedents - indents.append(column) - yield TokenInfo(INDENT, line[:pos], (lnum, 0), (lnum, pos), line) - while column < indents[-1]: - if column not in indents: - raise IndentationError( - "unindent does not match any outer indentation level", - ("", lnum, pos, line)) - indents = indents[:-1] - yield TokenInfo(DEDENT, '', (lnum, pos), (lnum, pos), line) - - else: # continued statement - if not line: - raise TokenError("EOF in multi-line statement", (lnum, 0)) - continued = 0 - - while pos < max: - pseudomatch = _compile(PseudoToken).match(line, pos) - if pseudomatch: # scan for tokens - start, end = pseudomatch.span(1) - spos, epos, pos = (lnum, start), (lnum, end), end - if start == end: - continue - token, initial = line[start:end], line[start] - - if (initial in numchars or # ordinary number - (initial == '.' and token != '.' and token != '...')): - yield TokenInfo(NUMBER, token, spos, epos, line) - elif initial in '\r\n': - yield TokenInfo(NL if parenlev > 0 else NEWLINE, - token, spos, epos, line) - elif initial == '#': - assert not token.endswith("\n") - yield TokenInfo(COMMENT, token, spos, epos, line) - elif token in triple_quoted: - endprog = _compile(endpats[token]) - endmatch = endprog.match(line, pos) - if endmatch: # all on one line - pos = endmatch.end(0) - token = line[start:pos] - yield TokenInfo(STRING, token, spos, (lnum, pos), line) - else: - strstart = (lnum, start) # multiple lines - contstr = line[start:] - contline = line - break - elif initial in single_quoted or \ - token[:2] in single_quoted or \ - token[:3] in single_quoted: - if token[-1] == '\n': # continued string - strstart = (lnum, start) - endprog = _compile(endpats[initial] or - endpats[token[1]] or - endpats[token[2]]) - contstr, needcont = line[start:], 1 - contline = line - break - else: # ordinary string - yield TokenInfo(STRING, token, spos, epos, line) - elif initial.isidentifier(): # ordinary name - yield TokenInfo(NAME, token, spos, epos, line) - elif initial == '\\': # continued stmt - continued = 1 - else: - if initial in '([{': - parenlev += 1 - elif initial in ')]}': - parenlev -= 1 - yield TokenInfo(OP, token, spos, epos, line) - else: - yield TokenInfo(ERRORTOKEN, line[pos], - (lnum, pos), (lnum, pos+1), line) - pos += 1 - - for _ in indents[1:]: # pop remaining indent levels - yield TokenInfo(DEDENT, '', (lnum, 0), (lnum, 0), '') - yield TokenInfo(ENDMARKER, '', (lnum, 0), (lnum, 0), '') diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 8902e90bd..f7774af44 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -5,7 +5,7 @@ import re import unittest -from ..compat import HAS_NUMPY, HAS_PROPER_BABEL, HAS_UNCERTAINTIES, NUMPY_VER +from ..compat import HAS_NUMPY, HAS_BABEL, HAS_UNCERTAINTIES, NUMPY_VER def requires_numpy18(): @@ -28,8 +28,8 @@ def requires_not_numpy(): return unittest.skipIf(HAS_NUMPY, 'Requires NumPy is not installed.') -def requires_proper_babel(): - return unittest.skipUnless(HAS_PROPER_BABEL, 'Requires Babel with units support') +def requires_babel(): + return unittest.skipUnless(HAS_BABEL, 'Requires Babel with units support') def requires_uncertainties(): diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index d9f7433fa..6978ffc55 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -7,7 +7,7 @@ class TestBabel(BaseTestCase): - @helpers.requires_proper_babel() + @helpers.requires_babel() def test_babel(self): ureg = UnitRegistry() dirname = os.path.dirname(__file__) From cfc2c3d3a834c5368e2e60152c8b1898fa0cf34e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 16:16:31 +0000 Subject: [PATCH 136/612] Rename compat/__init__.py to compat.py --- docs/developers_reference.rst | 15 +++------------ pint/{compat/__init__.py => compat.py} | 0 2 files changed, 3 insertions(+), 12 deletions(-) rename pint/{compat/__init__.py => compat.py} (100%) diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst index 0ba667974..157732805 100644 --- a/docs/developers_reference.rst +++ b/docs/developers_reference.rst @@ -11,6 +11,9 @@ Pint .. automodule:: pint.babel_names :members: +.. automodule:: pint.compat + :members: + .. automodule:: pint.context :members: @@ -53,18 +56,6 @@ Pint .. automodule:: pint.util :members: -.. automodule:: pint.compat.chainmap - :members: - -.. automodule:: pint.compat.lrucache - :members: - -.. automodule:: pint.compat.meta - :members: - -.. automodule:: pint.compat.tokenize - :members: - .. automodule:: pint.testsuite.helpers :members: diff --git a/pint/compat/__init__.py b/pint/compat.py similarity index 100% rename from pint/compat/__init__.py rename to pint/compat.py From 6502c7a138d32fc2f2c275cc84e2666a58d33d2d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 16:39:03 +0000 Subject: [PATCH 137/612] Optimize sets --- pint/context.py | 2 +- pint/registry.py | 8 +++++--- pint/testsuite/test_contexts.py | 34 +++++++++++++-------------------- pint/testsuite/test_systems.py | 6 +++--- pint/testsuite/test_util.py | 20 +++++++++---------- pint/util.py | 13 ++++++------- 6 files changed, 38 insertions(+), 45 deletions(-) diff --git a/pint/context.py b/pint/context.py index 815711535..071e89e0f 100644 --- a/pint/context.py +++ b/pint/context.py @@ -158,7 +158,7 @@ def to_num(val): lineno=lineno) if defaults: - missing_pars = set(defaults.keys()).difference(set(names)) + missing_pars = defaults.keys() - set(names) if missing_pars: raise DefinitionSyntaxError('Context parameters {} not found in any equation.'.format(missing_pars)) diff --git a/pint/registry.py b/pint/registry.py index 8d2fc1bf4..c79f78b5f 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -432,8 +432,10 @@ def _build_cache(self): """ self._dimensional_equivalents = dict() - deps = dict((name, set(definition.reference.keys() if definition.reference else {})) - for name, definition in self._units.items()) + deps = { + name: definition.reference.keys() if definition.reference else set() + for name, definition in self._units.items() + } for unit_names in solve_dependencies(deps): for unit_name in unit_names: @@ -1492,7 +1494,7 @@ def _get_compatible_units(self, input_units, group_or_system): members = self._groups[group_or_system].members else: raise ValueError("Unknown Group o System with name '%s'" % group_or_system) - return frozenset(ret.intersection(members)) + return frozenset(ret & members) return ret diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 10c5037e0..cb33daf25 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -123,17 +123,13 @@ def test_graph(self): c = UnitsContainer({'[current]': 1.}) g_sp = defaultdict(set) - g_sp.update({l: set((t,)), - t: set((l,))}) + g_sp.update({l: {t}, t: {l}}) g_ab = defaultdict(set) - g_ab.update({l: set((c,)), - c: set((l,))}) + g_ab.update({l: {c}, c: {l}}) g = defaultdict(set) - g.update({l: set((t, c)), - t: set((l,)), - c: set((l,))}) + g.update({l: {t, c}, t: {l}, c: {l}}) with ureg.context('lc'): self.assertEqual(ureg._active_ctx.graph, g_sp) @@ -166,17 +162,13 @@ def test_graph_enable(self): c = UnitsContainer({'[current]': 1.}) g_sp = defaultdict(set) - g_sp.update({l: set((t,)), - t: set((l,))}) + g_sp.update({l: {t}, t: {l}}) g_ab = defaultdict(set) - g_ab.update({l: set((c,)), - c: set((l,))}) + g_ab.update({l: {c}, c: {l}}) g = defaultdict(set) - g.update({l: set((t, c)), - t: set((l,)), - c: set((l,))}) + g.update({l: {t, c}, t: {l}, c: {l}}) ureg.enable_contexts('lc') self.assertEqual(ureg._active_ctx.graph, g_sp) @@ -534,7 +526,7 @@ def test_parse_simple(self): self.assertEqual(c.name, 'longcontextname') self.assertEqual(c.aliases, ()) self.assertEqual(c.defaults, {}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) s = ['@context longcontextname = lc', @@ -544,7 +536,7 @@ def test_parse_simple(self): self.assertEqual(c.name, 'longcontextname') self.assertEqual(c.aliases, ('lc', )) self.assertEqual(c.defaults, {}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) s = ['@context longcontextname = lc = lcn', @@ -554,7 +546,7 @@ def test_parse_simple(self): self.assertEqual(c.name, 'longcontextname') self.assertEqual(c.aliases, ('lc', 'lcn', )) self.assertEqual(c.defaults, {}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) def test_parse_auto_inverse(self): @@ -567,7 +559,7 @@ def test_parse_auto_inverse(self): c = Context.from_lines(s) self.assertEqual(c.defaults, {}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) def test_parse_define(self): @@ -578,7 +570,7 @@ def test_parse_define(self): '[length] <-> 1 / [time]: c / value'] c = Context.from_lines(s) self.assertEqual(c.defaults, {}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) def test_parse_parameterized(self): @@ -590,7 +582,7 @@ def test_parse_parameterized(self): c = Context.from_lines(s) self.assertEqual(c.defaults, {'n': 1}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) s = ['@context(n=1, bla=2) longcontextname', @@ -598,7 +590,7 @@ def test_parse_parameterized(self): c = Context.from_lines(s) self.assertEqual(c.defaults, {'n': 1, 'bla': 2}) - self.assertEqual(set(c.funcs.keys()), set((a, b))) + self.assertEqual(c.funcs.keys(), {a, b}) # If the variable is not present in the definition, then raise an error s = ['@context(n=1) longcontextname', diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index ba5c9e441..068b2e60b 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -23,7 +23,7 @@ def test_units_programatically(self): self.assertEqual(root._unit_names, {'meter', 'second'}) self.assertEqual(root.members, {'meter', 'second'}) - self.assertEqual(set(d.keys()), {'root'}) + self.assertEqual(d.keys(), {'root'}) def test_cyclic(self): ureg, root = self._build_empty_reg_root() @@ -40,7 +40,7 @@ def test_groups_programatically(self): d = ureg._groups g2 = ureg.Group('g2') - self.assertEqual(set(d.keys()), {'root', 'g2'}) + self.assertEqual(d.keys(), {'root', 'g2'}) self.assertEqual(root._used_groups, {'g2'}) self.assertEqual(root._used_by, set()) @@ -60,7 +60,7 @@ def test_simple(self): grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(set(d.keys()), {'root', 'mygroup'}) + self.assertEqual(d.keys(), {'root', 'mygroup'}) self.assertEqual(grp.name, 'mygroup') self.assertEqual(grp._unit_names, {'meter', 'second'}) diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index b10b04775..66de265e2 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -262,15 +262,15 @@ def test_names(self): class TestGraph(BaseTestCase): def test_start_not_in_graph(self): - g = collections.defaultdict(list) - g[1] = set((2,)) - g[2] = set((3,)) + g = collections.defaultdict(set) + g[1] = {2} + g[2] = {3} self.assertIs(find_connected_nodes(g, 9), None) def test_shortest_path(self): - g = collections.defaultdict(list) - g[1] = set((2,)) - g[2] = set((3,)) + g = collections.defaultdict(set) + g[1] = {2} + g[2] = {3} p = find_shortest_path(g, 1, 2) self.assertEqual(p, [1, 2]) p = find_shortest_path(g, 1, 3) @@ -278,10 +278,10 @@ def test_shortest_path(self): p = find_shortest_path(g, 3, 1) self.assertIs(p, None) - g = collections.defaultdict(list) - g[1] = set((2,)) - g[2] = set((3, 1)) - g[3] = set((2,)) + g = collections.defaultdict(set) + g[1] = {2} + g[2] = {3, 1} + g[3] = {2} p = find_shortest_path(g, 1, 2) self.assertEqual(p, [1, 2]) p = find_shortest_path(g, 1, 3) diff --git a/pint/util.py b/pint/util.py index d5ec5a292..cc66d861d 100644 --- a/pint/util.py +++ b/pint/util.py @@ -180,22 +180,21 @@ def solve_dependencies(dependencies): :return: list of sets, each containing keys of independents tasks dependent only of the previous tasks in the list. """ - d = dict((key, set(dependencies[key])) for key in dependencies) r = [] - while d: + while dependencies: # values not in keys (items without dep) - t = {i for v in d.values() for i in v} - d.keys() + t = {i for v in dependencies.values() for i in v} - dependencies.keys() # and keys without value (items without dep) - t.update(k for k, v in d.items() if not v) + t.update(k for k, v in dependencies.items() if not v) # can be done right away if not t: raise ValueError( 'Cyclic dependencies exist among these items: {}' - .format(', '.join(repr(x) for x in d.items())) + .format(', '.join(repr(x) for x in dependencies.items())) ) r.append(t) # and cleaned up - d = {k: v - t for k, v in d.items() if v} + dependencies = {k: v - t for k, v in dependencies.items() if v} return r @@ -219,7 +218,7 @@ def find_connected_nodes(graph, start, visited=None): if not start in graph: return None - visited = (visited or set()) + visited = visited or set() visited.add(start) for node in graph[start]: From 50285dc969e6859f758ef1ef62ceb276c08c9d0c Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 16:44:56 +0000 Subject: [PATCH 138/612] Remove metaclass backport --- docs/tutorial.rst | 4 ---- pint/testsuite/parameterized.py | 27 +++------------------------ 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 2894c0b65..b035b1076 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -336,10 +336,6 @@ LaTeX representations: >>> 'The HTML representation is {:H}'.format(accel) 'The HTML representation is 1.3 meter/second2' -.. note:: - In Python 2, run ``from __future__ import unicode_literals`` - or prefix pretty formatted strings with `u` to prevent ``UnicodeEncodeError``. - If you want to use abbreviated unit names, prefix the specification with `~`: .. doctest:: diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index c5336a737..0ddf29755 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -7,11 +7,6 @@ # Author: Marc Abramowitz, email: marc@marc-abramowitz.com # License: MIT # -# Fixed for to work in Python 2 & 3 with "add_metaclass" decorator from six -# https://pypi.python.org/pypi/six -# Author: Benjamin Peterson -# License: MIT -# # Use like this: # # from parameterizedtestcase import ParameterizedTestCase @@ -34,22 +29,6 @@ import unittest -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - def augment_method_docstring(method, new_class_dict, classname, param_names, param_values, new_method): param_assignments_str = '; '.join( @@ -117,8 +96,8 @@ def new_method(self): return new_method -@add_metaclass(ParameterizedTestCaseMetaClass) -class ParameterizedTestMixin: + +class ParameterizedTestMixin(metaclass=ParameterizedTestCaseMetaClass): @classmethod def parameterize(cls, param_names, data, func_name_format='{func_name}_{case_num:05d}'): @@ -144,6 +123,6 @@ def newfunc(*arg, **kwargs): return decorator -@add_metaclass(ParameterizedTestCaseMetaClass) + class ParameterizedTestCase(unittest.TestCase, ParameterizedTestMixin): pass From e9de47a5efaca9c5d8b93bc44285c0df0241732f Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 4 Dec 2019 16:51:08 +0000 Subject: [PATCH 139/612] Trivial comment --- pint/testsuite/test_errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index be0cb56f3..56ae7528b 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -92,8 +92,7 @@ def test_offset_unit_calculus_error(self): def test_pickle_definition_syntax_error(self): # OffsetUnitCalculusError raised from a custom ureg must be pickleable even if - # the ureg is not the application ureg - # pickled + # the ureg is not registered as the application ureg ureg = UnitRegistry(filename=None) ureg.define("foo = [bar]") ureg.define("bar = 2 foo") From 336579c1e568c206ac89160cf89a3c58260c5fb6 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 11:23:39 +0000 Subject: [PATCH 140/612] README --- README | 5 +++-- README.rst | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README b/README index e2d43b644..a63127937 100644 --- a/README +++ b/README @@ -11,8 +11,9 @@ and constants. Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 3.6+ -with no other dependency. It is licensed under BSD. +It has a complete test coverage. It runs in Python 3.6+ with no other dependency. +If you need Python 2.7 or 3.4/3.5 compatibility, use Pint 0.9. +It is licensed under BSD. It is extremely easy and natural to use: diff --git a/README.rst b/README.rst index 673e38d6e..6e5f270f0 100644 --- a/README.rst +++ b/README.rst @@ -40,8 +40,9 @@ and constants. Due to its modular design, you can extend (or even rewrite!) the complete list without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. -It has a complete test coverage. It runs in Python 3.6+ -with no other dependency. It is licensed under BSD. +It has a complete test coverage. It runs in Python 3.6+ with no other dependency. +If you need Python 2.7 or 3.4/3.5 compatibility, use Pint 0.9. +It is licensed under BSD. It is extremely easy and natural to use: From 2d8317128246d3c470f30d8989b9b18a4a823a56 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 11:25:23 +0000 Subject: [PATCH 141/612] Merge tweaks --- pint/registry.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 80454d1d3..13597c06e 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -82,16 +82,13 @@ class RegistryCache: """Cache to speed up unit registries """ - #: Maps dimensionality (UnitsContainer) to Units (str) def __init__(self): - + #: Maps dimensionality (UnitsContainer) to Units (str) self.dimensional_equivalents = {} #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) self.root_units = {} - #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) self.dimensionality = {} - #: Cache the unit name associated to user input. ('mV' -> 'millivolt') self.parse_unit = {} From 5e5dfacd23d64547cb3a75f1f8e0f3e41ed3fb59 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 13:01:12 +0000 Subject: [PATCH 142/612] Changes --- CHANGES | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6e56d90e1..9e80cf308 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,76 @@ Pint Changelog 0.10 (unreleased) ----------------- -- Nothing changed yet. +- **BREAKING CHANGE**: + Fix crash when applying pprint to large sets of Units. + DefinitionSyntaxError is now a subclass of SyntaxError (was ValueError). + DimensionalityError and OffsetUnitCalculusError are now subclasses of TypeError (was + ValueError). + (Issue #915, Thanks Guido Imperiale) +- TODO #913 +- TODO #911 +- Prevent 1500x slowdown when using .to() with a context + (Issue #909, Thanks Guido Imperiale) +- **BREAKING CHANGE**: + Drop support for Python < 3.6, numpy < 1.14, and uncertainties < 3.0; + if you still need them, please install pint 0.9. + Pint now adheres to NEP-29 + as a rolling dependencies version policy. + (Issues #908 and #910, Thanks Guido Imperiale) +- TODO #907 +- TODO #891 +- TODO #890 +- TODO #889 +- TODO #888 +- TODO #884 +- All Exceptions can now be pickled and can be accessed from the top-level package + (e.g. pint.DimensionalityError). + (Issues #880 and #915, Thanks Guido Imperiale) +- Quantity, Unit, and Measurement are now accessible as top-level classes + (pint.Quantity, pint.Unit, pint.Measurement) and can be + instantiated without explicitly creating a UnitRegistry + (Issue #880, Thanks Guido Imperiale) +- Contexts don't need to have a name anymore + (Issue #870, Thanks Guido Imperiale) +- "Board feet" unit added top default registry + (Issue #869, Thanks Guido Imperiale) +- New syntax to add aliases to already existing definitions + (Issue #868, Thanks Guido Imperiale) +- copy.deepcopy() can now copy a UnitRegistry + (Issues #864 and #877, Thanks Guido Imperiale) +- Enabled many tests in test_issues when numpy is not available + (Issue #863, Thanks Guido Imperiale) +- Document the '_' symbols found in the definitions files + (Issue #862, Thanks Guido Imperiale) +- TODO #846 +- TODO #837 +- TODO #835 +- TODO #834 +- TODO #830 +- TODO #829 +- TODO #825 +- TODO #824 +- TODO #822 +- TODO #813 +- TODO #811 +- TODO #809 +- TODO #805 +- TODO #802 +- TODO #798 +- TODO #796 +- TODO #791 +- TODO #788 +- TODO #781 +- TODO #776 +- TODO #784 +- TODO #773 +- TODO #768 +- TODO #767 +- TODO #762 +- TODO #759 +- TODO #756 +- TODO #754 +- TODO #680 0.9 (2019-01-12) From 8c8c4a61fb527721151f1c31963d4685969f3b51 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 13:02:46 +0000 Subject: [PATCH 143/612] Unused import --- pint/testsuite/test_issues.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index b0f868202..9f5fcd763 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -4,7 +4,6 @@ import math import unittest import pprint -import sys from pint import DimensionalityError, UnitRegistry from pint.unit import UnitsContainer From 3e88d304edc1cd5495f893d913f98439240a39c8 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 13:07:17 +0000 Subject: [PATCH 144/612] CHANGES tweak --- CHANGES | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 9e80cf308..16cc51528 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ Pint Changelog DimensionalityError and OffsetUnitCalculusError are now subclasses of TypeError (was ValueError). (Issue #915, Thanks Guido Imperiale) +- All Exceptions can now be pickled and can be accessed from the top-level package. + (Issue #915, Thanks Guido Imperiale) - TODO #913 - TODO #911 - Prevent 1500x slowdown when using .to() with a context @@ -26,9 +28,6 @@ Pint Changelog - TODO #889 - TODO #888 - TODO #884 -- All Exceptions can now be pickled and can be accessed from the top-level package - (e.g. pint.DimensionalityError). - (Issues #880 and #915, Thanks Guido Imperiale) - Quantity, Unit, and Measurement are now accessible as top-level classes (pint.Quantity, pint.Unit, pint.Measurement) and can be instantiated without explicitly creating a UnitRegistry From 5c36cdb79a07eb29395ad4ae3e8de924e9998a7f Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 5 Dec 2019 13:18:26 +0000 Subject: [PATCH 145/612] Correct comment --- pint/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/errors.py b/pint/errors.py index 82db18ea4..d5ae803f0 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -35,8 +35,8 @@ def __str__(self): @property def __dict__(self): - # For some reason, SyntaxError.__dict__ is always empty. - # There are no __slots__ either. This messes up pickling and deepcopy, as well + # SyntaxError.filename and lineno are special fields that don't appear in + # the __dict__. This messes up pickling and deepcopy, as well # as any other Python library that expects sane behaviour. return {"filename": self.filename, "lineno": self.lineno} From e74177a514a183a426b01c399b635bddd9622563 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 30 Oct 2019 17:43:05 -0500 Subject: [PATCH 146/612] Preliminary changes from prior implementation (compat, etc.) --- pint/compat.py | 40 +++++++++++++++++++++++++++++++++++++++ pint/testsuite/helpers.py | 14 +++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pint/compat.py b/pint/compat.py index 6c3221af0..483e3d623 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ import tokenize +import warnings from io import BytesIO from numbers import Number from decimal import Decimal @@ -21,6 +22,27 @@ def tokenizer(input_string): yield tokinfo +# TODO: remove this warning after v0.10 +class BehaviorChangeWarning(UserWarning): + pass +_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/xxxx. ' +'To hide this warning use the following code to import pint:' +""" +import warnings +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import pint +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 @@ -40,6 +62,23 @@ def _to_magnitude(value, force_ndarray=False): return np.asarray(value) return value + def _test_array_function_protocol(): + # Test if the __array_function__ protocol is enabled + try: + class FakeArray: + def __array_function__(self, *args, **kwargs): + return + + np.concatenate([FakeArray()]) + return True + except ValueError: + return False + + HAS_NUMPY_ARRAY_FUNCTION = _test_array_function_protocol() + + if HAS_NUMPY_ARRAY_FUNCTION: + warnings.warn(_msg, BehaviorChangeWarning) + except ImportError: np = None @@ -50,6 +89,7 @@ class ndarray: HAS_NUMPY = False NUMPY_VER = '0' NUMERIC_TYPES = (Number, Decimal) + HAS_NUMPY_ARRAY_FUNCTION = False def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index cb6d61001..2ecc5dd75 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -5,7 +5,19 @@ import re import unittest -from ..compat import HAS_NUMPY, HAS_BABEL, HAS_UNCERTAINTIES, NUMPY_VER +from ..compat import HAS_NUMPY, HAS_BABEL, HAS_UNCERTAINTIES, HAS_NUMPY_ARRAY_FUNCTION, NUMPY_VER + + +def requires_array_function_protocol(): + if not HAS_NUMPY: + return unittest.skip('Requires NumPy') + return unittest.skipUnless(HAS_NUMPY_ARRAY_FUNCTION, 'Requires __array_function__ protocol to be enabled') + + +def requires_not_array_function_protocol(): + if not HAS_NUMPY: + return unittest.skip('Requires NumPy') + return unittest.skipIf(HAS_NUMPY_ARRAY_FUNCTION, 'Requires __array_function__ protocol to be unavailable or disabled') def requires_numpy18(): From 6734633ba3e7083b78fb3108a01974f0bca07892 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 30 Oct 2019 17:58:06 -0500 Subject: [PATCH 147/612] Merge pint/testsuite/test_numpy.py from branch split_quantity (props @andrewgsavage) --- pint/testsuite/test_numpy.py | 622 +++++++++++++++++++++++++++-------- 1 file changed, 479 insertions(+), 143 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 1697ba8ba..9294fff07 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -4,7 +4,7 @@ import operator as op import unittest -from pint import DimensionalityError +from pint import DimensionalityError, OffsetUnitCalculusError, set_application_registry from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.test_umath import TestUFuncs @@ -24,28 +24,46 @@ def setUpClass(cls): @property def q(self): return [[1,2],[3,4]] * self.ureg.m - - def test_tolist(self): - self.assertEqual(self.q.tolist(), [[1*self.ureg.m, 2*self.ureg.m], [3*self.ureg.m, 4*self.ureg.m]]) - - def test_sum(self): - self.assertEqual(self.q.sum(), 10*self.ureg.m) - self.assertQuantityEqual(self.q.sum(0), [4, 6]*self.ureg.m) - self.assertQuantityEqual(self.q.sum(1), [3, 7]*self.ureg.m) - - def test_fill(self): - tmp = self.q - tmp.fill(6 * self.ureg.ft) - self.assertQuantityEqual(tmp, [[6, 6], [6, 6]] * self.ureg.ft) - tmp.fill(5 * self.ureg.m) - self.assertQuantityEqual(tmp, [[5, 5], [5, 5]] * self.ureg.m) - - def test_reshape(self): - self.assertQuantityEqual(self.q.reshape([1,4]), [[1, 2, 3, 4]] * self.ureg.m) - - def test_transpose(self): - self.assertQuantityEqual(self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m) - + @property + def q_nan(self): + return [[1,2],[3,np.nan]] * self.ureg.m + @property + def q_temperature(self): + return self.Q_([[1,2],[3,4]], self.ureg.degC) + + +class TestNumpyArrayCreation(TestNumpyMethods): + # https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html + + @helpers.requires_array_function_protocol() + def test_ones_like(self): + np.testing.assert_equal(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) + + @helpers.requires_array_function_protocol() + def test_zeros_like(self): + np.testing.assert_equal(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) + + @helpers.requires_array_function_protocol() + def test_empty_like(self): + ret = np.empty_like(self.q) + self.assertEqual(ret.shape, (2, 2)) + self.assertTrue(isinstance(ret, np.ndarray)) + + @helpers.requires_array_function_protocol() + def test_full_like(self): + self.assertQuantityEqual(np.full_like(self.q, self.Q_(0, self.ureg.degC)), + self.Q_([[0, 0], [0, 0]], self.ureg.degC)) + np.testing.assert_equal(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) + +class TestNumpyArrayManipulation(TestNumpyMethods): + #TODO + # https://www.numpy.org/devdocs/reference/routines.array-manipulation.html + # copyto + # broadcast , broadcast_arrays + # asarray asanyarray asmatrix asfarray asfortranarray ascontiguousarray asarray_chkfinite asscalar require + + # Changing array shape + def test_flatten(self): self.assertQuantityEqual(self.q.flatten(), [1, 2, 3, 4] * self.ureg.m) @@ -53,15 +71,260 @@ def test_flat(self): for q, v in zip(self.q.flat, [1, 2, 3, 4]): self.assertEqual(q, v * self.ureg.m) + def test_reshape(self): + self.assertQuantityEqual(self.q.reshape([1,4]), [[1, 2, 3, 4]] * self.ureg.m) + def test_ravel(self): self.assertQuantityEqual(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_ravel_numpy_func(self): + self.assertQuantityEqual(np.ravel(self.q), [1, 2, 3, 4] * self.ureg.m) + + # Transpose-like operations + + @helpers.requires_array_function_protocol() + def test_moveaxis(self): + self.assertQuantityEqual(np.moveaxis(self.q, 1,0), np.array([[1,2],[3,4]]).T * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_rollaxis(self): + self.assertQuantityEqual(np.rollaxis(self.q, 1), np.array([[1,2],[3,4]]).T * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_swapaxes(self): + self.assertQuantityEqual(np.swapaxes(self.q, 1,0), np.array([[1,2],[3,4]]).T * self.ureg.m) + + def test_transpose(self): + self.assertQuantityEqual(self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_transpose_numpy_func(self): + self.assertQuantityEqual(np.transpose(self.q), [[1, 3], [2, 4]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_flip_numpy_func(self): + self.assertQuantityEqual(np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m) + + # Changing number of dimensions + + @helpers.requires_array_function_protocol() + def test_atleast_1d(self): + self.assertQuantityEqual(np.atleast_1d(self.q), self.q) + + @helpers.requires_array_function_protocol() + def test_atleast_2d(self): + self.assertQuantityEqual(np.atleast_2d(self.q), self.q) + + @helpers.requires_array_function_protocol() + def test_atleast_3d(self): + self.assertQuantityEqual(np.atleast_3d(self.q), np.array([[[1],[2]],[[3],[4]]])* self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_broadcast_to(self): + self.assertQuantityEqual(np.broadcast_to(self.q[:,1], (2,2)), np.array([[2,4],[2,4]]) * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_expand_dims(self): + self.assertQuantityEqual(np.expand_dims(self.q, 0), np.array([[[1, 2],[3, 4]]])* self.ureg.m) + + @helpers.requires_array_function_protocol() def test_squeeze(self): + self.assertQuantityEqual(np.squeeze(self.q), self.q) self.assertQuantityEqual( self.q.reshape([1,4]).squeeze(), [1, 2, 3, 4] * self.ureg.m ) + + # Changing number of dimensions + # Joining arrays + @helpers.requires_array_function_protocol() + def test_concatentate(self): + self.assertQuantityEqual( + np.concatenate([self.q]*2), + self.Q_(np.concatenate([self.q.m]*2), self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_stack(self): + self.assertQuantityEqual( + np.stack([self.q]*2), + self.Q_(np.stack([self.q.m]*2), self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_column_stack(self): + self.assertQuantityEqual( + np.column_stack([self.q[:,0],self.q[:,1]]), + self.q + ) + + @helpers.requires_array_function_protocol() + def test_dstack(self): + self.assertQuantityEqual( + np.dstack([self.q]*2), + self.Q_(np.dstack([self.q.m]*2), self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_hstack(self): + self.assertQuantityEqual( + np.hstack([self.q]*2), + self.Q_(np.hstack([self.q.m]*2), self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_vstack(self): + self.assertQuantityEqual( + np.vstack([self.q]*2), + self.Q_(np.vstack([self.q.m]*2), self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_block(self): + self.assertQuantityEqual( + np.block([self.q[0,:],self.q[1,:]]), + self.Q_([1,2,3,4], self.ureg.m) + ) + + @helpers.requires_array_function_protocol() + def test_append(self): + self.assertQuantityEqual(np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), + [[1, 2], [3, 4], [0, 0]] * self.ureg.m) + +class TestNumpyMathematicalFunctions(TestNumpyMethods): + # https://www.numpy.org/devdocs/reference/routines.math.html + # Trigonometric functions + @helpers.requires_array_function_protocol() + def test_unwrap(self): + self.assertQuantityEqual(np.unwrap([0,3*np.pi]*self.ureg.radians), [0,np.pi]) + self.assertQuantityEqual(np.unwrap([0,540]*self.ureg.deg), [0,180]*self.ureg.deg) + + # Rounding + + @helpers.requires_array_function_protocol() + def test_fix(self): + self.assertQuantityEqual(np.fix(3.14 * self.ureg.m), 3.0 * self.ureg.m) + self.assertQuantityEqual(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) + self.assertQuantityEqual( + np.fix([2.1, 2.9, -2.1, -2.9] * self.ureg.m), + [2., 2., -2., -2.] * self.ureg.m + ) + # Sums, products, differences + def test_prod(self): + self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) + + def test_sum(self): + self.assertEqual(self.q.sum(), 10*self.ureg.m) + self.assertQuantityEqual(self.q.sum(0), [4, 6]*self.ureg.m) + self.assertQuantityEqual(self.q.sum(1), [3, 7]*self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_sum_numpy_func(self): + self.assertQuantityEqual(np.sum(self.q, axis=0), [4, 6] * self.ureg.m) + self.assertRaises(OffsetUnitCalculusError, np.sum, self.q_temperature) + + @helpers.requires_array_function_protocol() + def test_nansum_numpy_func(self): + self.assertQuantityEqual(np.nansum(self.q_nan, axis=0), [4, 2] * self.ureg.m) + + def test_cumprod(self): + self.assertRaises(ValueError, self.q.cumprod) + self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) + + @helpers.requires_array_function_protocol() + def test_cumprod_numpy_func(self): + self.assertRaises(DimensionalityError, np.cumprod, self.q) + self.assertRaises(DimensionalityError, np.cumproduct, self.q) + self.assertQuantityEqual(np.cumprod(self.q / self.ureg.m), [1, 2, 6, 24]) + self.assertQuantityEqual(np.cumproduct(self.q / self.ureg.m), [1, 2, 6, 24]) + + @helpers.requires_array_function_protocol() + def test_nancumprod_numpy_func(self): + self.assertRaises(DimensionalityError, np.nancumprod, self.q_nan) + self.assertQuantityEqual(np.nancumprod(self.q_nan / self.ureg.m), [1, 2, 6, 6]) + + @helpers.requires_array_function_protocol() + def test_diff(self): + self.assertQuantityEqual(np.diff(self.q, 1), [[1], [1]] * self.ureg.m) + self.assertQuantityEqual(np.diff(self.q_temperature, 1), [[1], [1]] * self.ureg.delta_degC) + + @helpers.requires_array_function_protocol() + def test_ediff1d(self): + self.assertQuantityEqual(np.ediff1d(self.q), [1, 1, 1] * self.ureg.m) + self.assertQuantityEqual(np.ediff1d(self.q_temperature), [1, 1, 1] * self.ureg.delta_degC) + + @helpers.requires_array_function_protocol() + def test_gradient(self): + l = np.gradient([[1,1],[3,4]] * self.ureg.m, 1 * self.ureg.J) + self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.m / self.ureg.J) + self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.m / self.ureg.J) + + l = np.gradient(self.Q_([[1,1],[3,4]] , self.ureg.degC), 1 * self.ureg.J) + self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.delta_degC / self.ureg.J) + self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.delta_degC / self.ureg.J) + + + @helpers.requires_array_function_protocol() + def test_cross(self): + a = [[3,-3, 1]] * self.ureg.kPa + b = [[4, 9, 2]] * self.ureg.m**2 + self.assertQuantityEqual(np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m**2) + + @helpers.requires_array_function_protocol() + def test_trapz(self): + self.assertQuantityEqual(np.trapz([1. ,2., 3., 4.] * self.ureg.J, dx=1*self.ureg.m), 7.5 * self.ureg.J*self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_dot_numpy_func(self): + self.assertQuantityEqual(np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m) + + # Arithmetic operations + + def test_power(self): + arr = np.array(range(3), dtype=np.float) + q = self.Q_(arr, 'meter') + + for op_ in [op.pow, op.ipow, np.power]: + q_cp = copy.copy(q) + self.assertRaises(DimensionalityError, op_, 2., q_cp) + arr_cp = copy.copy(arr) + arr_cp = copy.copy(arr) + q_cp = copy.copy(q) + self.assertRaises(DimensionalityError, op_, q_cp, arr_cp) + q_cp = copy.copy(q) + q2_cp = copy.copy(q) + self.assertRaises(DimensionalityError, op_, q_cp, q2_cp) + + @unittest.expectedFailure + @helpers.requires_numpy() + def test_exponentiation_array_exp_2(self): + arr = np.array(range(3), dtype=np.float) + #q = self.Q_(copy.copy(arr), None) + q = self.Q_(copy.copy(arr), 'meter') + arr_cp = copy.copy(arr) + q_cp = copy.copy(q) + # this fails as expected since numpy 1.8.0 but... + self.assertRaises(DimensionalityError, op.pow, arr_cp, q_cp) + # ..not for op.ipow ! + # q_cp is treated as if it is an array. The units are ignored. + # BaseQuantity.__ipow__ is never called + arr_cp = copy.copy(arr) + q_cp = copy.copy(q) + self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) + +class TestNumpyUnclassified(TestNumpyMethods): + def test_tolist(self): + self.assertEqual(self.q.tolist(), [[1*self.ureg.m, 2*self.ureg.m], [3*self.ureg.m, 4*self.ureg.m]]) + + def test_fill(self): + tmp = self.q + tmp.fill(6 * self.ureg.ft) + self.assertQuantityEqual(tmp, [[6, 6], [6, 6]] * self.ureg.ft) + tmp.fill(5 * self.ureg.m) + self.assertQuantityEqual(tmp, [[5, 5], [5, 5]] * self.ureg.m) + def test_take(self): self.assertQuantityEqual(self.q.take([0,1,2,3]), self.q.flatten()) @@ -92,14 +355,28 @@ def test_sort(self): q.sort() self.assertQuantityEqual(q, [1, 2, 3, 4, 5, 6] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_sort_numpy_func(self): + q = [4, 5, 2, 3, 1, 6] * self.ureg.m + self.assertQuantityEqual(np.sort(q), [1, 2, 3, 4, 5, 6] * self.ureg.m) + def test_argsort(self): q = [1, 4, 5, 6, 2, 9] * self.ureg.MeV np.testing.assert_array_equal(q.argsort(), [0, 4, 1, 2, 3, 5]) + @helpers.requires_array_function_protocol() + def test_argsort_numpy_func(self): + np.testing.assert_array_equal(np.argsort(self.q, axis=0), np.array([[0, 0], [1, 1]])) + def test_diagonal(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m self.assertQuantityEqual(q.diagonal(offset=1), [2, 3] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_diagonal_numpy_func(self): + q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m + self.assertQuantityEqual(np.diagonal(q, offset=-1), [1, 2] * self.ureg.m) + def test_compress(self): self.assertQuantityEqual(self.q.compress([False, True], axis=0), [[3, 4]] * self.ureg.m) @@ -113,6 +390,7 @@ def test_searchsorted(self): q = self.q.flatten() self.assertRaises(DimensionalityError, q.searchsorted, [1.5, 2.5]) + @helpers.requires_array_function_protocol() def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" q = self.q.flatten() @@ -123,21 +401,87 @@ def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m np.testing.assert_array_equal(q.nonzero()[0], [0, 2, 3, 5]) + @helpers.requires_array_function_protocol() + def test_nonzero_numpy_func(self): + q = [1, 0, 5, 6, 0, 9] * self.ureg.m + np.testing.assert_array_equal(np.nonzero(q)[0], [0, 2, 3, 5]) + + @helpers.requires_array_function_protocol() + def test_count_nonzero_numpy_func(self): + q = [1, 0, 5, 6, 0, 9] * self.ureg.m + self.assertEqual(np.count_nonzero(q), 4) + def test_max(self): self.assertEqual(self.q.max(), 4*self.ureg.m) + @helpers.requires_array_function_protocol() + def test_max_numpy_func(self): + self.assertEqual(np.max(self.q), 4 * self.ureg.m) + + @helpers.requires_not_array_function_protocol() + def test_max_numpy_func_old_behavior(self): + self.assertEqual(np.max(self.q), 4) + + @helpers.requires_array_function_protocol() + def test_max_with_axis_arg(self): + self.assertQuantityEqual(np.max(self.q, axis=1), [2, 4] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_max_with_initial_arg(self): + self.assertQuantityEqual(np.max(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_nanmax(self): + self.assertEqual(np.nanmax(self.q_nan), 3 * self.ureg.m) + def test_argmax(self): self.assertEqual(self.q.argmax(), 3) + @helpers.requires_array_function_protocol() + def test_argmax_numpy_func(self): + np.testing.assert_equal(np.argmax(self.q, axis=0), np.array([1, 1])) + + @helpers.requires_array_function_protocol() + def test_nanargmax_numpy_func(self): + np.testing.assert_equal(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) + def test_min(self): self.assertEqual(self.q.min(), 1 * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_min_numpy_func(self): + self.assertEqual(np.min(self.q), 1 * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_min_with_axis_arg(self): + self.assertQuantityEqual(np.min(self.q, axis=1), [1, 3] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_min_with_initial_arg(self): + self.assertQuantityEqual(np.min(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[1, 2], [3, 3]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_nanmin(self): + self.assertEqual(np.nanmin(self.q_nan), 1 * self.ureg.m) + def test_argmin(self): self.assertEqual(self.q.argmin(), 0) + @helpers.requires_array_function_protocol() + def test_argmin_numpy_func(self): + np.testing.assert_equal(np.argmin(self.q, axis=0), np.array([0, 0])) + + @helpers.requires_array_function_protocol() + def test_nanargmin_numpy_func(self): + np.testing.assert_equal(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) + def test_ptp(self): self.assertEqual(self.q.ptp(), 3 * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_ptp_numpy_func(self): + self.assertQuantityEqual(np.ptp(self.q, axis=0), [2, 2] * self.ureg.m) + def test_clip(self): self.assertQuantityEqual( self.q.clip(max=2*self.ureg.m), @@ -154,27 +498,78 @@ def test_clip(self): self.assertRaises(DimensionalityError, self.q.clip, self.ureg.J) self.assertRaises(DimensionalityError, self.q.clip, 1) + @helpers.requires_array_function_protocol() + def test_clip_numpy_func(self): + self.assertQuantityEqual(np.clip(self.q, 150 * self.ureg.cm, None), [[1.5, 2], [3, 4]] * self.ureg.m) + def test_round(self): q = [1, 1.33, 5.67, 22] * self.ureg.m self.assertQuantityEqual(q.round(0), [1, 1, 6, 22] * self.ureg.m) self.assertQuantityEqual(q.round(-1), [0, 0, 10, 20] * self.ureg.m) self.assertQuantityEqual(q.round(1), [1, 1.3, 5.7, 22] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_round_numpy_func(self): + self.assertQuantityEqual(np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m) + self.assertQuantityEqual(np.round_(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m) + def test_trace(self): self.assertEqual(self.q.trace(), (1+4) * self.ureg.m) def test_cumsum(self): self.assertQuantityEqual(self.q.cumsum(), [1, 3, 6, 10] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_cumsum_numpy_func(self): + self.assertQuantityEqual(np.cumsum(self.q, axis=0), [[1, 2], [4, 6]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_nancumsum_numpy_func(self): + self.assertQuantityEqual(np.nancumsum(self.q_nan, axis=0), [[1, 2], [4, 2]] * self.ureg.m) + def test_mean(self): self.assertEqual(self.q.mean(), 2.5 * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_mean_numpy_func(self): + self.assertEqual(np.mean(self.q), 2.5 * self.ureg.m) + self.assertRaises(OffsetUnitCalculusError, np.mean, self.q_temperature) + + @helpers.requires_array_function_protocol() + def test_nanmean_numpy_func(self): + self.assertEqual(np.nanmean(self.q_nan), 2 * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_average_numpy_func(self): + self.assertQuantityAlmostEqual(np.average(self.q, axis=0, weights=[1, 2]), [2.33333, 3.33333] * self.ureg.m, rtol=1e-5) + + @helpers.requires_array_function_protocol() + def test_median_numpy_func(self): + self.assertEqual(np.median(self.q), 2.5 * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_nanmedian_numpy_func(self): + self.assertEqual(np.nanmedian(self.q_nan), 2 * self.ureg.m) + def test_var(self): self.assertEqual(self.q.var(), 1.25*self.ureg.m**2) + @helpers.requires_array_function_protocol() + def test_var_numpy_func(self): + self.assertEqual(np.var(self.q), 1.25*self.ureg.m**2) + + @helpers.requires_array_function_protocol() + def test_nanvar_numpy_func(self): + self.assertQuantityAlmostEqual(np.nanvar(self.q_nan), 0.66667*self.ureg.m**2, rtol=1e-5) + def test_std(self): self.assertQuantityAlmostEqual(self.q.std(), 1.11803*self.ureg.m, rtol=1e-5) + @helpers.requires_array_function_protocol() + def test_std_numpy_func(self): + self.assertQuantityAlmostEqual(np.std(self.q), 1.11803*self.ureg.m, rtol=1e-5) + self.assertRaises(OffsetUnitCalculusError, np.std, self.q_temperature) + def test_prod(self): self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) @@ -182,6 +577,10 @@ def test_cumprod(self): self.assertRaises(DimensionalityError, self.q.cumprod) self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) + @helpers.requires_array_function_protocol() + def test_nanstd_numpy_func(self): + self.assertQuantityAlmostEqual(np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5) + @helpers.requires_numpy_previous_than('1.10') def test_integer_div(self): a = [1] * self.ureg.m @@ -256,13 +655,13 @@ def test_reversible_op(self): def test_pickle(self): import pickle - + set_application_registry(self.ureg) def pickle_test(q): pq = pickle.loads(pickle.dumps(q)) np.testing.assert_array_equal(q.magnitude, pq.magnitude) self.assertEqual(q.units, pq.units) - pickle_test([10, 20]*self.ureg.m) + pickle_test([10,20]*self.ureg.m) def test_equal(self): x = self.q.magnitude @@ -277,88 +676,61 @@ def test_shape(self): u.shape = 4, 3 self.assertEqual(u.magnitude.shape, (4, 3)) - -@helpers.requires_numpy() -class TestNumpyNeedsSubclassing(TestUFuncs): - - FORCE_NDARRAY = True - - @property - def q(self): - return [1. ,2., 3., 4.] * self.ureg.J - - @unittest.expectedFailure - def test_unwrap(self): - """unwrap depends on diff - """ - self.assertQuantityEqual(np.unwrap([0,3*np.pi]*self.ureg.radians), [0,np.pi]) - self.assertQuantityEqual(np.unwrap([0,540]*self.ureg.deg), [0,180]*self.ureg.deg) - - @unittest.expectedFailure - def test_trapz(self): - """Units are erased by asanyarray, Quantity does not inherit from NDArray - """ - self.assertQuantityEqual(np.trapz(self.q, dx=1*self.ureg.m), 7.5 * self.ureg.J*self.ureg.m) - - @unittest.expectedFailure - def test_diff(self): - """Units are erased by asanyarray, Quantity does not inherit from NDArray - """ - self.assertQuantityEqual(np.diff(self.q, 1), [1, 1, 1] * self.ureg.J) - - @unittest.expectedFailure - def test_ediff1d(self): - """Units are erased by asanyarray, Quantity does not inherit from NDArray - """ - self.assertQuantityEqual(np.ediff1d(self.q, 1 * self.ureg.J), [1, 1, 1] * self.ureg.J) - - @unittest.expectedFailure - def test_fix(self): - """Units are erased by asanyarray, Quantity does not inherit from NDArray - """ - self.assertQuantityEqual(np.fix(3.14 * self.ureg.m), 3.0 * self.ureg.m) - self.assertQuantityEqual(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) - self.assertQuantityEqual( - np.fix([2.1, 2.9, -2.1, -2.9] * self.ureg.m), - [2., 2., -2., -2.] * self.ureg.m - ) - - @unittest.expectedFailure - def test_gradient(self): - """shape is a property not a function - """ - l = np.gradient([[1,1],[3,4]] * self.ureg.J, 1 * self.ureg.m) - self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.J / self.ureg.m) - self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.J / self.ureg.m) - - @unittest.expectedFailure - def test_cross(self): - """Units are erased by asarray, Quantity does not inherit from NDArray - """ - a = [[3,-3, 1]] * self.ureg.kPa - b = [[4, 9, 2]] * self.ureg.m**2 - self.assertQuantityEqual(np.cross(a, b), [-15, -2, 39] * self.ureg.kPa * self.ureg.m**2) - - @unittest.expectedFailure - def test_power(self): - """This is not supported as different elements might end up with different units - - eg. ([1, 1] * m) ** [2, 3] - - Must force exponent to single value - """ - self._test2(np.power, self.q1, - (self.qless, np.asarray([1., 2, 3, 4])), - (self.q2, ),) - - @unittest.expectedFailure - def test_ones_like(self): - """Units are erased by emptyarra, Quantity does not inherit from NDArray - """ - self._test1(np.ones_like, - (self.q2, self.qs, self.qless, self.qi), - (), - 2) + @helpers.requires_array_function_protocol() + def test_shape_numpy_func(self): + self.assertEqual(np.shape(self.q), (2, 2)) + + @helpers.requires_array_function_protocol() + def test_alen_numpy_func(self): + self.assertEqual(np.alen(self.q), 2) + + @helpers.requires_array_function_protocol() + def test_ndim_numpy_func(self): + self.assertEqual(np.ndim(self.q), 2) + + @helpers.requires_array_function_protocol() + def test_copy_numpy_func(self): + q_copy = np.copy(self.q) + self.assertQuantityEqual(self.q, q_copy) + self.assertIsNot(self.q, q_copy) + + @helpers.requires_array_function_protocol() + def test_trim_zeros_numpy_func(self): + q = [0, 4, 3, 0, 2, 2, 0, 0, 0] * self.ureg.m + self.assertQuantityEqual(np.trim_zeros(q), [4, 3, 0, 2, 2] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_result_type_numpy_func(self): + self.assertEqual(np.result_type(self.q), np.dtype('int64')) + + @helpers.requires_array_function_protocol() + def test_nan_to_num_numpy_func(self): + self.assertQuantityEqual(np.nan_to_num(self.q_nan, nan=-999 * self.ureg.mm), + [[1, 2], [3, -0.999]] * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_meshgrid_numpy_func(self): + x = [1, 2] * self.ureg.m + y = [0, 50, 100] * self.ureg.mm + xx, yy = np.meshgrid(x, y) + self.assertQuantityEqual(xx, [[1, 2], [1, 2], [1, 2]] * self.ureg.m) + self.assertQuantityEqual(yy, [[0, 0], [50, 50], [100, 100]] * self.ureg.mm) + + @helpers.requires_array_function_protocol() + def test_isclose_numpy_func(self): + q2 = [[1000.05, 2000], [3000.00007, 4001]] * self.ureg.mm + np.testing.assert_equal(np.isclose(self.q, q2), np.array([[False, True], [True, False]])) + + @helpers.requires_array_function_protocol() + def test_interp_numpy_func(self): + x = [1, 4] * self.ureg.m + xp = np.linspace(0, 3, 5) * self.ureg.m + fp = self.Q_([0, 5, 10, 15, 20], self.ureg.degC) + self.assertQuantityAlmostEqual(np.interp(x, xp, fp), self.Q_([6.66667, 20.], self.ureg.degC), rtol=1e-5) + + def test_comparisons(self): + np.testing.assert_equal(self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]])) + np.testing.assert_equal(self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]])) @unittest.skip @@ -435,39 +807,3 @@ def test_right_shift(self): (self.qless, 2), (self.q1, self.q2, self.qs, ), 'same') - - -class TestNDArrayQuantityMath(QuantityTestCase): - - @helpers.requires_numpy() - def test_exponentiation_array_exp(self): - arr = np.array(range(3), dtype=np.float) - q = self.Q_(arr, 'meter') - - for op_ in [op.pow, op.ipow]: - q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, 2., q_cp) - arr_cp = copy.copy(arr) - arr_cp = copy.copy(arr) - q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, q_cp, arr_cp) - q_cp = copy.copy(q) - q2_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, q_cp, q2_cp) - - @unittest.expectedFailure - @helpers.requires_numpy() - def test_exponentiation_array_exp_2(self): - arr = np.array(range(3), dtype=np.float) - #q = self.Q_(copy.copy(arr), None) - q = self.Q_(copy.copy(arr), 'meter') - arr_cp = copy.copy(arr) - q_cp = copy.copy(q) - # this fails as expected since numpy 1.8.0 but... - self.assertRaises(DimensionalityError, op.pow, arr_cp, q_cp) - # ..not for op.ipow ! - # q_cp is treated as if it is an array. The units are ignored. - # Quantity.__ipow__ is never called - arr_cp = copy.copy(arr) - q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) From 66f67d3ee4344cf089bb30b5323113f19866a158 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 30 Oct 2019 18:13:11 -0500 Subject: [PATCH 148/612] Add __array_function__ based on changes by @andrewgsavage and @jthielen --- pint/quantity.py | 201 +++++++++++++++++++++++++++++++- pint/testsuite/test_numpy.py | 44 ++++--- pint/testsuite/test_quantity.py | 4 +- 3 files changed, 224 insertions(+), 25 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index f0bdd5fcd..53ad45929 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -26,10 +26,9 @@ from .definitions import UnitDefinition from .compat import ndarray, np, _to_magnitude from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, - to_units_container, infer_base_unit) + to_units_container, infer_base_unit, iterable, sized) from pint.compat import Loc - def _eq(first, second, check_all): """Comparison of scalars and arrays """ @@ -63,11 +62,10 @@ def wrapped(self, *args, **kwargs): return result return wrapped - def check_implemented(f): def wrapped(self, *args, **kwargs): other=args[0] - if other.__class__.__name__ in ["PintArray", "Series"]: + if other.__class__.__name__ in ["PintArray", "Series", "DataArray"]: return NotImplemented # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] # and expects Quantity * array[Quantity] should return NotImplemented @@ -77,6 +75,192 @@ def wrapped(self, *args, **kwargs): return result return wrapped +HANDLED_FUNCTIONS = {} + +def implements(numpy_function): + """Register an __array_function__ implementation for BaseQuantity objects.""" + def decorator(func): + HANDLED_FUNCTIONS[numpy_function] = func + return func + return decorator + +def _is_quantity_sequence(arg): + if iterable(arg) and sized(arg) and not isinstance(arg, string_types): + if isinstance(arg[0], Quantity): + if not all([isinstance(item, Quantity) for item in arg]): + raise TypeError("{} contains items that aren't Quantity type".format(arg)) + return True + return False + +def _get_first_input_units(args, kwargs={}): + args_combo = list(args)+list(kwargs.values()) + out_units=None + for arg in args_combo: + if isinstance(arg, Quantity): + out_units = arg.units + elif _is_quantity_sequence(arg): + out_units = arg[0].units + if out_units is not None: + break + return out_units + +def convert_to_consistent_units(pre_calc_units=None, *args, **kwargs): + """Takes the args for a numpy function and converts any Quantity or Sequence of Quantities + into the units of the first Quantiy/Sequence of quantities. Other args are left untouched. + """ + def convert_arg(arg): + if pre_calc_units is not None: + if isinstance(arg, Quantity): + return arg.m_as(pre_calc_units) + elif _is_quantity_sequence(arg): + return [item.m_as(pre_calc_units) for item in arg] + else: + if isinstance(arg, Quantity): + return arg.m + elif _is_quantity_sequence(arg): + return [item.m for item in arg] + return arg + + new_args=tuple(convert_arg(arg) for arg in args) + new_kwargs = {key:convert_arg(arg) for key,arg in kwargs.items()} + return new_args, new_kwargs + +def implement_func(func_str, pre_calc_units_, post_calc_units_, out_units_): + """ + :param func_str: The numpy function to implement + :type func_str: str + :param pre_calc_units: The units any quantity/ sequences of quantities should be converted to. + consistent_infer converts all qs to the first units found in args/kwargs + inconsistent does not convert any qs, eg for product + rad (or any other unit) converts qs to radians/ other unit + None converts qs to magnitudes without conversion + :type pre_calc_units: NoneType, str + :param pre_calc_units: The units the result of the function should be initiated as. + as_pre_calc uses the units it was converted to pre calc. Do not use with pre_calc_units="inconsistent" + rad (or any other unit) uses radians/ other unit + prod uses multiplies the input quantity units + None causes func to return without creating a quantity from the output, regardless of any out_units + :type out_units: NoneType, str + :param out_units: The units the result of the function should be returned to the user as. The quantity created in the post_calc_units will be converted to the out_units + None or as_post_calc uses the units the quantity was initiated in, ie the post_calc_units, without any conversion. + rad (or any other unit) uses radians/ other unit + infer_from_input uses the first input units found, as received by the function before any conversions. + :type out_units: NoneType, str + + """ + func = getattr(np,func_str) + + @implements(func) + def _(*args, **kwargs): + # TODO make work for kwargs + args_and_kwargs = list(args)+list(kwargs.values()) + + (pre_calc_units, post_calc_units, out_units)=(pre_calc_units_, post_calc_units_, out_units_) + first_input_units=_get_first_input_units(args, kwargs) + if pre_calc_units == "consistent_infer": + pre_calc_units = first_input_units + + if pre_calc_units == "inconsistent": + new_args, new_kwargs = args, kwargs + else: + new_args, new_kwargs = convert_to_consistent_units(pre_calc_units, *args, **kwargs) + res = func(*new_args, **new_kwargs) + + if post_calc_units is None: + return res + elif post_calc_units == "as_pre_calc": + post_calc_units = pre_calc_units + elif post_calc_units == "sum": + post_calc_units = (1*first_input_units + 1*first_input_units).units + elif post_calc_units == "prod": + product = 1 + for x in args_and_kwargs: + product *= x + post_calc_units = product.units + elif post_calc_units == "div": + product = first_input_units*first_input_units + for x in args_and_kwargs: + product /= x + post_calc_units = product.units + elif post_calc_units == "delta": + post_calc_units = (1*first_input_units-1*first_input_units).units + elif post_calc_units == "delta,div": + product=(1*first_input_units-1*first_input_units).units + for x in args_and_kwargs[1:]: + product /= x + post_calc_units = product.units + elif post_calc_units == "variance": + post_calc_units = ((1*first_input_units + 1*first_input_units)**2).units + Q_ = first_input_units._REGISTRY.Quantity + post_calc_Q_= Q_(res, post_calc_units) + + if out_units is None or out_units == "as_post_calc": + return post_calc_Q_ + elif out_units == "infer_from_input": + out_units = first_input_units + return post_calc_Q_.to(out_units) + +@implements(np.meshgrid) +def _meshgrid(*xi, **kwargs): + # Simply need to map input units to onto list of outputs + input_units = (x.units for x in xi) + res = np.meshgrid(*(x.m for x in xi), **kwargs) + return [out * unit for out, unit in zip(res, input_units)] + +@implements(np.full_like) +def _full_like(a, fill_value, dtype=None, order='K', subok=True, shape=None): + # Make full_like by multiplying with array from ones_like in a + # non-multiplicative-unit-safe way + if isinstance(fill_value, Quantity): + return fill_value._REGISTRY.Quantity( + np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) * fill_value.m, + fill_value.units) + else: + return (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) + * fill_value) + +@implements(np.interp) +def _interp(x, xp, fp, left=None, right=None, period=None): + # Need to handle x and y units separately + x_unit = _get_first_input_units([x, xp, period]) + y_unit = _get_first_input_units([fp, left, right]) + x_args, _ = convert_to_consistent_units(x_unit, x, xp, period) + y_args, _ = convert_to_consistent_units(y_unit, fp, left, right) + x, xp, period = x_args + fp, right, left = y_args + Q_ = y_unit._REGISTRY.Quantity + return Q_(np.interp(x, xp, fp, left=left, right=right, period=period), y_unit) + +for func_str in ['linspace', 'concatenate', 'block', 'stack', 'hstack', 'vstack', 'dstack', 'atleast_1d', 'column_stack', 'atleast_2d', 'atleast_3d', 'expand_dims','squeeze', 'swapaxes', 'compress', 'rollaxis', 'broadcast_to', 'moveaxis', 'fix', 'amax', 'amin', 'nanmax', 'nanmin', 'around', 'diagonal', 'mean', 'ptp', 'ravel', 'round_', 'sort', 'median', 'nanmedian', 'transpose', 'flip', 'copy', 'trim_zeros', 'append', 'clip', 'nan_to_num']: + implement_func(func_str, 'consistent_infer', 'as_pre_calc', 'as_post_calc') + +for func_str in ['isclose', 'searchsorted']: + implement_func(func_str, 'consistent_infer', None, None) + +for func_str in ['unwrap']: + implement_func(func_str, 'rad', 'rad', 'infer_from_input') + +for func_str in ['cumprod', 'cumproduct', 'nancumprod']: + implement_func(func_str, 'dimensionless', 'dimensionless', 'infer_from_input') + +for func_str in ['size', 'isreal', 'iscomplex', 'shape', 'ones_like', 'zeros_like', 'empty_like', 'argsort', 'argmin', 'argmax', 'alen', 'ndim', 'nanargmax', 'nanargmin', 'count_nonzero', 'nonzero', 'result_type']: + implement_func(func_str, None, None, None) + +for func_str in ['average', 'mean', 'std', 'nanmean', 'nanstd', 'sum', 'nansum', 'cumsum', 'nancumsum']: + implement_func(func_str, None, 'sum', None) + +for func_str in ['cross', 'trapz', 'dot']: + implement_func(func_str, None, 'prod', None) + +for func_str in ['diff', 'ediff1d',]: + implement_func(func_str, None, 'delta', None) + +for func_str in ['gradient', ]: + implement_func(func_str, None, 'delta,div', None) + +for func_str in ['var', 'nanvar']: + implement_func(func_str, None, 'variance', None) + @contextlib.contextmanager def printoptions(*args, **kwargs): @@ -102,6 +286,12 @@ class Quantity(PrettyIPython, SharedRegistryObject): :param units: units of the physical quantity to be created :type units: UnitsContainer, str or pint.Quantity """ + def __array_function__(self, func, types, args, kwargs): + if func not in HANDLED_FUNCTIONS: + return NotImplemented + if not all(issubclass(t, Quantity) for t in types): + return NotImplemented + return HANDLED_FUNCTIONS[func](*args, **kwargs) #: Default formatting string. default_format = '' @@ -1469,7 +1659,8 @@ def __getattr__(self, item): # Attributes starting with `__array_` are common attributes of NumPy ndarray. # They are requested by numpy functions. if item.startswith('__array_'): - warnings.warn("The unit of the quantity is stripped.", UnitStrippedWarning, stacklevel=2) + warnings.warn("The unit of the quantity is stripped when getting {} " + "attribute".format(item), UnitStrippedWarning, stacklevel=2) if isinstance(self._magnitude, ndarray): return getattr(self._magnitude, item) else: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 9294fff07..e32afa56b 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -30,18 +30,24 @@ def q_nan(self): @property def q_temperature(self): return self.Q_([[1,2],[3,4]], self.ureg.degC) - + + def assertNDArrayEqual(self, actual, desired): + # Assert that the given arrays are equal, and are not Quantities + np.testing.assert_array_equal(actual, desired) + self.assertFalse(isinstance(actual, self.Q_)) + self.assertFalse(isinstance(desired, self.Q_)) + class TestNumpyArrayCreation(TestNumpyMethods): # https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html @helpers.requires_array_function_protocol() def test_ones_like(self): - np.testing.assert_equal(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) + self.assertNDArrayEqual(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) @helpers.requires_array_function_protocol() def test_zeros_like(self): - np.testing.assert_equal(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) + self.assertNDArrayEqual(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) @helpers.requires_array_function_protocol() def test_empty_like(self): @@ -53,7 +59,7 @@ def test_empty_like(self): def test_full_like(self): self.assertQuantityEqual(np.full_like(self.q, self.Q_(0, self.ureg.degC)), self.Q_([[0, 0], [0, 0]], self.ureg.degC)) - np.testing.assert_equal(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) + self.assertNDArrayEqual(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) class TestNumpyArrayManipulation(TestNumpyMethods): #TODO @@ -309,7 +315,7 @@ def test_exponentiation_array_exp_2(self): self.assertRaises(DimensionalityError, op.pow, arr_cp, q_cp) # ..not for op.ipow ! # q_cp is treated as if it is an array. The units are ignored. - # BaseQuantity.__ipow__ is never called + # Quantity.__ipow__ is never called arr_cp = copy.copy(arr) q_cp = copy.copy(q) self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) @@ -362,11 +368,11 @@ def test_sort_numpy_func(self): def test_argsort(self): q = [1, 4, 5, 6, 2, 9] * self.ureg.MeV - np.testing.assert_array_equal(q.argsort(), [0, 4, 1, 2, 3, 5]) + self.assertNDArrayEqual(q.argsort(), [0, 4, 1, 2, 3, 5]) @helpers.requires_array_function_protocol() def test_argsort_numpy_func(self): - np.testing.assert_array_equal(np.argsort(self.q, axis=0), np.array([[0, 0], [1, 1]])) + self.assertNDArrayEqual(np.argsort(self.q, axis=0), np.array([[0, 0], [1, 1]])) def test_diagonal(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m @@ -385,7 +391,7 @@ def test_compress(self): def test_searchsorted(self): q = self.q.flatten() - np.testing.assert_array_equal(q.searchsorted([1.5, 2.5] * self.ureg.m), + self.assertNDArrayEqual(q.searchsorted([1.5, 2.5] * self.ureg.m), [1, 2]) q = self.q.flatten() self.assertRaises(DimensionalityError, q.searchsorted, [1.5, 2.5]) @@ -394,17 +400,17 @@ def test_searchsorted(self): def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" q = self.q.flatten() - np.testing.assert_array_equal(np.searchsorted(q, [1.5, 2.5] * self.ureg.m), + self.assertNDArrayEqual(np.searchsorted(q, [1.5, 2.5] * self.ureg.m), [1, 2]) def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m - np.testing.assert_array_equal(q.nonzero()[0], [0, 2, 3, 5]) + self.assertNDArrayEqual(q.nonzero()[0], [0, 2, 3, 5]) @helpers.requires_array_function_protocol() def test_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m - np.testing.assert_array_equal(np.nonzero(q)[0], [0, 2, 3, 5]) + self.assertNDArrayEqual(np.nonzero(q)[0], [0, 2, 3, 5]) @helpers.requires_array_function_protocol() def test_count_nonzero_numpy_func(self): @@ -439,11 +445,11 @@ def test_argmax(self): @helpers.requires_array_function_protocol() def test_argmax_numpy_func(self): - np.testing.assert_equal(np.argmax(self.q, axis=0), np.array([1, 1])) + self.assertNDArrayEqual(np.argmax(self.q, axis=0), np.array([1, 1])) @helpers.requires_array_function_protocol() def test_nanargmax_numpy_func(self): - np.testing.assert_equal(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) + self.assertNDArrayEqual(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) def test_min(self): self.assertEqual(self.q.min(), 1 * self.ureg.m) @@ -469,11 +475,11 @@ def test_argmin(self): @helpers.requires_array_function_protocol() def test_argmin_numpy_func(self): - np.testing.assert_equal(np.argmin(self.q, axis=0), np.array([0, 0])) + self.assertNDArrayEqual(np.argmin(self.q, axis=0), np.array([0, 0])) @helpers.requires_array_function_protocol() def test_nanargmin_numpy_func(self): - np.testing.assert_equal(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) + self.assertNDArrayEqual(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) def test_ptp(self): self.assertEqual(self.q.ptp(), 3 * self.ureg.m) @@ -658,7 +664,7 @@ def test_pickle(self): set_application_registry(self.ureg) def pickle_test(q): pq = pickle.loads(pickle.dumps(q)) - np.testing.assert_array_equal(q.magnitude, pq.magnitude) + self.assertNDArrayEqual(q.magnitude, pq.magnitude) self.assertEqual(q.units, pq.units) pickle_test([10,20]*self.ureg.m) @@ -719,7 +725,7 @@ def test_meshgrid_numpy_func(self): @helpers.requires_array_function_protocol() def test_isclose_numpy_func(self): q2 = [[1000.05, 2000], [3000.00007, 4001]] * self.ureg.mm - np.testing.assert_equal(np.isclose(self.q, q2), np.array([[False, True], [True, False]])) + self.assertNDArrayEqual(np.isclose(self.q, q2), np.array([[False, True], [True, False]])) @helpers.requires_array_function_protocol() def test_interp_numpy_func(self): @@ -729,8 +735,8 @@ def test_interp_numpy_func(self): self.assertQuantityAlmostEqual(np.interp(x, xp, fp), self.Q_([6.66667, 20.], self.ureg.degC), rtol=1e-5) def test_comparisons(self): - np.testing.assert_equal(self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]])) - np.testing.assert_equal(self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]])) + self.assertNDArrayEqual(self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]])) + self.assertNDArrayEqual(self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]])) @unittest.skip diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index e15923db8..c216a62c7 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -286,7 +286,9 @@ def test_retain_unit(self): self.assertEqual(q.u, q.reshape(2, 3).u) self.assertEqual(q.u, q.swapaxes(0, 1).u) self.assertEqual(q.u, q.mean().u) - self.assertEqual(q.u, np.compress((q==q[0,0]).any(0), q).u) + # TODO: Re-add np.compress implementation once mixed type is resolved + # (see https://github.com/hgrecco/pint/pull/764#issuecomment-523272038) + # self.assertEqual(q.u, np.compress((q==q[0,0]).any(0), q).u) def test_context_attr(self): self.assertEqual(self.ureg.meter, self.Q_(1, 'meter')) From 926ffdcafd1426678eb0d1a8bed914ed348f9a8f Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 10 Nov 2019 21:31:28 -0600 Subject: [PATCH 149/612] Add handling for mixed argument types --- pint/compat.py | 6 + pint/numpy_func.py | 450 ++++++++++++++++++++++++++ pint/quantity.py | 508 ++++-------------------------- pint/testsuite/test_numpy.py | 43 ++- pint/testsuite/test_numpy_func.py | 44 +++ 5 files changed, 604 insertions(+), 447 deletions(-) create mode 100644 pint/numpy_func.py create mode 100644 pint/testsuite/test_numpy_func.py diff --git a/pint/compat.py b/pint/compat.py index 483e3d623..aa3ed2c61 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -117,3 +117,9 @@ def _to_magnitude(value, force_ndarray=False): if not HAS_BABEL: Loc = babel_units = None + +# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and +# downcast/wrappable types +def is_upcast_type(other): + # Check if class name is in preset list + return other.__class__.__name__ in ("PintArray", "Series", "DataArray") diff --git a/pint/numpy_func.py b/pint/numpy_func.py new file mode 100644 index 000000000..557e7cbc9 --- /dev/null +++ b/pint/numpy_func.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +""" + pint.numpy_func + ~~~~~~~~~~~~~~~ + + :copyright: 2019 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from .compat import is_upcast_type, np, string_types +from .errors import DimensionalityError +from .util import iterable, sized + +HANDLED_UFUNCS = {} +HANDLED_FUNCTIONS = {} + + +# +# Shared Implementation Utilities +# + + +# TODO: make more robust and test +def _is_quantity_sequence(arg): + if iterable(arg) and sized(arg) and not isinstance(arg, string_types): + if hasattr(arg[0], 'units'): + if not all([hasattr(item, 'units') for item in arg]): + raise TypeError("{} contains items that aren't Quantity type".format(arg)) + return True + return False + + +def _get_first_input_units(args, kwargs={}): + args_combo = list(args) + list(kwargs.values()) + out_units=None + for arg in args_combo: + if hasattr(arg, 'units'): # TODO better check? + out_units = arg.units + elif _is_quantity_sequence(arg): + out_units = arg[0].units + if out_units is not None: + break + return out_units + + +def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): + """Takes the args for a numpy function and converts any Quantity or Sequence of Quantities + into the units of the first Quantiy/Sequence of quantities. Other args are left untouched + if pre_calc_units is None or dimensionless, otherwise a DimensionalityError is raised. + """ + def convert_arg(arg): + if pre_calc_units is not None: + if hasattr(arg, 'units'): # TODO better check? + return arg.m_as(pre_calc_units) + elif _is_quantity_sequence(arg): + return [item.m_as(pre_calc_units) for item in arg] + elif arg is not None: + if pre_calc_units.dimensionless: + return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) + else: + raise DimensionalityError('dimensionless', pre_calc_units) + else: + if hasattr(arg, 'units'): # TODO better check? + return arg.m + elif _is_quantity_sequence(arg): + return [item.m for item in arg] + return arg + + new_args = tuple(convert_arg(arg) for arg in args) + new_kwargs = {key: convert_arg(arg) for key, arg in kwargs.items()} + return new_args, new_kwargs + + +def unwrap_and_wrap_consistent_units(*args): + """Returns the given args as parsed by convert_to_consistent_units assuming units of first + arg with units, along with a wrapper to restore that unit to the output. + """ + first_input_units = _get_first_input_units(args) + args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) + return args, lambda value: first_input_units._REGISTRY.Quantity(value, first_input_units) + + +def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): + """Determine resulting unit from given operation.""" + if unit_op == "sum": + result_unit = (1 * first_input_units + 1 * first_input_units).units + elif unit_op == "mul": + product = first_input_units._REGISTRY.parse_units('') + for x in all_args: + print(x) + if hasattr(x, 'units'): + product *= x.units + result_unit = product + elif unit_op == "delta": + result_unit = (1 * first_input_units - 1 * first_input_units).units + elif unit_op == "delta,div": + product = (1 * first_input_units - 1 * first_input_units).units + for x in all_args[1:]: + if hasattr(x, 'units'): + product /= x.units + result_unit = product + elif unit_op == "div": + product = first_input_units._REGISTRY.parse_units('') + for x in all_args: + if hasattr(x, 'units'): + product /= x.units + result_unit = product + elif unit_op == "variance": + result_unit = ((1 * first_input_units + 1 * first_input_units)**2).units + elif unit_op == "square": + result_unit = first_input_units**2 + elif unit_op == "sqrt": + result_unit = first_input_units**0.5 + elif unit_op == "reciprocal": + result_unit = first_input_units**-1 + elif unit_op == "size": + if size is None: + raise ValueError('size argument must be given when unit_op=="size"') + result_unit = first_input_units**size + + else: + raise ValueError('Output unit method {} not understood'.format(unit_op)) + + return result_unit + + +def implements(numpy_func_string, func_type): + """Register an __array_function__/__array_ufunc__ implementation for Quantity objects.""" + def decorator(func): + if func_type == 'function': + HANDLED_FUNCTIONS[numpy_func_string] = func + elif func_type == 'ufunc': + HANDLED_UFUNCS[numpy_func_string] = func + else: + raise ValueError('Invalid func_type {}'.format(func_type)) + return func + return decorator + + +def implement_func(func_type, func_str, input_units=None, output_unit=None): + """TODO""" + func = getattr(np, func_str) + + @implements(func_str, func_type) + def implementation(*args, **kwargs): + args_and_kwargs = list(args) + list(kwargs.values()) + first_input_units = _get_first_input_units(args, kwargs) + if input_units == "all_consistent": + # Match all input args/kwargs to same units + stripped_args, stripped_kwargs = convert_to_consistent_units( + *args, pre_calc_units=first_input_units, **kwargs) + else: + # Match all input args/kwargs to input_units, or if input_units is None, simply + # strip units + stripped_args, stripped_kwargs = convert_to_consistent_units( + *args, pre_calc_units=input_units, **kwargs) + + # Determine result through base numpy function on stripped arguments + result_magnitude = func(*stripped_args, **stripped_kwargs) + + if output_unit is None: + # Short circuit and return magnitude alone + return result_magnitude + elif output_unit == "match_input": + result_unit = first_input_units + else: + result_unit = get_op_output_unit(output_unit, first_input_units, args_and_kwargs) + + return first_input_units._REGISTRY.Quantity(result_magnitude, result_unit) + + +""" +Define ufunc behavior collections. + +TODO: document as before + + +""" +matching_input_bare_output_ufuncs = ['equal', 'greater', 'greater_equal', 'less', + 'less_equal', 'not_equal'] +matching_input_set_units_output_ufuncs = {'arctan2': 'radian'} +set_units_ufuncs = {'cumprod': ('', ''), + 'arccos': ('', 'radian'), + 'arcsin': ('', 'radian'), + 'arctan': ('', 'radian'), + 'arccosh': ('', 'radian'), + 'arcsinh': ('', 'radian'), + 'arctanh': ('', 'radian'), + 'exp': ('', ''), + 'expm1': ('', ''), + 'exp2': ('', ''), + 'log': ('', ''), + 'log10': ('', ''), + 'log1p': ('', ''), + 'log2': ('', ''), + 'sin': ('radian', ''), + 'cos': ('radian', ''), + 'tan': ('radian', ''), + 'sinh': ('radian', ''), + 'cosh': ('radian', ''), + 'tanh': ('radian', ''), + 'radians': ('degree', 'radian'), + 'degrees': ('radian', 'degree'), + 'deg2rad': ('degree', 'radian'), + 'rad2deg': ('radian', 'degree'), + 'logaddexp': ('', ''), + 'logaddexp2': ('', '')} +matching_input_copy_units_output_ufuncs = ['compress', 'conj', 'conjugate', 'copy', + 'diagonal', 'max', 'mean', 'min', + 'ptp', 'ravel', 'repeat', 'reshape', 'round', + 'squeeze', 'swapaxes', 'take', 'trace', + 'transpose', 'ceil', 'floor', 'hypot', 'rint', + 'add', 'subtract', 'copysign', 'nextafter', + 'trunc', 'frexp', 'absolute', 'negative'] # TODO: review extra args/kwargs +copy_units_output_ufuncs = ['ldexp', 'fmod', 'mod', 'remainder'] +op_units_output_ufuncs = {'var': 'square', 'prod': 'size', 'multiply': 'mul', + 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', + 'remainder': 'div', 'sqrt': 'sqrt', 'square': 'square', + 'reciprocal': 'reciprocal', 'std': 'sum', 'sum': 'sum', + 'cumsum': 'sum'} + + +# Perform the standard ufunc implementations based on behavior collections +for ufunc_str in matching_input_bare_output_ufuncs: + # Require all inputs to match units, but output base ndarray + implement_func('ufunc', ufunc_str, input_units='all_consistent', output_unit=None) + +for ufunc_str, out_unit in matching_input_set_units_output_ufuncs.items(): + # Require all inputs to match units, but output in specified unit + implement_func('ufunc', ufunc_str, input_units='all_consistent', output_unit=out_unit) + +for ufunc_str, (in_unit, out_unit) in set_units_ufuncs.items(): + # Require inputs in specified unit, and output in specified unit + implement_func('ufunc', ufunc_str, input_units=in_unit, output_unit=out_unit) + +for ufunc_str in matching_input_copy_units_output_ufuncs: + # Require all inputs to match units, and output as first unit in arguments + implement_func('ufunc', ufunc_str, input_units='all_consistent', + output_unit='match_input') + +for ufunc_str in copy_units_output_ufuncs: + # Output as first unit in arguments, but do not convert inputs + implement_func('ufunc', ufunc_str, input_units=None, output_unit='match_input') + +for ufunc_str, unit_op in op_units_output_ufuncs.items(): + implement_func('ufunc', ufunc_str, input_units=None, output_unit=unit_op) + + +# TODO: modf (since it had the old modf__1) + + +""" +Define function behavior + +TODO: Document +""" + + +@implements('meshgrid', 'function') +def _meshgrid(*xi, **kwargs): + # Simply need to map input units to onto list of outputs + input_units = (x.units for x in xi) + res = np.meshgrid(*(x.m for x in xi), **kwargs) + return [out * unit for out, unit in zip(res, input_units)] + + +@implements('full_like', 'function') +def _full_like(a, fill_value, dtype=None, order='K', subok=True, shape=None): + # Make full_like by multiplying with array from ones_like in a + # non-multiplicative-unit-safe way + if hasattr(fill_value, '_REGISTRY'): + return fill_value._REGISTRY.Quantity( + (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) + * fill_value.m), fill_value.units) + else: + return (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) + * fill_value) + + +@implements('interp', 'function') +def _interp(x, xp, fp, left=None, right=None, period=None): + # Need to handle x and y units separately + (x, xp, period), _ = unwrap_and_wrap_consistent_units(x, xp, period) + (fp, right, left), output_wrap = unwrap_and_wrap_consistent_units(fp, left, right) + return output_wrap(np.interp(x, xp, fp, left=left, right=right, period=period)) + + +@implements('where', 'function') +def _where(condition, *args): + args, output_wrap = unwrap_and_wrap_consistent_units(*args) + return output_wrap(np.where(condition, *args)) + + +@implements('linspace', 'function') +def _linspace(start, stop, *args, **kwargs): + (start, stop), output_wrap = unwrap_and_wrap_consistent_units(start, stop) + return output_wrap(np.linspace(start, stop, *args, **kwargs)) + + +@implements('concatenate', 'function') +def _concatenate(sequence, *args, **kwargs): + sequence, output_wrap = unwrap_and_wrap_consistent_units(*sequence) + return output_wrap(np.concatenate(sequence, *args, **kwargs)) + + +@implements('stack', 'function') +def _stack(arrays, *args, **kwargs): + arrays, output_wrap = unwrap_and_wrap_consistent_units(*arrays) + return output_wrap(np.stack(arrays, *args, **kwargs)) + + +@implements('compress', 'function') +def _compress(condition, a, *args, **kwargs): + (a,), output_wrap = unwrap_and_wrap_consistent_units(a) + return output_wrap(np.compress(condition, a, *args, **kwargs)) + + +@implements('append', 'function') +def _append(arr, values, axis=None): + (arr, values), output_wrap = unwrap_and_wrap_consistent_units(arr, values) + return output_wrap(np.append(arr, values, axis)) + + +@implements('clip', 'function') +def _clip(a, a_min, a_max, *args, **kwargs): + (a, a_min, a_max), output_wrap = unwrap_and_wrap_consistent_units(a, a_min, a_max) + return output_wrap(np.clip(a, a_min, a_max, *args, **kwargs)) + + +@implements('nan_to_num', 'function') +def _nan_to_num(x, copy=True, nan=None, posinf=None, neginf=None): + if nan is None: + nan = 0.0 + elif isinstance(nan, x.__class__): + nan = nan.m_as(x.units) + posinf = posinf if not isinstance(posinf, x.__class__) else posinf.m_as(x.units) + neginf = neginf if not isinstance(neginf, x.__class__) else neginf.m_as(x.units) + result_magnitude = np.nan_to_num(x.magnitude, copy=False, nan=nan, posinf=posinf, + neginf=neginf) + if not copy: + x._magnitude = result_magnitude + + return x._REGISTRY.Quantity(result_magnitude, x.units) + + +@implements('isclose', 'function') +def _isclose(a, b, rtol=None, atol=None, equal_nan=False): + (a, b, rtol, atol), _ = unwrap_and_wrap_consistent_units(a, b, rtol, atol) + rtol = 1e-05 if rtol is None else rtol + atol = 1e-08 if atol is None else atol + return np.isclose(a, b, rtol, atol, equal_nan) + + +@implements('searchsorted', 'function') +def _searchsorted(a, v, *args, **kwargs): + (a, v), _ = unwrap_and_wrap_consistent_units(a, v) + return np.searchsorted(a, v, *args, **kwargs) + + +@implements('unwrap', 'function') +def _unwrap(p, discont=None, axis=-1): + # np.unwrap only dispatches over p argument, so assume it is a Quantity + discont = np.pi if discont is None else discont + return p._REGISTRY.Quantity(np.unwrap(p.m_as('rad'), discont, axis=axis), + 'rad').to(p.units) + +# Handle single unit argument operations (axis ops, aggregations, etc.) +def implement_single_unit(func_str): + func = getattr(np, func_str) + + @implements(func_str, 'function') + def implementation(a, *args, **kwargs): + (a,), output_wrap = unwrap_and_wrap_consistent_units(a) + return output_wrap(func(a, *args, **kwargs)) + + +for func_str in ['expand_dims', 'squeeze', 'rollaxis', 'moveaxis', 'fix', 'around', + 'diagonal', 'mean', 'ptp', 'ravel', 'round_', 'sort', 'median', 'nanmedian', + 'transpose', 'flip', 'copy', 'trim_zeros', 'average', 'nanmean']: + implement_single_unit(func_str) + + +# Handle atleast_nd functions +def implement_atleast_nd(func_str): + func = getattr(np, func_str) + @implements(func_str, 'function') + def implementation(*arrays): + stripped_arrays, _ = convert_to_consistent_units(*arrays) + arrays_magnitude = func(*stripped_arrays) + if len(arrays) > 1: + return [array_magnitude if not hasattr(original, '_REGISTRY') + else original._REGISTRY.Quantity(array_magnitude, original.units) + for array_magnitude, original in zip(arrays_magnitude, arrays)] + else: + output_unit = arrays[0].units + return output_unit._REGISTRY.Quantity(arrays_magnitude, output_unit) + + +for func_str in ['atleast_1d', 'atleast_2d', 'atleast_3d']: + implement_atleast_nd(func_str) + +# Handle single-argument consistent unit functions +for func_str in ['block', 'hstack', 'vstack', 'dstack', 'column_stack']: + implement_func('function', func_str, input_units='all_consistent', + output_unit='match_input') + +for func_str in ['cumprod', 'cumproduct', 'nancumprod']: + implement_func('function', func_str, input_units='dimensionless', + output_unit='match_input') + +for func_str in ['size', 'isreal', 'iscomplex', 'shape', 'ones_like', 'zeros_like', + 'empty_like', 'argsort', 'argmin', 'argmax', 'alen', 'ndim', 'nanargmax', + 'nanargmin', 'count_nonzero', 'nonzero', 'result_type']: + implement_func('function', func_str, input_units=None, output_unit=None) + +# TODO: Verify all these below with non-united other arguments \/ !! + +for func_str in ['std', 'nanstd', 'sum', 'nansum', 'cumsum', 'nancumsum']: + implement_func('function', func_str, input_units=None, output_unit='sum') + +for func_str in ['cross', 'trapz', 'dot']: + implement_func('function', func_str, input_units=None, output_unit='mul') + +for func_str in ['diff', 'ediff1d']: + implement_func('function', func_str, input_units=None, output_unit='delta') + +for func_str in ['gradient', ]: + implement_func('function', func_str, input_units=None, output_unit='delta,div') + +for func_str in ['var', 'nanvar']: + implement_func('function', func_str, input_units=None, output_unit='variance') + +# TODO: broadcast_to (how to handle subok?) +# TODO: result_type +# TODO: 'amax', 'amin', 'nanmax', 'nanmin' (version dependent signatures) +# TODO: nan_to_num (expected behavior with input values, sensible defaults?) + + +def numpy_wrap(func_type, func, args, kwargs, types): + # TODO: documentation + if func_type == 'function': + handled = HANDLED_FUNCTIONS + elif func_type == 'ufunc': + handled = HANDLED_UFUNCS + else: + raise ValueError('Invalid func_type {}'.format(func_type)) + + if func.__name__ not in handled or any(is_upcast_type(t) for t in types): + return NotImplemented + return handled[func.__name__](*args, **kwargs) diff --git a/pint/quantity.py b/pint/quantity.py index 53ad45929..dc3b9d63b 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -24,10 +24,15 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import ndarray, np, _to_magnitude +from .compat import Loc, ndarray, np, _to_magnitude, is_upcast_type from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, iterable, sized) -from pint.compat import Loc +from .numpy_func import (HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, + matching_input_bare_output_ufuncs, + matching_input_copy_units_output_ufuncs, + matching_input_set_units_output_ufuncs, numpy_wrap, + op_units_output_ufuncs, set_units_ufuncs) + def _eq(first, second, check_all): """Comparison of scalars and arrays @@ -62,10 +67,11 @@ def wrapped(self, *args, **kwargs): return result return wrapped + def check_implemented(f): def wrapped(self, *args, **kwargs): other=args[0] - if other.__class__.__name__ in ["PintArray", "Series", "DataArray"]: + if is_upcast_type(other): return NotImplemented # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] # and expects Quantity * array[Quantity] should return NotImplemented @@ -75,192 +81,6 @@ def wrapped(self, *args, **kwargs): return result return wrapped -HANDLED_FUNCTIONS = {} - -def implements(numpy_function): - """Register an __array_function__ implementation for BaseQuantity objects.""" - def decorator(func): - HANDLED_FUNCTIONS[numpy_function] = func - return func - return decorator - -def _is_quantity_sequence(arg): - if iterable(arg) and sized(arg) and not isinstance(arg, string_types): - if isinstance(arg[0], Quantity): - if not all([isinstance(item, Quantity) for item in arg]): - raise TypeError("{} contains items that aren't Quantity type".format(arg)) - return True - return False - -def _get_first_input_units(args, kwargs={}): - args_combo = list(args)+list(kwargs.values()) - out_units=None - for arg in args_combo: - if isinstance(arg, Quantity): - out_units = arg.units - elif _is_quantity_sequence(arg): - out_units = arg[0].units - if out_units is not None: - break - return out_units - -def convert_to_consistent_units(pre_calc_units=None, *args, **kwargs): - """Takes the args for a numpy function and converts any Quantity or Sequence of Quantities - into the units of the first Quantiy/Sequence of quantities. Other args are left untouched. - """ - def convert_arg(arg): - if pre_calc_units is not None: - if isinstance(arg, Quantity): - return arg.m_as(pre_calc_units) - elif _is_quantity_sequence(arg): - return [item.m_as(pre_calc_units) for item in arg] - else: - if isinstance(arg, Quantity): - return arg.m - elif _is_quantity_sequence(arg): - return [item.m for item in arg] - return arg - - new_args=tuple(convert_arg(arg) for arg in args) - new_kwargs = {key:convert_arg(arg) for key,arg in kwargs.items()} - return new_args, new_kwargs - -def implement_func(func_str, pre_calc_units_, post_calc_units_, out_units_): - """ - :param func_str: The numpy function to implement - :type func_str: str - :param pre_calc_units: The units any quantity/ sequences of quantities should be converted to. - consistent_infer converts all qs to the first units found in args/kwargs - inconsistent does not convert any qs, eg for product - rad (or any other unit) converts qs to radians/ other unit - None converts qs to magnitudes without conversion - :type pre_calc_units: NoneType, str - :param pre_calc_units: The units the result of the function should be initiated as. - as_pre_calc uses the units it was converted to pre calc. Do not use with pre_calc_units="inconsistent" - rad (or any other unit) uses radians/ other unit - prod uses multiplies the input quantity units - None causes func to return without creating a quantity from the output, regardless of any out_units - :type out_units: NoneType, str - :param out_units: The units the result of the function should be returned to the user as. The quantity created in the post_calc_units will be converted to the out_units - None or as_post_calc uses the units the quantity was initiated in, ie the post_calc_units, without any conversion. - rad (or any other unit) uses radians/ other unit - infer_from_input uses the first input units found, as received by the function before any conversions. - :type out_units: NoneType, str - - """ - func = getattr(np,func_str) - - @implements(func) - def _(*args, **kwargs): - # TODO make work for kwargs - args_and_kwargs = list(args)+list(kwargs.values()) - - (pre_calc_units, post_calc_units, out_units)=(pre_calc_units_, post_calc_units_, out_units_) - first_input_units=_get_first_input_units(args, kwargs) - if pre_calc_units == "consistent_infer": - pre_calc_units = first_input_units - - if pre_calc_units == "inconsistent": - new_args, new_kwargs = args, kwargs - else: - new_args, new_kwargs = convert_to_consistent_units(pre_calc_units, *args, **kwargs) - res = func(*new_args, **new_kwargs) - - if post_calc_units is None: - return res - elif post_calc_units == "as_pre_calc": - post_calc_units = pre_calc_units - elif post_calc_units == "sum": - post_calc_units = (1*first_input_units + 1*first_input_units).units - elif post_calc_units == "prod": - product = 1 - for x in args_and_kwargs: - product *= x - post_calc_units = product.units - elif post_calc_units == "div": - product = first_input_units*first_input_units - for x in args_and_kwargs: - product /= x - post_calc_units = product.units - elif post_calc_units == "delta": - post_calc_units = (1*first_input_units-1*first_input_units).units - elif post_calc_units == "delta,div": - product=(1*first_input_units-1*first_input_units).units - for x in args_and_kwargs[1:]: - product /= x - post_calc_units = product.units - elif post_calc_units == "variance": - post_calc_units = ((1*first_input_units + 1*first_input_units)**2).units - Q_ = first_input_units._REGISTRY.Quantity - post_calc_Q_= Q_(res, post_calc_units) - - if out_units is None or out_units == "as_post_calc": - return post_calc_Q_ - elif out_units == "infer_from_input": - out_units = first_input_units - return post_calc_Q_.to(out_units) - -@implements(np.meshgrid) -def _meshgrid(*xi, **kwargs): - # Simply need to map input units to onto list of outputs - input_units = (x.units for x in xi) - res = np.meshgrid(*(x.m for x in xi), **kwargs) - return [out * unit for out, unit in zip(res, input_units)] - -@implements(np.full_like) -def _full_like(a, fill_value, dtype=None, order='K', subok=True, shape=None): - # Make full_like by multiplying with array from ones_like in a - # non-multiplicative-unit-safe way - if isinstance(fill_value, Quantity): - return fill_value._REGISTRY.Quantity( - np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) * fill_value.m, - fill_value.units) - else: - return (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) - * fill_value) - -@implements(np.interp) -def _interp(x, xp, fp, left=None, right=None, period=None): - # Need to handle x and y units separately - x_unit = _get_first_input_units([x, xp, period]) - y_unit = _get_first_input_units([fp, left, right]) - x_args, _ = convert_to_consistent_units(x_unit, x, xp, period) - y_args, _ = convert_to_consistent_units(y_unit, fp, left, right) - x, xp, period = x_args - fp, right, left = y_args - Q_ = y_unit._REGISTRY.Quantity - return Q_(np.interp(x, xp, fp, left=left, right=right, period=period), y_unit) - -for func_str in ['linspace', 'concatenate', 'block', 'stack', 'hstack', 'vstack', 'dstack', 'atleast_1d', 'column_stack', 'atleast_2d', 'atleast_3d', 'expand_dims','squeeze', 'swapaxes', 'compress', 'rollaxis', 'broadcast_to', 'moveaxis', 'fix', 'amax', 'amin', 'nanmax', 'nanmin', 'around', 'diagonal', 'mean', 'ptp', 'ravel', 'round_', 'sort', 'median', 'nanmedian', 'transpose', 'flip', 'copy', 'trim_zeros', 'append', 'clip', 'nan_to_num']: - implement_func(func_str, 'consistent_infer', 'as_pre_calc', 'as_post_calc') - -for func_str in ['isclose', 'searchsorted']: - implement_func(func_str, 'consistent_infer', None, None) - -for func_str in ['unwrap']: - implement_func(func_str, 'rad', 'rad', 'infer_from_input') - -for func_str in ['cumprod', 'cumproduct', 'nancumprod']: - implement_func(func_str, 'dimensionless', 'dimensionless', 'infer_from_input') - -for func_str in ['size', 'isreal', 'iscomplex', 'shape', 'ones_like', 'zeros_like', 'empty_like', 'argsort', 'argmin', 'argmax', 'alen', 'ndim', 'nanargmax', 'nanargmin', 'count_nonzero', 'nonzero', 'result_type']: - implement_func(func_str, None, None, None) - -for func_str in ['average', 'mean', 'std', 'nanmean', 'nanstd', 'sum', 'nansum', 'cumsum', 'nancumsum']: - implement_func(func_str, None, 'sum', None) - -for func_str in ['cross', 'trapz', 'dot']: - implement_func(func_str, None, 'prod', None) - -for func_str in ['diff', 'ediff1d',]: - implement_func(func_str, None, 'delta', None) - -for func_str in ['gradient', ]: - implement_func(func_str, None, 'delta,div', None) - -for func_str in ['var', 'nanvar']: - implement_func(func_str, None, 'variance', None) - @contextlib.contextmanager def printoptions(*args, **kwargs): @@ -286,12 +106,6 @@ class Quantity(PrettyIPython, SharedRegistryObject): :param units: units of the physical quantity to be created :type units: UnitsContainer, str or pint.Quantity """ - def __array_function__(self, func, types, args, kwargs): - if func not in HANDLED_FUNCTIONS: - return NotImplemented - if not all(issubclass(t, Quantity) for t in types): - return NotImplemented - return HANDLED_FUNCTIONS[func](*args, **kwargs) #: Default formatting string. default_format = '' @@ -1492,62 +1306,57 @@ def __bool__(self): __nonzero__ = __bool__ - # NumPy Support - __radian = 'radian' - __same_units = 'equal greater greater_equal less less_equal not_equal arctan2'.split() - #: Dictionary mapping ufunc/attributes names to the units that they - #: require (conversion will be tried). - __require_units = {'cumprod': '', - 'arccos': '', 'arcsin': '', 'arctan': '', - 'arccosh': '', 'arcsinh': '', 'arctanh': '', - 'exp': '', 'expm1': '', 'exp2': '', - 'log': '', 'log10': '', 'log1p': '', 'log2': '', - 'sin': __radian, 'cos': __radian, 'tan': __radian, - 'sinh': __radian, 'cosh': __radian, 'tanh': __radian, - 'radians': 'degree', 'degrees': __radian, - 'deg2rad': 'degree', 'rad2deg': __radian, - 'logaddexp': '', 'logaddexp2': ''} - - #: Dictionary mapping ufunc/attributes names to the units that they - #: will set on output. - __set_units = {'cos': '', 'sin': '', 'tan': '', - 'cosh': '', 'sinh': '', 'tanh': '', - 'log': '', 'exp': '', - 'arccos': __radian, 'arcsin': __radian, - 'arctan': __radian, 'arctan2': __radian, - 'arccosh': __radian, 'arcsinh': __radian, - 'arctanh': __radian, - 'degrees': 'degree', 'radians': __radian, - 'expm1': '', 'cumprod': '', - 'rad2deg': 'degree', 'deg2rad': __radian} - - #: List of ufunc/attributes names in which units are copied from the - #: original. - __copy_units = 'compress conj conjugate copy cumsum diagonal flatten ' \ - 'max mean min ptp ravel repeat reshape round ' \ - 'squeeze std sum swapaxes take trace transpose ' \ - 'ceil floor hypot rint ' \ - 'add subtract ' \ - 'copysign nextafter trunc ' \ - 'frexp ldexp modf modf__1 ' \ - 'absolute negative remainder fmod mod'.split() - - #: Dictionary mapping ufunc/attributes names to the units that they will - #: set on output. The value is interpreted as the power to which the unit - #: will be raised. - __prod_units = {'var': 2, 'prod': 'size', 'multiply': 'mul', - 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', - 'remainder': 'div', - 'sqrt': .5, 'square': 2, 'reciprocal': -1} - - __skip_other_args = 'ldexp multiply ' \ - 'true_divide divide floor_divide fmod mod ' \ - 'remainder'.split() - - __handled = tuple(__same_units) + \ - tuple(__require_units.keys()) + \ - tuple(__prod_units.keys()) + \ - tuple(__copy_units) + tuple(__skip_other_args) + # NumPy function/ufunc support + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method != "__call__": + # Only handle ufuncs as callables + return NotImplemented + + # Replicate types from __array_function__ + types = set(type(arg) for arg in list(inputs) + list(kwargs.values()) + if hasattr(arg, "__array_ufunc__")) + + return numpy_wrap('ufunc', ufunc, inputs, kwargs, types) + + def __array_function__(self, func, types, args, kwargs): + return numpy_wrap('function', func, args, kwargs, types) + + def _ufunc_method_wrap(self, func, *args, **kwargs): + """Convenience method to wrap on the fly NumPy ndarray methods taking + care of the units. + """ + # Set input units if needed + if func.__name__ in set_units_ufuncs: + self.__ito_if_needed(set_units_ufuncs[func.__name__][0]) + + value = func(*args, **kwargs) + + # Set output units as needed + if (func.__name__ in + matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs): + output_unit = self._units + elif func.__name__ in set_units_ufuncs: + output_unit = set_units_ufuncs[func.__name__][1] + elif func.__name__ in matching_input_set_units_output_ufuncs: + output_unit = matching_input_set_units_output_ufuncs[func.__name__] + elif func.__name__ in op_units_output_ufuncs: + output_unit = get_op_output_unit(op_units_output_ufuncs[func.__name__], + self.units, + list(args) + list(kwargs.values()), + self._magnitude.size) + else: + output_unit = None + + if output_unit is not None: + return self.__class__(value, output_unit) + else: + return value + + def flatten(self, order='C'): + """Wrap ndarray.flatten.""" + return self.__class__( + _to_magnitude(self._magnitude, force_ndarray=True).flatten(order=order), + self._units) def clip(self, first=None, second=None, out=None, **kwargs): min = kwargs.get('min', first) @@ -1632,26 +1441,6 @@ def __ito_if_needed(self, to_units): self.ito(to_units) - def __numpy_method_wrap(self, func, *args, **kwargs): - """Convenience method to wrap on the fly numpy method taking - care of the units. - """ - if func.__name__ in self.__require_units: - self.__ito_if_needed(self.__require_units[func.__name__]) - - value = func(*args, **kwargs) - - if func.__name__ in self.__copy_units: - return self.__class__(value, self._units) - - if func.__name__ in self.__prod_units: - tmp = self.__prod_units[func.__name__] - if tmp == 'size': - return self.__class__(value, self._units ** self._magnitude.size) - return self.__class__(value, self._units ** tmp) - - return value - def __len__(self): return len(self._magnitude) @@ -1668,13 +1457,14 @@ def __getattr__(self, item): # we convert the magnitude to a numpy ndarray. self._magnitude = _to_magnitude(self._magnitude, force_ndarray=True) return getattr(self._magnitude, item) - elif item in self.__handled: - if not isinstance(self._magnitude, ndarray): - self._magnitude = _to_magnitude(self._magnitude, True) + elif item in HANDLED_UFUNCS: + # TODO: Fix for duck arrays + magnitude = _to_magnitude(self._magnitude, True) attr = getattr(self._magnitude, item) if callable(attr): - return functools.partial(self.__numpy_method_wrap, attr) - return attr + return functools.partial(self._ufunc_method_wrap, attr) + else: + raise AttributeError('NumPy ufunc attribute {} was not callable.'.format(item)) try: return getattr(self._magnitude, item) except AttributeError as ex: @@ -1730,172 +1520,6 @@ def tolist(self): return [self.__class__(value, units).tolist() if isinstance(value, list) else self.__class__(value, units) for value in self._magnitude.tolist()] - __array_priority__ = 17 - - def _call_ufunc(self, ufunc, *inputs, **kwargs): - # Store the destination units - dst_units = None - # List of magnitudes of Quantities with the right units - # to be used as argument of the ufunc - mobjs = None - - if ufunc.__name__ in self.__require_units: - # ufuncs in __require_units - # require specific units - # This is more complex that it should be due to automatic - # conversion between radians/dimensionless - # TODO: maybe could be simplified using Contexts - dst_units = self.__require_units[ufunc.__name__] - if dst_units == 'radian': - mobjs = [] - for other in inputs: - unt = getattr(other, '_units', '') - if unt == 'radian': - mobjs.append(getattr(other, 'magnitude', other)) - else: - factor, units = self._REGISTRY._get_root_units(unt) - if units and units != UnitsContainer({'radian': 1}): - raise DimensionalityError(units, dst_units) - mobjs.append(getattr(other, 'magnitude', other) * factor) - mobjs = tuple(mobjs) - else: - dst_units = self._REGISTRY.parse_expression(dst_units)._units - - elif len(inputs) > 1 and ufunc.__name__ not in self.__skip_other_args: - # ufunc with multiple arguments require that all inputs have - # the same arguments unless they are in __skip_other_args - dst_units = getattr(inputs[0], "_units", None) - - # Do the conversion (if needed) and extract the magnitude for each input. - if mobjs is None: - if dst_units is not None: - mobjs = tuple(self._REGISTRY.convert(getattr(other, 'magnitude', other), - getattr(other, 'units', ''), - dst_units) - for other in inputs) - else: - mobjs = tuple(getattr(other, 'magnitude', other) - for other in inputs) - - # call the ufunc - try: - return ufunc(*mobjs) - except Exception as ex: - raise _Exception(ex) - - - def _wrap_output(self, ufname, i, objs, out): - """we set the units of the output value""" - if i > 0: - ufname = "{}__{}".format(ufname, i) - - if ufname in self.__set_units: - try: - out = self.__class__(out, self.__set_units[ufname]) - except: - raise _Exception(ValueError) - elif ufname in self.__copy_units: - try: - out = self.__class__(out, self._units) - except: - raise _Exception(ValueError) - elif ufname in self.__prod_units: - tmp = self.__prod_units[ufname] - if tmp == 'size': - out = self.__class__(out, self._units ** self._magnitude.size) - elif tmp == 'div': - units1 = objs[0]._units if isinstance(objs[0], self.__class__) else UnitsContainer() - units2 = objs[1]._units if isinstance(objs[1], self.__class__) else UnitsContainer() - out = self.__class__(out, units1 / units2) - elif tmp == 'mul': - units1 = objs[0]._units if isinstance(objs[0], self.__class__) else UnitsContainer() - units2 = objs[1]._units if isinstance(objs[1], self.__class__) else UnitsContainer() - out = self.__class__(out, units1 * units2) - else: - out = self.__class__(out, self._units ** tmp) - - return out - - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - if method != "__call__": - return NotImplemented - - try: - out = self._call_ufunc(ufunc, *inputs, **kwargs) - if isinstance(out, tuple): - ret = tuple(self._wrap_output(ufunc.__name__, i, inputs, o) - for i, o in enumerate(out)) - return ret - else: - return self._wrap_output(ufunc.__name__, 0, inputs, out) - except (DimensionalityError, UndefinedUnitError): - raise - except _Exception as ex: - raise ex.internal - except: - return NotImplemented - - - def __array_prepare__(self, obj, context=None): - # If this uf is handled by Pint, write it down in the handling dictionary. - - # name of the ufunc, argument of the ufunc, domain of the ufunc - # In ufuncs with multiple outputs, domain indicates which output - # is currently being prepared (eg. see modf). - # In ufuncs with a single output, domain is 0 - uf, objs, i_out = context - - if uf.__name__ in self.__handled and i_out == 0: - # Only one ufunc should be handled at a time. - # If a ufunc is already being handled (and this is not another domain), - # something is wrong.. - if self.__handling: - raise Exception('Cannot handled nested ufuncs.\n' - 'Current: {}\n' - 'New: {}'.format(context, self.__handling)) - self.__handling = context - - return obj - - def __array_wrap__(self, obj, context=None): - uf, objs, i_out = context - - # if this ufunc is not handled by Pint, pass it to the magnitude. - if uf.__name__ not in self.__handled: - return self.magnitude.__array_wrap__(obj, context) - - try: - # First, we check the units of the input arguments. - - if i_out == 0: - out = self._call_ufunc(uf, *objs) - # If there are multiple outputs, - # store them in __handling (uf, objs, i_out, out0, out1, ...) - # and return the first - if uf.nout > 1: - self.__handling += out - out = out[0] - else: - # If this is not the first output, - # just grab the result that was previously calculated. - out = self.__handling[3 + i_out] - - return self._wrap_output(uf.__name__, i_out, objs, out) - except (DimensionalityError, UndefinedUnitError) as ex: - raise ex - except _Exception as ex: - raise ex.internal - except Exception as ex: - print(ex) - finally: - # If this is the last output argument for the ufunc, - # we are done handling this ufunc. - if uf.nout == i_out + 1: - self.__handling = None - - return self.magnitude.__array_wrap__(obj, context) - # Measurement support def plus_minus(self, error, relative=False): if isinstance(error, self.__class__): diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index e32afa56b..7e70b1ccd 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -116,14 +116,26 @@ def test_flip_numpy_func(self): @helpers.requires_array_function_protocol() def test_atleast_1d(self): + actual = np.atleast_1d(self.Q_(0, self.ureg.degC), self.q.flatten()) + expected = (self.Q_(np.array([0]), self.ureg.degC), self.q.flatten()) + for ind_actual, ind_expected in zip(actual, expected): + self.assertQuantityEqual(ind_actual, ind_expected) self.assertQuantityEqual(np.atleast_1d(self.q), self.q) - + @helpers.requires_array_function_protocol() def test_atleast_2d(self): + actual = np.atleast_2d(self.Q_(0, self.ureg.degC), self.q.flatten()) + expected = (self.Q_(np.array([[0]]), self.ureg.degC), np.array([[1, 2, 3, 4]]) * self.ureg.m) + for ind_actual, ind_expected in zip(actual, expected): + self.assertQuantityEqual(ind_actual, ind_expected) self.assertQuantityEqual(np.atleast_2d(self.q), self.q) - + @helpers.requires_array_function_protocol() def test_atleast_3d(self): + actual = np.atleast_3d(self.Q_(0, self.ureg.degC), self.q.flatten()) + expected = (self.Q_(np.array([[[0]]]), self.ureg.degC), np.array([[[1], [2], [3], [4]]]) * self.ureg.m) + for ind_actual, ind_expected in zip(actual, expected): + self.assertQuantityEqual(ind_actual, ind_expected) self.assertQuantityEqual(np.atleast_3d(self.q), np.array([[[1],[2]],[[3],[4]]])* self.ureg.m) @helpers.requires_array_function_protocol() @@ -271,7 +283,6 @@ def test_gradient(self): self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.delta_degC / self.ureg.J) self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.delta_degC / self.ureg.J) - @helpers.requires_array_function_protocol() def test_cross(self): a = [[3,-3, 1]] * self.ureg.kPa @@ -287,7 +298,18 @@ def test_dot_numpy_func(self): self.assertQuantityEqual(np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m) # Arithmetic operations - + def test_addition_with_scalar(self): + a = np.array([0, 1, 2]) + b = 10. * self.ureg('gram/kilogram') + self.assertQuantityAlmostEqual(a + b, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless)) + self.assertQuantityAlmostEqual(b + a, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless)) + + def test_addition_with_incompatible_scalar(self): + a = np.array([0, 1, 2]) + b = 1. * self.ureg.m + self.assertRaises(DimensionalityError, op.add, a, b) + self.assertRaises(DimensionalityError, op.add, b, a) + def test_power(self): arr = np.array(range(3), dtype=np.float) q = self.Q_(arr, 'meter') @@ -389,6 +411,11 @@ def test_compress(self): self.assertQuantityEqual(self.q.compress([False, True], axis=1), [[2], [4]] * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_compress(self): + self.assertQuantityEqual(np.compress([False, True], self.q, axis=1), + [[2], [4]] * self.ureg.m) + def test_searchsorted(self): q = self.q.flatten() self.assertNDArrayEqual(q.searchsorted([1.5, 2.5] * self.ureg.m), @@ -539,7 +566,7 @@ def test_mean(self): @helpers.requires_array_function_protocol() def test_mean_numpy_func(self): self.assertEqual(np.mean(self.q), 2.5 * self.ureg.m) - self.assertRaises(OffsetUnitCalculusError, np.mean, self.q_temperature) + self.assertEqual(np.mean(self.q_temperature), self.Q_(2.5, self.ureg.degC)) @helpers.requires_array_function_protocol() def test_nanmean_numpy_func(self): @@ -738,6 +765,12 @@ def test_comparisons(self): self.assertNDArrayEqual(self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]])) self.assertNDArrayEqual(self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]])) + @helpers.requires_array_function_protocol() + def test_where(self): + self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 0 * self.ureg.m), + [[0, 2], [3, 4]] * self.ureg.m) + self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py new file mode 100644 index 000000000..d21f62e58 --- /dev/null +++ b/pint/testsuite/test_numpy_func.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +import pint.numpy_func + +from pint.testsuite import QuantityTestCase, helpers +from pint.numpy_func import implements +from unittest.mock import patch + + +@helpers.requires_numpy() +class TestNumPyFuncUtils(QuantityTestCase): + + FORCE_NDARRAY = True + + @classmethod + def setUpClass(cls): + from pint import _DEFAULT_REGISTRY + cls.ureg = _DEFAULT_REGISTRY + cls.Q_ = cls.ureg.Quantity + + @patch('pint.numpy_func.HANDLED_FUNCTIONS', {}) + @patch('pint.numpy_func.HANDLED_UFUNCS', {}) + def test_implements(self): + # Test for functions + @implements('test', 'function') + def test_function(): + pass + + self.assertEqual(pint.numpy_func.HANDLED_FUNCTIONS['test'], test_function) + + # Test for ufuncs + @implements('test', 'ufunc') + def test_ufunc(): + pass + + self.assertEqual(pint.numpy_func.HANDLED_UFUNCS['test'], test_ufunc) + + # Test for invalid func type + with self.assertRaises(ValueError): + @implements('test', 'invalid') + def test_invalid(): + pass + + # TODO: fill in other functions in numpy_func From 966ed8c137c2d9a34cb8b336c1d927ee21f8f04b Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 3 Dec 2019 15:24:21 -0600 Subject: [PATCH 150/612] Initial changed behavior warning modifications suggested by @crusaderky --- pint/compat.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index aa3ed2c61..365a3864d 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -25,22 +25,21 @@ def tokenizer(input_string): # TODO: remove this warning after v0.10 class BehaviorChangeWarning(UserWarning): pass -_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/xxxx. ' -'To hide this warning use the following code to import pint:' -""" +_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 use the following code to import pint: + import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") import pint + To disable the new behavior, see https://www.numpy.org/neps/nep-0018-array-function-protocol.html#implementation ---- -""") +""" try: From 8ff3c7123b00b7c6890256be24fd705d4792baaf Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 3 Dec 2019 21:40:23 -0600 Subject: [PATCH 151/612] Remove oft-problematic in-place conversion to ndarray, with notes for future array-like compat --- pint/quantity.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index dc3b9d63b..a936aa1cd 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1455,12 +1455,13 @@ def __getattr__(self, item): else: # If an `__array_` attributes is requested but the magnitude is not an ndarray, # we convert the magnitude to a numpy ndarray. - self._magnitude = _to_magnitude(self._magnitude, force_ndarray=True) - return getattr(self._magnitude, item) + # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays + magnitude_as_array = _to_magnitude(self._magnitude, force_ndarray=True) + return getattr(magnitude_as_array, item) elif item in HANDLED_UFUNCS: - # TODO: Fix for duck arrays - magnitude = _to_magnitude(self._magnitude, True) - attr = getattr(self._magnitude, item) + # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars + magnitude_as_array = _to_magnitude(self._magnitude, True) + attr = getattr(magnitude_as_array, item) if callable(attr): return functools.partial(self._ufunc_method_wrap, attr) else: From fea90f29d466e6c08f02bfdb403ad5ede08228d7 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 3 Dec 2019 23:46:08 -0600 Subject: [PATCH 152/612] Fixup to get all tests passing --- pint/compat.py | 12 ++++ pint/numpy_func.py | 117 +++++++++++++++++++++++++++----- pint/quantity.py | 32 +++------ pint/testsuite/test_numpy.py | 5 -- pint/testsuite/test_quantity.py | 4 +- 5 files changed, 124 insertions(+), 46 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 365a3864d..254a7b11c 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -78,6 +78,8 @@ def __array_function__(self, *args, **kwargs): if HAS_NUMPY_ARRAY_FUNCTION: warnings.warn(_msg, BehaviorChangeWarning) + NP_NO_VALUE = np._NoValue + except ImportError: np = None @@ -89,6 +91,7 @@ class ndarray: NUMPY_VER = '0' NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False + NP_NO_VALUE = None def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: @@ -122,3 +125,12 @@ def _to_magnitude(value, force_ndarray=False): def is_upcast_type(other): # Check if class name is in preset list return other.__class__.__name__ in ("PintArray", "Series", "DataArray") + + +def eq(first, second, check_all): + """Comparison of scalars and arrays + """ + out = first == second + if check_all and isinstance(out, ndarray): + return np.all(out) + return out diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 557e7cbc9..eb13daad0 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -7,7 +7,7 @@ :license: BSD, see LICENSE for more details. """ -from .compat import is_upcast_type, np, string_types +from .compat import NP_NO_VALUE, is_upcast_type, np, string_types, eq from .errors import DimensionalityError from .util import iterable, sized @@ -100,8 +100,9 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): product /= x.units result_unit = product elif unit_op == "div": - product = first_input_units._REGISTRY.parse_units('') - for x in all_args: + # Start with first arg in numerator, all others in denominator + product = getattr(all_args[0], 'units', first_input_units._REGISTRY.parse_units('')) + for x in all_args[1:]: if hasattr(x, 'units'): product /= x.units result_unit = product @@ -139,6 +140,10 @@ def decorator(func): def implement_func(func_type, func_str, input_units=None, output_unit=None): """TODO""" + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + func = getattr(np, func_str) @implements(func_str, func_type) @@ -151,7 +156,7 @@ def implementation(*args, **kwargs): *args, pre_calc_units=first_input_units, **kwargs) else: # Match all input args/kwargs to input_units, or if input_units is None, simply - # strip units + # strip units stripped_args, stripped_kwargs = convert_to_consistent_units( *args, pre_calc_units=input_units, **kwargs) @@ -163,8 +168,11 @@ def implementation(*args, **kwargs): return result_magnitude elif output_unit == "match_input": result_unit = first_input_units - else: + elif output_unit in ['sum', 'mul', 'delta', 'delta,div', 'div', 'variance', 'square', + 'sqrt', 'reciprocal', 'size']: result_unit = get_op_output_unit(output_unit, first_input_units, args_and_kwargs) + else: + result_unit = output_unit return first_input_units._REGISTRY.Quantity(result_magnitude, result_unit) @@ -176,6 +184,7 @@ def implementation(*args, **kwargs): """ +strip_unit_input_output_ufuncs = ['isnan', 'isinf', 'isfinite', 'signbit'] matching_input_bare_output_ufuncs = ['equal', 'greater', 'greater_equal', 'less', 'less_equal', 'not_equal'] matching_input_set_units_output_ufuncs = {'arctan2': 'radian'} @@ -205,22 +214,28 @@ def implementation(*args, **kwargs): 'rad2deg': ('radian', 'degree'), 'logaddexp': ('', ''), 'logaddexp2': ('', '')} +# TODO (#905 follow-up): while this matches previous behavior, some of these have optional +# arguments that should not be Quantities. This should be fixed, and tests using these +# optional arguments should be added. matching_input_copy_units_output_ufuncs = ['compress', 'conj', 'conjugate', 'copy', 'diagonal', 'max', 'mean', 'min', 'ptp', 'ravel', 'repeat', 'reshape', 'round', 'squeeze', 'swapaxes', 'take', 'trace', 'transpose', 'ceil', 'floor', 'hypot', 'rint', - 'add', 'subtract', 'copysign', 'nextafter', - 'trunc', 'frexp', 'absolute', 'negative'] # TODO: review extra args/kwargs + 'copysign', 'nextafter', 'trunc', 'absolute', + 'negative'] copy_units_output_ufuncs = ['ldexp', 'fmod', 'mod', 'remainder'] op_units_output_ufuncs = {'var': 'square', 'prod': 'size', 'multiply': 'mul', 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', - 'remainder': 'div', 'sqrt': 'sqrt', 'square': 'square', - 'reciprocal': 'reciprocal', 'std': 'sum', 'sum': 'sum', - 'cumsum': 'sum'} + 'sqrt': 'sqrt', 'square': 'square', 'reciprocal': 'reciprocal', + 'std': 'sum', 'sum': 'sum', 'cumsum': 'sum'} # Perform the standard ufunc implementations based on behavior collections +for ufunc_str in strip_unit_input_output_ufuncs: + # Ignore units + implement_func('ufunc', ufunc_str, input_units=None, output_unit=None) + for ufunc_str in matching_input_bare_output_ufuncs: # Require all inputs to match units, but output base ndarray implement_func('ufunc', ufunc_str, input_units='all_consistent', output_unit=None) @@ -246,7 +261,48 @@ def implementation(*args, **kwargs): implement_func('ufunc', ufunc_str, input_units=None, output_unit=unit_op) -# TODO: modf (since it had the old modf__1) +@implements('modf', 'ufunc') +def _modf(x, *args, **kwargs): + (x,), output_wrap = unwrap_and_wrap_consistent_units(x) + return tuple(output_wrap(y) for y in np.modf(x, *args, **kwargs)) + + +@implements('frexp', 'ufunc') +def _frexp(x, *args, **kwargs): + (x,), output_wrap = unwrap_and_wrap_consistent_units(x) + mantissa, exponent = np.frexp(x, *args, **kwargs) + return output_wrap(mantissa), exponent + + +@implements('power', 'ufunc') +def _power(x1, x2): + # Hand off to __pow__ + return x1**x2 + + +def _add_subtract_handle_non_quantity_zero(x1, x2): + # As in #121/#122, if a value is 0 (but not Quantity 0) do the operation without checking + # units. We do the calculation instead of just returning the same value to enforce any + # shape checking and type casting due to the operation. + if eq(x1, 0, True): + (x2,), output_wrap = unwrap_and_wrap_consistent_units(x2) + elif eq(x2, 0, True): + (x1,), output_wrap = unwrap_and_wrap_consistent_units(x1) + else: + (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) + return x1, x2, output_wrap + + +@implements('add', 'ufunc') +def _add(x1, x2, *args, **kwargs): + x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) + return output_wrap(np.add(x1, x2, *args, **kwargs)) + + +@implements('subtract', 'ufunc') +def _subtract(x1, x2, *args, **kwargs): + x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) + return output_wrap(np.subtract(x1, x2, *args, **kwargs)) """ @@ -364,8 +420,35 @@ def _unwrap(p, discont=None, axis=-1): return p._REGISTRY.Quantity(np.unwrap(p.m_as('rad'), discont, axis=axis), 'rad').to(p.units) + +@implements('amin', 'function') +def _amin(a, axis=None, out=None, keepdims=NP_NO_VALUE, initial=NP_NO_VALUE, + where=NP_NO_VALUE): + if initial == NP_NO_VALUE: + (a,), output_wrap = unwrap_and_wrap_consistent_units(a) + else: + (a, initial), output_wrap = unwrap_and_wrap_consistent_units(a, initial) + return output_wrap(np.amin(a, axis=axis, out=out, keepdims=keepdims, initial=initial, + where=where)) + + +@implements('amax', 'function') +def _amax(a, axis=None, out=None, keepdims=NP_NO_VALUE, initial=NP_NO_VALUE, + where=NP_NO_VALUE): + if initial == NP_NO_VALUE: + (a,), output_wrap = unwrap_and_wrap_consistent_units(a) + else: + (a, initial), output_wrap = unwrap_and_wrap_consistent_units(a, initial) + return output_wrap(np.amax(a, axis=axis, out=out, keepdims=keepdims, initial=initial, + where=where)) + + # Handle single unit argument operations (axis ops, aggregations, etc.) def implement_single_unit(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + func = getattr(np, func_str) @implements(func_str, 'function') @@ -376,12 +459,17 @@ def implementation(a, *args, **kwargs): for func_str in ['expand_dims', 'squeeze', 'rollaxis', 'moveaxis', 'fix', 'around', 'diagonal', 'mean', 'ptp', 'ravel', 'round_', 'sort', 'median', 'nanmedian', - 'transpose', 'flip', 'copy', 'trim_zeros', 'average', 'nanmean']: + 'transpose', 'flip', 'copy', 'trim_zeros', 'average', 'nanmean', + 'broadcast_to', 'swapaxes', 'nanmin', 'nanmax']: implement_single_unit(func_str) # Handle atleast_nd functions def implement_atleast_nd(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + func = getattr(np, func_str) @implements(func_str, 'function') def implementation(*arrays): @@ -430,11 +518,6 @@ def implementation(*arrays): for func_str in ['var', 'nanvar']: implement_func('function', func_str, input_units=None, output_unit='variance') -# TODO: broadcast_to (how to handle subok?) -# TODO: result_type -# TODO: 'amax', 'amin', 'nanmax', 'nanmin' (version dependent signatures) -# TODO: nan_to_num (expected behavior with input values, sensible defaults?) - def numpy_wrap(func_type, func, args, kwargs, types): # TODO: documentation diff --git a/pint/quantity.py b/pint/quantity.py index a936aa1cd..1af4ae5cf 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -24,7 +24,7 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import Loc, ndarray, np, _to_magnitude, is_upcast_type +from .compat import Loc, ndarray, np, _to_magnitude, is_upcast_type, eq from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, iterable, sized) from .numpy_func import (HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, @@ -34,15 +34,6 @@ op_units_output_ufuncs, set_units_ufuncs) -def _eq(first, second, check_all): - """Comparison of scalars and arrays - """ - out = first == second - if check_all and isinstance(out, ndarray): - return np.all(out) - return out - - class _Exception(Exception): # pragma: no cover def __init__(self, internal): @@ -633,8 +624,7 @@ def _iadd_sub(self, other, op): raise except TypeError: return NotImplemented - - if _eq(other, 0, True): + if eq(other, 0, True): # If the other value is 0 (but not Quantity 0) # do the operation without checking units. # We do the calculation instead of just returning the same @@ -729,7 +719,7 @@ def _add_sub(self, other, op): """ if not self._check(other): # other not from same Registry or not a Quantity - if _eq(other, 0, True): + if eq(other, 0, True): # If the other value is 0 (but not Quantity 0) # do the operation without checking units. # We do the calculation instead of just returning the same @@ -1232,31 +1222,31 @@ def __eq__(self, other): # We compare to the base class of Quantity because # each Quantity class is unique. if not isinstance(other, Quantity): - if _eq(other, 0, True): + if eq(other, 0, True): # Handle the special case in which we compare to zero # (or an array of zeros) if self._is_multiplicative: # compare magnitude - return _eq(self._magnitude, other, False) + return eq(self._magnitude, other, False) else: # compare the magnitude after converting the # non-multiplicative quantity to base units if self._REGISTRY.autoconvert_offset_to_baseunit: - return _eq(self.to_base_units()._magnitude, other, False) + return eq(self.to_base_units()._magnitude, other, False) else: raise OffsetUnitCalculusError(self._units) return (self.dimensionless and - _eq(self._convert_magnitude(UnitsContainer()), other, False)) + eq(self._convert_magnitude(UnitsContainer()), other, False)) - if _eq(self._magnitude, 0, True) and _eq(other._magnitude, 0, True): + if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): return self.dimensionality == other.dimensionality if self._units == other._units: - return _eq(self._magnitude, other._magnitude, False) + return eq(self._magnitude, other._magnitude, False) try: - return _eq(self._convert_magnitude_not_inplace(other._units), + return eq(self._convert_magnitude_not_inplace(other._units), other._magnitude, False) except DimensionalityError: return False @@ -1272,7 +1262,7 @@ def compare(self, other, op): if not isinstance(other, self.__class__): if self.dimensionless: return op(self._convert_magnitude_not_inplace(UnitsContainer()), other) - elif _eq(other, 0, True): + elif eq(other, 0, True): # Handle the special case in which we compare to zero # (or an array of zeros) if self._is_multiplicative: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 7e70b1ccd..63f5d0125 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -447,14 +447,9 @@ def test_count_nonzero_numpy_func(self): def test_max(self): self.assertEqual(self.q.max(), 4*self.ureg.m) - @helpers.requires_array_function_protocol() def test_max_numpy_func(self): self.assertEqual(np.max(self.q), 4 * self.ureg.m) - @helpers.requires_not_array_function_protocol() - def test_max_numpy_func_old_behavior(self): - self.assertEqual(np.max(self.q), 4) - @helpers.requires_array_function_protocol() def test_max_with_axis_arg(self): self.assertQuantityEqual(np.max(self.q, axis=1), [2, 4] * self.ureg.m) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index c216a62c7..e15923db8 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -286,9 +286,7 @@ def test_retain_unit(self): self.assertEqual(q.u, q.reshape(2, 3).u) self.assertEqual(q.u, q.swapaxes(0, 1).u) self.assertEqual(q.u, q.mean().u) - # TODO: Re-add np.compress implementation once mixed type is resolved - # (see https://github.com/hgrecco/pint/pull/764#issuecomment-523272038) - # self.assertEqual(q.u, np.compress((q==q[0,0]).any(0), q).u) + self.assertEqual(q.u, np.compress((q==q[0,0]).any(0), q).u) def test_context_attr(self): self.assertEqual(self.ureg.meter, self.Q_(1, 'meter')) From e44c22e59404f63c088fa456185945548e189a36 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 4 Dec 2019 20:26:50 -0600 Subject: [PATCH 153/612] Move matmul support from #865 with simplified implimentation --- pint/numpy_func.py | 2 +- pint/quantity.py | 9 +++++++++ pint/testsuite/test_quantity.py | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index eb13daad0..866d06f88 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -228,7 +228,7 @@ def implementation(*args, **kwargs): op_units_output_ufuncs = {'var': 'square', 'prod': 'size', 'multiply': 'mul', 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', 'sqrt': 'sqrt', 'square': 'square', 'reciprocal': 'reciprocal', - 'std': 'sum', 'sum': 'sum', 'cumsum': 'sum'} + 'std': 'sum', 'sum': 'sum', 'cumsum': 'sum', 'matmul': 'mul'} # Perform the standard ufunc implementations based on behavior collections diff --git a/pint/quantity.py b/pint/quantity.py index 1af4ae5cf..24ca26e39 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -976,6 +976,15 @@ def __mul__(self, other): __rmul__ = __mul__ + def __matmul__(self, other): + # Use NumPy ufunc for matrix multiplication + try: + return np.matmul(self, other) + except AttributeError: + return NotImplemented + + __rmatmul__ = __matmul__ + def __itruediv__(self, other): if not isinstance(self._magnitude, ndarray): return self._mul_div(other, operator.truediv) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index e15923db8..7030e141c 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1303,6 +1303,15 @@ def test_inplace_exponentiation(self, input_tuple, expected): in1_cp = copy.copy(in1) self.assertQuantityAlmostEqual(op.ipow(in1_cp, in2), expected) + @helpers.requires_numpy() + def test_matmul_with_numpy(self): + A = [[1, 2], [3, 4]] * self.ureg.m + B = np.array([[0, -1], [-1, 0]]) + b = [[1], [0]] * self.ureg.m + self.assertQuantityEqual(A @ B, [[-2, -1], [-4, -3]] * self.ureg.m) + self.assertQuantityEqual(A @ b, [[1], [3]] * self.ureg.m**2) + self.assertQuantityEqual(B @ b, [[0], [-1]] * self.ureg.m) + class TestDimensionReduction(QuantityTestCase): def _calc_mass(self, ureg): From 1d120ab5cac8b95d7d895a9dd9b3cb32beb88711 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 4 Dec 2019 22:07:13 -0600 Subject: [PATCH 154/612] Updates for quantity check in numpy_func.py --- pint/numpy_func.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 866d06f88..f78c7c7ed 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -19,22 +19,24 @@ # Shared Implementation Utilities # +def _is_quantity(arg): + """Test for _units and _magnitude attrs. + + This is done in place of isinstance(Quantity, arg), which would cause a circular import. + """ + return hasattr(arg, '_units') and hasattr(arg, '_magnitude') -# TODO: make more robust and test def _is_quantity_sequence(arg): - if iterable(arg) and sized(arg) and not isinstance(arg, string_types): - if hasattr(arg[0], 'units'): - if not all([hasattr(item, 'units') for item in arg]): - raise TypeError("{} contains items that aren't Quantity type".format(arg)) - return True - return False + """Test for sequences of quantities.""" + return (iterable(arg) and sized(arg) and not isinstance(arg, str) + and all(_is_quantity(item) for item in arg)) def _get_first_input_units(args, kwargs={}): args_combo = list(args) + list(kwargs.values()) out_units=None for arg in args_combo: - if hasattr(arg, 'units'): # TODO better check? + if _is_quantity(arg): out_units = arg.units elif _is_quantity_sequence(arg): out_units = arg[0].units @@ -50,7 +52,7 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): """ def convert_arg(arg): if pre_calc_units is not None: - if hasattr(arg, 'units'): # TODO better check? + if _is_quantity(arg): return arg.m_as(pre_calc_units) elif _is_quantity_sequence(arg): return [item.m_as(pre_calc_units) for item in arg] @@ -60,7 +62,7 @@ def convert_arg(arg): else: raise DimensionalityError('dimensionless', pre_calc_units) else: - if hasattr(arg, 'units'): # TODO better check? + if _is_quantity(arg): return arg.m elif _is_quantity_sequence(arg): return [item.m for item in arg] @@ -139,7 +141,7 @@ def decorator(func): def implement_func(func_type, func_str, input_units=None, output_unit=None): - """TODO""" + """Add default-behavior NumPy function/ufunc to the handled list.""" # If NumPy is not available, do not attempt implement that which does not exist if np is None: return From f1cebed3bcfcb7f1686a0f0099f3aaf29a9a583e Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 4 Dec 2019 23:42:56 -0600 Subject: [PATCH 155/612] Refactor some simple implementations to use a signature-based implementor --- pint/numpy_func.py | 154 +++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 91 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index f78c7c7ed..9c3fed274 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -7,6 +7,8 @@ :license: BSD, see LICENSE for more details. """ +from inspect import signature + from .compat import NP_NO_VALUE, is_upcast_type, np, string_types, eq from .errors import DimensionalityError from .util import iterable, sized @@ -89,7 +91,6 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): elif unit_op == "mul": product = first_input_units._REGISTRY.parse_units('') for x in all_args: - print(x) if hasattr(x, 'units'): product *= x.units result_unit = product @@ -183,8 +184,6 @@ def implementation(*args, **kwargs): Define ufunc behavior collections. TODO: document as before - - """ strip_unit_input_output_ufuncs = ['isnan', 'isinf', 'isfinite', 'signbit'] matching_input_bare_output_ufuncs = ['equal', 'greater', 'greater_equal', 'less', @@ -349,12 +348,6 @@ def _where(condition, *args): return output_wrap(np.where(condition, *args)) -@implements('linspace', 'function') -def _linspace(start, stop, *args, **kwargs): - (start, stop), output_wrap = unwrap_and_wrap_consistent_units(start, stop) - return output_wrap(np.linspace(start, stop, *args, **kwargs)) - - @implements('concatenate', 'function') def _concatenate(sequence, *args, **kwargs): sequence, output_wrap = unwrap_and_wrap_consistent_units(*sequence) @@ -367,54 +360,6 @@ def _stack(arrays, *args, **kwargs): return output_wrap(np.stack(arrays, *args, **kwargs)) -@implements('compress', 'function') -def _compress(condition, a, *args, **kwargs): - (a,), output_wrap = unwrap_and_wrap_consistent_units(a) - return output_wrap(np.compress(condition, a, *args, **kwargs)) - - -@implements('append', 'function') -def _append(arr, values, axis=None): - (arr, values), output_wrap = unwrap_and_wrap_consistent_units(arr, values) - return output_wrap(np.append(arr, values, axis)) - - -@implements('clip', 'function') -def _clip(a, a_min, a_max, *args, **kwargs): - (a, a_min, a_max), output_wrap = unwrap_and_wrap_consistent_units(a, a_min, a_max) - return output_wrap(np.clip(a, a_min, a_max, *args, **kwargs)) - - -@implements('nan_to_num', 'function') -def _nan_to_num(x, copy=True, nan=None, posinf=None, neginf=None): - if nan is None: - nan = 0.0 - elif isinstance(nan, x.__class__): - nan = nan.m_as(x.units) - posinf = posinf if not isinstance(posinf, x.__class__) else posinf.m_as(x.units) - neginf = neginf if not isinstance(neginf, x.__class__) else neginf.m_as(x.units) - result_magnitude = np.nan_to_num(x.magnitude, copy=False, nan=nan, posinf=posinf, - neginf=neginf) - if not copy: - x._magnitude = result_magnitude - - return x._REGISTRY.Quantity(result_magnitude, x.units) - - -@implements('isclose', 'function') -def _isclose(a, b, rtol=None, atol=None, equal_nan=False): - (a, b, rtol, atol), _ = unwrap_and_wrap_consistent_units(a, b, rtol, atol) - rtol = 1e-05 if rtol is None else rtol - atol = 1e-08 if atol is None else atol - return np.isclose(a, b, rtol, atol, equal_nan) - - -@implements('searchsorted', 'function') -def _searchsorted(a, v, *args, **kwargs): - (a, v), _ = unwrap_and_wrap_consistent_units(a, v) - return np.searchsorted(a, v, *args, **kwargs) - - @implements('unwrap', 'function') def _unwrap(p, discont=None, axis=-1): # np.unwrap only dispatches over p argument, so assume it is a Quantity @@ -423,30 +368,7 @@ def _unwrap(p, discont=None, axis=-1): 'rad').to(p.units) -@implements('amin', 'function') -def _amin(a, axis=None, out=None, keepdims=NP_NO_VALUE, initial=NP_NO_VALUE, - where=NP_NO_VALUE): - if initial == NP_NO_VALUE: - (a,), output_wrap = unwrap_and_wrap_consistent_units(a) - else: - (a, initial), output_wrap = unwrap_and_wrap_consistent_units(a, initial) - return output_wrap(np.amin(a, axis=axis, out=out, keepdims=keepdims, initial=initial, - where=where)) - - -@implements('amax', 'function') -def _amax(a, axis=None, out=None, keepdims=NP_NO_VALUE, initial=NP_NO_VALUE, - where=NP_NO_VALUE): - if initial == NP_NO_VALUE: - (a,), output_wrap = unwrap_and_wrap_consistent_units(a) - else: - (a, initial), output_wrap = unwrap_and_wrap_consistent_units(a, initial) - return output_wrap(np.amax(a, axis=axis, out=out, keepdims=keepdims, initial=initial, - where=where)) - - -# Handle single unit argument operations (axis ops, aggregations, etc.) -def implement_single_unit(func_str): +def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: return @@ -454,16 +376,66 @@ def implement_single_unit(func_str): func = getattr(np, func_str) @implements(func_str, 'function') - def implementation(a, *args, **kwargs): - (a,), output_wrap = unwrap_and_wrap_consistent_units(a) - return output_wrap(func(a, *args, **kwargs)) - - -for func_str in ['expand_dims', 'squeeze', 'rollaxis', 'moveaxis', 'fix', 'around', - 'diagonal', 'mean', 'ptp', 'ravel', 'round_', 'sort', 'median', 'nanmedian', - 'transpose', 'flip', 'copy', 'trim_zeros', 'average', 'nanmean', - 'broadcast_to', 'swapaxes', 'nanmin', 'nanmax']: - implement_single_unit(func_str) + def implementation(*args, **kwargs): + # Bind given arguments to the NumPy function signature + bound_args = signature(func).bind(*args, **kwargs) + + # Skip unit arguments that are supplied as None + valid_unit_arguments = [label for label in unit_arguments + if label in bound_args.arguments + and bound_args.arguments[label] is not None] + + # Unwrap valid unit arguments, ensure consistency, and obtain output wrapper + unwrapped_unit_args, output_wrap = unwrap_and_wrap_consistent_units( + *(bound_args.arguments[label] for label in valid_unit_arguments)) + + # Call NumPy function with updated arguments + for i, unwrapped_unit_arg in enumerate(unwrapped_unit_args): + bound_args.arguments[valid_unit_arguments[i]] = unwrapped_unit_arg + ret = func(*bound_args.args, **bound_args.kwargs) + + # Conditionally wrap output + if wrap_output: + return output_wrap(ret) + else: + return ret + + +for func_str, unit_arguments, wrap_output in [('expand_dims', 'a', True), + ('squeeze', 'a', True), + ('rollaxis', 'a', True), + ('moveaxis', 'a', True), + ('around', 'a', True), + ('diagonal', 'a', True), + ('mean', 'a', True), + ('ptp', 'a', True), + ('ravel', 'a', True), + ('round_', 'a', True), + ('sort', 'a', True), + ('median', 'a', True), + ('nanmedian', 'a', True), + ('transpose', 'a', True), + ('copy', 'a', True), + ('average', 'a', True), + ('nanmean', 'a', True), + ('swapaxes', 'a', True), + ('nanmin', 'a', True), + ('nanmax', 'a', True), + ('flip', 'm', True), + ('fix', 'x', True), + ('trim_zeros', ['filt'], True), + ('broadcast_to', ['array'], True), + ('amax', ['a', 'initial'], True), + ('amin', ['a', 'initial'], True), + ('searchsorted', ['a', 'v'], False), + ('isclose', ['a', 'b', 'rtol', 'atol'], False), + ('nan_to_num', ['x', 'nan', 'posinf', 'neginf'], + True), + ('clip', ['a', 'a_min', 'a_max'], True), + ('append', ['arr', 'values'], True), + ('compress', 'a', True), + ('linspace', ['start', 'stop'], True)]: + implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) # Handle atleast_nd functions From 0b14cd329919f4b117cb98a6582a67367bef76f3 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Dec 2019 00:01:23 -0600 Subject: [PATCH 156/612] Add astype and item as wrapped NumPy methods (and refactor flatten to be likewise) --- pint/quantity.py | 20 +++++++++----------- pint/testsuite/test_numpy.py | 9 +++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 24ca26e39..2e4ddb522 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1320,7 +1320,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): def __array_function__(self, func, types, args, kwargs): return numpy_wrap('function', func, args, kwargs, types) - def _ufunc_method_wrap(self, func, *args, **kwargs): + _wrapped_numpy_methods = ['flatten', 'astype', 'item'] + + def _numpy_method_wrap(self, func, *args, **kwargs): """Convenience method to wrap on the fly NumPy ndarray methods taking care of the units. """ @@ -1332,7 +1334,8 @@ def _ufunc_method_wrap(self, func, *args, **kwargs): # Set output units as needed if (func.__name__ in - matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs): + matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs + + _wrapped_numpy_methods): output_unit = self._units elif func.__name__ in set_units_ufuncs: output_unit = set_units_ufuncs[func.__name__][1] @@ -1351,12 +1354,6 @@ def _ufunc_method_wrap(self, func, *args, **kwargs): else: return value - def flatten(self, order='C'): - """Wrap ndarray.flatten.""" - return self.__class__( - _to_magnitude(self._magnitude, force_ndarray=True).flatten(order=order), - self._units) - def clip(self, first=None, second=None, out=None, **kwargs): min = kwargs.get('min', first) max = kwargs.get('max', second) @@ -1457,14 +1454,15 @@ def __getattr__(self, item): # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays magnitude_as_array = _to_magnitude(self._magnitude, force_ndarray=True) return getattr(magnitude_as_array, item) - elif item in HANDLED_UFUNCS: + elif item in HANDLED_UFUNCS or _wrapped_numpy_methods: # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars magnitude_as_array = _to_magnitude(self._magnitude, True) attr = getattr(magnitude_as_array, item) if callable(attr): - return functools.partial(self._ufunc_method_wrap, attr) + return functools.partial(self._numpy_method_wrap, attr) else: - raise AttributeError('NumPy ufunc attribute {} was not callable.'.format(item)) + raise AttributeError('NumPy method {} was not callable.'.format(item)) + try: return getattr(self._magnitude, item) except AttributeError as ex: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 63f5d0125..f94b5a89a 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -209,6 +209,15 @@ def test_block(self): def test_append(self): self.assertQuantityEqual(np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), [[1, 2], [3, 4], [0, 0]] * self.ureg.m) + + def test_astype(self): + actual = self.q.astype(np.float32) + expected = self.Q_(np.array([[1., 2.], [3., 4.]], dtype=np.float32), 'm') + self.assertQuantityEqual(actual, expected) + self.assertEqual(actual.m.dtype, expected.m.dtype) + + def test_item(self): + self.assertQuantityEqual(self.Q_([[0]], 'm').item(), 0 * self.ureg.m) class TestNumpyMathematicalFunctions(TestNumpyMethods): # https://www.numpy.org/devdocs/reference/routines.math.html From dacfeb73fbb19b452b0f107e80b4320ee8fdf406 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Dec 2019 00:31:29 -0600 Subject: [PATCH 157/612] Add fabs, minimum, and maximum ufuncs --- pint/numpy_func.py | 2 +- pint/quantity.py | 6 +++--- pint/testsuite/test_numpy.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 9c3fed274..69d204637 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -224,7 +224,7 @@ def implementation(*args, **kwargs): 'squeeze', 'swapaxes', 'take', 'trace', 'transpose', 'ceil', 'floor', 'hypot', 'rint', 'copysign', 'nextafter', 'trunc', 'absolute', - 'negative'] + 'negative', 'maximum', 'minimum', 'fabs'] copy_units_output_ufuncs = ['ldexp', 'fmod', 'mod', 'remainder'] op_units_output_ufuncs = {'var': 'square', 'prod': 'size', 'multiply': 'mul', 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', diff --git a/pint/quantity.py b/pint/quantity.py index 2e4ddb522..738730712 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1334,8 +1334,8 @@ def _numpy_method_wrap(self, func, *args, **kwargs): # Set output units as needed if (func.__name__ in - matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs - + _wrapped_numpy_methods): + (matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs + + self._wrapped_numpy_methods)): output_unit = self._units elif func.__name__ in set_units_ufuncs: output_unit = set_units_ufuncs[func.__name__][1] @@ -1454,7 +1454,7 @@ def __getattr__(self, item): # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays magnitude_as_array = _to_magnitude(self._magnitude, force_ndarray=True) return getattr(magnitude_as_array, item) - elif item in HANDLED_UFUNCS or _wrapped_numpy_methods: + elif item in HANDLED_UFUNCS or self._wrapped_numpy_methods: # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars magnitude_as_array = _to_magnitude(self._magnitude, True) attr = getattr(magnitude_as_array, item) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index f94b5a89a..af9deb83c 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -482,6 +482,10 @@ def test_argmax_numpy_func(self): def test_nanargmax_numpy_func(self): self.assertNDArrayEqual(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) + def test_maximum(self): + self.assertQuantityEqual(np.maximum(self.q, self.Q_([0, 5], 'm')), + self.Q_([[1, 5], [3, 5]], 'm')) + def test_min(self): self.assertEqual(self.q.min(), 1 * self.ureg.m) @@ -512,6 +516,10 @@ def test_argmin_numpy_func(self): def test_nanargmin_numpy_func(self): self.assertNDArrayEqual(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) + def test_minimum(self): + self.assertQuantityEqual(np.minimum(self.q, self.Q_([0, 5], 'm')), + self.Q_([[0, 2], [0, 4]], 'm')) + def test_ptp(self): self.assertEqual(self.q.ptp(), 3 * self.ureg.m) @@ -775,6 +783,9 @@ def test_where(self): [[0, 2], [3, 4]] * self.ureg.m) self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0) + def test_fabs(self): + self.assertQuantityEqual(np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], 'm')) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): From 258e42be6cbc5c9563f7649b4edc4d38bfc8fe18 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Dec 2019 00:58:15 -0600 Subject: [PATCH 158/612] Add nanpercentile, copyto, and isin --- pint/numpy_func.py | 20 ++++++++++++++++++-- pint/testsuite/test_numpy.py | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 69d204637..8d5d46bc1 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -8,9 +8,10 @@ """ from inspect import signature +import warnings from .compat import NP_NO_VALUE, is_upcast_type, np, string_types, eq -from .errors import DimensionalityError +from .errors import DimensionalityError, UnitStrippedWarning from .util import iterable, sized HANDLED_UFUNCS = {} @@ -368,6 +369,18 @@ def _unwrap(p, discont=None, axis=-1): 'rad').to(p.units) +@implements('copyto', 'function') +def copyto(dst, src, casting='same_kind', where=True): + if _is_quantity(dst): + if _is_quantity(src): + src = src.m_as(dst.units) + np.copyto(dst._magnitude, src, casting=casting, where=where) + else: + warnings.warn("The unit of the quantity is stripped when getting copying to " + "non-quantity", stacklevel=2) + np.copyto(dst, src.m, casting=casting, where=where) + + def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -421,6 +434,8 @@ def implementation(*args, **kwargs): ('swapaxes', 'a', True), ('nanmin', 'a', True), ('nanmax', 'a', True), + ('percentile', 'a', True), + ('nanpercentile', 'a', True), ('flip', 'm', True), ('fix', 'x', True), ('trim_zeros', ['filt'], True), @@ -434,7 +449,8 @@ def implementation(*args, **kwargs): ('clip', ['a', 'a_min', 'a_max'], True), ('append', ['arr', 'values'], True), ('compress', 'a', True), - ('linspace', ['start', 'stop'], True)]: + ('linspace', ['start', 'stop'], True), + ('isin', ['element', 'test_elements'], False)]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index af9deb83c..4576511b4 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -786,6 +786,29 @@ def test_where(self): def test_fabs(self): self.assertQuantityEqual(np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], 'm')) + def test_isin(self): + self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'm')), + np.array([[False, True], [False, True]])) + + @helpers.requires_array_function_protocol() + def test_percentile(self): + self.assertQuantityEqual(np.percentile(self.q, 25), self.Q_(1.75, 'm')) + + @helpers.requires_array_function_protocol() + def test_nanpercentile(self): + self.assertQuantityEqual(np.nanpercentile(self.q_nan, 25), self.Q_(1.5, 'm')) + + @helpers.requires_array_function_protocol() + def test_copyto(self): + a = self.q.m + q = copy.copy(self.q) + np.copyto(q, 2 * q, where=[True, False]) + self.assertQuantityEqual(q, self.Q_([[2, 2], [6, 4]], 'm')) + np.copyto(q, 0, where=[[False, False], [True, False]]) + self.assertQuantityEqual(q, self.Q_([[2, 2], [0, 4]], 'm')) + np.copyto(a, q) + self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): From 1b48d823e11a7647fa713674397eda3bee85fb43 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Dec 2019 12:37:56 -0600 Subject: [PATCH 159/612] Fixup cumprod test broken from rebase --- pint/numpy_func.py | 2 +- pint/testsuite/test_numpy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 8d5d46bc1..f5ee3bd99 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -10,7 +10,7 @@ from inspect import signature import warnings -from .compat import NP_NO_VALUE, is_upcast_type, np, string_types, eq +from .compat import NP_NO_VALUE, is_upcast_type, np, eq from .errors import DimensionalityError, UnitStrippedWarning from .util import iterable, sized diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 4576511b4..2552380cb 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -257,7 +257,7 @@ def test_nansum_numpy_func(self): self.assertQuantityEqual(np.nansum(self.q_nan, axis=0), [4, 2] * self.ureg.m) def test_cumprod(self): - self.assertRaises(ValueError, self.q.cumprod) + self.assertRaises(DimensionalityError, self.q.cumprod) self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) @helpers.requires_array_function_protocol() From 8ac84cc9a56114401647d93919895edf1b5b8652 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Dec 2019 14:56:29 -0600 Subject: [PATCH 160/612] Fix bad code in getattr fallback --- pint/numpy_func.py | 4 ++-- pint/quantity.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index f5ee3bd99..cf584806f 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -376,8 +376,8 @@ def copyto(dst, src, casting='same_kind', where=True): src = src.m_as(dst.units) np.copyto(dst._magnitude, src, casting=casting, where=where) else: - warnings.warn("The unit of the quantity is stripped when getting copying to " - "non-quantity", stacklevel=2) + warnings.warn("The unit of the quantity is stripped when copying to non-quantity", + stacklevel=2) np.copyto(dst, src.m, casting=casting, where=where) diff --git a/pint/quantity.py b/pint/quantity.py index 738730712..76344a806 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1454,7 +1454,7 @@ def __getattr__(self, item): # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays magnitude_as_array = _to_magnitude(self._magnitude, force_ndarray=True) return getattr(magnitude_as_array, item) - elif item in HANDLED_UFUNCS or self._wrapped_numpy_methods: + elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods: # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars magnitude_as_array = _to_magnitude(self._magnitude, True) attr = getattr(magnitude_as_array, item) @@ -1465,7 +1465,7 @@ def __getattr__(self, item): try: return getattr(self._magnitude, item) - except AttributeError as ex: + except AttributeError: raise AttributeError("Neither Quantity object nor its magnitude ({}) " "has attribute '{}'".format(self._magnitude, item)) From ca10ca5dc0c2edbed6fbce13fba2faba5cd6ce07 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 14:03:57 -0600 Subject: [PATCH 161/612] Add einsum, tile, rot90, insert --- pint/numpy_func.py | 14 ++++++++++++-- pint/testsuite/test_numpy.py | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index cf584806f..a300df4ba 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -370,7 +370,7 @@ def _unwrap(p, discont=None, axis=-1): @implements('copyto', 'function') -def copyto(dst, src, casting='same_kind', where=True): +def _copyto(dst, src, casting='same_kind', where=True): if _is_quantity(dst): if _is_quantity(src): src = src.m_as(dst.units) @@ -381,6 +381,13 @@ def copyto(dst, src, casting='same_kind', where=True): np.copyto(dst, src.m, casting=casting, where=where) +@implements('einsum', 'function') +def _einsum(subscripts, *operands, **kwargs): + operand_magnitudes, _ = convert_to_consistent_units(*operands, pre_calc_units=None) + output_unit = get_op_output_unit("mul", _get_first_input_units(operands), operands) + return np.einsum(subscripts, *operand_magnitudes, **kwargs) * output_unit + + def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -450,7 +457,10 @@ def implementation(*args, **kwargs): ('append', ['arr', 'values'], True), ('compress', 'a', True), ('linspace', ['start', 'stop'], True), - ('isin', ['element', 'test_elements'], False)]: + ('isin', ['element', 'test_elements'], False), + ('tile', 'A', True), + ('rot90', 'm', True), + ('insert', ['arr', 'values'], True)]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 2552380cb..64b2007f1 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -306,6 +306,15 @@ def test_trapz(self): def test_dot_numpy_func(self): self.assertQuantityEqual(np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m) + @helpers.requires_array_function_protocol() + def test_einsum(self): + a = np.arange(25).reshape(5, 5) * self.ureg.m + b = np.arange(5) * self.ureg.m + self.assertQuantityEqual(np.einsum('ii', a), 60 * self.ureg.m) + self.assertQuantityEqual(np.einsum('ii->i', a), np.array([ 0, 6, 12, 18, 24]) * self.ureg.m) + self.assertQuantityEqual(np.einsum('i,i', b, b), 30 * self.ureg.m**2) + self.assertQuantityEqual(np.einsum('ij,j', a, b), np.array([ 30, 80, 130, 180, 230]) * self.ureg.m**2) + # Arithmetic operations def test_addition_with_scalar(self): a = np.array([0, 1, 2]) @@ -809,6 +818,19 @@ def test_copyto(self): np.copyto(a, q) self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) + @helpers.requires_array_function_protocol() + def test_tile(self): + self.assertQuantityEqual(np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_rot90(self): + self.assertQuantityEqual(np.rot90(self.q), np.array([[2, 4], [1, 3]]) * self.ureg.m) + + @helpers.requires_array_function_protocol() + def test_insert(self): + self.assertQuantityEqual(np.insert(self.q, 1, 0 * self.ureg.m, axis=1), + np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): From 3fb2e0141b077d4a2aaad1bdae4f29eb59cd7cdd Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 14:22:41 -0600 Subject: [PATCH 162/612] Reimplement isin to be False rather than raising error on incompatible unit --- pint/numpy_func.py | 26 +++++++++++++++++++++++++- pint/testsuite/test_numpy.py | 5 +++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index a300df4ba..344fdf893 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -388,6 +388,31 @@ def _einsum(subscripts, *operands, **kwargs): return np.einsum(subscripts, *operand_magnitudes, **kwargs) * output_unit +@implements('isin', 'function') +def _isin(element, test_elements, assume_unique=False, invert=False): + if not _is_quantity(element): + raise ValueError('Cannot test if unit-aware elements are in not-unit-aware array') + + if _is_quantity(test_elements): + try: + test_elements = test_elements.m_as(element.units) + except DimensionalityError: + # Incompatible unit test elements cannot be in element + return np.full(element.shape, False) + elif _is_quantity_sequence(test_elements): + compatible_test_elements = [] + for test_element in test_elements: + try: + compatible_test_elements.append(test_element.m_as(element.units)) + except DimensionalityError: + # Incompatible unit test elements cannot be in element, but others in sequence + # may + pass + test_elements = compatible_test_elements + + return np.isin(element.m, test_elements, assume_unique=assume_unique, invert=invert) + + def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -457,7 +482,6 @@ def implementation(*args, **kwargs): ('append', ['arr', 'values'], True), ('compress', 'a', True), ('linspace', ['start', 'stop'], True), - ('isin', ['element', 'test_elements'], False), ('tile', 'A', True), ('rot90', 'm', True), ('insert', ['arr', 'values'], True)]: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 64b2007f1..9618d165f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -798,6 +798,11 @@ def test_fabs(self): def test_isin(self): self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'm')), np.array([[False, True], [False, True]])) + self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'J')), + np.array([[False, False], [False, False]])) + self.assertNDArrayEqual(np.isin(self.q, [self.Q_(2, 'm'), self.Q_(4, 'J')]), + np.array([[False, True], [False, False]])) + self.assertRaises(ValueError, np.isin, self.q.m, self.q) @helpers.requires_array_function_protocol() def test_percentile(self): From 5d3cdf2da182d2fc3adff13f2eec1aef369d8d3e Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 15:04:58 -0600 Subject: [PATCH 163/612] Reimplement where for custom handling of zero and nan --- pint/numpy_func.py | 12 +++++++++++- pint/testsuite/test_numpy.py | 12 ++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 344fdf893..e0e0630f4 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -345,7 +345,17 @@ def _interp(x, xp, fp, left=None, right=None, period=None): @implements('where', 'function') def _where(condition, *args): - args, output_wrap = unwrap_and_wrap_consistent_units(*args) + if len(args) == 2 and np.isscalar(args[1]) and (args[1] == 0 or np.isnan(args[1])): + print(0, args) + (x,), output_wrap = unwrap_and_wrap_consistent_units(args[0]) + args = x, args[1] + elif len(args) == 2 and np.isscalar(args[0]) and (args[0] == 0 or np.isnan(args[0])): + print(1, args) + (y,), output_wrap = unwrap_and_wrap_consistent_units(args[1]) + args = args[0], y + else: + print(2, args) + args, output_wrap = unwrap_and_wrap_consistent_units(*args) return output_wrap(np.where(condition, *args)) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 9618d165f..ae39ee6b6 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -788,9 +788,17 @@ def test_comparisons(self): @helpers.requires_array_function_protocol() def test_where(self): - self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 0 * self.ureg.m), + self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 20 * self.ureg.m), + [[20, 2], [3, 4]] * self.ureg.m) + self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 0), [[0, 2], [3, 4]] * self.ureg.m) - self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0) + self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, np.nan), + [[np.nan, 2], [3, 4]] * self.ureg.m) + self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, 0, self.q), + [[1, 2], [0, 0]] * self.ureg.m) + self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, np.nan, self.q), + [[1, 2], [np.nan, np.nan]] * self.ureg.m) + self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0 * self.ureg.J) def test_fabs(self): self.assertQuantityEqual(np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], 'm')) From 8f2ced304b042095413271e2f5a22d457bfe7628 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 20:04:18 -0600 Subject: [PATCH 164/612] Patch where and isin based on feedback --- pint/numpy_func.py | 19 ++++++++++++++----- pint/testsuite/test_numpy.py | 8 ++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index e0e0630f4..78388fefe 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -345,16 +345,17 @@ def _interp(x, xp, fp, left=None, right=None, period=None): @implements('where', 'function') def _where(condition, *args): - if len(args) == 2 and np.isscalar(args[1]) and (args[1] == 0 or np.isnan(args[1])): - print(0, args) + if (len(args) == 2 and not _is_quantity(args[1]) and not iterable(args[1]) + and (args[1] == 0 or np.isnan(args[1]))): + # Special case for y being bare zero or nan (x,), output_wrap = unwrap_and_wrap_consistent_units(args[0]) args = x, args[1] - elif len(args) == 2 and np.isscalar(args[0]) and (args[0] == 0 or np.isnan(args[0])): - print(1, args) + elif (len(args) == 2 and not _is_quantity(args[0]) and not iterable(args[0]) + and (args[0] == 0 or np.isnan(args[0]))): + # Special case for x being bare zero or nan (y,), output_wrap = unwrap_and_wrap_consistent_units(args[1]) args = args[0], y else: - print(2, args) args, output_wrap = unwrap_and_wrap_consistent_units(*args) return output_wrap(np.where(condition, *args)) @@ -419,6 +420,14 @@ def _isin(element, test_elements, assume_unique=False, invert=False): # may pass test_elements = compatible_test_elements + else: + # Consider non-quantity like dimensionless quantity + if not element.dimensionless: + # Unit do not match, so all false + return np.full(element.shape, False) + else: + # Convert to units of element + element._REGISTRY.Quantity(test_elements).m_as(element.units) return np.isin(element.m, test_elements, assume_unique=assume_unique, invert=invert) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index ae39ee6b6..c8147442c 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -798,6 +798,10 @@ def test_where(self): [[1, 2], [0, 0]] * self.ureg.m) self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, np.nan, self.q), [[1, 2], [np.nan, np.nan]] * self.ureg.m) + self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, np.array(np.nan)), + [[np.nan, 2], [3, 4]] * self.ureg.m) + self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, np.array(np.nan), self.q), + [[1, 2], [np.nan, np.nan]] * self.ureg.m) self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0 * self.ureg.J) def test_fabs(self): @@ -810,6 +814,10 @@ def test_isin(self): np.array([[False, False], [False, False]])) self.assertNDArrayEqual(np.isin(self.q, [self.Q_(2, 'm'), self.Q_(4, 'J')]), np.array([[False, True], [False, False]])) + self.assertNDArrayEqual(np.isin(self.q, self.q.m), + np.array([[False, False], [False, False]])) + self.assertNDArrayEqual(np.isin(self.q / self.ureg.cm, [1, 3]), + np.array([[True, False], [True, False]])) self.assertRaises(ValueError, np.isin, self.q.m, self.q) @helpers.requires_array_function_protocol() From f0acac70b82d4e1c54904c437623b7605a20f786 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 20:09:26 -0600 Subject: [PATCH 165/612] Add Quantity.dot method as wrapper of np.dot function --- pint/quantity.py | 7 +++++++ pint/testsuite/test_numpy.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index 76344a806..c7de3048a 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1431,6 +1431,13 @@ def searchsorted(self, v, side='left', sorter=None): raise DimensionalityError('dimensionless', self._units) return self.magnitude.searchsorted(v, side) + def dot(self, b): + """Dot product of two arrays. + + Wraps np.dot(). + """ + return np.dot(self, b) + def __ito_if_needed(self, to_units): if self.unitless and to_units == 'radian': return diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index c8147442c..735e87dfc 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -302,6 +302,10 @@ def test_cross(self): def test_trapz(self): self.assertQuantityEqual(np.trapz([1. ,2., 3., 4.] * self.ureg.J, dx=1*self.ureg.m), 7.5 * self.ureg.J*self.ureg.m) + @helpers.requires_array_function_protocol() + def test_dot(self): + self.assertQuantityEqual(self.q.ravel().dot(np.array([1, 0, 0, 1])), 5 * self.ureg.m) + @helpers.requires_array_function_protocol() def test_dot_numpy_func(self): self.assertQuantityEqual(np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m) From 58400a924917d8f6a86b8c952f62696074b1c87d Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 7 Dec 2019 21:04:50 -0600 Subject: [PATCH 166/612] Update matmul test noting matmul has only been a proper ufunc since NumPy 1.16 --- pint/quantity.py | 10 ++++++---- pint/testsuite/helpers.py | 6 ++++++ pint/testsuite/test_numpy.py | 2 ++ pint/testsuite/test_quantity.py | 3 ++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index c7de3048a..cfa9df593 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -19,12 +19,14 @@ import re import warnings +from distutils.version import StrictVersion + from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts) from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import Loc, ndarray, np, _to_magnitude, is_upcast_type, eq +from .compat import Loc, NUMPY_VER, ndarray, np, _to_magnitude, is_upcast_type, eq from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, iterable, sized) from .numpy_func import (HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, @@ -977,10 +979,10 @@ def __mul__(self, other): __rmul__ = __mul__ def __matmul__(self, other): - # Use NumPy ufunc for matrix multiplication - try: + # Use NumPy ufunc (existing since 1.16) for matrix multiplication + if StrictVersion(NUMPY_VER) >= StrictVersion('1.16'): return np.matmul(self, other) - except AttributeError: + else: return NotImplemented __rmatmul__ = __matmul__ diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 2ecc5dd75..cbea4c0a9 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -32,6 +32,12 @@ def requires_numpy_previous_than(version): return unittest.skipUnless(StrictVersion(NUMPY_VER) < StrictVersion(version), 'Requires NumPy < %s' % version) +def requires_numpy_at_least(version): + if not HAS_NUMPY: + return unittest.skip('Requires NumPy') + return unittest.skipUnless(StrictVersion(NUMPY_VER) >= StrictVersion(version), 'Requires NumPy >= %s' % version) + + def requires_numpy(): return unittest.skipUnless(HAS_NUMPY, 'Requires NumPy') diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 735e87dfc..370fbeaec 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -808,9 +808,11 @@ def test_where(self): [[1, 2], [np.nan, np.nan]] * self.ureg.m) self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0 * self.ureg.J) + @helpers.requires_array_function_protocol() def test_fabs(self): self.assertQuantityEqual(np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], 'm')) + @helpers.requires_array_function_protocol() def test_isin(self): self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'm')), np.array([[False, True], [False, True]])) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 7030e141c..d07e22983 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1303,7 +1303,8 @@ def test_inplace_exponentiation(self, input_tuple, expected): in1_cp = copy.copy(in1) self.assertQuantityAlmostEqual(op.ipow(in1_cp, in2), expected) - @helpers.requires_numpy() + # matmul is only a ufunc since 1.16 + @helpers.requires_numpy_at_least('1.16') def test_matmul_with_numpy(self): A = [[1, 2], [3, 4]] * self.ureg.m B = np.array([[0, -1], [-1, 0]]) From be5b23a541ef84a8e1b11289fafddb46011e5d0d Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 8 Dec 2019 17:40:36 -0600 Subject: [PATCH 167/612] Add documentation and unit test for numpy_func.py --- pint/numpy_func.py | 77 ++++++++++++----- pint/testsuite/test_numpy_func.py | 138 +++++++++++++++++++++++++++--- 2 files changed, 180 insertions(+), 35 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 78388fefe..beafd4fdf 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -18,9 +18,7 @@ HANDLED_FUNCTIONS = {} -# # Shared Implementation Utilities -# def _is_quantity(arg): """Test for _units and _magnitude attrs. @@ -29,6 +27,7 @@ def _is_quantity(arg): """ return hasattr(arg, '_units') and hasattr(arg, '_magnitude') + def _is_quantity_sequence(arg): """Test for sequences of quantities.""" return (iterable(arg) and sized(arg) and not isinstance(arg, str) @@ -36,6 +35,7 @@ def _is_quantity_sequence(arg): def _get_first_input_units(args, kwargs={}): + """Obtain the first valid unit from a collection of args and kwargs.""" args_combo = list(args) + list(kwargs.values()) out_units=None for arg in args_combo: @@ -49,9 +49,12 @@ def _get_first_input_units(args, kwargs={}): def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): - """Takes the args for a numpy function and converts any Quantity or Sequence of Quantities - into the units of the first Quantiy/Sequence of quantities. Other args are left untouched - if pre_calc_units is None or dimensionless, otherwise a DimensionalityError is raised. + """Prepare args and kwargs for wrapping by unit conversion and stripping. + + If pre_calc_units is not None, takes the args and kwargs for a NumPy function and converts + any Quantity or Sequence of Quantities into the units of the first Quantiy/Sequence of + Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless + Quantities. If pre_calc_units is None, units are simply stripped. """ def convert_arg(arg): if pre_calc_units is not None: @@ -77,7 +80,9 @@ def convert_arg(arg): def unwrap_and_wrap_consistent_units(*args): - """Returns the given args as parsed by convert_to_consistent_units assuming units of first + """Strip units from args while providing a rewrapping function. + + Returns the given args as parsed by convert_to_consistent_units assuming units of first arg with units, along with a wrapper to restore that unit to the output. """ first_input_units = _get_first_input_units(args) @@ -86,7 +91,24 @@ def unwrap_and_wrap_consistent_units(*args): def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): - """Determine resulting unit from given operation.""" + """Determine resulting unit from given operation. + + Options for `unit_op`: + + - "sum": `first_input_units`, unless non-multiplicative, which raises + OffsetUnitCalculusError + - "mul": product of all units in `all_args` + - "delta": `first_input_units`, unless non-multiplicative, which uses delta version + - "delta,div": like "delta", but divided by all units in `all_args` except the first + - "div": unit of first argument in `all_args` (or dimensionless if not a Quantity) divided + by all following units + - "variance": square of `first_input_units`, unless non-multiplicative, which raises + OffsetUnitCalculusError + - "square": square of `first_input_units` + - "sqrt": square root of `first_input_units` + - "reciprocal": reciprocal of `first_input_units` + - "size": `first_input_units` raised to the power of `size` + """ if unit_op == "sum": result_unit = (1 * first_input_units + 1 * first_input_units).units elif unit_op == "mul": @@ -184,7 +206,20 @@ def implementation(*args, **kwargs): """ Define ufunc behavior collections. -TODO: document as before +- `strip_unit_input_output_ufuncs`: units should be ignored on both input and output +- `matching_input_bare_output_ufuncs`: inputs are converted to matching units, but outputs are + returned as-is +- `matching_input_set_units_output_ufuncs`: inputs are converted to matching units, and the + output units are as set by the dict value +- `set_units_ufuncs`: dict values are specified as (in_unit, out_unit), so that inputs are + converted to in_unit before having magnitude passed to NumPy ufunc, and outputs are set to + have out_unit +- `matching_input_copy_units_output_ufuncs`: inputs are converted to matching units, and + outputs are set to that unit +- `copy_units_output_ufuncs`: input units (except the first) are ignored, and output is set to + that of the first input unit +- `op_units_output_ufuncs`: determine output unit from input unit as determined by operation + (see `get_op_output_unit`) """ strip_unit_input_output_ufuncs = ['isnan', 'isinf', 'isfinite', 'signbit'] matching_input_bare_output_ufuncs = ['equal', 'greater', 'greater_equal', 'less', @@ -234,6 +269,7 @@ def implementation(*args, **kwargs): # Perform the standard ufunc implementations based on behavior collections + for ufunc_str in strip_unit_input_output_ufuncs: # Ignore units implement_func('ufunc', ufunc_str, input_units=None, output_unit=None) @@ -263,6 +299,8 @@ def implementation(*args, **kwargs): implement_func('ufunc', ufunc_str, input_units=None, output_unit=unit_op) +# Define custom ufunc implementations for atypical cases + @implements('modf', 'ufunc') def _modf(x, *args, **kwargs): (x,), output_wrap = unwrap_and_wrap_consistent_units(x) @@ -278,7 +316,6 @@ def _frexp(x, *args, **kwargs): @implements('power', 'ufunc') def _power(x1, x2): - # Hand off to __pow__ return x1**x2 @@ -307,12 +344,7 @@ def _subtract(x1, x2, *args, **kwargs): return output_wrap(np.subtract(x1, x2, *args, **kwargs)) -""" -Define function behavior - -TODO: Document -""" - +# Define custom function implementations @implements('meshgrid', 'function') def _meshgrid(*xi, **kwargs): @@ -432,6 +464,8 @@ def _isin(element, test_elements, assume_unique=False, invert=False): return np.isin(element.m, test_elements, assume_unique=assume_unique, invert=invert) +# Implement simple matching-unit or stripped-unit functions based on signature + def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -508,6 +542,7 @@ def implementation(*args, **kwargs): # Handle atleast_nd functions + def implement_atleast_nd(func_str): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -535,35 +570,33 @@ def implementation(*arrays): implement_func('function', func_str, input_units='all_consistent', output_unit='match_input') +# Handle cumulative products (which must be dimensionless for consistent units across output +# array) for func_str in ['cumprod', 'cumproduct', 'nancumprod']: implement_func('function', func_str, input_units='dimensionless', output_unit='match_input') +# Handle functions that ignore units on input and output for func_str in ['size', 'isreal', 'iscomplex', 'shape', 'ones_like', 'zeros_like', 'empty_like', 'argsort', 'argmin', 'argmax', 'alen', 'ndim', 'nanargmax', 'nanargmin', 'count_nonzero', 'nonzero', 'result_type']: implement_func('function', func_str, input_units=None, output_unit=None) -# TODO: Verify all these below with non-united other arguments \/ !! - +# Handle functions with output unit defined by operation for func_str in ['std', 'nanstd', 'sum', 'nansum', 'cumsum', 'nancumsum']: implement_func('function', func_str, input_units=None, output_unit='sum') - for func_str in ['cross', 'trapz', 'dot']: implement_func('function', func_str, input_units=None, output_unit='mul') - for func_str in ['diff', 'ediff1d']: implement_func('function', func_str, input_units=None, output_unit='delta') - for func_str in ['gradient', ]: implement_func('function', func_str, input_units=None, output_unit='delta,div') - for func_str in ['var', 'nanvar']: implement_func('function', func_str, input_units=None, output_unit='variance') def numpy_wrap(func_type, func, args, kwargs, types): - # TODO: documentation + """Return the result from a NumPy function/ufunc as wrapped by Pint.""" if func_type == 'function': handled = HANDLED_FUNCTIONS elif func_type == 'ufunc': diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index d21f62e58..31772d518 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -2,21 +2,16 @@ import pint.numpy_func -from pint.testsuite import QuantityTestCase, helpers -from pint.numpy_func import implements +from pint import DimensionalityError, OffsetUnitCalculusError +from pint.compat import np +from pint.testsuite.test_numpy import TestNumpyMethods +from pint.numpy_func import (_is_quantity, _is_quantity_sequence, _get_first_input_units, + convert_to_consistent_units, unwrap_and_wrap_consistent_units, + get_op_output_unit, implements, numpy_wrap) from unittest.mock import patch -@helpers.requires_numpy() -class TestNumPyFuncUtils(QuantityTestCase): - - FORCE_NDARRAY = True - - @classmethod - def setUpClass(cls): - from pint import _DEFAULT_REGISTRY - cls.ureg = _DEFAULT_REGISTRY - cls.Q_ = cls.ureg.Quantity +class TestNumPyFuncUtils(TestNumpyMethods): @patch('pint.numpy_func.HANDLED_FUNCTIONS', {}) @patch('pint.numpy_func.HANDLED_UFUNCS', {}) @@ -41,4 +36,121 @@ def test_ufunc(): def test_invalid(): pass - # TODO: fill in other functions in numpy_func + def test_is_quantity(self): + self.assertTrue(_is_quantity(self.Q_(0))) + self.assertTrue(_is_quantity(np.arange(4) * self.ureg.m)) + self.assertFalse(_is_quantity(1.)) + self.assertFalse(_is_quantity(np.array([1, 1, 2, 3, 5, 8]))) + self.assertFalse(_is_quantity('not-a-quantity')) + # TODO (#905 follow-up): test other duck arrays that wrap or are wrapped by Pint + + def test_is_quantity_sequence(self): + self.assertTrue(_is_quantity_sequence((self.Q_(0, 'm'), self.Q_(32., 'degF')))) + self.assertTrue(_is_quantity_sequence(np.arange(4) * self.ureg.m)) + self.assertFalse(_is_quantity_sequence((self.Q_(0), True))) + self.assertFalse(_is_quantity_sequence([1, 3, 5])) + self.assertFalse(_is_quantity_sequence(9 * self.ureg.m)) + self.assertFalse(_is_quantity_sequence(np.arange(4))) + self.assertFalse(_is_quantity_sequence('0123')) + + def test_convert_to_consistent_units_with_pre_calc_units(self): + args, kwargs = convert_to_consistent_units( + self.Q_(50, 'cm'), + np.arange(4).reshape(2, 2) * self.ureg.m, + [0.042] * self.ureg.km, + (self.Q_(0, 'm'), self.Q_(1, 'dam')), + a=6378 * self.ureg.km, + pre_calc_units=self.ureg.meter) + self.assertEqual(args[0], 0.5) + self.assertNDArrayEqual(args[1], np.array([[0, 1], [2, 3]])) + self.assertNDArrayEqual(args[2], np.array([42])) + self.assertEqual(args[3][0], 0) + self.assertEqual(args[3][1], 10) + self.assertEqual(kwargs['a'], 6.378e6) + + def test_convert_to_consistent_units_with_dimensionless(self): + args, kwargs = convert_to_consistent_units( + np.arange(2), + pre_calc_units=self.ureg.g/self.ureg.kg) + self.assertNDArrayEqual(args[0], np.array([0, 1000])) + self.assertEqual(kwargs, {}) + + def test_convert_to_consistent_units_with_dimensionality_error(self): + self.assertRaises(DimensionalityError, convert_to_consistent_units, + self.Q_(32., 'degF'), pre_calc_units=self.ureg.meter) + self.assertRaises(DimensionalityError, convert_to_consistent_units, np.arange(4), + pre_calc_units=self.ureg.meter) + + def test_convert_to_consistent_units_without_pre_calc_units(self): + args, kwargs = convert_to_consistent_units( + (self.Q_(0), self.Q_(10, 'degC')), + [1, 2, 3, 5, 7] * self.ureg.m, + pre_calc_units=None) + self.assertEqual(args[0][0], 0) + self.assertEqual(args[0][1], 10) + self.assertNDArrayEqual(args[1], np.array([1, 2, 3, 5, 7])) + self.assertEqual(kwargs, {}) + + def test_unwrap_and_wrap_constistent_units(self): + (a, ), output_wrap_a = unwrap_and_wrap_consistent_units([2, 4, 8] * self.ureg.m) + (b, c), output_wrap_c = unwrap_and_wrap_consistent_units( + np.arange(4), self.Q_(1, 'g/kg')) + + self.assertNDArrayEqual(a, np.array([2, 4, 8])) + self.assertNDArrayEqual(b, np.array([0, 1000, 2000, 3000])) + self.assertEqual(c, 1) + + self.assertQuantityEqual(output_wrap_a(0), 0 * self.ureg.m) + self.assertQuantityEqual(output_wrap_c(0), self.Q_(0, 'g/kg')) + + def test_op_output_unit_sum(self): + self.assertEqual(get_op_output_unit('sum', self.ureg.m), self.ureg.m) + self.assertRaises(OffsetUnitCalculusError, get_op_output_unit, 'sum', self.ureg.degC) + + def test_op_output_unit_mul(self): + self.assertEqual(get_op_output_unit('mul', self.ureg.s, + (self.Q_(1, 'm'), self.Q_(1, 'm**2'))), + self.ureg.m**3) + + def test_op_output_unit_delta(self): + self.assertEqual(get_op_output_unit('delta', self.ureg.m), self.ureg.m) + self.assertEqual(get_op_output_unit('delta', self.ureg.degC), self.ureg.delta_degC) + + def test_op_output_unit_delta_div(self): + self.assertEqual(get_op_output_unit('delta,div', self.ureg.m, + (self.Q_(1, 'm'), self.Q_(1, 's'))), + self.ureg.m / self.ureg.s) + self.assertEqual(get_op_output_unit('delta,div', self.ureg.degC, + (self.Q_(1, 'degC'), self.Q_(1, 'm'))), + self.ureg.delta_degC / self.ureg.m) + + def test_op_output_unit_div(self): + self.assertEqual(get_op_output_unit('div', self.ureg.m, + (self.Q_(1, 'm'), self.Q_(1, 's'), + self.Q_(1, 'K'))), + self.ureg.m / self.ureg.s / self.ureg.K) + self.assertEqual(get_op_output_unit('div', self.ureg.s, (1, self.Q_(1, 's'))), + self.ureg.s**-1) + + def test_op_output_unit_variance(self): + self.assertEqual(get_op_output_unit('variance', self.ureg.m), self.ureg.m**2) + self.assertRaises(OffsetUnitCalculusError, get_op_output_unit, 'variance', + self.ureg.degC) + + def test_op_output_unit_square(self): + self.assertEqual(get_op_output_unit('square', self.ureg.m), self.ureg.m**2) + + def test_op_output_unit_sqrt(self): + self.assertEqual(get_op_output_unit('sqrt', self.ureg.m), self.ureg.m**0.5) + + def test_op_output_unit_reciprocal(self): + self.assertEqual(get_op_output_unit('reciprocal', self.ureg.m), self.ureg.m**-1) + + def test_op_output_unit_size(self): + self.assertEqual(get_op_output_unit('size', self.ureg.m, size=3), self.ureg.m**3) + self.assertRaises(ValueError, get_op_output_unit, 'size', self.ureg.m) + + def test_numpy_wrap(self): + self.assertRaises(ValueError, numpy_wrap, 'invalid', np.ones, [], {}, []) + # TODO (#905 follow-up): test that NotImplemented is returned when upcast types + # present From 3fe1d273cb5515dddb3935b12c869039d3b6b5b7 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 8 Dec 2019 18:25:24 -0600 Subject: [PATCH 168/612] Update docs and changelog for NEP-18 support --- CHANGES | 15 +++++++++++++++ docs/numpy.rst | 49 ++++++++++++++++++++++++++++++------------------ pint/quantity.py | 2 ++ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index 16cc51528..87ac0cc73 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,21 @@ Pint Changelog 0.10 (unreleased) ----------------- +- **BREAKING CHANGE**: + Implement NEP-18 for + Pint Quantities. Most NumPy functions that previously stripped units when applied to + Pint Quantities will now return Quantities with proper units (on NumPy v1.16 with + the array_function protocol enabled or v1.17+ by default) instead of ndarrays. Any + non-explictly-handled functions will now raise a "no implementation found" TypeError + instead of stripping units. The previous behavior is maintained for NumPy < v1.16 and + when the array_function protocol is disabled. + (Issue #905, Thanks Jon Thielen and andrewgsavage) +- Implementation of NumPy ufuncs has been refactored to share common utilities with + NumPy function implementations + (Issue #905, Thanks Jon Thielen) +- Pint Quantities now support the `@` matrix mulitiplication operator (on NumPy v1.16+), + as well as the `dot`, `flatten`, `astype`, and `item` methods. + (Issue #905, Thanks Jon Thielen) - **BREAKING CHANGE**: Fix crash when applying pprint to large sets of Units. DefinitionSyntaxError is now a subclass of SyntaxError (was ValueError). diff --git a/docs/numpy.rst b/docs/numpy.rst index e5b040b73..bf23b36ae 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -1,12 +1,13 @@ .. _numpy: -NumPy support +NumPy Support ============= -The magnitude of a Pint quantity can be of any numerical type and you are free -to choose it according to your needs. In numerical applications, it is quite -convenient to use `NumPy ndarray`_ and therefore they are supported by Pint. +The magnitude of a Pint quantity can be of any numerical scalar type, and you are free +to choose it according to your needs. For numerical applications requiring arrays, it is +quite convenient to use `NumPy ndarray`_ (or `ndarray-like types supporting NEP-18`_), +and therefore these are the array types supported by Pint. First, we import the relevant packages: @@ -81,11 +82,11 @@ is applied before the requested calculation: >>> print(angles) [ 0.64350111 0.92729522] radian -You can convert the result to degrees using the corresponding NumPy function: +You can convert the result to degrees using usual unit conversion: .. doctest:: - >>> print(np.rad2deg(angles)) + >>> print(angles.to('degree')) [ 36.86989765 53.13010235] degree Applying a function that expects angles to a quantity with a different dimensionality @@ -109,13 +110,16 @@ The following ufuncs_ can be applied to a Quantity object: - **Comparison functions**: greater, greater_equal, less, less_equal, not_equal, equal - **Floating functions**: isreal,iscomplex, isfinite, isinf, isnan, signbit, copysign, nextafter, modf, ldexp, frexp, fmod, floor, ceil, trunc -And the following `NumPy ndarray methods`_ and functions: +And the following NumPy functions: -- sum, fill, reshape, transpose, flatten, ravel, squeeze, take, put, repeat, sort, argsort, diagonal, compress, nonzero, searchsorted, max, argmax, min, argmin, ptp, clip, round, trace, cumsum, mean, var, std, prod, cumprod, conj, conjugate, flatten +- alen, amax, amin, append, argmax, argmin, argsort, around, atleast_1d, atleast_2d, atleast_3d, average, block, broadcast_to, clip, column_stack, compress, concatenate, copy, copyto, count_nonzero, cross, cumprod, cumproduct, cumsum, diagonal, diff, dot, dstack, ediff1d, einsum, empty_like, expand_dims, fix, flip, full_like, gradient, hstack, insert, interp, isclose, iscomplex, isin, isreal, linspace, mean, median, meshgrid, moveaxis, nan_to_num, nanargmax, nanargmin, nancumprod, nancumsum, nanmax, nanmean, nanmedian, nanmin, nanpercentile, nanstd, nanvar, ndim, nonzero, ones_like, percentile, ptp, ravel, result_type, rollaxis, rot90, round\_, searchsorted, shape, size, sort, squeeze, stack, std, sum, swapaxes, tile, transpose, trapz, trim_zeros, unwrap, var, vstack, where, zeros_like -`Quantity` is not a subclass of `ndarray`. This might change in the future, but for this reason functions that call `numpy.asanyarray` are currently not supported. These functions are: +And the following `NumPy ndarray methods`_: -- unwrap, trapz, diff, ediff1d, fix, gradient, cross, ones_like +- argmax, argmin, argsort, astype, clip, compress, conj, conjugate, cumprod, cumsum, diagonal, dot, fill, flatten, flatten, item, max, mean, min, nonzero, prod, ptp, put, ravel, repeat, reshape, round, searchsorted, sort, squeeze, std, sum, take, trace, transpose, var + +Pull requests are welcome for any NumPy function, ufunc, or method that is not currently +supported. Comments @@ -133,22 +137,31 @@ will be raised. In some functions that take 2 or more arguments (e.g. `arctan2`), the second argument is converted to the units of the first. Again, a `DimensionalityError` -exception will be raised if this is not possible. +exception will be raised if this is not possible. ndarray or ndarray-like arguments +are generally treated as if they were dimensionless quantities, except for declared +upcast types to which Pint defers (see +). To date, these "upcast types" are: + +- ``PintArray``, as defined by pint-pandas +- ``Series``, as defined by pandas +- ``DataArray``, as defined by xarray + +To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and +``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that +functions and ufuncs that Pint does not explicitly handle will error, rather than return +a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more +information on these protocols, see +. This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional -memory and CPU cycles. On top of this, all `ufuncs` are implemented in the -`Quantity` class by overriding `__array_wrap__`, a NumPy hook that is executed -after the calculation and before returning the value. To our knowledge, there -is no way to signal back to NumPy that our code will take care of the -calculation. For this reason the calculation is actually done twice: -first in the original ndarray and then in the one that has been -converted to the right units. Therefore, for numerically intensive code, you +memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude. .. _`NumPy ndarray`: http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html +.. _`ndarray-like types supporting NEP-18`: https://numpy.org/neps/nep-0018-array-function-protocol.html .. _ufuncs: http://docs.scipy.org/doc/numpy/reference/ufuncs.html .. _`NumPy ndarray methods`: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods diff --git a/pint/quantity.py b/pint/quantity.py index cfa9df593..ca6f25f2b 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1308,6 +1308,8 @@ def __bool__(self): __nonzero__ = __bool__ # NumPy function/ufunc support + __array_priority__ = 17 + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if method != "__call__": # Only handle ufuncs as callables From 65a9af13631191230e6dbb477f0c49a35adcb31a Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 8 Dec 2019 21:28:27 -0600 Subject: [PATCH 169/612] Move BehaviorChangeWarning to first array Quantity creation --- pint/compat.py | 21 ++++++++++++--------- pint/quantity.py | 10 +++++++++- pint/testsuite/test_quantity.py | 14 +++++++++++++- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 254a7b11c..abe51266c 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -25,17 +25,21 @@ def tokenizer(input_string): # TODO: remove this warning after v0.10 class BehaviorChangeWarning(UserWarning): pass -_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. +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 use the following code to import pint: +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") - import pint + Quantity([]) To disable the new behavior, see https://www.numpy.org/neps/nep-0018-array-function-protocol.html#implementation @@ -74,9 +78,7 @@ def __array_function__(self, *args, **kwargs): return False HAS_NUMPY_ARRAY_FUNCTION = _test_array_function_protocol() - - if HAS_NUMPY_ARRAY_FUNCTION: - warnings.warn(_msg, BehaviorChangeWarning) + SKIP_ARRAY_FUNCTION_CHANGE_WARNING = not HAS_NUMPY_ARRAY_FUNCTION NP_NO_VALUE = np._NoValue @@ -91,6 +93,7 @@ class ndarray: NUMPY_VER = '0' NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False + SKIP_ARRAY_FUNCTION_CHANGE_WARNING = True NP_NO_VALUE = None def _to_magnitude(value, force_ndarray=False): diff --git a/pint/quantity.py b/pint/quantity.py index ca6f25f2b..6d57e4281 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -26,7 +26,9 @@ from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, UndefinedUnitError, UnitStrippedWarning) from .definitions import UnitDefinition -from .compat import Loc, NUMPY_VER, ndarray, np, _to_magnitude, is_upcast_type, eq +from .compat import (Loc, NUMPY_VER, SKIP_ARRAY_FUNCTION_CHANGE_WARNING, + BehaviorChangeWarning, ndarray, np, _to_magnitude, is_upcast_type, eq, + array_function_change_msg) from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, to_units_container, infer_base_unit, iterable, sized) from .numpy_func import (HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, @@ -118,6 +120,8 @@ def __reduce__(self): return _unpickle, (Quantity, self.magnitude, self._units) def __new__(cls, value, units=None): + global SKIP_ARRAY_FUNCTION_CHANGE_WARNING + if units is None: if isinstance(value, str): if value == '': @@ -156,6 +160,10 @@ def __new__(cls, value, units=None): 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 diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index d07e22983..dc82ff2f0 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -4,12 +4,14 @@ import datetime import math import operator as op +import warnings from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry from pint.unit import UnitsContainer -from pint.compat import np +from pint.compat import BehaviorChangeWarning, np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase +from unittest.mock import patch class TestQuantity(QuantityTestCase): @@ -402,6 +404,16 @@ def test_notiter(self): with self.assertRaises(TypeError): 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_([]) + with warnings.catch_warnings(): + warnings.filterwarnings('error') + self.Q_([]) + class TestQuantityToCompact(QuantityTestCase): From 8773bdb6f6f56d12e8f8558d3699707053ce3c91 Mon Sep 17 00:00:00 2001 From: Giuseppe Corbelli Date: Mon, 9 Dec 2019 10:00:42 +0100 Subject: [PATCH 170/612] Fixed the definition of RKM unit as gf / tex --- pint/default_en.txt | 2 +- pint/testsuite/test_contexts.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index c860a301f..c13888502 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -626,7 +626,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N denier = gram / (9 * kilometer) = den = Td jute = pound / (14400 * yard) = Tj aberdeen = jute = Ta - RKM = kgf * 1000 / tex + RKM = gf / tex number_english = 840 * yard / pound = Ne = NeC = ECC number_meter = kilometer / kilogram = Nm diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 336f4dea3..32ea70d79 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -673,6 +673,12 @@ def test_textile(self): msg = '{} <-> {}'.format(a, b) self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) + # Check RKM <-> cN/tex conversion + self.assertQuantityAlmostEqual(1 * ureg.RKM, 0.980665 * ureg.cN / ureg.tex) + self.assertQuantityAlmostEqual((1/0.980665) * ureg.RKM, 1 * ureg.cN / ureg.tex) + self.assertAlmostEqual((1 * ureg.RKM).to(ureg.cN / ureg.tex).m, 0.980665) + self.assertAlmostEqual((1 * ureg.cN / ureg.tex).to(ureg.RKM).m, 1/0.980665) + def test_decorator(self): ureg = self.ureg From 157ebbfd3e9e6fc5def6b70a2c3f29f59cfdda77 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 9 Dec 2019 15:58:36 +0000 Subject: [PATCH 171/612] Fix bug in #909 where every time a context is reactivated it creates a brand new cache --- CHANGES | 2 +- pint/context.py | 25 ++++++++++++++++++------- pint/registry.py | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index 16cc51528..803e0c641 100644 --- a/CHANGES +++ b/CHANGES @@ -15,7 +15,7 @@ Pint Changelog - TODO #913 - TODO #911 - Prevent 1500x slowdown when using .to() with a context - (Issue #909, Thanks Guido Imperiale) + (Issues #909 and #923, Thanks Guido Imperiale) - **BREAKING CHANGE**: Drop support for Python < 3.6, numpy < 1.14, and uncertainties < 3.0; if you still need them, please install pint 0.9. diff --git a/pint/context.py b/pint/context.py index c1782b597..fc2780359 100644 --- a/pint/context.py +++ b/pint/context.py @@ -191,6 +191,18 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) + def hashable(self): + """Generate a unique hashable and comparable representation of self, which can + be used as a key in a dict. This class cannot define ``__hash__`` because it is + mutable, and the Python interpreter does cache the output of ``__hash__``. + """ + return ( + self.name, + tuple(self.aliases), + frozenset((k, id(v)) for k, v in self.funcs.items()), + frozenset(self.defaults.items()), + ) + class ContextChain(ChainMap): """A specialized ChainMap for contexts that simplifies finding rules @@ -209,7 +221,7 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts.insert(0, contexts) + self._contexts = list(contexts) + self._contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None @@ -244,10 +256,9 @@ def transform(self, src, dst, registry, value): """ return self[(src, dst)].transform(src, dst, registry, value) - def context_ids(self): - """Hashable unique identifier of the current contents of the context chain. This - is not implemented as ``__hash__`` as doing so on a mutable object can provoke - unpredictable behaviour, as interpreter-level optimizations can cache the output - of ``__hash__``. + def hashable(self): + """Generate a unique hashable and comparable representation of self, which can + be used as a key in a dict. This class cannot define ``__hash__`` because it is + mutable, and the Python interpreter does cache the output of ``__hash__``. """ - return tuple(id(ctx) for ctx in self._contexts) + return tuple(ctx.hashable() for ctx in self._contexts) diff --git a/pint/registry.py b/pint/registry.py index 7e49ab563..3be1f5ae8 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1137,7 +1137,7 @@ def remove_context(self, name_or_alias): return context def _build_cache(self): - key = self._active_ctx.context_ids() + key = self._active_ctx.hashable() try: self._cache = self._caches[key] except KeyError: From a217701a8ee58e2b965cd37f87481949baaec1e1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 9 Dec 2019 16:06:48 +0000 Subject: [PATCH 172/612] Fix ContextChain._contexts --- pint/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/context.py b/pint/context.py index fc2780359..5768d130f 100644 --- a/pint/context.py +++ b/pint/context.py @@ -221,7 +221,7 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts = list(contexts) + self._contexts + self._contexts = list(reversed(contexts)) + self._contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None From aace78d3a61591bf9dfbf7fa7fe780c2ad220cc9 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 9 Dec 2019 16:46:53 -0600 Subject: [PATCH 173/612] More post-rebase cleanup --- pint/testsuite/test_numpy.py | 71 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 370fbeaec..ead8895ee 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -24,9 +24,11 @@ def setUpClass(cls): @property def q(self): return [[1,2],[3,4]] * self.ureg.m + @property def q_nan(self): return [[1,2],[3,np.nan]] * self.ureg.m + @property def q_temperature(self): return self.Q_([[1,2],[3,4]], self.ureg.degC) @@ -40,7 +42,7 @@ def assertNDArrayEqual(self, actual, desired): class TestNumpyArrayCreation(TestNumpyMethods): # https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html - + @helpers.requires_array_function_protocol() def test_ones_like(self): self.assertNDArrayEqual(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) @@ -61,15 +63,16 @@ def test_full_like(self): self.Q_([[0, 0], [0, 0]], self.ureg.degC)) self.assertNDArrayEqual(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) + class TestNumpyArrayManipulation(TestNumpyMethods): - #TODO + # TODO # https://www.numpy.org/devdocs/reference/routines.array-manipulation.html # copyto # broadcast , broadcast_arrays # asarray asanyarray asmatrix asfarray asfortranarray ascontiguousarray asarray_chkfinite asscalar require - + # Changing array shape - + def test_flatten(self): self.assertQuantityEqual(self.q.flatten(), [1, 2, 3, 4] * self.ureg.m) @@ -79,7 +82,7 @@ def test_flat(self): def test_reshape(self): self.assertQuantityEqual(self.q.reshape([1,4]), [[1, 2, 3, 4]] * self.ureg.m) - + def test_ravel(self): self.assertQuantityEqual(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) @@ -96,7 +99,7 @@ def test_moveaxis(self): @helpers.requires_array_function_protocol() def test_rollaxis(self): self.assertQuantityEqual(np.rollaxis(self.q, 1), np.array([[1,2],[3,4]]).T * self.ureg.m) - + @helpers.requires_array_function_protocol() def test_swapaxes(self): self.assertQuantityEqual(np.swapaxes(self.q, 1,0), np.array([[1,2],[3,4]]).T * self.ureg.m) @@ -111,9 +114,9 @@ def test_transpose_numpy_func(self): @helpers.requires_array_function_protocol() def test_flip_numpy_func(self): self.assertQuantityEqual(np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m) - + # Changing number of dimensions - + @helpers.requires_array_function_protocol() def test_atleast_1d(self): actual = np.atleast_1d(self.Q_(0, self.ureg.degC), self.q.flatten()) @@ -137,23 +140,23 @@ def test_atleast_3d(self): for ind_actual, ind_expected in zip(actual, expected): self.assertQuantityEqual(ind_actual, ind_expected) self.assertQuantityEqual(np.atleast_3d(self.q), np.array([[[1],[2]],[[3],[4]]])* self.ureg.m) - + @helpers.requires_array_function_protocol() def test_broadcast_to(self): self.assertQuantityEqual(np.broadcast_to(self.q[:,1], (2,2)), np.array([[2,4],[2,4]]) * self.ureg.m) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_expand_dims(self): self.assertQuantityEqual(np.expand_dims(self.q, 0), np.array([[[1, 2],[3, 4]]])* self.ureg.m) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_squeeze(self): self.assertQuantityEqual(np.squeeze(self.q), self.q) self.assertQuantityEqual( self.q.reshape([1,4]).squeeze(), [1, 2, 3, 4] * self.ureg.m ) - + # Changing number of dimensions # Joining arrays @helpers.requires_array_function_protocol() @@ -162,29 +165,29 @@ def test_concatentate(self): np.concatenate([self.q]*2), self.Q_(np.concatenate([self.q.m]*2), self.ureg.m) ) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_stack(self): self.assertQuantityEqual( np.stack([self.q]*2), self.Q_(np.stack([self.q.m]*2), self.ureg.m) ) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_column_stack(self): self.assertQuantityEqual( np.column_stack([self.q[:,0],self.q[:,1]]), self.q ) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_dstack(self): self.assertQuantityEqual( np.dstack([self.q]*2), self.Q_(np.dstack([self.q.m]*2), self.ureg.m) ) - - @helpers.requires_array_function_protocol() + + @helpers.requires_array_function_protocol() def test_hstack(self): self.assertQuantityEqual( np.hstack([self.q]*2), @@ -201,7 +204,7 @@ def test_vstack(self): @helpers.requires_array_function_protocol() def test_block(self): self.assertQuantityEqual( - np.block([self.q[0,:],self.q[1,:]]), + np.block([self.q[0, :],self.q[1, :]]), self.Q_([1,2,3,4], self.ureg.m) ) @@ -218,7 +221,8 @@ def test_astype(self): def test_item(self): self.assertQuantityEqual(self.Q_([[0]], 'm').item(), 0 * self.ureg.m) - + + class TestNumpyMathematicalFunctions(TestNumpyMethods): # https://www.numpy.org/devdocs/reference/routines.math.html # Trigonometric functions @@ -226,7 +230,7 @@ class TestNumpyMathematicalFunctions(TestNumpyMethods): def test_unwrap(self): self.assertQuantityEqual(np.unwrap([0,3*np.pi]*self.ureg.radians), [0,np.pi]) self.assertQuantityEqual(np.unwrap([0,540]*self.ureg.deg), [0,180]*self.ureg.deg) - + # Rounding @helpers.requires_array_function_protocol() @@ -241,7 +245,7 @@ def test_fix(self): def test_prod(self): self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) - + def test_sum(self): self.assertEqual(self.q.sum(), 10*self.ureg.m) self.assertQuantityEqual(self.q.sum(0), [4, 6]*self.ureg.m) @@ -364,6 +368,7 @@ def test_exponentiation_array_exp_2(self): q_cp = copy.copy(q) self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) + class TestNumpyUnclassified(TestNumpyMethods): def test_tolist(self): self.assertEqual(self.q.tolist(), [[1*self.ureg.m, 2*self.ureg.m], [3*self.ureg.m, 4*self.ureg.m]]) @@ -374,7 +379,7 @@ def test_fill(self): self.assertQuantityEqual(tmp, [[6, 6], [6, 6]] * self.ureg.ft) tmp.fill(5 * self.ureg.m) self.assertQuantityEqual(tmp, [[5, 5], [5, 5]] * self.ureg.m) - + def test_take(self): self.assertQuantityEqual(self.q.take([0,1,2,3]), self.q.flatten()) @@ -440,8 +445,7 @@ def test_compress(self): def test_searchsorted(self): q = self.q.flatten() - self.assertNDArrayEqual(q.searchsorted([1.5, 2.5] * self.ureg.m), - [1, 2]) + self.assertNDArrayEqual(q.searchsorted([1.5, 2.5] * self.ureg.m), [1, 2]) q = self.q.flatten() self.assertRaises(DimensionalityError, q.searchsorted, [1.5, 2.5]) @@ -449,8 +453,7 @@ def test_searchsorted(self): def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" q = self.q.flatten() - self.assertNDArrayEqual(np.searchsorted(q, [1.5, 2.5] * self.ureg.m), - [1, 2]) + self.assertNDArrayEqual(np.searchsorted(q, [1.5, 2.5] * self.ureg.m), [1, 2]) def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m @@ -638,7 +641,7 @@ def test_cumprod(self): @helpers.requires_array_function_protocol() def test_nanstd_numpy_func(self): self.assertQuantityAlmostEqual(np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5) - + @helpers.requires_numpy_previous_than('1.10') def test_integer_div(self): a = [1] * self.ureg.m @@ -713,13 +716,13 @@ def test_reversible_op(self): def test_pickle(self): import pickle - set_application_registry(self.ureg) + def pickle_test(q): pq = pickle.loads(pickle.dumps(q)) self.assertNDArrayEqual(q.magnitude, pq.magnitude) self.assertEqual(q.units, pq.units) - pickle_test([10,20]*self.ureg.m) + pickle_test([10, 20]*self.ureg.m) def test_equal(self): x = self.q.magnitude From 8707693c0418fb7c49e85d872f02b3021f057cae Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 9 Dec 2019 21:04:54 -0600 Subject: [PATCH 174/612] Add link to wraps in NumPy doc commentary on performance --- docs/numpy.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/numpy.rst b/docs/numpy.rst index bf23b36ae..c4965eebe 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -156,7 +156,8 @@ information on these protocols, see This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you -might want to convert the objects first and then use directly the magnitude. +might want to convert the objects first and then use directly the magnitude, +such as by using Pint's `wraps` utility (see :ref:`wrapping`). From e0d6686d08aa9447270d0ba4139e7d81824482a3 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 10 Dec 2019 22:40:42 -0600 Subject: [PATCH 175/612] NumPy function util cleanup based on feedback --- pint/numpy_func.py | 63 +++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index beafd4fdf..f7ecf7db1 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -8,6 +8,7 @@ """ from inspect import signature +from itertools import chain import warnings from .compat import NP_NO_VALUE, is_upcast_type, np, eq @@ -36,16 +37,34 @@ def _is_quantity_sequence(arg): def _get_first_input_units(args, kwargs={}): """Obtain the first valid unit from a collection of args and kwargs.""" - args_combo = list(args) + list(kwargs.values()) - out_units=None - for arg in args_combo: + for arg in chain(args, kwargs.values()): if _is_quantity(arg): - out_units = arg.units + return arg.units elif _is_quantity_sequence(arg): - out_units = arg[0].units - if out_units is not None: - break - return out_units + return arg[0].units + + +def convert_arg(arg, pre_calc_units): + """Convert quantities and sequences of quantities to pre_calc_units and strip units. + + Helper function for convert_to_consistent_units. + """ + if pre_calc_units is not None: + if _is_quantity(arg): + return arg.m_as(pre_calc_units) + elif _is_quantity_sequence(arg): + return [item.m_as(pre_calc_units) for item in arg] + elif arg is not None: + if pre_calc_units.dimensionless: + return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) + else: + raise DimensionalityError('dimensionless', pre_calc_units) + else: + if _is_quantity(arg): + return arg.m + elif _is_quantity_sequence(arg): + return [item.m for item in arg] + return arg def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): @@ -56,27 +75,9 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless Quantities. If pre_calc_units is None, units are simply stripped. """ - def convert_arg(arg): - if pre_calc_units is not None: - if _is_quantity(arg): - return arg.m_as(pre_calc_units) - elif _is_quantity_sequence(arg): - return [item.m_as(pre_calc_units) for item in arg] - elif arg is not None: - if pre_calc_units.dimensionless: - return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) - else: - raise DimensionalityError('dimensionless', pre_calc_units) - else: - if _is_quantity(arg): - return arg.m - elif _is_quantity_sequence(arg): - return [item.m for item in arg] - return arg - - new_args = tuple(convert_arg(arg) for arg in args) - new_kwargs = {key: convert_arg(arg) for key, arg in kwargs.items()} - return new_args, new_kwargs + return (tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), + {key: convert_arg(arg, pre_calc_units=pre_calc_units) + for key, arg in kwargs.items()}) def unwrap_and_wrap_consistent_units(*args): @@ -174,7 +175,6 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): @implements(func_str, func_type) def implementation(*args, **kwargs): - args_and_kwargs = list(args) + list(kwargs.values()) first_input_units = _get_first_input_units(args, kwargs) if input_units == "all_consistent": # Match all input args/kwargs to same units @@ -196,7 +196,8 @@ def implementation(*args, **kwargs): result_unit = first_input_units elif output_unit in ['sum', 'mul', 'delta', 'delta,div', 'div', 'variance', 'square', 'sqrt', 'reciprocal', 'size']: - result_unit = get_op_output_unit(output_unit, first_input_units, args_and_kwargs) + result_unit = get_op_output_unit(output_unit, first_input_units, + tuple(chain(args, kwargs.values()))) else: result_unit = output_unit From 9d7766514f64fe33e36260a629d944ee4c7cbd41 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 12 Dec 2019 13:33:41 +0100 Subject: [PATCH 176/612] use LegacyVersion instead of StrictVersion which breaks for numpy dev --- pint/quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 6d57e4281..f05c7cf7a 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -19,7 +19,7 @@ import re import warnings -from distutils.version import StrictVersion +from pkg_resources.extern.packaging.version import LegacyVersion from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts) @@ -988,7 +988,7 @@ def __mul__(self, other): def __matmul__(self, other): # Use NumPy ufunc (existing since 1.16) for matrix multiplication - if StrictVersion(NUMPY_VER) >= StrictVersion('1.16'): + if LegacyVersion(NUMPY_VER) >= LegacyVersion('1.16'): return np.matmul(self, other) else: return NotImplemented From 506ae0660b0f655a04504e8d8eb5fc58d8dd4622 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 12 Dec 2019 13:45:40 +0100 Subject: [PATCH 177/612] use version.parse to be more robust --- pint/quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index f05c7cf7a..8c8625bef 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -19,7 +19,7 @@ import re import warnings -from pkg_resources.extern.packaging.version import LegacyVersion +from pkg_resources.extern.packaging import version from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts) @@ -988,7 +988,7 @@ def __mul__(self, other): def __matmul__(self, other): # Use NumPy ufunc (existing since 1.16) for matrix multiplication - if LegacyVersion(NUMPY_VER) >= LegacyVersion('1.16'): + if version.parse(NUMPY_VER) >= version.parse('1.16'): return np.matmul(self, other) else: return NotImplemented From 4b3fc6c38dad1fc8c7219be21242f0bf59d2ca70 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 12 Dec 2019 14:07:25 +0100 Subject: [PATCH 178/612] require setuptools --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4fab6b104..71d5bc9fb 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def read(filename): 'Programming Language :: Python :: 3.8', ], python_requires='>=3.6', + install_requires=['setuptools'], extras_require={ 'numpy': ['numpy >= 1.14'], 'uncertainties': ['uncertainties >= 3.0'], From 147d21358b6e10ebc05228a35f909ef61b372896 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 12 Dec 2019 17:53:06 +0000 Subject: [PATCH 179/612] Fix Jupyter and LateX repr --- CHANGES | 2 + pint/formatting.py | 20 +++++--- pint/testsuite/test_measurement.py | 12 ++--- pint/testsuite/test_quantity.py | 12 ++--- pint/testsuite/test_unit.py | 81 +++++++++++++++++++----------- 5 files changed, 79 insertions(+), 48 deletions(-) diff --git a/CHANGES b/CHANGES index b4694eca1..0909b1d03 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Fix HTML (Jupyter Notebook) and LateX representation of some units + (Issue #927, Thanks Guido Imperiale) - **BREAKING CHANGE**: Implement NEP-18 for Pint Quantities. Most NumPy functions that previously stripped units when applied to diff --git a/pint/formatting.py b/pint/formatting.py index cd23de82f..dc92d8671 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -75,7 +75,7 @@ def _pretty_fmt_exponent(num): 'single_denominator': True, 'product_fmt': r' ', 'division_fmt': r'{}/{}', - 'power_fmt': '{}{}', + 'power_fmt': '{}^{}', 'parentheses_fmt': r'({})', }, @@ -215,13 +215,19 @@ def format_unit(unit, spec, **kwspec): fmt.update(kwspec) if spec == 'L': - rm = [(r'\mathrm{{{}}}'.format(u), p) for u, p in unit.items()] - result = formatter(rm, **fmt) + # Latex + rm = [ + (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: - result = formatter(unit.items(), **fmt) - if spec == 'L': - result = result.replace('[', '{').replace(']', '}') - return result + # Plain text + return formatter(unit.items(), **fmt) def siunitx_format_unit(units): diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 47edc5c59..189ad4f10 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -48,13 +48,13 @@ def test_format(self): #self.assertEqual('{:!r}'.format(m), '') self.assertEqual('{0:P}'.format(m), '(4.00 ± 0.10) second²') self.assertEqual('{0:L}'.format(m), r'\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:H}'.format(m), '(4.00 ± 0.10) second2') + self.assertEqual('{0:H}'.format(m), '(4.00 ± 0.10) second^2') self.assertEqual('{0:C}'.format(m), '(4.00+/-0.10) second**2') self.assertEqual('{0:Lx}'.format(m), r'\SI[separate-uncertainty=true]{4.00(10)}{\second\squared}') self.assertEqual('{0:.1f}'.format(m), '(4.0 +/- 0.1) second ** 2') self.assertEqual('{0:.1fP}'.format(m), '(4.0 ± 0.1) second²') self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second2') + self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second^2') self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') self.assertEqual('{0:.1fLx}'.format(m), r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') @@ -65,7 +65,7 @@ def test_format_paru(self): self.assertEqual('{0:.3uS}'.format(m), '0.2000(100) second ** 2') self.assertEqual('{0:.3uSP}'.format(m), '0.2000(100) second²') self.assertEqual('{0:.3uSL}'.format(m), r'0.2000\left(100\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.3uSH}'.format(m), '0.2000(100) second2') + self.assertEqual('{0:.3uSH}'.format(m), '0.2000(100) second^2') self.assertEqual('{0:.3uSC}'.format(m), '0.2000(100) second**2') def test_format_u(self): @@ -74,7 +74,7 @@ def test_format_u(self): self.assertEqual('{0:.3u}'.format(m), '(0.2000 +/- 0.0100) second ** 2') self.assertEqual('{0:.3uP}'.format(m), '(0.2000 ± 0.0100) second²') self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second2') + self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second^2') self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') self.assertEqual('{0:.3uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') self.assertEqual('{0:.1uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') @@ -86,7 +86,7 @@ def test_format_percu(self): self.assertEqual('{0:.1u%}'.format(m), '(20 +/- 1)% second ** 2') self.assertEqual('{0:.1u%P}'.format(m), '(20 ± 1)% second²') self.assertEqual('{0:.1u%L}'.format(m), r'\left(20 \pm 1\right) \%\ \mathrm{second}^{2}') - self.assertEqual('{0:.1u%H}'.format(m), '(20 ± 1)% second2') + self.assertEqual('{0:.1u%H}'.format(m), '(20 ± 1)% second^2') self.assertEqual('{0:.1u%C}'.format(m), '(20+/-1)% second**2') def test_format_perce(self): @@ -95,7 +95,7 @@ def test_format_perce(self): self.assertEqual('{0:.1ue}'.format(m), '(2.0 +/- 0.1)e-01 second ** 2') self.assertEqual('{0:.1ueP}'.format(m), '(2.0 ± 0.1)×10⁻¹ second²') self.assertEqual('{0:.1ueL}'.format(m), r'\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}') - self.assertEqual('{0:.1ueH}'.format(m), '(2.0 ± 0.1)e-01 second2') + self.assertEqual('{0:.1ueH}'.format(m), '(2.0 ± 0.1)e-01 second^2') self.assertEqual('{0:.1ueC}'.format(m), '(2.0+/-0.1)e-01 second**2') def test_raise_build(self): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index dc82ff2f0..fb183e6bf 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -114,12 +114,12 @@ def test_quantity_format(self): ('{0:.4f}', '{0:.4f} {1!s}'.format(x.magnitude, x.units)), ('{0:L}', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), ('{0:P}', '4.12345678 kilogram·meter²/second'), - ('{0:H}', '4.12345678 kilogram meter2/second'), + ('{0:H}', '4.12345678 kilogram meter^2/second'), ('{0:C}', '4.12345678 kilogram*meter**2/second'), ('{0:~}', '4.12345678 kg * m ** 2 / s'), ('{0:L~}', r'4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}'), ('{0:P~}', '4.12345678 kg·m²/s'), - ('{0:H~}', '4.12345678 kg m2/s'), + ('{0:H~}', '4.12345678 kg m^2/s'), ('{0:C~}', '4.12345678 kg*m**2/s'), ('{0:Lx}', r'\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}'), ): @@ -167,12 +167,12 @@ def test_default_formatting(self): x = ureg.Quantity(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in (('L', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), ('P', '4.12345678 kilogram·meter²/second'), - ('H', '4.12345678 kilogram meter2/second'), + ('H', '4.12345678 kilogram meter^2/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~', '4.12345678 kg m2/s'), + ('H~', '4.12345678 kg m^2/s'), ('C~', '4.12345678 kg*m**2/s'), ): ureg.default_format = spec @@ -205,14 +205,14 @@ def pretty(cls, data): ureg = UnitRegistry() x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) self.assertEqual(x._repr_html_(), - "3.5 kilogram meter2/second") + "3.5 kilogram meter^2/second") self.assertEqual(x._repr_latex_(), r'$3.5\ \frac{\mathrm{kilogram} \cdot ' r'\mathrm{meter}^{2}}{\mathrm{second}}$') x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "3.5 kilogram·meter²/second") ureg.default_format = "~" - self.assertEqual(x._repr_html_(), "3.5 kg m2/s") + self.assertEqual(x._repr_html_(), "3.5 kg m^2/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 193211742..30ccec0ee 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -33,37 +33,60 @@ def test_unit_repr(self): def test_unit_formatting(self): x = self.U_(UnitsContainer(meter=2, kilogram=1, second=-1)) - for spec, result in (('{0}', str(x)), ('{0!s}', str(x)), - ('{0!r}', repr(x)), - ('{0:L}', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('{0:P}', 'kilogram·meter²/second'), - ('{0:H}', 'kilogram meter2/second'), - ('{0:C}', 'kilogram*meter**2/second'), - ('{0:Lx}', r'\si[]{\kilo\gram\meter\squared\per\second}'), - ('{0:~}', 'kg * m ** 2 / s'), - ('{0:L~}', r'\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}'), - ('{0:P~}', 'kg·m²/s'), - ('{0:H~}', 'kg m2/s'), - ('{0:C~}', 'kg*m**2/s'), - ): - self.assertEqual(spec.format(x), result) + for spec, result in ( + ('{}', str(x)), + ('{!s}', str(x)), + ('{!r}', repr(x)), + ('{:L}', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), + ('{:P}', 'kilogram·meter²/second'), + ('{:H}', 'kilogram meter^2/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~}', 'kg m^2/s'), + ('{:C~}', 'kg*m**2/s'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(x), result) def test_unit_default_formatting(self): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - for spec, result in (('L', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('P', 'kilogram·meter²/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~', 'kg m2/s'), - ('C~', 'kg*m**2/s'), - ): - ureg.default_format = spec - self.assertEqual('{0}'.format(x), result, - 'Failed for {0}, {1}'.format(spec, result)) + for spec, result in ( + ('L', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), + ('P', 'kilogram·meter²/second'), + ('H', 'kilogram meter^2/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~', 'kg m^2/s'), + ('C~', 'kg*m**2/s'), + ): + with self.subTest(spec): + ureg.default_format = spec + self.assertEqual(f'{x}', result, f'Failed for {spec}, {result}') + + def test_unit_formatting_snake_case(self): + # Test that snake_case units are escaped where appropriate + ureg = UnitRegistry() + x = ureg.Unit(UnitsContainer(oil_barrel=1)) + for spec, result in ( + ('L', r'\mathrm{oil\_barrel}'), + ('P', 'oil_barrel'), + ('H', 'oil\_barrel'), + ('C', 'oil_barrel'), + ('~', 'oil_bbl'), + ('L~', r'\mathrm{oil\_bbl}'), + ('P~', 'oil_bbl'), + ('H~', 'oil\_bbl'), + ('C~', 'oil_bbl'), + ): + with self.subTest(spec): + ureg.default_format = spec + self.assertEqual(f'{x}', result, f'Failed for {spec}, {result}') def test_ipython(self): alltext = [] @@ -75,13 +98,13 @@ def text(text): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), "kilogram meter2/second") + self.assertEqual(x._repr_html_(), "kilogram meter^2/second") self.assertEqual(x._repr_latex_(), r'$\frac{\mathrm{kilogram} \cdot ' r'\mathrm{meter}^{2}}{\mathrm{second}}$') x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "kilogram·meter²/second") ureg.default_format = "~" - self.assertEqual(x._repr_html_(), "kg m2/s") + self.assertEqual(x._repr_html_(), "kg m^2/s") self.assertEqual(x._repr_latex_(), r'$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$') alltext = [] From 227aabeaf5025834ce50ccfca642d02e3062c93b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 12 Dec 2019 18:05:39 +0000 Subject: [PATCH 180/612] Tests cleanup --- pint/testsuite/test_measurement.py | 98 ++++++++++++++++++------------ pint/testsuite/test_quantity.py | 89 +++++++++++++++------------ 2 files changed, 110 insertions(+), 77 deletions(-) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 189ad4f10..931be4900 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -42,61 +42,83 @@ def test_build(self): def test_format(self): v, u = self.Q_(4.0, 's ** 2'), self.Q_(.1, 's ** 2') m = self.ureg.Measurement(v, u) - self.assertEqual(str(m), '(4.00 +/- 0.10) second ** 2') - self.assertEqual(repr(m), '') - #self.assertEqual('{:!s}'.format(m), '(4.00 +/- 0.10) second ** 2') - #self.assertEqual('{:!r}'.format(m), '') - self.assertEqual('{0:P}'.format(m), '(4.00 ± 0.10) second²') - self.assertEqual('{0:L}'.format(m), r'\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:H}'.format(m), '(4.00 ± 0.10) second^2') - self.assertEqual('{0:C}'.format(m), '(4.00+/-0.10) second**2') - self.assertEqual('{0:Lx}'.format(m), r'\SI[separate-uncertainty=true]{4.00(10)}{\second\squared}') - self.assertEqual('{0:.1f}'.format(m), '(4.0 +/- 0.1) second ** 2') - self.assertEqual('{0:.1fP}'.format(m), '(4.0 ± 0.1) second²') - self.assertEqual('{0:.1fL}'.format(m), r'\left(4.0 \pm 0.1\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.1fH}'.format(m), '(4.0 ± 0.1) second^2') - self.assertEqual('{0:.1fC}'.format(m), '(4.0+/-0.1) second**2') - self.assertEqual('{0:.1fLx}'.format(m), r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}') + + for spec, result in ( + ("{}", '(4.00 +/- 0.10) second ** 2'), + ("{!r}", ''), + ('{:P}', '(4.00 ± 0.10) second²'), + ('{:L}', r'\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}'), + ('{:H}', '(4.00 ± 0.10) second^2'), + ('{:C}', '(4.00+/-0.10) second**2'), + ('{:Lx}', r'\SI[separate-uncertainty=true]{4.00(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}', '(4.0 ± 0.1) second^2'), + ('{:.1fC}', '(4.0+/-0.1) second**2'), + ('{:.1fLx}', r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) def test_format_paru(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') m = self.ureg.Measurement(v, u) - self.assertEqual('{0:uS}'.format(m), '0.200(10) second ** 2') - self.assertEqual('{0:.3uS}'.format(m), '0.2000(100) second ** 2') - self.assertEqual('{0:.3uSP}'.format(m), '0.2000(100) second²') - self.assertEqual('{0:.3uSL}'.format(m), r'0.2000\left(100\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.3uSH}'.format(m), '0.2000(100) second^2') - self.assertEqual('{0:.3uSC}'.format(m), '0.2000(100) second**2') + + for spec, result in ( + ('{:uS}', '0.200(10) second ** 2'), + ('{:.3uS}', '0.2000(100) second ** 2'), + ('{:.3uSP}', '0.2000(100) second²'), + ('{:.3uSL}', r'0.2000\left(100\right)\ \mathrm{second}^{2}'), + ('{:.3uSH}', '0.2000(100) second^2'), + ('{:.3uSC}', '0.2000(100) second**2'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) def test_format_u(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') m = self.ureg.Measurement(v, u) - self.assertEqual('{0:.3u}'.format(m), '(0.2000 +/- 0.0100) second ** 2') - self.assertEqual('{0:.3uP}'.format(m), '(0.2000 ± 0.0100) second²') - self.assertEqual('{0:.3uL}'.format(m), r'\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}') - self.assertEqual('{0:.3uH}'.format(m), '(0.2000 ± 0.0100) second^2') - self.assertEqual('{0:.3uC}'.format(m), '(0.2000+/-0.0100) second**2') - self.assertEqual('{0:.3uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}') - self.assertEqual('{0:.1uLx}'.format(m), r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}') + + for spec, result in ( + ('{:.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}', '(0.2000 ± 0.0100) second^2'), + ('{:.3uC}', '(0.2000+/-0.0100) second**2'), + ('{:.3uLx}', r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}'), + ('{:.1uLx}', r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) def test_format_percu(self): self.test_format_perce() v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') m = self.ureg.Measurement(v, u) - self.assertEqual('{0:.1u%}'.format(m), '(20 +/- 1)% second ** 2') - self.assertEqual('{0:.1u%P}'.format(m), '(20 ± 1)% second²') - self.assertEqual('{0:.1u%L}'.format(m), r'\left(20 \pm 1\right) \%\ \mathrm{second}^{2}') - self.assertEqual('{0:.1u%H}'.format(m), '(20 ± 1)% second^2') - self.assertEqual('{0:.1u%C}'.format(m), '(20+/-1)% second**2') + + for spec, result in ( + ('{:.1u%}', '(20 +/- 1)% second ** 2'), + ('{:.1u%P}', '(20 ± 1)% second²'), + ('{:.1u%L}', r'\left(20 \pm 1\right) \%\ \mathrm{second}^{2}'), + ('{:.1u%H}', '(20 ± 1)% second^2'), + ('{:.1u%C}', '(20+/-1)% second**2'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) def test_format_perce(self): v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') m = self.ureg.Measurement(v, u) - self.assertEqual('{0:.1ue}'.format(m), '(2.0 +/- 0.1)e-01 second ** 2') - self.assertEqual('{0:.1ueP}'.format(m), '(2.0 ± 0.1)×10⁻¹ second²') - self.assertEqual('{0:.1ueL}'.format(m), r'\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}') - self.assertEqual('{0:.1ueH}'.format(m), '(2.0 ± 0.1)e-01 second^2') - self.assertEqual('{0:.1ueC}'.format(m), '(2.0+/-0.1)e-01 second**2') + for spec, result in ( + ('{:.1ue}', '(2.0 +/- 0.1)e-01 second ** 2'), + ('{:.1ueP}', '(2.0 ± 0.1)×10⁻¹ second²'), + ('{:.1ueL}', r'\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}'), + ('{:.1ueH}', '(2.0 ± 0.1)e-01 second^2'), + ('{:.1ueC}', '(2.0+/-0.1)e-01 second**2'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) def test_raise_build(self): v, u = self.Q_(1.0, 's'), self.Q_(.1, 's') diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fb183e6bf..71c9e3521 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -107,23 +107,31 @@ def test_quantity_hash(self): def test_quantity_format(self): x = self.Q_(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) - for spec, result in (('{0}', str(x)), ('{0!s}', str(x)), ('{0!r}', repr(x)), - ('{0.magnitude}', str(x.magnitude)), ('{0.units}', str(x.units)), - ('{0.magnitude!s}', str(x.magnitude)), ('{0.units!s}', str(x.units)), - ('{0.magnitude!r}', repr(x.magnitude)), ('{0.units!r}', repr(x.units)), - ('{0:.4f}', '{0:.4f} {1!s}'.format(x.magnitude, x.units)), - ('{0:L}', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('{0:P}', '4.12345678 kilogram·meter²/second'), - ('{0:H}', '4.12345678 kilogram meter^2/second'), - ('{0:C}', '4.12345678 kilogram*meter**2/second'), - ('{0:~}', '4.12345678 kg * m ** 2 / s'), - ('{0:L~}', r'4.12345678\ \frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}'), - ('{0:P~}', '4.12345678 kg·m²/s'), - ('{0:H~}', '4.12345678 kg m^2/s'), - ('{0:C~}', '4.12345678 kg*m**2/s'), - ('{0:Lx}', r'\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}'), - ): - self.assertEqual(spec.format(x), result) + for spec, result in ( + ('{}', str(x)), + ('{!s}', str(x)), + ('{!r}', repr(x)), + ('{.magnitude}', str(x.magnitude)), + ('{.units}', str(x.units)), + ('{.magnitude!s}', str(x.magnitude)), + ('{.units!s}', str(x.units)), + ('{.magnitude!r}', repr(x.magnitude)), + ('{.units!r}', repr(x.units)), + ('{:.4f}', f'{x.magnitude:.4f} {x.units!s}'), + ('{:L}', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), + ('{:P}', '4.12345678 kilogram·meter²/second'), + ('{:H}', '4.12345678 kilogram meter^2/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~}', '4.12345678 kg m^2/s'), + ('{:C~}', '4.12345678 kg*m**2/s'), + ('{:Lx}', r'\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}'), + ): + with self.subTest(spec): + self.assertEqual(spec.format(x), result) + # Check the special case that prevents e.g. '3 1 / second' x = self.Q_(3, UnitsContainer(second=-1)) self.assertEqual('{0}'.format(x), '3 / second') @@ -131,15 +139,17 @@ def test_quantity_format(self): @helpers.requires_numpy() def test_quantity_array_format(self): x = self.Q_(np.array([1e-16, 1.0000001, 10000000.0, 1e12, np.nan, np.inf]), "kg * m ** 2") - for spec, result in (('{0}', str(x)), - ('{0.magnitude}', str(x.magnitude)), - ('{0:e}', "[1.000000e-16 1.000000e+00 1.000000e+07 1.000000e+12 nan inf] kilogram * meter ** 2"), - ('{0:E}', "[1.000000E-16 1.000000E+00 1.000000E+07 1.000000E+12 NAN INF] kilogram * meter ** 2"), - ('{0:.2f}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kilogram * meter ** 2"), - ('{0:.2f~P}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kg·m²"), - ('{0:g~P}', "[1e-16 1 1e+07 1e+12 nan inf] kg·m²"), - ): - self.assertEqual(spec.format(x), result) + for spec, result in ( + ('{}', str(x)), + ('{.magnitude}', str(x.magnitude)), + ('{:e}', "[1.000000e-16 1.000000e+00 1.000000e+07 1.000000e+12 nan inf] kilogram * meter ** 2"), + ('{:E}', "[1.000000E-16 1.000000E+00 1.000000E+07 1.000000E+12 NAN INF] kilogram * meter ** 2"), + ('{:.2f}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kilogram * meter ** 2"), + ('{:.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²"), + ): + with self.subTest(spec): + self.assertEqual(spec.format(x), result) def test_format_compact(self): q1 = (200e-9 * self.ureg.s).to_compact() @@ -161,22 +171,23 @@ def test_format_compact(self): self.assertEqual('{0:#.1f}'.format(q2), '{0}'.format(q2b)) self.assertEqual('{0:#.1f}'.format(q3), '{0}'.format(q3b)) - def test_default_formatting(self): ureg = UnitRegistry() x = ureg.Quantity(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) - for spec, result in (('L', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('P', '4.12345678 kilogram·meter²/second'), - ('H', '4.12345678 kilogram meter^2/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~', '4.12345678 kg m^2/s'), - ('C~', '4.12345678 kg*m**2/s'), - ): - ureg.default_format = spec - self.assertEqual('{0}'.format(x), result) + for spec, result in ( + ('L', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), + ('P', '4.12345678 kilogram·meter²/second'), + ('H', '4.12345678 kilogram meter^2/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~', '4.12345678 kg m^2/s'), + ('C~', '4.12345678 kg*m**2/s'), + ): + with self.subTest(spec): + ureg.default_format = spec + self.assertEqual(f'{x}', result) def test_exponent_formatting(self): ureg = UnitRegistry() From 165f85e21d5e1493bee3c5d188432999e7d6dee7 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 11:08:49 +0000 Subject: [PATCH 181/612] black --- bench/bench.py | 36 +- docs/_themes/flask_theme_support.py | 147 +-- docs/conf.py | 188 ++-- pint/__init__.py | 43 +- pint/babel_names.py | 259 +++-- pint/compat.py | 30 +- pint/context.py | 48 +- pint/definitions.py | 64 +- pint/formatting.py | 210 ++-- pint/matplotlib.py | 12 +- pint/measurement.py | 77 +- pint/numpy_func.py | 489 +++++--- pint/pint_eval.py | 109 +- pint/quantity.py | 620 ++++++----- pint/registry.py | 394 ++++--- pint/registry_helpers.py | 94 +- pint/systems.py | 70 +- pint/testsuite/__init__.py | 57 +- pint/testsuite/helpers.py | 91 +- pint/testsuite/parameterized.py | 48 +- pint/testsuite/test_babel.py | 25 +- pint/testsuite/test_contexts.py | 495 +++++---- pint/testsuite/test_converters.py | 14 +- pint/testsuite/test_definitions.py | 74 +- pint/testsuite/test_formatter.py | 59 +- pint/testsuite/test_infer_base_unit.py | 22 +- pint/testsuite/test_issues.py | 358 +++--- pint/testsuite/test_measurement.py | 160 +-- pint/testsuite/test_numpy.py | 519 +++++---- pint/testsuite/test_numpy_func.py | 156 ++- pint/testsuite/test_pint_eval.py | 59 +- pint/testsuite/test_pitheorem.py | 24 +- pint/testsuite/test_quantity.py | 1409 +++++++++++++----------- pint/testsuite/test_systems.py | 274 ++--- pint/testsuite/test_umath.py | 580 +++++----- pint/testsuite/test_unit.py | 716 +++++++----- pint/testsuite/test_util.py | 265 ++--- pint/unit.py | 80 +- pint/util.py | 175 +-- setup.py | 67 +- 40 files changed, 4790 insertions(+), 3827 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index 2d2576430..6593376be 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -6,7 +6,7 @@ import yaml -def time_stmt(stmt='pass', setup='pass', number=0, repeat=3): +def time_stmt(stmt="pass", setup="pass", number=0, repeat=3): """Timer function with the same behaviour as running `python -m timeit ` in the command line. @@ -25,7 +25,7 @@ def time_stmt(stmt='pass', setup='pass', number=0, repeat=3): x = t.timeit(number) except Exception: print(t.print_exc()) - return float('NaN') + return float("NaN") if x >= 0.2: break @@ -34,34 +34,34 @@ def time_stmt(stmt='pass', setup='pass', number=0, repeat=3): r = t.repeat(repeat, number) except Exception: print(t.print_exc()) - return float('NaN') + return float("NaN") best = min(r) return best / number -def build_task(task, name='', setup='', number=0, repeat=3): +def build_task(task, name="", setup="", number=0, repeat=3): nt = copy.copy(task) - nt['name'] = (name + ' ' + task.get('name', '')).strip() - nt['setup'] = (setup + '\n' + task.get('setup', '')).strip('\n') - nt['stmt'] = task.get('stmt', '') - nt['number'] = task.get('number', number) - nt['repeat'] = task.get('repeat', repeat) + nt["name"] = (name + " " + task.get("name", "")).strip() + nt["setup"] = (setup + "\n" + task.get("setup", "")).strip("\n") + nt["stmt"] = task.get("stmt", "") + nt["number"] = task.get("number", number) + nt["repeat"] = task.get("repeat", repeat) return nt -def time_task(name, stmt='pass', setup='pass', number=0, repeat=3, stmts='', base=''): +def time_task(name, stmt="pass", setup="pass", number=0, repeat=3, stmts="", base=""): if base: nvalue = time_stmt(stmt=base, setup=setup, number=number, repeat=repeat) - yield name + ' (base)', nvalue - suffix = ' (normalized)' + yield name + " (base)", nvalue + suffix = " (normalized)" else: - nvalue = 1. - suffix = '' + nvalue = 1.0 + suffix = "" if stmt: value = time_stmt(stmt=stmt, setup=setup, number=number, repeat=repeat) @@ -73,12 +73,12 @@ def time_task(name, stmt='pass', setup='pass', number=0, repeat=3, stmts='', bas yield task_name + suffix, value / nvalue -def time_file(filename, name='', setup='', number=0, repeat=3): +def time_file(filename, name="", setup="", number=0, repeat=3): """Open a yaml benchmark file an time each statement, yields a tuple with filename, task name, time in seconds. """ - with open(filename, 'r') as fp: + with open(filename, "r") as fp: tasks = yaml.load(fp) for task in tasks: @@ -87,7 +87,7 @@ def time_file(filename, name='', setup='', number=0, repeat=3): yield task_name, value -def recursive_glob(rootdir='.', pattern='*'): +def recursive_glob(rootdir=".", pattern="*"): return [ os.path.join(looproot, filename) for looproot, _, filenames in os.walk(rootdir) @@ -104,7 +104,7 @@ def main(filenames=None): for filename in filenames: print(filename) - print('-' * len(filename)) + print("-" * len(filename)) print() for task_name, value in time_file(filename): print(f"{value:.2e} {task_name}") diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 33f47449c..0dcf53b75 100644 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,7 +1,19 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal +from pygments.token import ( + Keyword, + Name, + Comment, + String, + Error, + Number, + Operator, + Generic, + Whitespace, + Punctuation, + Other, + Literal, +) class FlaskyStyle(Style): @@ -10,77 +22,68 @@ class FlaskyStyle(Style): styles = { # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - + # Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + Number: "#990000", # class: 'm' + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' } diff --git a/docs/conf.py b/docs/conf.py index 5afe625ea..2d740f327 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,224 +18,230 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', 'matplotlib.sphinxext.plot_directive', 'nbsphinx'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "matplotlib.sphinxext.plot_directive", + "nbsphinx", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'pint' -author = 'Hernan E. Grecco' +project = "pint" +author = "Hernan E. Grecco" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -try: # pragma: no cover +try: # pragma: no cover version = pkg_resources.get_distribution(project).version -except: # pragma: no cover +except: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown version = "unknown" release = version this_year = datetime.date.today().year -copyright = '%s, %s' % (this_year, author) +copyright = "%s, %s" % (this_year, author) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'default' -html_theme = 'flask' +# html_theme = 'default' +html_theme = "flask" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] -html_theme_path = ['_themes'] +# html_theme_path = [] +html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} html_sidebars = { - 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], - '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', - 'sourcelink.html', 'searchbox.html'] + "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html"], + "**": [ + "sidebarlogo.html", + "localtoc.html", + "relations.html", + "sourcelink.html", + "searchbox.html", + ], } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'pintdoc' +htmlhelp_basename = "pintdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. - 'preamble': "".join(( - '\DeclareUnicodeCharacter{2212}{-}', # MINUS - )), + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + "preamble": "".join(("\DeclareUnicodeCharacter{2212}{-}",)) # MINUS } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'pint.tex', 'pint Documentation', - 'Hernan E. Grecco', 'manual'), + ("index", "pint.tex", "pint Documentation", "Hernan E. Grecco", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pint', 'pint Documentation', - ['Hernan E. Grecco'], 1) -] +man_pages = [("index", "pint", "pint Documentation", ["Hernan E. Grecco"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -244,19 +250,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'pint', 'pint Documentation', - 'Hernan E. Grecco', 'pint', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "pint", + "pint Documentation", + "Hernan E. Grecco", + "pint", + "One line description of project.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- @@ -269,38 +281,38 @@ # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -#epub_exclude_files = [] +# epub_exclude_files = [] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {"http://docs.python.org/": None} diff --git a/pint/__init__.py b/pint/__init__.py index 22c983007..7e27f55b2 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -21,7 +21,7 @@ OffsetUnitCalculusError, RedefinitionError, UndefinedUnitError, - UnitStrippedWarning + UnitStrippedWarning, ) from .formatting import formatter from .measurement import Measurement @@ -32,16 +32,18 @@ import sys + try: from pintpandas import PintType, PintArray + _HAS_PINTPANDAS = True except Exception: _HAS_PINTPANDAS = False _, _pintpandas_error, _ = sys.exc_info() -try: # pragma: no cover - __version__ = pkg_resources.get_distribution('pint').version -except: # pragma: no cover +try: # pragma: no cover + __version__ = pkg_resources.get_distribution("pint").version +except: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown __version__ = "unknown" @@ -83,7 +85,7 @@ def set_application_registry(registry): if not isinstance(registry, (LazyRegistry, UnitRegistry)): raise TypeError("Expected UnitRegistry; got %s" % type(registry)) global _APP_REGISTRY - logger.debug('Changing app registry from %r to %r.', _APP_REGISTRY, registry) + logger.debug("Changing app registry from %r to %r.", _APP_REGISTRY, registry) _APP_REGISTRY = registry @@ -103,6 +105,7 @@ def test(): :return: a :class:`unittest.TestResult` object """ from .testsuite import run + return run() @@ -110,20 +113,18 @@ def test(): # Hint to intersphinx that, when building objects.inv, these objects must be registered # under the top-level module and not in their original submodules __all__ = ( - 'Context', - 'Measurement', - 'Quantity', - 'Unit', - 'UnitRegistry', - - 'DefinitionSyntaxError', - 'DimensionalityError', - 'OffsetUnitCalculusError', - 'RedefinitionError', - 'UndefinedUnitError', - 'UnitStrippedWarning', - - 'get_application_registry', - 'set_application_registry', - '__version__', + "Context", + "Measurement", + "Quantity", + "Unit", + "UnitRegistry", + "DefinitionSyntaxError", + "DimensionalityError", + "OffsetUnitCalculusError", + "RedefinitionError", + "UndefinedUnitError", + "UnitStrippedWarning", + "get_application_registry", + "set_application_registry", + "__version__", ) diff --git a/pint/babel_names.py b/pint/babel_names.py index b68e378ea..e4bd1a002 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -10,141 +10,136 @@ from pint.compat import HAS_BABEL _babel_units = dict( - standard_gravity='acceleration-g-force', - millibar='pressure-millibar', - metric_ton='mass-metric-ton', - megawatt='power-megawatt', - degF='temperature-fahrenheit', - dietary_calorie='energy-foodcalorie', - millisecond='duration-millisecond', - mph='speed-mile-per-hour', - acre_foot='volume-acre-foot', - mebibit='digital-megabit', - gibibit='digital-gigabit', - tebibit='digital-terabit', - mebibyte='digital-megabyte', - kibibyte='digital-kilobyte', - mm_Hg='pressure-millimeter-of-mercury', - month='duration-month', - kilocalorie='energy-kilocalorie', - cubic_mile='volume-cubic-mile', - arcsecond='angle-arc-second', - byte='digital-byte', - metric_cup='volume-cup-metric', - kilojoule='energy-kilojoule', - meter_per_second_squared='acceleration-meter-per-second-squared', - pint='volume-pint', - square_centimeter='area-square-centimeter', - in_Hg='pressure-inch-hg', - milliampere='electric-milliampere', - arcminute='angle-arc-minute', - MPG='consumption-mile-per-gallon', - hertz='frequency-hertz', - day='duration-day', - mps='speed-meter-per-second', - kilometer='length-kilometer', - square_yard='area-square-yard', - kelvin='temperature-kelvin', - kilogram='mass-kilogram', - kilohertz='frequency-kilohertz', - megahertz='frequency-megahertz', - meter='length-meter', - cubic_inch='volume-cubic-inch', - kilowatt_hour='energy-kilowatt-hour', - second='duration-second', - yard='length-yard', - light_year='length-light-year', - millimeter='length-millimeter', - metric_horsepower='power-horsepower', - gibibyte='digital-gigabyte', - ## 'temperature-generic', - liter='volume-liter', - turn='angle-revolution', - microsecond='duration-microsecond', - pound='mass-pound', - ounce='mass-ounce', - calorie='energy-calorie', - centimeter='length-centimeter', - inch='length-inch', - centiliter='volume-centiliter', - troy_ounce='mass-ounce-troy', - gram='mass-gram', - kilowatt='power-kilowatt', - knot='speed-knot', - lux='light-lux', - hectoliter='volume-hectoliter', - microgram='mass-microgram', - degC='temperature-celsius', - tablespoon='volume-tablespoon', - cubic_yard='volume-cubic-yard', - square_foot='area-square-foot', - tebibyte='digital-terabyte', - square_inch='area-square-inch', - carat='mass-carat', - hectopascal='pressure-hectopascal', - gigawatt='power-gigawatt', - watt='power-watt', - micrometer='length-micrometer', - volt='electric-volt', - bit='digital-bit', - gigahertz='frequency-gigahertz', - teaspoon='volume-teaspoon', - ohm='electric-ohm', - joule='energy-joule', - cup='volume-cup', - square_mile='area-square-mile', - nautical_mile='length-nautical-mile', - square_meter='area-square-meter', - mile='length-mile', - acre='area-acre', - nanometer='length-nanometer', - hour='duration-hour', - astronomical_unit='length-astronomical-unit', - liter_per_100kilometers ='consumption-liter-per-100kilometers', - megaliter='volume-megaliter', - ton='mass-ton', - hectare='area-hectare', - square_kilometer='area-square-kilometer', - kibibit='digital-kilobit', - mile_scandinavian='length-mile-scandinavian', - liter_per_kilometer='consumption-liter-per-kilometer', - century='duration-century', - cubic_foot='volume-cubic-foot', - deciliter='volume-deciliter', - ##pint='volume-pint-metric', - cubic_meter='volume-cubic-meter', - cubic_kilometer='volume-cubic-kilometer', - quart='volume-quart', - cc='volume-cubic-centimeter', - pound_force_per_square_inch='pressure-pound-per-square-inch', - milligram='mass-milligram', - kph='speed-kilometer-per-hour', - minute='duration-minute', - parsec='length-parsec', - picometer='length-picometer', - degree='angle-degree', - milliwatt='power-milliwatt', - week='duration-week', - ampere='electric-ampere', - milliliter='volume-milliliter', - decimeter='length-decimeter', - fluid_ounce='volume-fluid-ounce', - nanosecond='duration-nanosecond', - foot='length-foot', - karat='proportion-karat', - year='duration-year', - gallon='volume-gallon', - radian='angle-radian', + standard_gravity="acceleration-g-force", + millibar="pressure-millibar", + metric_ton="mass-metric-ton", + megawatt="power-megawatt", + degF="temperature-fahrenheit", + dietary_calorie="energy-foodcalorie", + millisecond="duration-millisecond", + mph="speed-mile-per-hour", + acre_foot="volume-acre-foot", + mebibit="digital-megabit", + gibibit="digital-gigabit", + tebibit="digital-terabit", + mebibyte="digital-megabyte", + kibibyte="digital-kilobyte", + mm_Hg="pressure-millimeter-of-mercury", + month="duration-month", + kilocalorie="energy-kilocalorie", + cubic_mile="volume-cubic-mile", + arcsecond="angle-arc-second", + byte="digital-byte", + metric_cup="volume-cup-metric", + kilojoule="energy-kilojoule", + meter_per_second_squared="acceleration-meter-per-second-squared", + pint="volume-pint", + square_centimeter="area-square-centimeter", + in_Hg="pressure-inch-hg", + milliampere="electric-milliampere", + arcminute="angle-arc-minute", + MPG="consumption-mile-per-gallon", + hertz="frequency-hertz", + day="duration-day", + mps="speed-meter-per-second", + kilometer="length-kilometer", + square_yard="area-square-yard", + kelvin="temperature-kelvin", + kilogram="mass-kilogram", + kilohertz="frequency-kilohertz", + megahertz="frequency-megahertz", + meter="length-meter", + cubic_inch="volume-cubic-inch", + kilowatt_hour="energy-kilowatt-hour", + second="duration-second", + yard="length-yard", + light_year="length-light-year", + millimeter="length-millimeter", + metric_horsepower="power-horsepower", + gibibyte="digital-gigabyte", + ## 'temperature-generic', + liter="volume-liter", + turn="angle-revolution", + microsecond="duration-microsecond", + pound="mass-pound", + ounce="mass-ounce", + calorie="energy-calorie", + centimeter="length-centimeter", + inch="length-inch", + centiliter="volume-centiliter", + troy_ounce="mass-ounce-troy", + gram="mass-gram", + kilowatt="power-kilowatt", + knot="speed-knot", + lux="light-lux", + hectoliter="volume-hectoliter", + microgram="mass-microgram", + degC="temperature-celsius", + tablespoon="volume-tablespoon", + cubic_yard="volume-cubic-yard", + square_foot="area-square-foot", + tebibyte="digital-terabyte", + square_inch="area-square-inch", + carat="mass-carat", + hectopascal="pressure-hectopascal", + gigawatt="power-gigawatt", + watt="power-watt", + micrometer="length-micrometer", + volt="electric-volt", + bit="digital-bit", + gigahertz="frequency-gigahertz", + teaspoon="volume-teaspoon", + ohm="electric-ohm", + joule="energy-joule", + cup="volume-cup", + square_mile="area-square-mile", + nautical_mile="length-nautical-mile", + square_meter="area-square-meter", + mile="length-mile", + acre="area-acre", + nanometer="length-nanometer", + hour="duration-hour", + astronomical_unit="length-astronomical-unit", + liter_per_100kilometers="consumption-liter-per-100kilometers", + megaliter="volume-megaliter", + ton="mass-ton", + hectare="area-hectare", + square_kilometer="area-square-kilometer", + kibibit="digital-kilobit", + mile_scandinavian="length-mile-scandinavian", + liter_per_kilometer="consumption-liter-per-kilometer", + century="duration-century", + cubic_foot="volume-cubic-foot", + deciliter="volume-deciliter", + ##pint='volume-pint-metric', + cubic_meter="volume-cubic-meter", + cubic_kilometer="volume-cubic-kilometer", + quart="volume-quart", + cc="volume-cubic-centimeter", + pound_force_per_square_inch="pressure-pound-per-square-inch", + milligram="mass-milligram", + kph="speed-kilometer-per-hour", + minute="duration-minute", + parsec="length-parsec", + picometer="length-picometer", + degree="angle-degree", + milliwatt="power-milliwatt", + week="duration-week", + ampere="electric-ampere", + milliliter="volume-milliliter", + decimeter="length-decimeter", + fluid_ounce="volume-fluid-ounce", + nanosecond="duration-nanosecond", + foot="length-foot", + karat="proportion-karat", + year="duration-year", + gallon="volume-gallon", + radian="angle-radian", ) if not HAS_BABEL: _babel_units = {} -_babel_systems = dict( - mks='metric', - imperial='uksystem', - US='ussystem', -) - -_babel_lengths = ['narrow', 'short', 'long'] +_babel_systems = dict(mks="metric", imperial="uksystem", US="ussystem") +_babel_lengths = ["narrow", "short", "long"] diff --git a/pint/compat.py b/pint/compat.py index abe51266c..3f7e49fb5 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -16,7 +16,7 @@ def tokenizer(input_string): - for tokinfo in tokenize.tokenize(BytesIO(input_string.encode('utf-8')).readline): + for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline): if tokinfo.type == tokenize.ENCODING: continue yield tokinfo @@ -25,6 +25,8 @@ def tokenizer(input_string): # TODO: remove this warning after v0.10 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 @@ -56,9 +58,9 @@ class BehaviorChangeWarning(UserWarning): def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: - raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, str) and value == '': - raise ValueError('Quantity magnitude cannot be an empty string.') + raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value)) + elif isinstance(value, str) and value == "": + raise ValueError("Quantity magnitude cannot be an empty string.") elif isinstance(value, (list, tuple)): return np.asarray(value) if force_ndarray: @@ -68,6 +70,7 @@ def _to_magnitude(value, force_ndarray=False): def _test_array_function_protocol(): # Test if the __array_function__ protocol is enabled try: + class FakeArray: def __array_function__(self, *args, **kwargs): return @@ -90,7 +93,7 @@ class ndarray: pass HAS_NUMPY = False - NUMPY_VER = '0' + NUMPY_VER = "0" NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False SKIP_ARRAY_FUNCTION_CHANGE_WARNING = True @@ -98,16 +101,20 @@ class ndarray: def _to_magnitude(value, force_ndarray=False): if isinstance(value, (dict, bool)) or value is None: - raise TypeError('Invalid magnitude for Quantity: {0!r}'.format(value)) - elif isinstance(value, str) and value == '': - raise ValueError('Quantity magnitude cannot be an empty string.') + raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value)) + elif isinstance(value, str) and value == "": + raise ValueError("Quantity magnitude cannot be an empty string.") elif isinstance(value, (list, tuple)): - raise TypeError('lists and tuples are valid magnitudes for ' - 'Quantity only when NumPy is present.') + raise TypeError( + "lists and tuples are valid magnitudes for " + "Quantity only when NumPy is present." + ) return value + try: from uncertainties import ufloat + HAS_UNCERTAINTIES = True except ImportError: ufloat = None @@ -116,7 +123,8 @@ def _to_magnitude(value, force_ndarray=False): try: from babel import Locale as Loc from babel import units as babel_units - HAS_BABEL = hasattr(babel_units, 'format_unit') + + HAS_BABEL = hasattr(babel_units, "format_unit") except ImportError: HAS_BABEL = False diff --git a/pint/context.py b/pint/context.py index 5768d130f..e027864b1 100644 --- a/pint/context.py +++ b/pint/context.py @@ -13,20 +13,22 @@ import weakref from collections import ChainMap, defaultdict -from .util import (ParserHelper, UnitsContainer, - to_units_container, SourceIterator) +from .util import ParserHelper, UnitsContainer, to_units_container, SourceIterator from .errors import DefinitionSyntaxError #: Regex to match the header parts of a context. -_header_re = re.compile(r'@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*') +_header_re = re.compile( + r"@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*" +) #: Regex to match variable names in an equation. -_varname_re = re.compile(r'[A-Za-z_][A-Za-z0-9_]*') +_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") def _expression_to_function(eq): def func(ureg, value, **kwargs): return ureg.parse_expression(eq, value=value, **kwargs) + return func @@ -97,18 +99,20 @@ def from_lines(cls, lines, to_base_func=None): lineno, header = next(lines) try: r = _header_re.search(header) - name = r.groupdict()['name'].strip() - aliases = r.groupdict()['aliases'] + name = r.groupdict()["name"].strip() + aliases = r.groupdict()["aliases"] if aliases: - aliases = tuple(a.strip() for a in r.groupdict()['aliases'].split('=')) + aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("=")) else: aliases = () - defaults = r.groupdict()['defaults'] + defaults = r.groupdict()["defaults"] except: - raise DefinitionSyntaxError("Could not parse the Context header '%s'" % header, - lineno=lineno) + raise DefinitionSyntaxError( + "Could not parse the Context header '%s'" % header, lineno=lineno + ) if defaults: + def to_num(val): val = complex(val) if not val.imag: @@ -117,12 +121,12 @@ def to_num(val): txt = defaults try: - defaults = (part.split('=') for part in defaults.strip('()').split(',')) + defaults = (part.split("=") for part in defaults.strip("()").split(",")) defaults = {str(k).strip(): to_num(v) for k, v in defaults} except (ValueError, TypeError): raise DefinitionSyntaxError( f"Could not parse Context definition defaults: '{txt}'", - lineno=lineno + lineno=lineno, ) ctx = cls(name, aliases, defaults) @@ -132,22 +136,20 @@ def to_num(val): names = set() for lineno, line in lines: try: - rel, eq = line.split(':') + rel, eq = line.split(":") names.update(_varname_re.findall(eq)) func = _expression_to_function(eq) - if '<->' in rel: - src, dst = (ParserHelper.from_string(s) - for s in rel.split('<->')) + if "<->" in rel: + src, dst = (ParserHelper.from_string(s) for s in rel.split("<->")) if to_base_func: src = to_base_func(src) dst = to_base_func(dst) ctx.add_transformation(src, dst, func) ctx.add_transformation(dst, src, func) - elif '->' in rel: - src, dst = (ParserHelper.from_string(s) - for s in rel.split('->')) + elif "->" in rel: + src, dst = (ParserHelper.from_string(s) for s in rel.split("->")) if to_base_func: src = to_base_func(src) dst = to_base_func(dst) @@ -155,14 +157,16 @@ def to_num(val): else: raise Exception except: - raise DefinitionSyntaxError("Could not parse Context %s relation '%s'" % (name, line), - lineno=lineno) + raise DefinitionSyntaxError( + "Could not parse Context %s relation '%s'" % (name, line), + lineno=lineno, + ) if defaults: missing_pars = defaults.keys() - set(names) if missing_pars: raise DefinitionSyntaxError( - f'Context parameters {missing_pars} not found in any equation' + f"Context parameters {missing_pars} not found in any equation" ) return ctx diff --git a/pint/definitions.py b/pint/definitions.py index aefaf81c5..e6ad101cb 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -36,27 +36,26 @@ def is_multiplicative(self): def from_string(cls, definition): """Parse a definition """ - name, definition = definition.split('=', 1) + name, definition = definition.split("=", 1) name = name.strip() - result = [res.strip() for res in definition.split('=')] + result = [res.strip() for res in definition.split("=")] # @alias name = alias1 = alias2 = ... if name.startswith("@alias "): - name = name[len("@alias "):].lstrip() + name = name[len("@alias ") :].lstrip() return AliasDefinition(name, tuple(result)) - value, aliases = result[0], tuple([x for x in result[1:] if x != '']) - symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, - aliases) - if symbol == '_': + value, aliases = result[0], tuple([x for x in result[1:] if x != ""]) + symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) + if symbol == "_": symbol = None - aliases = tuple([x for x in aliases if x != '_']) + aliases = tuple([x for x in aliases if x != "_"]) - if name.startswith('['): + if name.startswith("["): return DimensionDefinition(name, symbol, aliases, value) - elif name.endswith('-'): - name = name.rstrip('-') + elif name.endswith("-"): + name = name.rstrip("-") return PrefixDefinition(name, symbol, aliases, value) else: return UnitDefinition(name, symbol, aliases, value) @@ -96,9 +95,9 @@ class PrefixDefinition(Definition): def __init__(self, name, symbol, aliases, converter): if isinstance(converter, str): converter = ScaleConverter(eval(converter)) - aliases = tuple(alias.strip('-') for alias in aliases) + aliases = tuple(alias.strip("-") for alias in aliases) if symbol: - symbol = symbol.strip('-') + symbol = symbol.strip("-") super().__init__(name, symbol, aliases, converter) @@ -109,16 +108,16 @@ class UnitDefinition(Definition): :param is_base: indicates if it is a base unit. """ - def __init__(self, name, symbol, aliases, converter, - reference=None, is_base=False): + def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base if isinstance(converter, str): - if ';' in converter: - [converter, modifiers] = converter.split(';', 2) - modifiers = dict((key.strip(), eval(value)) for key, value in - (part.split(':') - for part in modifiers.split(';'))) + if ";" in converter: + [converter, modifiers] = converter.split(";", 2) + modifiers = dict( + (key.strip(), eval(value)) + for key, value in (part.split(":") for part in modifiers.split(";")) + ) else: modifiers = {} @@ -128,13 +127,14 @@ def __init__(self, name, symbol, aliases, converter, elif all(_is_dim(key) for key in converter.keys()): self.is_base = True else: - raise ValueError('Cannot mix dimensions and units in the same definition. ' - 'Base units must be referenced only to dimensions. ' - 'Derived units must be referenced only to units.') + raise ValueError( + "Cannot mix dimensions and units in the same definition. " + "Base units must be referenced only to dimensions. " + "Derived units must be referenced only to units." + ) self.reference = UnitsContainer(converter) - if modifiers.get('offset', 0.) != 0.: - converter = OffsetConverter(converter.scale, - modifiers['offset']) + if modifiers.get("offset", 0.0) != 0.0: + converter = OffsetConverter(converter.scale, modifiers["offset"]) else: converter = ScaleConverter(converter.scale) @@ -145,8 +145,7 @@ class DimensionDefinition(Definition): """Definition of a dimension. """ - def __init__(self, name, symbol, aliases, converter, - reference=None, is_base=False): + def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base if isinstance(converter, str): @@ -156,9 +155,11 @@ def __init__(self, name, symbol, aliases, converter, elif all(_is_dim(key) for key in converter.keys()): self.is_base = False else: - raise ValueError('Base dimensions must be referenced to None. ' - 'Derived dimensions must only be referenced ' - 'to dimensions.') + raise ValueError( + "Base dimensions must be referenced to None. " + "Derived dimensions must only be referenced " + "to dimensions." + ) self.reference = UnitsContainer(converter) super().__init__(name, symbol, aliases, converter=None) @@ -167,5 +168,6 @@ def __init__(self, name, symbol, aliases, converter, class AliasDefinition(Definition): """Additional alias(es) for an already existing unit """ + def __init__(self, name, aliases): super().__init__(name=name, symbol=None, aliases=aliases, converter=None) diff --git a/pint/formatting.py b/pint/formatting.py index dc92d8671..931e15414 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -25,7 +25,7 @@ def _join(fmt, iterable): - The concatenating string (eg. ' * ') """ if not iterable: - return '' + return "" if not __JOIN_REG_EXP.search(fmt): return fmt.join(iterable) miter = iter(iterable) @@ -35,14 +35,15 @@ def _join(fmt, iterable): first = ret return first -_PRETTY_EXPONENTS = '⁰¹²³⁴⁵⁶⁷⁸⁹' + +_PRETTY_EXPONENTS = "⁰¹²³⁴⁵⁶⁷⁸⁹" def _pretty_fmt_exponent(num): """Format an number into a pretty printed exponent. """ # TODO: Will not work for decimals - ret = '{0:n}'.format(num).replace('-', '⁻') + ret = "{0:n}".format(num).replace("-", "⁻") for n in range(10): ret = ret.replace(str(n), _PRETTY_EXPONENTS[n]) return ret @@ -51,58 +52,63 @@ def _pretty_fmt_exponent(num): #: _FORMATS maps format specifications to the corresponding argument set to #: formatter(). _FORMATS = { - 'P': { # Pretty format. - 'as_ratio': True, - 'single_denominator': False, - 'product_fmt': '·', - 'division_fmt': '/', - 'power_fmt': '{}{}', - 'parentheses_fmt': '({})', - 'exp_call': _pretty_fmt_exponent, - }, - - 'L': { # Latex format. - 'as_ratio': True, - 'single_denominator': True, - 'product_fmt': r' \cdot ', - 'division_fmt': r'\frac[{}][{}]', - 'power_fmt': '{}^[{}]', - 'parentheses_fmt': r'\left({}\right)', - }, - - 'H': { # HTML format. - 'as_ratio': True, - 'single_denominator': True, - 'product_fmt': r' ', - 'division_fmt': r'{}/{}', - 'power_fmt': '{}^{}', - 'parentheses_fmt': r'({})', - }, - - '': { # Default format. - 'as_ratio': True, - 'single_denominator': False, - 'product_fmt': ' * ', - 'division_fmt': ' / ', - 'power_fmt': '{} ** {}', - 'parentheses_fmt': r'({})', - }, - - 'C': { # Compact format. - 'as_ratio': True, - 'single_denominator': False, - 'product_fmt': '*', # TODO: Should this just be ''? - 'division_fmt': '/', - 'power_fmt': '{}**{}', - 'parentheses_fmt': r'({})', - }, - } - - -def formatter(items, as_ratio=True, single_denominator=False, - product_fmt=' * ', division_fmt=' / ', power_fmt='{} ** {}', - parentheses_fmt='({0})', exp_call=lambda x: '{0:n}'.format(x), - locale=None, babel_length='long', babel_plural_form='one'): + "P": { # Pretty format. + "as_ratio": True, + "single_denominator": False, + "product_fmt": "·", + "division_fmt": "/", + "power_fmt": "{}{}", + "parentheses_fmt": "({})", + "exp_call": _pretty_fmt_exponent, + }, + "L": { # Latex format. + "as_ratio": True, + "single_denominator": True, + "product_fmt": r" \cdot ", + "division_fmt": r"\frac[{}][{}]", + "power_fmt": "{}^[{}]", + "parentheses_fmt": r"\left({}\right)", + }, + "H": { # HTML format. + "as_ratio": True, + "single_denominator": True, + "product_fmt": r" ", + "division_fmt": r"{}/{}", + "power_fmt": "{}^{}", + "parentheses_fmt": r"({})", + }, + "": { # Default format. + "as_ratio": True, + "single_denominator": False, + "product_fmt": " * ", + "division_fmt": " / ", + "power_fmt": "{} ** {}", + "parentheses_fmt": r"({})", + }, + "C": { # Compact format. + "as_ratio": True, + "single_denominator": False, + "product_fmt": "*", # TODO: Should this just be ''? + "division_fmt": "/", + "power_fmt": "{}**{}", + "parentheses_fmt": r"({})", + }, +} + + +def formatter( + items, + as_ratio=True, + single_denominator=False, + product_fmt=" * ", + division_fmt=" / ", + power_fmt="{} ** {}", + parentheses_fmt="({0})", + exp_call=lambda x: "{0:n}".format(x), + locale=None, + babel_length="long", + babel_plural_form="one", +): """Format a list of (name, exponent) pairs. :param items: a list of (name, exponent) pairs. @@ -121,7 +127,7 @@ def formatter(items, as_ratio=True, single_denominator=False, """ if not items: - return '' + return "" if as_ratio: fun = lambda x: exp_call(abs(x)) @@ -134,12 +140,13 @@ def formatter(items, as_ratio=True, single_denominator=False, if locale and babel_length and babel_plural_form and key in _babel_units: _key = _babel_units[key] locale = Loc.parse(locale) - unit_patterns = locale._data['unit_patterns'] + unit_patterns = locale._data["unit_patterns"] compound_unit_patterns = locale._data["compound_unit_patterns"] - plural = 'one' if abs(value) <= 0 else babel_plural_form + plural = "one" if abs(value) <= 0 else babel_plural_form if babel_length not in _babel_lengths: other_lengths = [ - _babel_length for _babel_length in reversed(_babel_lengths) \ + _babel_length + for _babel_length in reversed(_babel_lengths) if babel_length != _babel_length ] else: @@ -148,10 +155,12 @@ def formatter(items, as_ratio=True, single_denominator=False, pat = unit_patterns.get(_key, {}).get(_babel_length, {}).get(plural) if pat is not None: # Don't remove this positional! This is the format used in Babel - key = pat.replace('{0}', '').strip() + key = pat.replace("{0}", "").strip() break - division_fmt = compound_unit_patterns.get("per", {}).get(babel_length, division_fmt) - power_fmt = '{}{}' + division_fmt = compound_unit_patterns.get("per", {}).get( + babel_length, division_fmt + ) + power_fmt = "{}{}" exp_call = _pretty_fmt_exponent if value == 1: pos_terms.append(key) @@ -167,7 +176,7 @@ def formatter(items, as_ratio=True, single_denominator=False, return _join(product_fmt, pos_terms + neg_terms) # Show as Ratio: positive terms / negative terms - pos_ret = _join(product_fmt, pos_terms) or '1' + pos_ret = _join(product_fmt, pos_terms) or "1" if not neg_terms: return pos_ret @@ -181,17 +190,19 @@ def formatter(items, as_ratio=True, single_denominator=False, return _join(division_fmt, [pos_ret, neg_ret]) + # Extract just the type from the specification mini-langage: see # http://docs.python.org/2/library/string.html#format-specification-mini-language # We also add uS for uncertainties. -_BASIC_TYPES = frozenset('bcdeEfFgGnosxX%uS') +_BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS") + def _parse_spec(spec): - result = '' + result = "" for ch in reversed(spec): - if ch == '~' or ch in _BASIC_TYPES: + if ch == "~" or ch in _BASIC_TYPES: continue - elif ch in list(_FORMATS.keys()) + ['~']: + elif ch in list(_FORMATS.keys()) + ["~"]: if result: raise ValueError("expected ':' after format specifier") else: @@ -199,28 +210,27 @@ def _parse_spec(spec): elif ch.isalpha(): raise ValueError("Unknown conversion specified " + ch) else: - break + break return result def format_unit(unit, spec, **kwspec): if not unit: - if spec.endswith('%'): - return '' + if spec.endswith("%"): + return "" else: - return 'dimensionless' + return "dimensionless" spec = _parse_spec(spec) fmt = dict(_FORMATS[spec]) fmt.update(kwspec) - if spec == 'L': + if spec == "L": # Latex rm = [ - (r'\mathrm{{{}}}'.format(u.replace("_", r"\_")), p) - for u, p in unit.items() + (r"\mathrm{{{}}}".format(u.replace("_", r"\_")), p) for u, p in unit.items() ] - return formatter(rm, **fmt).replace('[', '{').replace(']', '}') + return formatter(rm, **fmt).replace("[", "{").replace("]", "}") elif spec == "H": # HTML (Jupyter Notebook) rm = [(u.replace("_", r"\_"), p) for u, p in unit.items()] @@ -231,23 +241,23 @@ def format_unit(unit, spec, **kwspec): def siunitx_format_unit(units): - '''Returns LaTeX code for the unit that can be put into an siunitx command.''' + """Returns LaTeX code for the unit that can be put into an siunitx command.""" # NOTE: unit registry is required to identify unit prefixes. registry = units._REGISTRY def _tothe(power): if isinstance(power, int) or (isinstance(power, float) and power.is_integer()): if power == 1: - return '' + return "" elif power == 2: - return r'\squared' + return r"\squared" elif power == 3: - return r'\cubed' + return r"\cubed" else: - return r'\tothe{{{:d}}}'.format(int(power)) + return r"\tothe{{{:d}}}".format(int(power)) else: # limit float powers to 3 decimal places - return r'\tothe{{{:.3f}}}'.format(power).rstrip('0') + return r"\tothe{{{:.3f}}}".format(power).rstrip("0") lpos = [] lneg = [] @@ -262,39 +272,39 @@ def _tothe(power): p = str(p) if len(p) > 0 and unit.find(p) == 0: prefix = p - unit = unit.replace(prefix, '', 1) + unit = unit.replace(prefix, "", 1) if power < 0: - l.append(r'\per') + l.append(r"\per") if prefix is not None: - l.append(r'\{}'.format(prefix)) - l.append(r'\{}'.format(unit)) - l.append(r'{}'.format(_tothe(abs(power)))) + l.append(r"\{}".format(prefix)) + l.append(r"\{}".format(unit)) + l.append(r"{}".format(_tothe(abs(power)))) - return ''.join(lpos) + ''.join(lneg) + return "".join(lpos) + "".join(lneg) def remove_custom_flags(spec): - for flag in list(_FORMATS.keys()) + ['~']: - if flag: - spec = spec.replace(flag, '') + for flag in list(_FORMATS.keys()) + ["~"]: + if flag: + spec = spec.replace(flag, "") return spec -def vector_to_latex(vec, fmtfun=lambda x: format(x, '.2f')): +def vector_to_latex(vec, fmtfun=lambda x: format(x, ".2f")): return matrix_to_latex([vec], fmtfun) -def matrix_to_latex(matrix, fmtfun=lambda x: format(x, '.2f')): +def matrix_to_latex(matrix, fmtfun=lambda x: format(x, ".2f")): ret = [] for row in matrix: - ret += [' & '.join(fmtfun(f) for f in row)] + ret += [" & ".join(fmtfun(f) for f in row)] - return r'\begin{pmatrix}%s\end{pmatrix}' % '\\\\ \n'.join(ret) + return r"\begin{pmatrix}%s\end{pmatrix}" % "\\\\ \n".join(ret) -def ndarray_to_latex_parts(ndarr, fmtfun=lambda x: format(x, '.2f'), dim=()): +def ndarray_to_latex_parts(ndarr, fmtfun=lambda x: format(x, ".2f"), dim=()): if isinstance(fmtfun, str): fmt = fmtfun fmtfun = lambda x: format(x, fmt) @@ -309,15 +319,15 @@ def ndarray_to_latex_parts(ndarr, fmtfun=lambda x: format(x, '.2f'), dim=()): else: ret = [] if ndarr.ndim == 3: - header = ('arr[%s,' % ','.join('%d' % d for d in dim)) + '%d,:,:]' + header = ("arr[%s," % ",".join("%d" % d for d in dim)) + "%d,:,:]" for elno, el in enumerate(ndarr): - ret += [header % elno + ' = ' + matrix_to_latex(el, fmtfun)] + ret += [header % elno + " = " + matrix_to_latex(el, fmtfun)] else: for elno, el in enumerate(ndarr): - ret += ndarray_to_latex_parts(el, fmtfun, dim + (elno, )) + ret += ndarray_to_latex_parts(el, fmtfun, dim + (elno,)) return ret -def ndarray_to_latex(ndarr, fmtfun=lambda x: format(x, '.2f'), dim=()): - return '\n'.join(ndarray_to_latex_parts(ndarr, fmtfun, dim)) +def ndarray_to_latex(ndarr, fmtfun=lambda x: format(x, ".2f"), dim=()): + return "\n".join(ndarray_to_latex_parts(ndarr, fmtfun, dim)) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 49aad2090..2390677f0 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -20,7 +20,7 @@ class PintAxisInfo(matplotlib.units.AxisInfo): def __init__(self, units): """Set the default label to the pretty-print of the unit.""" - super().__init__(label='{:P}'.format(units)) + super().__init__(label="{:P}".format(units)) class PintConverter(matplotlib.units.ConversionInterface): @@ -39,7 +39,7 @@ def convert(self, value, unit, axis): def _convert_value(self, value, unit, axis): """Handle converting using attached unit or falling back to axis units.""" - if hasattr(value, 'units'): + if hasattr(value, "units"): return value.to(unit).magnitude else: return self._reg.Quantity(value, axis.get_units()).to(unit).magnitude @@ -53,8 +53,8 @@ def axisinfo(unit, axis): def default_units(x, axis): """Get the default unit to use for the given combination of unit and axis.""" if iterable(x) and sized(x): - return getattr(x[0], 'units', None) - return getattr(x, 'units', None) + return getattr(x[0], "units", None) + return getattr(x, "units", None) def setup_matplotlib_handlers(registry, enable): @@ -64,8 +64,8 @@ def setup_matplotlib_handlers(registry, enable): :param enable: whether support should be enabled or disabled :type enable: bool """ - if matplotlib.__version__ < '2.0': - raise RuntimeError('Matplotlib >= 2.0 required to work with pint.') + if matplotlib.__version__ < "2.0": + raise RuntimeError("Matplotlib >= 2.0 required to work with pint.") if enable: matplotlib.units.registry[registry.Quantity] = PintConverter(registry) diff --git a/pint/measurement.py b/pint/measurement.py index 55ed4b5ec..54df1d802 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -34,7 +34,7 @@ def __new__(cls, value, error, units=MISSING): units = error error = MISSING # used for check below else: - units = '' + units = "" try: error = error.to(units).magnitude except AttributeError: @@ -43,7 +43,9 @@ def __new__(cls, value, error, units=MISSING): if error is MISSING: mag = value elif error < 0: - raise ValueError('The magnitude of the error cannot be negative'.format(value, error)) + raise ValueError( + "The magnitude of the error cannot be negative".format(value, error) + ) else: mag = ufloat(value, error) @@ -69,64 +71,63 @@ def __reduce__(self): return _unpickle, (Measurement, self.magnitude, self._units) def __repr__(self): - return "".format(self.magnitude.nominal_value, - self.magnitude.std_dev, - self.units) + return "".format( + self.magnitude.nominal_value, self.magnitude.std_dev, self.units + ) def __str__(self): - return '{0}'.format(self) + return "{0}".format(self) def __format__(self, spec): # special cases - if 'Lx' in spec: # the LaTeX siunitx code + if "Lx" in spec: # the LaTeX siunitx code # the uncertainties module supports formatting # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), # which siunitx actually accepts as input. we just need to give the 'S' # formatting option for the uncertainties module. - spec = spec.replace('Lx','S') + spec = spec.replace("Lx", "S") # todo: add support for extracting options - opts = 'separate-uncertainty=true' - mstr = format( self.magnitude, spec ) + opts = "separate-uncertainty=true" + mstr = format(self.magnitude, spec) ustr = siunitx_format_unit(self.units) - ret = r'\SI[%s]{%s}{%s}'%( opts, mstr, ustr ) + ret = r"\SI[%s]{%s}{%s}" % (opts, mstr, ustr) return ret - # standard cases - if 'L' in spec: - newpm = pm = r' \pm ' - pars = _FORMATS['L']['parentheses_fmt'] - elif 'P' in spec: - newpm = pm = '±' - pars = _FORMATS['P']['parentheses_fmt'] + if "L" in spec: + newpm = pm = r" \pm " + pars = _FORMATS["L"]["parentheses_fmt"] + elif "P" in spec: + newpm = pm = "±" + pars = _FORMATS["P"]["parentheses_fmt"] else: - newpm = pm = '+/-' - pars = _FORMATS['']['parentheses_fmt'] + newpm = pm = "+/-" + pars = _FORMATS[""]["parentheses_fmt"] - if 'C' in spec: - sp = '' - newspec = spec.replace('C', '') - pars = _FORMATS['C']['parentheses_fmt'] + if "C" in spec: + sp = "" + newspec = spec.replace("C", "") + pars = _FORMATS["C"]["parentheses_fmt"] else: - sp = ' ' + sp = " " newspec = spec - if 'H' in spec: - newpm = '±' - newspec = spec.replace('H', '') - pars = _FORMATS['H']['parentheses_fmt'] + if "H" in spec: + newpm = "±" + newspec = spec.replace("H", "") + pars = _FORMATS["H"]["parentheses_fmt"] mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) - if 'L' in newspec and 'S' in newspec: - mag = mag.replace('(', r'\left(').replace(')', r'\right)') + if "L" in newspec and "S" in newspec: + mag = mag.replace("(", r"\left(").replace(")", r"\right)") - if 'L' in newspec: - space = r'\ ' + if "L" in newspec: + space = r"\ " else: - space = ' ' + space = " " - if 'uS' in newspec or 'ue' in newspec or 'u%' in newspec: + if "uS" in newspec or "ue" in newspec or "u%" in newspec: return mag + space + format(self.units, spec) else: return pars.format(mag) + space + format(self.units, spec) @@ -138,13 +139,17 @@ def __format__(self, spec): def build_measurement_class(registry): if ufloat is None: + class Measurement: _REGISTRY = registry def __init__(self, *args): - raise RuntimeError("Pint requires the 'uncertainties' package to create a Measurement object.") + raise RuntimeError( + "Pint requires the 'uncertainties' package to create a Measurement object." + ) else: + class Measurement(_Measurement): _REGISTRY = registry diff --git a/pint/numpy_func.py b/pint/numpy_func.py index f7ecf7db1..52d791dea 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -21,18 +21,23 @@ # Shared Implementation Utilities + def _is_quantity(arg): """Test for _units and _magnitude attrs. This is done in place of isinstance(Quantity, arg), which would cause a circular import. """ - return hasattr(arg, '_units') and hasattr(arg, '_magnitude') + return hasattr(arg, "_units") and hasattr(arg, "_magnitude") def _is_quantity_sequence(arg): """Test for sequences of quantities.""" - return (iterable(arg) and sized(arg) and not isinstance(arg, str) - and all(_is_quantity(item) for item in arg)) + return ( + iterable(arg) + and sized(arg) + and not isinstance(arg, str) + and all(_is_quantity(item) for item in arg) + ) def _get_first_input_units(args, kwargs={}): @@ -58,7 +63,7 @@ def convert_arg(arg, pre_calc_units): if pre_calc_units.dimensionless: return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) else: - raise DimensionalityError('dimensionless', pre_calc_units) + raise DimensionalityError("dimensionless", pre_calc_units) else: if _is_quantity(arg): return arg.m @@ -75,9 +80,13 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless Quantities. If pre_calc_units is None, units are simply stripped. """ - return (tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), - {key: convert_arg(arg, pre_calc_units=pre_calc_units) - for key, arg in kwargs.items()}) + return ( + tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), + { + key: convert_arg(arg, pre_calc_units=pre_calc_units) + for key, arg in kwargs.items() + }, + ) def unwrap_and_wrap_consistent_units(*args): @@ -88,7 +97,10 @@ def unwrap_and_wrap_consistent_units(*args): """ first_input_units = _get_first_input_units(args) args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) - return args, lambda value: first_input_units._REGISTRY.Quantity(value, first_input_units) + return ( + args, + lambda value: first_input_units._REGISTRY.Quantity(value, first_input_units), + ) def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): @@ -113,9 +125,9 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): if unit_op == "sum": result_unit = (1 * first_input_units + 1 * first_input_units).units elif unit_op == "mul": - product = first_input_units._REGISTRY.parse_units('') + product = first_input_units._REGISTRY.parse_units("") for x in all_args: - if hasattr(x, 'units'): + if hasattr(x, "units"): product *= x.units result_unit = product elif unit_op == "delta": @@ -123,45 +135,49 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): elif unit_op == "delta,div": product = (1 * first_input_units - 1 * first_input_units).units for x in all_args[1:]: - if hasattr(x, 'units'): + if hasattr(x, "units"): product /= x.units result_unit = product elif unit_op == "div": # Start with first arg in numerator, all others in denominator - product = getattr(all_args[0], 'units', first_input_units._REGISTRY.parse_units('')) + product = getattr( + all_args[0], "units", first_input_units._REGISTRY.parse_units("") + ) for x in all_args[1:]: - if hasattr(x, 'units'): + if hasattr(x, "units"): product /= x.units result_unit = product elif unit_op == "variance": - result_unit = ((1 * first_input_units + 1 * first_input_units)**2).units + result_unit = ((1 * first_input_units + 1 * first_input_units) ** 2).units elif unit_op == "square": - result_unit = first_input_units**2 + result_unit = first_input_units ** 2 elif unit_op == "sqrt": - result_unit = first_input_units**0.5 + result_unit = first_input_units ** 0.5 elif unit_op == "reciprocal": - result_unit = first_input_units**-1 + result_unit = first_input_units ** -1 elif unit_op == "size": if size is None: raise ValueError('size argument must be given when unit_op=="size"') - result_unit = first_input_units**size + result_unit = first_input_units ** size else: - raise ValueError('Output unit method {} not understood'.format(unit_op)) + raise ValueError("Output unit method {} not understood".format(unit_op)) return result_unit def implements(numpy_func_string, func_type): """Register an __array_function__/__array_ufunc__ implementation for Quantity objects.""" + def decorator(func): - if func_type == 'function': + if func_type == "function": HANDLED_FUNCTIONS[numpy_func_string] = func - elif func_type == 'ufunc': + elif func_type == "ufunc": HANDLED_UFUNCS[numpy_func_string] = func else: - raise ValueError('Invalid func_type {}'.format(func_type)) + raise ValueError("Invalid func_type {}".format(func_type)) return func + return decorator @@ -179,12 +195,14 @@ def implementation(*args, **kwargs): if input_units == "all_consistent": # Match all input args/kwargs to same units stripped_args, stripped_kwargs = convert_to_consistent_units( - *args, pre_calc_units=first_input_units, **kwargs) + *args, pre_calc_units=first_input_units, **kwargs + ) else: # Match all input args/kwargs to input_units, or if input_units is None, simply # strip units stripped_args, stripped_kwargs = convert_to_consistent_units( - *args, pre_calc_units=input_units, **kwargs) + *args, pre_calc_units=input_units, **kwargs + ) # Determine result through base numpy function on stripped arguments result_magnitude = func(*stripped_args, **stripped_kwargs) @@ -194,10 +212,21 @@ def implementation(*args, **kwargs): return result_magnitude elif output_unit == "match_input": result_unit = first_input_units - elif output_unit in ['sum', 'mul', 'delta', 'delta,div', 'div', 'variance', 'square', - 'sqrt', 'reciprocal', 'size']: - result_unit = get_op_output_unit(output_unit, first_input_units, - tuple(chain(args, kwargs.values()))) + elif output_unit in [ + "sum", + "mul", + "delta", + "delta,div", + "div", + "variance", + "square", + "sqrt", + "reciprocal", + "size", + ]: + result_unit = get_op_output_unit( + output_unit, first_input_units, tuple(chain(args, kwargs.values())) + ) else: result_unit = output_unit @@ -222,102 +251,150 @@ def implementation(*args, **kwargs): - `op_units_output_ufuncs`: determine output unit from input unit as determined by operation (see `get_op_output_unit`) """ -strip_unit_input_output_ufuncs = ['isnan', 'isinf', 'isfinite', 'signbit'] -matching_input_bare_output_ufuncs = ['equal', 'greater', 'greater_equal', 'less', - 'less_equal', 'not_equal'] -matching_input_set_units_output_ufuncs = {'arctan2': 'radian'} -set_units_ufuncs = {'cumprod': ('', ''), - 'arccos': ('', 'radian'), - 'arcsin': ('', 'radian'), - 'arctan': ('', 'radian'), - 'arccosh': ('', 'radian'), - 'arcsinh': ('', 'radian'), - 'arctanh': ('', 'radian'), - 'exp': ('', ''), - 'expm1': ('', ''), - 'exp2': ('', ''), - 'log': ('', ''), - 'log10': ('', ''), - 'log1p': ('', ''), - 'log2': ('', ''), - 'sin': ('radian', ''), - 'cos': ('radian', ''), - 'tan': ('radian', ''), - 'sinh': ('radian', ''), - 'cosh': ('radian', ''), - 'tanh': ('radian', ''), - 'radians': ('degree', 'radian'), - 'degrees': ('radian', 'degree'), - 'deg2rad': ('degree', 'radian'), - 'rad2deg': ('radian', 'degree'), - 'logaddexp': ('', ''), - 'logaddexp2': ('', '')} +strip_unit_input_output_ufuncs = ["isnan", "isinf", "isfinite", "signbit"] +matching_input_bare_output_ufuncs = [ + "equal", + "greater", + "greater_equal", + "less", + "less_equal", + "not_equal", +] +matching_input_set_units_output_ufuncs = {"arctan2": "radian"} +set_units_ufuncs = { + "cumprod": ("", ""), + "arccos": ("", "radian"), + "arcsin": ("", "radian"), + "arctan": ("", "radian"), + "arccosh": ("", "radian"), + "arcsinh": ("", "radian"), + "arctanh": ("", "radian"), + "exp": ("", ""), + "expm1": ("", ""), + "exp2": ("", ""), + "log": ("", ""), + "log10": ("", ""), + "log1p": ("", ""), + "log2": ("", ""), + "sin": ("radian", ""), + "cos": ("radian", ""), + "tan": ("radian", ""), + "sinh": ("radian", ""), + "cosh": ("radian", ""), + "tanh": ("radian", ""), + "radians": ("degree", "radian"), + "degrees": ("radian", "degree"), + "deg2rad": ("degree", "radian"), + "rad2deg": ("radian", "degree"), + "logaddexp": ("", ""), + "logaddexp2": ("", ""), +} # TODO (#905 follow-up): while this matches previous behavior, some of these have optional # arguments that should not be Quantities. This should be fixed, and tests using these # optional arguments should be added. -matching_input_copy_units_output_ufuncs = ['compress', 'conj', 'conjugate', 'copy', - 'diagonal', 'max', 'mean', 'min', - 'ptp', 'ravel', 'repeat', 'reshape', 'round', - 'squeeze', 'swapaxes', 'take', 'trace', - 'transpose', 'ceil', 'floor', 'hypot', 'rint', - 'copysign', 'nextafter', 'trunc', 'absolute', - 'negative', 'maximum', 'minimum', 'fabs'] -copy_units_output_ufuncs = ['ldexp', 'fmod', 'mod', 'remainder'] -op_units_output_ufuncs = {'var': 'square', 'prod': 'size', 'multiply': 'mul', - 'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div', - 'sqrt': 'sqrt', 'square': 'square', 'reciprocal': 'reciprocal', - 'std': 'sum', 'sum': 'sum', 'cumsum': 'sum', 'matmul': 'mul'} +matching_input_copy_units_output_ufuncs = [ + "compress", + "conj", + "conjugate", + "copy", + "diagonal", + "max", + "mean", + "min", + "ptp", + "ravel", + "repeat", + "reshape", + "round", + "squeeze", + "swapaxes", + "take", + "trace", + "transpose", + "ceil", + "floor", + "hypot", + "rint", + "copysign", + "nextafter", + "trunc", + "absolute", + "negative", + "maximum", + "minimum", + "fabs", +] +copy_units_output_ufuncs = ["ldexp", "fmod", "mod", "remainder"] +op_units_output_ufuncs = { + "var": "square", + "prod": "size", + "multiply": "mul", + "true_divide": "div", + "divide": "div", + "floor_divide": "div", + "sqrt": "sqrt", + "square": "square", + "reciprocal": "reciprocal", + "std": "sum", + "sum": "sum", + "cumsum": "sum", + "matmul": "mul", +} # Perform the standard ufunc implementations based on behavior collections for ufunc_str in strip_unit_input_output_ufuncs: # Ignore units - implement_func('ufunc', ufunc_str, input_units=None, output_unit=None) + implement_func("ufunc", ufunc_str, input_units=None, output_unit=None) for ufunc_str in matching_input_bare_output_ufuncs: # Require all inputs to match units, but output base ndarray - implement_func('ufunc', ufunc_str, input_units='all_consistent', output_unit=None) + implement_func("ufunc", ufunc_str, input_units="all_consistent", output_unit=None) for ufunc_str, out_unit in matching_input_set_units_output_ufuncs.items(): # Require all inputs to match units, but output in specified unit - implement_func('ufunc', ufunc_str, input_units='all_consistent', output_unit=out_unit) + implement_func( + "ufunc", ufunc_str, input_units="all_consistent", output_unit=out_unit + ) for ufunc_str, (in_unit, out_unit) in set_units_ufuncs.items(): # Require inputs in specified unit, and output in specified unit - implement_func('ufunc', ufunc_str, input_units=in_unit, output_unit=out_unit) + implement_func("ufunc", ufunc_str, input_units=in_unit, output_unit=out_unit) for ufunc_str in matching_input_copy_units_output_ufuncs: # Require all inputs to match units, and output as first unit in arguments - implement_func('ufunc', ufunc_str, input_units='all_consistent', - output_unit='match_input') + implement_func( + "ufunc", ufunc_str, input_units="all_consistent", output_unit="match_input" + ) for ufunc_str in copy_units_output_ufuncs: # Output as first unit in arguments, but do not convert inputs - implement_func('ufunc', ufunc_str, input_units=None, output_unit='match_input') + implement_func("ufunc", ufunc_str, input_units=None, output_unit="match_input") for ufunc_str, unit_op in op_units_output_ufuncs.items(): - implement_func('ufunc', ufunc_str, input_units=None, output_unit=unit_op) + implement_func("ufunc", ufunc_str, input_units=None, output_unit=unit_op) # Define custom ufunc implementations for atypical cases -@implements('modf', 'ufunc') + +@implements("modf", "ufunc") def _modf(x, *args, **kwargs): (x,), output_wrap = unwrap_and_wrap_consistent_units(x) return tuple(output_wrap(y) for y in np.modf(x, *args, **kwargs)) -@implements('frexp', 'ufunc') +@implements("frexp", "ufunc") def _frexp(x, *args, **kwargs): (x,), output_wrap = unwrap_and_wrap_consistent_units(x) mantissa, exponent = np.frexp(x, *args, **kwargs) return output_wrap(mantissa), exponent -@implements('power', 'ufunc') +@implements("power", "ufunc") def _power(x1, x2): - return x1**x2 + return x1 ** x2 def _add_subtract_handle_non_quantity_zero(x1, x2): @@ -333,13 +410,13 @@ def _add_subtract_handle_non_quantity_zero(x1, x2): return x1, x2, output_wrap -@implements('add', 'ufunc') +@implements("add", "ufunc") def _add(x1, x2, *args, **kwargs): x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) return output_wrap(np.add(x1, x2, *args, **kwargs)) -@implements('subtract', 'ufunc') +@implements("subtract", "ufunc") def _subtract(x1, x2, *args, **kwargs): x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) return output_wrap(np.subtract(x1, x2, *args, **kwargs)) @@ -347,7 +424,8 @@ def _subtract(x1, x2, *args, **kwargs): # Define custom function implementations -@implements('meshgrid', 'function') + +@implements("meshgrid", "function") def _meshgrid(*xi, **kwargs): # Simply need to map input units to onto list of outputs input_units = (x.units for x in xi) @@ -355,20 +433,26 @@ def _meshgrid(*xi, **kwargs): return [out * unit for out, unit in zip(res, input_units)] -@implements('full_like', 'function') -def _full_like(a, fill_value, dtype=None, order='K', subok=True, shape=None): +@implements("full_like", "function") +def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None): # Make full_like by multiplying with array from ones_like in a # non-multiplicative-unit-safe way - if hasattr(fill_value, '_REGISTRY'): + if hasattr(fill_value, "_REGISTRY"): return fill_value._REGISTRY.Quantity( - (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) - * fill_value.m), fill_value.units) + ( + np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) + * fill_value.m + ), + fill_value.units, + ) else: - return (np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) - * fill_value) + return ( + np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape) + * fill_value + ) -@implements('interp', 'function') +@implements("interp", "function") def _interp(x, xp, fp, left=None, right=None, period=None): # Need to handle x and y units separately (x, xp, period), _ = unwrap_and_wrap_consistent_units(x, xp, period) @@ -376,15 +460,23 @@ def _interp(x, xp, fp, left=None, right=None, period=None): return output_wrap(np.interp(x, xp, fp, left=left, right=right, period=period)) -@implements('where', 'function') +@implements("where", "function") def _where(condition, *args): - if (len(args) == 2 and not _is_quantity(args[1]) and not iterable(args[1]) - and (args[1] == 0 or np.isnan(args[1]))): + if ( + len(args) == 2 + and not _is_quantity(args[1]) + and not iterable(args[1]) + and (args[1] == 0 or np.isnan(args[1])) + ): # Special case for y being bare zero or nan (x,), output_wrap = unwrap_and_wrap_consistent_units(args[0]) args = x, args[1] - elif (len(args) == 2 and not _is_quantity(args[0]) and not iterable(args[0]) - and (args[0] == 0 or np.isnan(args[0]))): + elif ( + len(args) == 2 + and not _is_quantity(args[0]) + and not iterable(args[0]) + and (args[0] == 0 or np.isnan(args[0])) + ): # Special case for x being bare zero or nan (y,), output_wrap = unwrap_and_wrap_consistent_units(args[1]) args = args[0], y @@ -393,49 +485,54 @@ def _where(condition, *args): return output_wrap(np.where(condition, *args)) -@implements('concatenate', 'function') +@implements("concatenate", "function") def _concatenate(sequence, *args, **kwargs): sequence, output_wrap = unwrap_and_wrap_consistent_units(*sequence) return output_wrap(np.concatenate(sequence, *args, **kwargs)) -@implements('stack', 'function') +@implements("stack", "function") def _stack(arrays, *args, **kwargs): arrays, output_wrap = unwrap_and_wrap_consistent_units(*arrays) return output_wrap(np.stack(arrays, *args, **kwargs)) -@implements('unwrap', 'function') +@implements("unwrap", "function") def _unwrap(p, discont=None, axis=-1): # np.unwrap only dispatches over p argument, so assume it is a Quantity discont = np.pi if discont is None else discont - return p._REGISTRY.Quantity(np.unwrap(p.m_as('rad'), discont, axis=axis), - 'rad').to(p.units) + return p._REGISTRY.Quantity(np.unwrap(p.m_as("rad"), discont, axis=axis), "rad").to( + p.units + ) -@implements('copyto', 'function') -def _copyto(dst, src, casting='same_kind', where=True): +@implements("copyto", "function") +def _copyto(dst, src, casting="same_kind", where=True): if _is_quantity(dst): if _is_quantity(src): src = src.m_as(dst.units) np.copyto(dst._magnitude, src, casting=casting, where=where) else: - warnings.warn("The unit of the quantity is stripped when copying to non-quantity", - stacklevel=2) + warnings.warn( + "The unit of the quantity is stripped when copying to non-quantity", + stacklevel=2, + ) np.copyto(dst, src.m, casting=casting, where=where) -@implements('einsum', 'function') +@implements("einsum", "function") def _einsum(subscripts, *operands, **kwargs): operand_magnitudes, _ = convert_to_consistent_units(*operands, pre_calc_units=None) output_unit = get_op_output_unit("mul", _get_first_input_units(operands), operands) return np.einsum(subscripts, *operand_magnitudes, **kwargs) * output_unit -@implements('isin', 'function') +@implements("isin", "function") def _isin(element, test_elements, assume_unique=False, invert=False): if not _is_quantity(element): - raise ValueError('Cannot test if unit-aware elements are in not-unit-aware array') + raise ValueError( + "Cannot test if unit-aware elements are in not-unit-aware array" + ) if _is_quantity(test_elements): try: @@ -467,6 +564,7 @@ def _isin(element, test_elements, assume_unique=False, invert=False): # Implement simple matching-unit or stripped-unit functions based on signature + def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -474,19 +572,22 @@ def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output func = getattr(np, func_str) - @implements(func_str, 'function') + @implements(func_str, "function") def implementation(*args, **kwargs): # Bind given arguments to the NumPy function signature bound_args = signature(func).bind(*args, **kwargs) # Skip unit arguments that are supplied as None - valid_unit_arguments = [label for label in unit_arguments - if label in bound_args.arguments - and bound_args.arguments[label] is not None] + valid_unit_arguments = [ + label + for label in unit_arguments + if label in bound_args.arguments and bound_args.arguments[label] is not None + ] # Unwrap valid unit arguments, ensure consistency, and obtain output wrapper unwrapped_unit_args, output_wrap = unwrap_and_wrap_consistent_units( - *(bound_args.arguments[label] for label in valid_unit_arguments)) + *(bound_args.arguments[label] for label in valid_unit_arguments) + ) # Call NumPy function with updated arguments for i, unwrapped_unit_arg in enumerate(unwrapped_unit_args): @@ -500,110 +601,134 @@ def implementation(*args, **kwargs): return ret -for func_str, unit_arguments, wrap_output in [('expand_dims', 'a', True), - ('squeeze', 'a', True), - ('rollaxis', 'a', True), - ('moveaxis', 'a', True), - ('around', 'a', True), - ('diagonal', 'a', True), - ('mean', 'a', True), - ('ptp', 'a', True), - ('ravel', 'a', True), - ('round_', 'a', True), - ('sort', 'a', True), - ('median', 'a', True), - ('nanmedian', 'a', True), - ('transpose', 'a', True), - ('copy', 'a', True), - ('average', 'a', True), - ('nanmean', 'a', True), - ('swapaxes', 'a', True), - ('nanmin', 'a', True), - ('nanmax', 'a', True), - ('percentile', 'a', True), - ('nanpercentile', 'a', True), - ('flip', 'm', True), - ('fix', 'x', True), - ('trim_zeros', ['filt'], True), - ('broadcast_to', ['array'], True), - ('amax', ['a', 'initial'], True), - ('amin', ['a', 'initial'], True), - ('searchsorted', ['a', 'v'], False), - ('isclose', ['a', 'b', 'rtol', 'atol'], False), - ('nan_to_num', ['x', 'nan', 'posinf', 'neginf'], - True), - ('clip', ['a', 'a_min', 'a_max'], True), - ('append', ['arr', 'values'], True), - ('compress', 'a', True), - ('linspace', ['start', 'stop'], True), - ('tile', 'A', True), - ('rot90', 'm', True), - ('insert', ['arr', 'values'], True)]: +for func_str, unit_arguments, wrap_output in [ + ("expand_dims", "a", True), + ("squeeze", "a", True), + ("rollaxis", "a", True), + ("moveaxis", "a", True), + ("around", "a", True), + ("diagonal", "a", True), + ("mean", "a", True), + ("ptp", "a", True), + ("ravel", "a", True), + ("round_", "a", True), + ("sort", "a", True), + ("median", "a", True), + ("nanmedian", "a", True), + ("transpose", "a", True), + ("copy", "a", True), + ("average", "a", True), + ("nanmean", "a", True), + ("swapaxes", "a", True), + ("nanmin", "a", True), + ("nanmax", "a", True), + ("percentile", "a", True), + ("nanpercentile", "a", True), + ("flip", "m", True), + ("fix", "x", True), + ("trim_zeros", ["filt"], True), + ("broadcast_to", ["array"], True), + ("amax", ["a", "initial"], True), + ("amin", ["a", "initial"], True), + ("searchsorted", ["a", "v"], False), + ("isclose", ["a", "b", "rtol", "atol"], False), + ("nan_to_num", ["x", "nan", "posinf", "neginf"], True), + ("clip", ["a", "a_min", "a_max"], True), + ("append", ["arr", "values"], True), + ("compress", "a", True), + ("linspace", ["start", "stop"], True), + ("tile", "A", True), + ("rot90", "m", True), + ("insert", ["arr", "values"], True), +]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) # Handle atleast_nd functions + def implement_atleast_nd(func_str): # If NumPy is not available, do not attempt implement that which does not exist if np is None: return func = getattr(np, func_str) - @implements(func_str, 'function') + + @implements(func_str, "function") def implementation(*arrays): stripped_arrays, _ = convert_to_consistent_units(*arrays) arrays_magnitude = func(*stripped_arrays) if len(arrays) > 1: - return [array_magnitude if not hasattr(original, '_REGISTRY') - else original._REGISTRY.Quantity(array_magnitude, original.units) - for array_magnitude, original in zip(arrays_magnitude, arrays)] + return [ + array_magnitude + if not hasattr(original, "_REGISTRY") + else original._REGISTRY.Quantity(array_magnitude, original.units) + for array_magnitude, original in zip(arrays_magnitude, arrays) + ] else: output_unit = arrays[0].units return output_unit._REGISTRY.Quantity(arrays_magnitude, output_unit) -for func_str in ['atleast_1d', 'atleast_2d', 'atleast_3d']: +for func_str in ["atleast_1d", "atleast_2d", "atleast_3d"]: implement_atleast_nd(func_str) # Handle single-argument consistent unit functions -for func_str in ['block', 'hstack', 'vstack', 'dstack', 'column_stack']: - implement_func('function', func_str, input_units='all_consistent', - output_unit='match_input') +for func_str in ["block", "hstack", "vstack", "dstack", "column_stack"]: + implement_func( + "function", func_str, input_units="all_consistent", output_unit="match_input" + ) # Handle cumulative products (which must be dimensionless for consistent units across output # array) -for func_str in ['cumprod', 'cumproduct', 'nancumprod']: - implement_func('function', func_str, input_units='dimensionless', - output_unit='match_input') +for func_str in ["cumprod", "cumproduct", "nancumprod"]: + implement_func( + "function", func_str, input_units="dimensionless", output_unit="match_input" + ) # Handle functions that ignore units on input and output -for func_str in ['size', 'isreal', 'iscomplex', 'shape', 'ones_like', 'zeros_like', - 'empty_like', 'argsort', 'argmin', 'argmax', 'alen', 'ndim', 'nanargmax', - 'nanargmin', 'count_nonzero', 'nonzero', 'result_type']: - implement_func('function', func_str, input_units=None, output_unit=None) +for func_str in [ + "size", + "isreal", + "iscomplex", + "shape", + "ones_like", + "zeros_like", + "empty_like", + "argsort", + "argmin", + "argmax", + "alen", + "ndim", + "nanargmax", + "nanargmin", + "count_nonzero", + "nonzero", + "result_type", +]: + implement_func("function", func_str, input_units=None, output_unit=None) # Handle functions with output unit defined by operation -for func_str in ['std', 'nanstd', 'sum', 'nansum', 'cumsum', 'nancumsum']: - implement_func('function', func_str, input_units=None, output_unit='sum') -for func_str in ['cross', 'trapz', 'dot']: - implement_func('function', func_str, input_units=None, output_unit='mul') -for func_str in ['diff', 'ediff1d']: - implement_func('function', func_str, input_units=None, output_unit='delta') -for func_str in ['gradient', ]: - implement_func('function', func_str, input_units=None, output_unit='delta,div') -for func_str in ['var', 'nanvar']: - implement_func('function', func_str, input_units=None, output_unit='variance') +for func_str in ["std", "nanstd", "sum", "nansum", "cumsum", "nancumsum"]: + implement_func("function", func_str, input_units=None, output_unit="sum") +for func_str in ["cross", "trapz", "dot"]: + implement_func("function", func_str, input_units=None, output_unit="mul") +for func_str in ["diff", "ediff1d"]: + implement_func("function", func_str, input_units=None, output_unit="delta") +for func_str in ["gradient"]: + implement_func("function", func_str, input_units=None, output_unit="delta,div") +for func_str in ["var", "nanvar"]: + implement_func("function", func_str, input_units=None, output_unit="variance") def numpy_wrap(func_type, func, args, kwargs, types): """Return the result from a NumPy function/ufunc as wrapped by Pint.""" - if func_type == 'function': + if func_type == "function": handled = HANDLED_FUNCTIONS - elif func_type == 'ufunc': + elif func_type == "ufunc": handled = HANDLED_UFUNCS else: - raise ValueError('Invalid func_type {}'.format(func_type)) + raise ValueError("Invalid func_type {}".format(func_type)) if func.__name__ not in handled or any(is_upcast_type(t) for t in types): return NotImplemented diff --git a/pint/pint_eval.py b/pint/pint_eval.py index d00cc8a39..f7e08a218 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -17,33 +17,29 @@ # For controlling order of operations _OP_PRIORITY = { - '**': 3, - '^': 3, - 'unary': 2, - '*': 1, - '': 1, # operator for implicit ops - '/': 1, - '+': 0, - '-' : 0 + "**": 3, + "^": 3, + "unary": 2, + "*": 1, + "": 1, # operator for implicit ops + "/": 1, + "+": 0, + "-": 0, } _BINARY_OPERATOR_MAP = { - '**': operator.pow, - '*': operator.mul, - '': operator.mul, # operator for implicit ops - '/': operator.truediv, - '+': operator.add, - '-': operator.sub + "**": operator.pow, + "*": operator.mul, + "": operator.mul, # operator for implicit ops + "/": operator.truediv, + "+": operator.add, + "-": operator.sub, } -_UNARY_OPERATOR_MAP = { - '+': lambda x: x, - '-': lambda x: x * -1 -} +_UNARY_OPERATOR_MAP = {"+": lambda x: x, "-": lambda x: x * -1} class EvalTreeNode: - def __init__(self, left, operator=None, right=None): """ left + operator + right --> binary op @@ -54,7 +50,7 @@ def __init__(self, left, operator=None, right=None): self.left = left self.operator = operator self.right = right - + def to_string(self): # For debugging purposes if self.right: @@ -66,17 +62,19 @@ def to_string(self): comps = [self.operator[1], self.left.to_string()] else: return self.left[1] - return '(%s)' % ' '.join(comps) - - def evaluate(self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR_MAP): + return "(%s)" % " ".join(comps) + + def evaluate( + self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR_MAP + ): """ define_op is a callable that translates tokens into objects bin_op and un_op provide functions for performing binary and unary operations """ - + if self.right: # binary or implicit operator - op_text = self.operator[1] if self.operator else '' + op_text = self.operator[1] if self.operator else "" if op_text not in bin_op: raise DefinitionSyntaxError('missing binary operator "%s"' % op_text) left = self.left.evaluate(define_op, bin_op, un_op) @@ -90,9 +88,9 @@ def evaluate(self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR else: # single value return define_op(self.left) - -def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op=None, ): + +def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op=None): """ Params: Index, depth, and prev_op used recursively, so don't touch. @@ -114,29 +112,33 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= if depth == 0 and prev_op == None: # ensure tokens is list so we can access by index tokens = list(tokens) - + result = None - + while True: current_token = tokens[index] token_type = current_token[0] token_text = current_token[1] - + if token_type == tokenlib.OP: - if token_text == ')': + if token_text == ")": if prev_op is None: - raise DefinitionSyntaxError('unopened parentheses in tokens: %s' % current_token) - elif prev_op == '(': + raise DefinitionSyntaxError( + "unopened parentheses in tokens: %s" % current_token + ) + elif prev_op == "(": # close parenthetical group return result, index else: # parenthetical group ending, but we need to close sub-operations within group return result, index - 1 - elif token_text == '(': + elif token_text == "(": # gather parenthetical group - right, index = build_eval_tree(tokens, op_priority, index+1, 0, token_text) - if not tokens[index][1] == ')': - raise DefinitionSyntaxError('weird exit from parentheses') + right, index = build_eval_tree( + tokens, op_priority, index + 1, 0, token_text + ) + if not tokens[index][1] == ")": + raise DefinitionSyntaxError("weird exit from parentheses") if result: # implicit op with a parenthetical group, i.e. "3 (kg ** 2)" result = EvalTreeNode(left=result, right=right) @@ -150,41 +152,50 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= # this allows us to get the expected behavior for multiple exponents # (2^3^4) --> (2^(3^4)) # (2 * 3 / 4) --> ((2 * 3) / 4) - if op_priority[token_text] <= op_priority.get(prev_op, -1) and token_text not in ['**', '^']: + if op_priority[token_text] <= op_priority.get( + prev_op, -1 + ) and token_text not in ["**", "^"]: # previous operator is higher priority, so end previous binary op return result, index - 1 # get right side of binary op - right, index = build_eval_tree(tokens, op_priority, index+1, depth+1, token_text) - result = EvalTreeNode(left=result, operator=current_token, right=right) + right, index = build_eval_tree( + tokens, op_priority, index + 1, depth + 1, token_text + ) + result = EvalTreeNode( + left=result, operator=current_token, right=right + ) else: # unary operator - right, index = build_eval_tree(tokens, op_priority, index+1, depth+1, 'unary') + right, index = build_eval_tree( + tokens, op_priority, index + 1, depth + 1, "unary" + ) result = EvalTreeNode(left=right, operator=current_token) elif token_type == tokenlib.NUMBER or token_type == tokenlib.NAME: if result: # tokens with an implicit operation i.e. "1 kg" - if op_priority[''] <= op_priority.get(prev_op, -1): + if op_priority[""] <= op_priority.get(prev_op, -1): # previous operator is higher priority than implicit, so end previous binary op return result, index - 1 - right, index = build_eval_tree(tokens, op_priority, index, depth+1, '') + right, index = build_eval_tree( + tokens, op_priority, index, depth + 1, "" + ) result = EvalTreeNode(left=result, right=right) else: # get first token result = EvalTreeNode(left=current_token) - + if tokens[index][0] == tokenlib.ENDMARKER: - if prev_op == '(': - raise DefinitionSyntaxError('unclosed parentheses in tokens') + if prev_op == "(": + raise DefinitionSyntaxError("unclosed parentheses in tokens") if depth > 0 or prev_op: # have to close recursion return result, index else: # recursion all closed, so just return the final result return result - + if index + 1 >= len(tokens): # should hit ENDMARKER before this ever happens - raise DefinitionSyntaxError('unexpected end to tokens') + raise DefinitionSyntaxError("unexpected end to tokens") index += 1 - diff --git a/pint/quantity.py b/pint/quantity.py index 8c8625bef..1aff1c238 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -21,25 +21,56 @@ from pkg_resources.extern.packaging import version -from .formatting import (remove_custom_flags, siunitx_format_unit, ndarray_to_latex, - ndarray_to_latex_parts) -from .errors import (DimensionalityError, OffsetUnitCalculusError, PintTypeError, - UndefinedUnitError, UnitStrippedWarning) +from .formatting import ( + remove_custom_flags, + siunitx_format_unit, + ndarray_to_latex, + ndarray_to_latex_parts, +) +from .errors import ( + DimensionalityError, + OffsetUnitCalculusError, + PintTypeError, + UndefinedUnitError, + UnitStrippedWarning, +) from .definitions import UnitDefinition -from .compat import (Loc, NUMPY_VER, SKIP_ARRAY_FUNCTION_CHANGE_WARNING, - BehaviorChangeWarning, ndarray, np, _to_magnitude, is_upcast_type, eq, - array_function_change_msg) -from .util import (PrettyIPython, logger, UnitsContainer, SharedRegistryObject, - to_units_container, infer_base_unit, iterable, sized) -from .numpy_func import (HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, - matching_input_bare_output_ufuncs, - matching_input_copy_units_output_ufuncs, - matching_input_set_units_output_ufuncs, numpy_wrap, - op_units_output_ufuncs, set_units_ufuncs) - - -class _Exception(Exception): # pragma: no cover - +from .compat import ( + Loc, + NUMPY_VER, + SKIP_ARRAY_FUNCTION_CHANGE_WARNING, + BehaviorChangeWarning, + ndarray, + np, + _to_magnitude, + is_upcast_type, + eq, + array_function_change_msg, +) +from .util import ( + PrettyIPython, + logger, + UnitsContainer, + SharedRegistryObject, + to_units_container, + infer_base_unit, + iterable, + sized, +) +from .numpy_func import ( + HANDLED_UFUNCS, + copy_units_output_ufuncs, + get_op_output_unit, + matching_input_bare_output_ufuncs, + matching_input_copy_units_output_ufuncs, + matching_input_set_units_output_ufuncs, + numpy_wrap, + op_units_output_ufuncs, + set_units_ufuncs, +) + + +class _Exception(Exception): # pragma: no cover def __init__(self, internal): self.internal = internal @@ -51,6 +82,7 @@ def wrapped(self, *args, **kwargs): return result.to_reduced_units() else: return result + return wrapped @@ -60,12 +92,13 @@ def wrapped(self, *args, **kwargs): if result._REGISTRY.auto_reduce_dimensions: result.ito_reduced_units() return result + return wrapped def check_implemented(f): def wrapped(self, *args, **kwargs): - other=args[0] + other = args[0] if is_upcast_type(other): return NotImplemented # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] @@ -74,6 +107,7 @@ def wrapped(self, *args, **kwargs): return NotImplemented result = f(self, *args, **kwargs) return result + return wrapped @@ -103,7 +137,7 @@ class Quantity(PrettyIPython, SharedRegistryObject): """ #: Default formatting string. - default_format = '' + default_format = "" @property def force_ndarray(self): @@ -124,9 +158,10 @@ def __new__(cls, value, units=None): if units is None: if isinstance(value, str): - if value == '': - raise ValueError('Expression to parse as Quantity cannot ' - 'be an empty string.') + if value == "": + raise ValueError( + "Expression to parse as Quantity cannot " "be an empty string." + ) ureg = SharedRegistryObject.__new__(cls)._REGISTRY inst = ureg.parse_expression(value) return cls.__new__(cls, inst) @@ -147,20 +182,25 @@ def __new__(cls, value, units=None): elif isinstance(units, SharedRegistryObject): if isinstance(units, Quantity) and units.magnitude != 1: inst = copy.copy(units) - logger.warning('Creating new Quantity using a non unity ' - 'Quantity as units.') + logger.warning( + "Creating new Quantity using a non unity " "Quantity as units." + ) else: inst = SharedRegistryObject.__new__(cls) inst._units = units._units inst._magnitude = _to_magnitude(value, inst.force_ndarray) else: - raise TypeError('units must be of type str, Quantity or ' - 'UnitsContainer; not {}.'.format(type(units))) + raise TypeError( + "units must be of type str, Quantity or " + "UnitsContainer; not {}.".format(type(units)) + ) inst.__used = False inst.__handling = None - if not SKIP_ARRAY_FUNCTION_CHANGE_WARNING and isinstance(inst._magnitude, ndarray): + 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 @@ -188,8 +228,9 @@ def __copy__(self): return ret def __deepcopy__(self, memo): - ret = self.__class__(copy.deepcopy(self._magnitude, memo), - copy.deepcopy(self._units, memo)) + ret = self.__class__( + copy.deepcopy(self._magnitude, memo), copy.deepcopy(self._units, memo) + ) ret.__used = self.__used return ret @@ -214,56 +255,56 @@ def __hash__(self): def __format__(self, spec): spec = spec or self.default_format - if 'L' in spec: - allf = plain_allf = r'{}\ {}' + if "L" in spec: + allf = plain_allf = r"{}\ {}" else: - allf = plain_allf = '{} {}' + allf = plain_allf = "{} {}" mstr, ustr = None, None # If Compact is selected, do it at the beginning - if '#' in spec: - spec = spec.replace('#', '') + if "#" in spec: + spec = spec.replace("#", "") obj = self.to_compact() else: obj = self # the LaTeX siunitx code - if 'Lx' in spec: - spec = spec.replace('Lx','') + if "Lx" in spec: + spec = spec.replace("Lx", "") # todo: add support for extracting options - opts = '' + opts = "" ustr = siunitx_format_unit(obj.units) - allf = r'\SI[%s]{{{}}}{{{}}}'% opts + allf = r"\SI[%s]{{{}}}{{{}}}" % opts else: ustr = format(obj.units, spec) mspec = remove_custom_flags(spec) if isinstance(self.magnitude, ndarray): - if 'L' in spec: + if "L" in spec: mstr = ndarray_to_latex(obj.magnitude, mspec) - elif 'H' in spec: + elif "H" in spec: # this is required to have the magnitude and unit in the same line - allf = r'\[{} {}\]' + allf = r"\[{} {}\]" parts = ndarray_to_latex_parts(obj.magnitude, mspec) if len(parts) > 1: - return '\n'.join(allf.format(part, ustr) for part in parts) + return "\n".join(allf.format(part, ustr) for part in parts) mstr = parts[0] else: formatter = "{{:{}}}".format(mspec) with printoptions(formatter={"float_kind": formatter.format}): - mstr = format(obj.magnitude).replace('\n', '') + mstr = format(obj.magnitude).replace("\n", "") else: - mstr = format(obj.magnitude, mspec).replace('\n', '') + mstr = format(obj.magnitude, mspec).replace("\n", "") - if 'L' in spec: + if "L" in spec: mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr) - elif 'H' in spec: + elif "H" in spec: mstr = self._exp_pattern.sub(r"\1×10\2\3", mstr) - if allf == plain_allf and ustr.startswith('1 /'): + if allf == plain_allf and ustr.startswith("1 /"): # Write e.g. "3 / s" instead of "3 1 / s" ustr = ustr[2:] return allf.format(mstr, ustr).strip() @@ -276,23 +317,24 @@ def _repr_pretty_(self, p, cycle): p.text(" ") p.pretty(self.units) - def format_babel(self, spec='', **kwspec): + def format_babel(self, spec="", **kwspec): spec = spec or self.default_format # standard cases - if '#' in spec: - spec = spec.replace('#', '') + if "#" in spec: + spec = spec.replace("#", "") obj = self.to_compact() else: obj = self kwspec = dict(kwspec) - if 'length' in kwspec: - kwspec['babel_length'] = kwspec.pop('length') - kwspec['locale'] = Loc.parse(kwspec['locale']) - kwspec['babel_plural_form'] = kwspec['locale'].plural_form(obj.magnitude) - return '{} {}'.format( + if "length" in kwspec: + kwspec["babel_length"] = kwspec.pop("length") + kwspec["locale"] = Loc.parse(kwspec["locale"]) + kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) + return "{} {}".format( format(obj.magnitude, remove_custom_flags(spec)), - obj.units.format_babel(spec, **kwspec)).replace('\n', '') + obj.units.format_babel(spec, **kwspec), + ).replace("\n", "") @property def magnitude(self): @@ -360,7 +402,6 @@ def check(self, dimension): """ return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) - @classmethod def from_list(cls, quant_list, units=None): """Transforms a list of Quantities into an numpy.array quantity. @@ -396,16 +437,15 @@ def from_sequence(cls, seq, units=None): if len_seq: units = seq[0].u else: - raise ValueError('Cannot determine units from empty sequence!') + raise ValueError("Cannot determine units from empty sequence!") a = np.empty(len_seq) - + for i, seq_i in enumerate(seq): a[i] = seq_i.m_as(units) # raises DimensionalityError if incompatible units are used in the sequence - - return cls(a, units) + return cls(a, units) @classmethod def from_tuple(cls, tup): @@ -433,8 +473,12 @@ def _convert_magnitude(self, other, *contexts, **ctx_kwargs): with self._REGISTRY.context(*contexts, **ctx_kwargs): return self._REGISTRY.convert(self._magnitude, self._units, other) - return self._REGISTRY.convert(self._magnitude, self._units, other, - inplace=isinstance(self._magnitude, ndarray)) + return self._REGISTRY.convert( + self._magnitude, + self._units, + other, + inplace=isinstance(self._magnitude, ndarray), + ) def ito(self, other=None, *contexts, **ctx_kwargs): """Inplace rescale to different units. @@ -444,8 +488,7 @@ def ito(self, other=None, *contexts, **ctx_kwargs): """ other = to_units_container(other, self._REGISTRY) - self._magnitude = self._convert_magnitude(other, *contexts, - **ctx_kwargs) + self._magnitude = self._convert_magnitude(other, *contexts, **ctx_kwargs) self._units = other return None @@ -507,22 +550,21 @@ def ito_reduced_units(self): dimension. This will not reduce compound units (intentionally), nor can it make use of contexts at this time. """ - #shortcuts in case we're dimensionless or only a single unit + # shortcuts in case we're dimensionless or only a single unit if self.dimensionless: return self.ito({}) if len(self._units) == 1: return None newunits = self._units.copy() - #loop through individual units and compare to each other unit - #can we do better than a nested loop here? + # loop through individual units and compare to each other unit + # can we do better than a nested loop here? for unit1, exp in self._units.items(): for unit2 in newunits: if unit1 != unit2: - power = self._REGISTRY._get_dimensionality_ratio(unit1, - unit2) + power = self._REGISTRY._get_dimensionality_ratio(unit1, unit2) if power: - newunits = newunits.add(unit2, exp/power).remove(unit1) + newunits = newunits.add(unit2, exp / power).remove(unit1) break return self.ito(newunits) @@ -532,7 +574,7 @@ def to_reduced_units(self): dimension. This will not reduce compound units (intentionally), nor can it make use of contexts at this time. """ - #can we make this more efficient? + # can we make this more efficient? newq = copy.copy(self) newq.ito_reduced_units() return newq @@ -550,14 +592,20 @@ def to_compact(self, unit=None): """ if not isinstance(self.magnitude, numbers.Number): - msg = ("to_compact applied to non numerical types " - "has an undefined behavior.") + msg = ( + "to_compact applied to non numerical types " + "has an undefined behavior." + ) w = RuntimeWarning(msg) warnings.warn(w, stacklevel=2) return self - if (self.unitless or self.magnitude==0 or - math.isnan(self.magnitude) or math.isinf(self.magnitude)): + if ( + self.unitless + or self.magnitude == 0 + or math.isnan(self.magnitude) + or math.isinf(self.magnitude) + ): return self SI_prefixes = {} @@ -569,7 +617,7 @@ def to_compact(self, unit=None): if log10_scale == math.log10(scale): SI_prefixes[log10_scale] = prefix.name except: - SI_prefixes[0] = '' + SI_prefixes[0] = "" SI_prefixes = sorted(SI_prefixes.items()) SI_powers = [item[0] for item in SI_prefixes] @@ -597,7 +645,7 @@ def to_compact(self, unit=None): prefix = SI_bases[bisect.bisect_left(SI_powers, power)] - new_unit_str = prefix+unit_str + new_unit_str = prefix + unit_str new_unit_container = q_base._units.rename(unit_str, new_unit_str) return self.to(new_unit_container) @@ -606,17 +654,17 @@ def to_compact(self, unit=None): def __int__(self): if self.dimensionless: return int(self._convert_magnitude_not_inplace(UnitsContainer())) - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") def __float__(self): if self.dimensionless: return float(self._convert_magnitude_not_inplace(UnitsContainer())) - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") def __complex__(self): if self.dimensionless: return complex(self._convert_magnitude_not_inplace(UnitsContainer())) - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") def _iadd_sub(self, other, op): """Perform addition or subtraction operation in-place and return the result. @@ -645,13 +693,13 @@ def _iadd_sub(self, other, op): self.ito(UnitsContainer()) self._magnitude = op(self._magnitude, other_magnitude) else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") return self if not self.dimensionality == other.dimensionality: - raise DimensionalityError(self._units, other._units, - self.dimensionality, - other.dimensionality) + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) # Next we define some variables to make if-clauses more readable. self_non_mul_units = self._get_non_multiplicative_units() @@ -669,48 +717,55 @@ def _iadd_sub(self, other, op): self._magnitude = op(self._magnitude, other._magnitude) # If only self has a delta unit, other determines unit of result. elif self._get_delta_units() and not other._get_delta_units(): - self._magnitude = op(self._convert_magnitude(other._units), - other._magnitude) + self._magnitude = op( + self._convert_magnitude(other._units), other._magnitude + ) self._units = other._units else: - self._magnitude = op(self._magnitude, - other.to(self._units)._magnitude) - - elif (op == operator.isub and len(self_non_mul_units) == 1 - and self._units[self_non_mul_unit] == 1 - and not other._has_compatible_delta(self_non_mul_unit)): + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + + elif ( + op == operator.isub + and len(self_non_mul_units) == 1 + and self._units[self_non_mul_unit] == 1 + and not other._has_compatible_delta(self_non_mul_unit) + ): if self._units == other._units: self._magnitude = op(self._magnitude, other._magnitude) else: - self._magnitude = op(self._magnitude, - other.to(self._units)._magnitude) - self._units = self._units.rename(self_non_mul_unit, - 'delta_' + self_non_mul_unit) - - elif (op == operator.isub and len(other_non_mul_units) == 1 - and other._units[other_non_mul_unit] == 1 - and not self._has_compatible_delta(other_non_mul_unit)): + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + self._units = self._units.rename( + self_non_mul_unit, "delta_" + self_non_mul_unit + ) + + elif ( + op == operator.isub + and len(other_non_mul_units) == 1 + and other._units[other_non_mul_unit] == 1 + and not self._has_compatible_delta(other_non_mul_unit) + ): # we convert to self directly since it is multiplicative - self._magnitude = op(self._magnitude, - other.to(self._units)._magnitude) - - elif (len(self_non_mul_units) == 1 - # order of the dimension of offset unit == 1 ? - and self._units[self_non_mul_unit] == 1 - and other._has_compatible_delta(self_non_mul_unit)): + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + + elif ( + len(self_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and self._units[self_non_mul_unit] == 1 + and other._has_compatible_delta(self_non_mul_unit) + ): # Replace offset unit in self by the corresponding delta unit. # This is done to prevent a shift by offset in the to()-call. - tu = self._units.rename(self_non_mul_unit, - 'delta_' + self_non_mul_unit) + tu = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) self._magnitude = op(self._magnitude, other.to(tu)._magnitude) - elif (len(other_non_mul_units) == 1 - # order of the dimension of offset unit == 1 ? - and other._units[other_non_mul_unit] == 1 - and self._has_compatible_delta(other_non_mul_unit)): + elif ( + len(other_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and other._units[other_non_mul_unit] == 1 + and self._has_compatible_delta(other_non_mul_unit) + ): # Replace offset unit in other by the corresponding delta unit. # This is done to prevent a shift by offset in the to()-call. - tu = other._units.rename(other_non_mul_unit, - 'delta_' + other_non_mul_unit) + tu = other._units.rename(other_non_mul_unit, "delta_" + other_non_mul_unit) self._magnitude = op(self._convert_magnitude(tu), other._magnitude) self._units = other._units else: @@ -736,20 +791,22 @@ def _add_sub(self, other, op): # value to enforce any shape checking and type casting due to # the operation. units = self._units - magnitude = op(self._magnitude, - _to_magnitude(other, self.force_ndarray)) + magnitude = op( + self._magnitude, _to_magnitude(other, self.force_ndarray) + ) elif self.dimensionless: units = UnitsContainer() - magnitude = op(self.to(units)._magnitude, - _to_magnitude(other, self.force_ndarray)) + magnitude = op( + self.to(units)._magnitude, _to_magnitude(other, self.force_ndarray) + ) else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") return self.__class__(magnitude, units) if not self.dimensionality == other.dimensionality: - raise DimensionalityError(self._units, other._units, - self.dimensionality, - other.dimensionality) + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) # Next we define some variables to make if-clauses more readable. self_non_mul_units = self._get_non_multiplicative_units() @@ -768,51 +825,54 @@ def _add_sub(self, other, op): units = self._units # If only self has a delta unit, other determines unit of result. elif self._get_delta_units() and not other._get_delta_units(): - magnitude = op(self._convert_magnitude(other._units), - other._magnitude) + magnitude = op(self._convert_magnitude(other._units), other._magnitude) units = other._units else: units = self._units - magnitude = op(self._magnitude, - other.to(self._units).magnitude) - - elif (op == operator.sub and len(self_non_mul_units) == 1 - and self._units[self_non_mul_unit] == 1 - and not other._has_compatible_delta(self_non_mul_unit)): + magnitude = op(self._magnitude, other.to(self._units).magnitude) + + elif ( + op == operator.sub + and len(self_non_mul_units) == 1 + and self._units[self_non_mul_unit] == 1 + and not other._has_compatible_delta(self_non_mul_unit) + ): if self._units == other._units: magnitude = op(self._magnitude, other._magnitude) else: - magnitude = op(self._magnitude, - other.to(self._units)._magnitude) - units = self._units.rename(self_non_mul_unit, - 'delta_' + self_non_mul_unit) - - elif (op == operator.sub and len(other_non_mul_units) == 1 - and other._units[other_non_mul_unit] == 1 - and not self._has_compatible_delta(other_non_mul_unit)): + magnitude = op(self._magnitude, other.to(self._units)._magnitude) + units = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) + + elif ( + op == operator.sub + and len(other_non_mul_units) == 1 + and other._units[other_non_mul_unit] == 1 + and not self._has_compatible_delta(other_non_mul_unit) + ): # we convert to self directly since it is multiplicative - magnitude = op(self._magnitude, - other.to(self._units)._magnitude) + magnitude = op(self._magnitude, other.to(self._units)._magnitude) units = self._units - elif (len(self_non_mul_units) == 1 - # order of the dimension of offset unit == 1 ? - and self._units[self_non_mul_unit] == 1 - and other._has_compatible_delta(self_non_mul_unit)): + elif ( + len(self_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and self._units[self_non_mul_unit] == 1 + and other._has_compatible_delta(self_non_mul_unit) + ): # Replace offset unit in self by the corresponding delta unit. # This is done to prevent a shift by offset in the to()-call. - tu = self._units.rename(self_non_mul_unit, - 'delta_' + self_non_mul_unit) + tu = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) magnitude = op(self._magnitude, other.to(tu).magnitude) units = self._units - elif (len(other_non_mul_units) == 1 - # order of the dimension of offset unit == 1 ? - and other._units[other_non_mul_unit] == 1 - and self._has_compatible_delta(other_non_mul_unit)): + elif ( + len(other_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and other._units[other_non_mul_unit] == 1 + and self._has_compatible_delta(other_non_mul_unit) + ): # Replace offset unit in other by the corresponding delta unit. # This is done to prevent a shift by offset in the to()-call. - tu = other._units.rename(other_non_mul_unit, - 'delta_' + other_non_mul_unit) + tu = other._units.rename(other_non_mul_unit, "delta_" + other_non_mul_unit) magnitude = op(self._convert_magnitude(tu), other._magnitude) units = other._units else: @@ -874,13 +934,15 @@ def _imul_div(self, other, magnitude_op, units_op=None): if not self._check(other): if not self._ok_for_muldiv(no_offset_units_self): - raise OffsetUnitCalculusError(self._units, - getattr(other, 'units', '')) + raise OffsetUnitCalculusError(self._units, getattr(other, "units", "")) if len(offset_units_self) == 1: - if (self._units[offset_units_self[0]] != 1 - or magnitude_op not in [operator.mul, operator.imul]): - raise OffsetUnitCalculusError(self._units, - getattr(other, 'units', '')) + if self._units[offset_units_self[0]] != 1 or magnitude_op not in [ + operator.mul, + operator.imul, + ]: + raise OffsetUnitCalculusError( + self._units, getattr(other, "units", "") + ) try: other_magnitude = _to_magnitude(other, self.force_ndarray) except PintTypeError: @@ -897,7 +959,7 @@ def _imul_div(self, other, magnitude_op, units_op=None): if not self._ok_for_muldiv(no_offset_units_self): raise OffsetUnitCalculusError(self._units, other._units) elif no_offset_units_self == 1 and len(self._units) == 1: - self.ito_root_units() + self.ito_root_units() no_offset_units_other = len(other._get_non_multiplicative_units()) @@ -934,13 +996,15 @@ def _mul_div(self, other, magnitude_op, units_op=None): if not self._check(other): if not self._ok_for_muldiv(no_offset_units_self): - raise OffsetUnitCalculusError(self._units, - getattr(other, 'units', '')) + raise OffsetUnitCalculusError(self._units, getattr(other, "units", "")) if len(offset_units_self) == 1: - if (self._units[offset_units_self[0]] != 1 - or magnitude_op not in [operator.mul, operator.imul]): - raise OffsetUnitCalculusError(self._units, - getattr(other, 'units', '')) + if self._units[offset_units_self[0]] != 1 or magnitude_op not in [ + operator.mul, + operator.imul, + ]: + raise OffsetUnitCalculusError( + self._units, getattr(other, "units", "") + ) try: other_magnitude = _to_magnitude(other, self.force_ndarray) except PintTypeError: @@ -988,7 +1052,7 @@ def __mul__(self, other): def __matmul__(self, other): # Use NumPy ufunc (existing since 1.16) for matrix multiplication - if version.parse(NUMPY_VER) >= version.parse('1.16'): + if version.parse(NUMPY_VER) >= version.parse("1.16"): return np.matmul(self, other) else: return NotImplemented @@ -1014,11 +1078,12 @@ def __rtruediv__(self, other): no_offset_units_self = len(self._get_non_multiplicative_units()) if not self._ok_for_muldiv(no_offset_units_self): - raise OffsetUnitCalculusError(self._units, '') + raise OffsetUnitCalculusError(self._units, "") elif no_offset_units_self == 1 and len(self._units) == 1: self = self.to_root_units() return self.__class__(other_magnitude / self._magnitude, 1 / self._units) + __div__ = __truediv__ __rdiv__ = __rtruediv__ __idiv__ = __itruediv__ @@ -1027,9 +1092,9 @@ def __ifloordiv__(self, other): if self._check(other): self._magnitude //= other.to(self._units)._magnitude elif self.dimensionless: - self._magnitude = self.to('')._magnitude // other + self._magnitude = self.to("")._magnitude // other else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") self._units = UnitsContainer({}) return self @@ -1038,9 +1103,9 @@ def __floordiv__(self, other): if self._check(other): magnitude = self._magnitude // other.to(self._units)._magnitude elif self.dimensionless: - magnitude = self.to('')._magnitude // other + magnitude = self.to("")._magnitude // other else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") return self.__class__(magnitude, UnitsContainer({})) @check_implemented @@ -1048,9 +1113,9 @@ def __rfloordiv__(self, other): if self._check(other): magnitude = other._magnitude // self.to(other._units)._magnitude elif self.dimensionless: - magnitude = other // self.to('')._magnitude + magnitude = other // self.to("")._magnitude else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") return self.__class__(magnitude, UnitsContainer({})) def __imod__(self, other): @@ -1072,18 +1137,17 @@ def __rmod__(self, other): magnitude = other._magnitude % self.to(other._units)._magnitude return self.__class__(magnitude, other._units) elif self.dimensionless: - magnitude = other % self.to('')._magnitude + magnitude = other % self.to("")._magnitude return self.__class__(magnitude, UnitsContainer({})) else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") @check_implemented def __divmod__(self, other): if not self._check(other): other = self.__class__(other, UnitsContainer({})) q, r = divmod(self._magnitude, other.to(self._units)._magnitude) - return (self.__class__(q, UnitsContainer({})), - self.__class__(r, self._units)) + return (self.__class__(q, UnitsContainer({})), self.__class__(r, self._units)) @check_implemented def __rdivmod__(self, other): @@ -1091,10 +1155,10 @@ def __rdivmod__(self, other): q, r = divmod(other._magnitude, self.to(other._units)._magnitude) unit = other._units elif self.dimensionless: - q, r = divmod(other, self.to('')._magnitude) + q, r = divmod(other, self.to("")._magnitude) unit = UnitsContainer({}) else: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") return (self.__class__(q, UnitsContainer({})), self.__class__(r, unit)) def __ipow__(self, other): @@ -1111,25 +1175,25 @@ def __ipow__(self, other): if not self._ok_for_muldiv: raise OffsetUnitCalculusError(self._units) - if isinstance(getattr(other, '_magnitude', other), ndarray): + if isinstance(getattr(other, "_magnitude", other), ndarray): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units # unless the base is dimensionless. if self.dimensionless: - if getattr(other, 'dimensionless', False): - self._magnitude **= other.m_as('') + if getattr(other, "dimensionless", False): + self._magnitude **= other.m_as("") return self - elif not getattr(other, 'dimensionless', True): - raise DimensionalityError(other._units, 'dimensionless') + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") else: self._magnitude **= other return self elif np.size(other) > 1: raise DimensionalityError( self._units, - 'dimensionless', + "dimensionless", extra_msg=". Quantity array exponents are only allowed if the " - "base is dimensionless" + "base is dimensionless", ) if other == 1: @@ -1143,11 +1207,11 @@ def __ipow__(self, other): else: raise OffsetUnitCalculusError(self._units) - if getattr(other, 'dimensionless', False): + if getattr(other, "dimensionless", False): other = other.to_base_units().magnitude self._units **= other - elif not getattr(other, 'dimensionless', True): - raise DimensionalityError(self._units, 'dimensionless') + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(self._units, "dimensionless") else: self._units **= other @@ -1166,23 +1230,23 @@ def __pow__(self, other): if not self._ok_for_muldiv: raise OffsetUnitCalculusError(self._units) - if isinstance(getattr(other, '_magnitude', other), ndarray): + if isinstance(getattr(other, "_magnitude", other), ndarray): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units # unless the base is dimensionless. if self.dimensionless: - if getattr(other, 'dimensionless', False): - return self.__class__(self.m ** other.m_as('')) - elif not getattr(other, 'dimensionless', True): - raise DimensionalityError(other._units, 'dimensionless') + if getattr(other, "dimensionless", False): + return self.__class__(self.m ** other.m_as("")) + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") else: return self.__class__(self.m ** other) elif np.size(other) > 1: raise DimensionalityError( self._units, - 'dimensionless', + "dimensionless", extra_msg=". Quantity array exponents are only allowed if the " - "base is dimensionless" + "base is dimensionless", ) new_self = self @@ -1197,10 +1261,10 @@ def __pow__(self, other): else: raise OffsetUnitCalculusError(self._units) - if getattr(other, 'dimensionless', False): + if getattr(other, "dimensionless", False): units = new_self._units ** other.to_root_units().magnitude - elif not getattr(other, 'dimensionless', True): - raise DimensionalityError(other._units, 'dimensionless') + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") else: units = new_self._units ** other @@ -1217,12 +1281,12 @@ def __rpow__(self, other): return NotImplemented else: if not self.dimensionless: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") if isinstance(self._magnitude, ndarray): if np.size(self._magnitude) > 1: - raise DimensionalityError(self._units, 'dimensionless') + raise DimensionalityError(self._units, "dimensionless") new_self = self.to_root_units() - return other**new_self._magnitude + return other ** new_self._magnitude def __abs__(self): return self.__class__(abs(self._magnitude), self._units) @@ -1255,8 +1319,9 @@ def __eq__(self, other): else: raise OffsetUnitCalculusError(self._units) - return (self.dimensionless and - eq(self._convert_magnitude(UnitsContainer()), other, False)) + return self.dimensionless and eq( + self._convert_magnitude(UnitsContainer()), other, False + ) if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): return self.dimensionality == other.dimensionality @@ -1265,8 +1330,11 @@ def __eq__(self, other): return eq(self._magnitude, other._magnitude, False) try: - return eq(self._convert_magnitude_not_inplace(other._units), - other._magnitude, False) + return eq( + self._convert_magnitude_not_inplace(other._units), + other._magnitude, + False, + ) except DimensionalityError: return False @@ -1295,15 +1363,15 @@ def compare(self, other, op): else: raise OffsetUnitCalculusError(self._units) else: - raise ValueError('Cannot compare Quantity and {}'.format(type(other))) + raise ValueError("Cannot compare Quantity and {}".format(type(other))) if self._units == other._units: return op(self._magnitude, other._magnitude) if self.dimensionality != other.dimensionality: - raise DimensionalityError(self._units, other._units, - self.dimensionality, other.dimensionality) - return op(self.to_root_units().magnitude, - other.to_root_units().magnitude) + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) + return op(self.to_root_units().magnitude, other.to_root_units().magnitude) __lt__ = lambda self, other: self.compare(other, op=operator.lt) __le__ = lambda self, other: self.compare(other, op=operator.le) @@ -1324,15 +1392,18 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): return NotImplemented # Replicate types from __array_function__ - types = set(type(arg) for arg in list(inputs) + list(kwargs.values()) - if hasattr(arg, "__array_ufunc__")) + types = set( + type(arg) + for arg in list(inputs) + list(kwargs.values()) + if hasattr(arg, "__array_ufunc__") + ) - return numpy_wrap('ufunc', ufunc, inputs, kwargs, types) + return numpy_wrap("ufunc", ufunc, inputs, kwargs, types) def __array_function__(self, func, types, args, kwargs): - return numpy_wrap('function', func, args, kwargs, types) + return numpy_wrap("function", func, args, kwargs, types) - _wrapped_numpy_methods = ['flatten', 'astype', 'item'] + _wrapped_numpy_methods = ["flatten", "astype", "item"] def _numpy_method_wrap(self, func, *args, **kwargs): """Convenience method to wrap on the fly NumPy ndarray methods taking @@ -1345,19 +1416,23 @@ def _numpy_method_wrap(self, func, *args, **kwargs): value = func(*args, **kwargs) # Set output units as needed - if (func.__name__ in - (matching_input_copy_units_output_ufuncs + copy_units_output_ufuncs - + self._wrapped_numpy_methods)): + if func.__name__ in ( + matching_input_copy_units_output_ufuncs + + copy_units_output_ufuncs + + self._wrapped_numpy_methods + ): output_unit = self._units elif func.__name__ in set_units_ufuncs: output_unit = set_units_ufuncs[func.__name__][1] elif func.__name__ in matching_input_set_units_output_ufuncs: output_unit = matching_input_set_units_output_ufuncs[func.__name__] elif func.__name__ in op_units_output_ufuncs: - output_unit = get_op_output_unit(op_units_output_ufuncs[func.__name__], - self.units, - list(args) + list(kwargs.values()), - self._magnitude.size) + output_unit = get_op_output_unit( + op_units_output_ufuncs[func.__name__], + self.units, + list(args) + list(kwargs.values()), + self._magnitude.size, + ) else: output_unit = None @@ -1367,32 +1442,32 @@ def _numpy_method_wrap(self, func, *args, **kwargs): return value def clip(self, first=None, second=None, out=None, **kwargs): - min = kwargs.get('min', first) - max = kwargs.get('max', second) + min = kwargs.get("min", first) + max = kwargs.get("max", second) if min is None and max is None: - raise TypeError('clip() takes at least 3 arguments (2 given)') + raise TypeError("clip() takes at least 3 arguments (2 given)") - if max is None and 'min' not in kwargs: + if max is None and "min" not in kwargs: min, max = max, min - kwargs = {'out': out} + kwargs = {"out": out} if min is not None: if isinstance(min, self.__class__): - kwargs['min'] = min.to(self).magnitude + kwargs["min"] = min.to(self).magnitude elif self.dimensionless: - kwargs['min'] = min + kwargs["min"] = min else: - raise DimensionalityError('dimensionless', self._units) + raise DimensionalityError("dimensionless", self._units) if max is not None: if isinstance(max, self.__class__): - kwargs['max'] = max.to(self).magnitude + kwargs["max"] = max.to(self).magnitude elif self.dimensionless: - kwargs['max'] = max + kwargs["max"] = max else: - raise DimensionalityError('dimensionless', self._units) + raise DimensionalityError("dimensionless", self._units) return self.__class__(self.magnitude.clip(**kwargs), self._units) @@ -1400,13 +1475,13 @@ def fill(self, value): self._units = value._units return self.magnitude.fill(value.magnitude) - def put(self, indices, values, mode='raise'): + def put(self, indices, values, mode="raise"): if isinstance(values, self.__class__): values = values.to(self).magnitude elif self.dimensionless: - values = self.__class__(values, '').to(self) + values = self.__class__(values, "").to(self) else: - raise DimensionalityError('dimensionless', self._units) + raise DimensionalityError("dimensionless", self._units) self.magnitude.put(indices, values, mode) @property @@ -1434,13 +1509,13 @@ def shape(self): def shape(self, value): self._magnitude.shape = value - def searchsorted(self, v, side='left', sorter=None): + def searchsorted(self, v, side="left", sorter=None): if isinstance(v, self.__class__): v = v.to(self).magnitude elif self.dimensionless: - v = self.__class__(v, '').to(self) + v = self.__class__(v, "").to(self) else: - raise DimensionalityError('dimensionless', self._units) + raise DimensionalityError("dimensionless", self._units) return self.magnitude.searchsorted(v, side) def dot(self, b): @@ -1451,7 +1526,7 @@ def dot(self, b): return np.dot(self, b) def __ito_if_needed(self, to_units): - if self.unitless and to_units == 'radian': + if self.unitless and to_units == "radian": return self.ito(to_units) @@ -1462,9 +1537,13 @@ def __len__(self): def __getattr__(self, item): # Attributes starting with `__array_` are common attributes of NumPy ndarray. # They are requested by numpy functions. - if item.startswith('__array_'): - warnings.warn("The unit of the quantity is stripped when getting {} " - "attribute".format(item), UnitStrippedWarning, stacklevel=2) + if item.startswith("__array_"): + warnings.warn( + "The unit of the quantity is stripped when getting {} " + "attribute".format(item), + UnitStrippedWarning, + stacklevel=2, + ) if isinstance(self._magnitude, ndarray): return getattr(self._magnitude, item) else: @@ -1480,13 +1559,15 @@ def __getattr__(self, item): if callable(attr): return functools.partial(self._numpy_method_wrap, attr) else: - raise AttributeError('NumPy method {} was not callable.'.format(item)) + raise AttributeError("NumPy method {} was not callable.".format(item)) try: return getattr(self._magnitude, item) except AttributeError: - raise AttributeError("Neither Quantity object nor its magnitude ({}) " - "has attribute '{}'".format(self._magnitude, item)) + raise AttributeError( + "Neither Quantity object nor its magnitude ({}) " + "has attribute '{}'".format(self._magnitude, item) + ) def __getitem__(self, key): try: @@ -1494,8 +1575,10 @@ def __getitem__(self, key): except PintTypeError: raise except TypeError: - raise TypeError("Neither Quantity object nor its magnitude ({})" - "supports indexing".format(self._magnitude)) + raise TypeError( + "Neither Quantity object nor its magnitude ({})" + "supports indexing".format(self._magnitude) + ) def __setitem__(self, key, value): try: @@ -1507,7 +1590,9 @@ def __setitem__(self, key, value): try: if isinstance(value, self.__class__): - factor = self.__class__(value.magnitude, value._units / self._units).to_root_units() + factor = self.__class__( + value.magnitude, value._units / self._units + ).to_root_units() else: factor = self.__class__(value, self._units ** (-1)).to_root_units() @@ -1518,7 +1603,7 @@ def __setitem__(self, key, value): self.units, extra_msg=". Assign a quantity with the same dimensionality " "or access the magnitude directly as " - f"`obj.magnitude[{key}] = {value}`." + f"`obj.magnitude[{key}] = {value}`.", ) self._magnitude[key] = factor.magnitude else: @@ -1534,14 +1619,18 @@ def __setitem__(self, key, value): def tolist(self): units = self._units - return [self.__class__(value, units).tolist() if isinstance(value, list) else self.__class__(value, units) - for value in self._magnitude.tolist()] + return [ + self.__class__(value, units).tolist() + if isinstance(value, list) + else self.__class__(value, units) + for value in self._magnitude.tolist() + ] # Measurement support def plus_minus(self, error, relative=False): if isinstance(error, self.__class__): if relative: - raise ValueError('{} is not a valid relative error.'.format(error)) + raise ValueError("{} is not a valid relative error.".format(error)) error = error.to(self._units).magnitude else: if relative: @@ -1559,8 +1648,11 @@ def _is_multiplicative(self): def _get_non_multiplicative_units(self): """Return a list of the of non-multiplicative units of the Quantity object """ - offset_units = [unit for unit in self._units.keys() - if not self._REGISTRY._units[unit].is_multiplicative] + offset_units = [ + unit + for unit in self._units.keys() + if not self._REGISTRY._units[unit].is_multiplicative + ] return offset_units def _get_delta_units(self): @@ -1573,7 +1665,7 @@ def _has_compatible_delta(self, unit): """"Check if Quantity object has a delta_unit that is compatible with unit """ deltas = self._get_delta_units() - if 'delta_' + unit in deltas: + if "delta_" + unit in deltas: return True else: # Look for delta units with same dimension as the offset unit offset_unit_dim = self._REGISTRY._units[unit].reference @@ -1596,15 +1688,17 @@ def _ok_for_muldiv(self, no_offset_units=None): if no_offset_units == 1: if len(self._units) > 1: is_ok = False - if (len(self._units) == 1 - and not self._REGISTRY.autoconvert_offset_to_baseunit): + if ( + len(self._units) == 1 + and not self._REGISTRY.autoconvert_offset_to_baseunit + ): is_ok = False if next(iter(self._units.values())) != 1: is_ok = False return is_ok def to_timedelta(self): - return datetime.timedelta(microseconds=self.to('microseconds').magnitude) + return datetime.timedelta(microseconds=self.to("microseconds").magnitude) _Quantity = Quantity diff --git a/pint/registry.py b/pint/registry.py index 3be1f5ae8..0cb585b54 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -48,23 +48,41 @@ from . import registry_helpers from .context import Context, ContextChain -from .util import (getattr_maybe_raise, - logger, pi_theorem, solve_dependencies, ParserHelper, - string_preprocessor, find_connected_nodes, - find_shortest_path, UnitsContainer, _is_dim, - to_units_container, SourceIterator) +from .util import ( + getattr_maybe_raise, + logger, + pi_theorem, + solve_dependencies, + ParserHelper, + string_preprocessor, + find_connected_nodes, + find_shortest_path, + UnitsContainer, + _is_dim, + to_units_container, + SourceIterator, +) from .compat import tokenizer -from .definitions import (Definition, UnitDefinition, PrefixDefinition, - DimensionDefinition, AliasDefinition) +from .definitions import ( + Definition, + UnitDefinition, + PrefixDefinition, + DimensionDefinition, + AliasDefinition, +) from .converters import ScaleConverter -from .errors import (DimensionalityError, UndefinedUnitError, - DefinitionSyntaxError, RedefinitionError) +from .errors import ( + DimensionalityError, + UndefinedUnitError, + DefinitionSyntaxError, + RedefinitionError, +) from .pint_eval import build_eval_tree from . import systems -_BLOCK_RE = re.compile(r' |\(') +_BLOCK_RE = re.compile(r" |\(") class RegistryMeta(type): @@ -124,15 +142,31 @@ class BaseRegistry(metaclass=RegistryMeta): #: List to be used in addition of units when dir(registry) is called. #: Also used for autocompletion in IPython. - _dir = ['Quantity', 'Unit', 'Measurement', - 'define', 'load_definitions', - 'get_name', 'get_symbol', 'get_dimensionality', - 'get_base_units', 'get_root_units', - 'parse_unit_name', 'parse_units', 'parse_expression', - 'convert'] - - def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', - auto_reduce_dimensions=False, preprocessors=None): + _dir = [ + "Quantity", + "Unit", + "Measurement", + "define", + "load_definitions", + "get_name", + "get_symbol", + "get_dimensionality", + "get_base_units", + "get_root_units", + "parse_unit_name", + "parse_units", + "parse_expression", + "convert", + ] + + def __init__( + self, + filename="", + force_ndarray=False, + on_redefinition="warn", + auto_reduce_dimensions=False, + preprocessors=None, + ): self._register_parsers() self._init_dynamic_classes() @@ -163,10 +197,10 @@ def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', self._units_casei = defaultdict(set) #: Map prefix name (string) to its definition (PrefixDefinition). - self._prefixes = {'': PrefixDefinition('', '', (), 1)} + self._prefixes = {"": PrefixDefinition("", "", (), 1)} #: Map suffix name (string) to canonical , and unit alias to canonical unit name - self._suffixes = {'': None, 's': ''} + self._suffixes = {"": None, "s": ""} #: Map contexts to RegistryCache self._cache = RegistryCache() @@ -177,21 +211,24 @@ def _init_dynamic_classes(self): """Generate subclasses on the fly and attach them to self """ from .unit import build_unit_class + self.Unit = build_unit_class(self) from .quantity import build_quantity_class + self.Quantity = build_quantity_class(self) from .measurement import build_measurement_class + self.Measurement = build_measurement_class(self) def _after_init(self): """This should be called after all __init__ """ - self.define(UnitDefinition('pi', 'π', (), ScaleConverter(math.pi))) + self.define(UnitDefinition("pi", "π", (), ScaleConverter(math.pi))) - if self._filename == '': - self.load_definitions('default_en.txt', True) + if self._filename == "": + self.load_definitions("default_en.txt", True) elif self._filename is not None: self.load_definitions(self._filename) @@ -199,7 +236,7 @@ def _after_init(self): self._initialized = True def _register_parsers(self): - self._register_parser('@defaults', self._parse_defaults) + self._register_parser("@defaults", self._parse_defaults) def _parse_defaults(self, ifile): """Loader for a @default section. @@ -208,7 +245,7 @@ def _parse_defaults(self, ifile): """ next(ifile) for lineno, part in ifile.block_iter(): - k, v = part.split('=') + k, v = part.split("=") self._defaults[k.strip()] = v.strip() def __deepcopy__(self, memo): @@ -222,8 +259,10 @@ def __getattr__(self, item): return self.Unit(item) def __getitem__(self, item): - logger.warning('Calling the getitem method from a UnitRegistry is deprecated. ' - 'use `parse_expression` method or use the registry as a callable.') + logger.warning( + "Calling the getitem method from a UnitRegistry is deprecated. " + "use `parse_expression` method or use the registry as a callable." + ) return self.parse_expression(item) def __dir__(self): @@ -248,7 +287,7 @@ def define(self, definition): """ if isinstance(definition, str): - for line in definition.split('\n'): + for line in definition.split("\n"): self._define(Definition.from_string(line)) else: self._define(definition) @@ -276,13 +315,15 @@ def _define(self, definition): if definition.is_base: for dimension in definition.reference.keys(): if dimension in self._dimensions: - if dimension != '[]': + if dimension != "[]": raise DefinitionSyntaxError( - 'Only one unit per dimension can be a base unit' + "Only one unit per dimension can be a base unit" ) continue - self.define(DimensionDefinition(dimension, '', (), None, is_base=True)) + self.define( + DimensionDefinition(dimension, "", (), None, is_base=True) + ) elif isinstance(definition, PrefixDefinition): d, di = self._prefixes, None @@ -293,31 +334,37 @@ def _define(self, definition): return d[definition.name], d, di else: - raise TypeError('{} is not a valid definition.'.format(definition)) + raise TypeError("{} is not a valid definition.".format(definition)) # define "delta_" units for units with an offset if getattr(definition.converter, "offset", 0.0) != 0.0: - if definition.name.startswith('['): - d_name = '[delta_' + definition.name[1:] + if definition.name.startswith("["): + d_name = "[delta_" + definition.name[1:] else: - d_name = 'delta_' + definition.name + d_name = "delta_" + definition.name if definition.symbol: - d_symbol = 'Δ' + definition.symbol + d_symbol = "Δ" + definition.symbol else: d_symbol = None - d_aliases = tuple('Δ' + alias for alias in definition.aliases) + \ - tuple('delta_' + alias for alias in definition.aliases) + d_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple( + "delta_" + alias for alias in definition.aliases + ) d_reference = UnitsContainer( {ref: value for ref, value in definition.reference.items()} ) - d_def = UnitDefinition(d_name, d_symbol, d_aliases, - ScaleConverter(definition.converter.scale), - d_reference, definition.is_base) + d_def = UnitDefinition( + d_name, + d_symbol, + d_aliases, + ScaleConverter(definition.converter.scale), + d_reference, + definition.is_base, + ) else: d_def = definition @@ -329,14 +376,18 @@ def _define_adder(self, definition, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. It stores the definition under its name, symbol and aliases. """ - self._define_single_adder(definition.name, definition, unit_dict, casei_unit_dict) + self._define_single_adder( + definition.name, definition, unit_dict, casei_unit_dict + ) if definition.has_symbol: - self._define_single_adder(definition.symbol, definition, unit_dict, casei_unit_dict) + self._define_single_adder( + definition.symbol, definition, unit_dict, casei_unit_dict + ) for alias in definition.aliases: - if ' ' in alias: - logger.warn('Alias cannot contain a space: ' + alias) + if " " in alias: + logger.warn("Alias cannot contain a space: " + alias) self._define_single_adder(alias, definition, unit_dict, casei_unit_dict) @@ -346,9 +397,9 @@ def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): It warns or raise error on redefinition. """ if key in unit_dict: - if self._on_redefinition == 'raise': + if self._on_redefinition == "raise": raise RedefinitionError(key, type(value)) - elif self._on_redefinition == 'warn': + elif self._on_redefinition == "warn": logger.warning("Redefining '%s' (%s)" % (key, type(value))) unit_dict[key] = value @@ -372,7 +423,7 @@ def _register_parser(self, prefix, parserfunc): if self._parsers is None: self._parsers = {} - if prefix and prefix[0] == '@': + if prefix and prefix[0] == "@": self._parsers[prefix] = parserfunc else: raise ValueError("Prefix directives must start with '@'") @@ -390,22 +441,24 @@ def load_definitions(self, file, is_resource=False): if is_resource: with closing(pkg_resources.resource_stream(__name__, file)) as fp: rbytes = fp.read() - return self.load_definitions(StringIO(rbytes.decode('utf-8')), is_resource) + return self.load_definitions( + StringIO(rbytes.decode("utf-8")), is_resource + ) else: - with open(file, encoding='utf-8') as fp: + with open(file, encoding="utf-8") as fp: return self.load_definitions(fp, is_resource) except (RedefinitionError, DefinitionSyntaxError) as e: if e.filename is None: e.filename = file raise e except Exception as e: - msg = getattr(e, 'message', '') or str(e) - raise ValueError('While opening {}\n{}'.format(file, msg)) + msg = getattr(e, "message", "") or str(e) + raise ValueError("While opening {}\n{}".format(file, msg)) ifile = SourceIterator(file) for no, line in ifile: - if line.startswith('@') and not line.startswith('@alias'): - if line.startswith('@import'): + if line.startswith("@") and not line.startswith("@alias"): + if line.startswith("@import"): if is_resource: path = line[7:].strip() else: @@ -418,10 +471,14 @@ def load_definitions(self, file, is_resource=False): else: parts = _BLOCK_RE.split(line) - loader = self._parsers.get(parts[0], None) if self._parsers else None + loader = ( + self._parsers.get(parts[0], None) if self._parsers else None + ) if loader is None: - raise DefinitionSyntaxError('Unknown directive %s' % line, lineno=no) + raise DefinitionSyntaxError( + "Unknown directive %s" % line, lineno=no + ) try: loader(ifile) @@ -451,7 +508,7 @@ def _build_cache(self): for unit_names in solve_dependencies(deps): for unit_name in unit_names: - if '[' in unit_name: + if "[" in unit_name: continue parsed_names = tuple(self.parse_unit_name(unit_name)) _prefix = None @@ -476,7 +533,7 @@ def _build_cache(self): dimeq_set.add(self._units[base_name]._name) except Exception as e: - logger.warning('Could not resolve {0}: {1!r}'.format(unit_name, e)) + logger.warning("Could not resolve {0}: {1!r}".format(unit_name, e)) def _dedup_candidates(self, candidates): """Given a list of unit triplets (prefix, name, suffix), @@ -502,30 +559,35 @@ def get_name(self, name_or_alias, case_sensitive=True): """Return the canonical name of a unit. """ - if name_or_alias == 'dimensionless': - return '' + if name_or_alias == "dimensionless": + return "" try: return self._units[name_or_alias]._name except KeyError: pass - candidates = self._dedup_candidates(self.parse_unit_name(name_or_alias, case_sensitive)) + candidates = self._dedup_candidates( + self.parse_unit_name(name_or_alias, case_sensitive) + ) if not candidates: raise UndefinedUnitError(name_or_alias) elif len(candidates) == 1: prefix, unit_name, _ = candidates[0] else: - logger.warning('Parsing {} yield multiple results. ' - 'Options are: {}'.format(name_or_alias, candidates)) + logger.warning( + "Parsing {} yield multiple results. " + "Options are: {}".format(name_or_alias, candidates) + ) prefix, unit_name, _ = candidates[0] if prefix: name = prefix + unit_name symbol = self.get_symbol(name) prefix_def = self._prefixes[prefix] - self._units[name] = UnitDefinition(name, symbol, (), prefix_def.converter, - UnitsContainer({unit_name: 1})) + self._units[name] = UnitDefinition( + name, symbol, (), prefix_def.converter, UnitsContainer({unit_name: 1}) + ) return prefix + unit_name return unit_name @@ -539,8 +601,10 @@ def get_symbol(self, name_or_alias): elif len(candidates) == 1: prefix, unit_name, _ = candidates[0] else: - logger.warning('Parsing {0} yield multiple results. ' - 'Options are: {1!r}'.format(name_or_alias, candidates)) + logger.warning( + "Parsing {0} yield multiple results. " + "Options are: {1!r}".format(name_or_alias, candidates) + ) prefix, unit_name, _ = candidates[0] return self._prefixes[prefix].symbol + self._units[unit_name].symbol @@ -578,8 +642,8 @@ def _get_dimensionality(self, input_units): accumulator = defaultdict(float) self._get_dimensionality_recurse(input_units, 1.0, accumulator) - if '[]' in accumulator: - del accumulator['[]'] + if "[]" in accumulator: + del accumulator["[]"] dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0.0}) @@ -589,7 +653,7 @@ def _get_dimensionality(self, input_units): def _get_dimensionality_recurse(self, ref, exp, accumulator): for key in ref: - exp2 = exp*ref[key] + exp2 = exp * ref[key] if _is_dim(key): reg = self._dimensions[key] if reg.is_base: @@ -609,17 +673,17 @@ def _get_dimensionality_ratio(self, unit1, unit2): :type unit2: UnitsContainer compatible (str, Unit, UnitsContainer, dict) :returns: exponential proportionality or None if the units cannot be converted """ - #shortcut in case of equal units + # shortcut in case of equal units if unit1 == unit2: return 1 dim1, dim2 = (self.get_dimensionality(unit) for unit in (unit1, unit2)) - if not dim1 or not dim2 or dim1.keys() != dim2.keys(): #not comparable + if not dim1 or not dim2 or dim1.keys() != dim2.keys(): # not comparable return None - ratios = (dim2[key]/val for key, val in dim1.items()) + ratios = (dim2[key] / val for key, val in dim1.items()) first = next(ratios) - if all(r == first for r in ratios): #all are same, we're good + if all(r == first for r in ratios): # all are same, we're good return first return None @@ -656,7 +720,7 @@ def _get_root_units(self, input_units, check_nonmult=True): :return: multiplicative factor, base units """ if not input_units: - return 1., UnitsContainer() + return 1.0, UnitsContainer() # The cache is only done for check_nonmult=True cache = self._cache.root_units @@ -666,7 +730,7 @@ def _get_root_units(self, input_units, check_nonmult=True): except KeyError: pass - accumulators = [1., defaultdict(float)] + accumulators = [1.0, defaultdict(float)] self._get_root_units_recurse(input_units, 1.0, accumulators) factor = accumulators[0] @@ -701,7 +765,7 @@ def get_base_units(self, input_units, check_nonmult=True, system=None): def _get_root_units_recurse(self, ref, exp, accumulators): for key in sorted(ref): - exp2 = exp*ref[key] + exp2 = exp * ref[key] key = self.get_name(key) reg = self._units[key] if reg.is_base: @@ -709,8 +773,7 @@ def _get_root_units_recurse(self, ref, exp, accumulators): else: accumulators[0] *= reg._converter.scale ** exp2 if reg.reference is not None: - self._get_root_units_recurse(reg.reference, exp2, - accumulators) + self._get_root_units_recurse(reg.reference, exp2, accumulators) def get_compatible_units(self, input_units, group_or_system=None): """ @@ -800,21 +863,25 @@ def parse_unit_name(self, unit_name, case_sensitive=True): edw = unit_name.endswith for suffix, prefix in itertools.product(self._suffixes, self._prefixes): if stw(prefix) and edw(suffix): - name = unit_name[len(prefix):] + name = unit_name[len(prefix) :] if suffix: - name = name[:-len(suffix)] + name = name[: -len(suffix)] if len(name) == 1: continue if case_sensitive: if name in self._units: - yield (self._prefixes[prefix].name, - self._units[name].name, - self._suffixes[suffix]) + yield ( + self._prefixes[prefix].name, + self._units[name].name, + self._suffixes[suffix], + ) else: for real_name in self._units_casei.get(name.lower(), ()): - yield (self._prefixes[prefix].name, - self._units[real_name].name, - self._suffixes[suffix]) + yield ( + self._prefixes[prefix].name, + self._units[real_name].name, + self._suffixes[suffix], + ) def parse_units(self, input_string, as_delta=None): """Parse a units expression and returns a UnitContainer with @@ -852,7 +919,7 @@ def _parse_units(self, input_string, as_delta=True): units = ParserHelper.from_string(input_string) if units.scale != 1: - raise ValueError('Unit expression cannot have a scaling factor.') + raise ValueError("Unit expression cannot have a scaling factor.") ret = {} many = len(units) > 1 @@ -864,7 +931,7 @@ def _parse_units(self, input_string, as_delta=True): if as_delta and (many or (not many and value != 1)): definition = self._units[cname] if not definition.is_multiplicative: - cname = 'delta_' + cname + cname = "delta_" + cname ret[cname] = value ret = UnitsContainer(ret) @@ -878,19 +945,25 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): token_type = token[0] token_text = token[1] if token_type == NAME: - if token_text == 'dimensionless': + if token_text == "dimensionless": return 1 * self.dimensionless elif token_text in values: return self.Quantity(values[token_text]) else: - return self.Quantity(1, UnitsContainer({self.get_name(token_text, - case_sensitive=case_sensitive) : 1})) + return self.Quantity( + 1, + UnitsContainer( + {self.get_name(token_text, case_sensitive=case_sensitive): 1} + ), + ) elif token_type == NUMBER: return ParserHelper.eval_token(token, use_decimal=use_decimal) else: - raise Exception('unknown token type') + raise Exception("unknown token type") - def parse_expression(self, input_string, case_sensitive=True, use_decimal=False, **values): + def parse_expression( + self, input_string, case_sensitive=True, use_decimal=False, **values + ): """Parse a mathematical expression including units and return a quantity object. Numerical constants can be specified as keyword arguments and will take precedence @@ -905,10 +978,11 @@ def parse_expression(self, input_string, case_sensitive=True, use_decimal=False, input_string = string_preprocessor(input_string) gen = tokenizer(input_string) - return build_eval_tree(gen).evaluate(lambda x: self._eval_token(x, - case_sensitive=case_sensitive, - use_decimal=use_decimal, - **values)) + return build_eval_tree(gen).evaluate( + lambda x: self._eval_token( + x, case_sensitive=case_sensitive, use_decimal=use_decimal, **values + ) + ) __call__ = parse_expression @@ -926,7 +1000,9 @@ class NonMultiplicativeRegistry(BaseRegistry): converted to base units in multiplications. """ - def __init__(self, default_as_delta=True, autoconvert_offset_to_baseunit=False, **kwargs): + def __init__( + self, default_as_delta=True, autoconvert_offset_to_baseunit=False, **kwargs + ): super().__init__(**kwargs) #: When performing a multiplication of units, interpret @@ -981,13 +1057,14 @@ def _is_multiplicative(self, u): def _validate_and_extract(self, units): - nonmult_units = [(u, e) for u, e in units.items() - if not self._is_multiplicative(u)] + nonmult_units = [ + (u, e) for u, e in units.items() if not self._is_multiplicative(u) + ] # Let's validate source offset units if len(nonmult_units) > 1: # More than one src offset unit is not allowed - raise ValueError('more than one offset unit.') + raise ValueError("more than one offset unit.") elif len(nonmult_units) == 1: # A single src offset unit is present. Extract it @@ -997,10 +1074,10 @@ def _validate_and_extract(self, units): nonmult_unit, exponent = nonmult_units.pop() if exponent != 1: - raise ValueError('offset units in higher order.') + raise ValueError("offset units in higher order.") if len(units) > 1 and not self.autoconvert_offset_to_baseunit: - raise ValueError('offset unit used in multiplicative context.') + raise ValueError("offset unit used in multiplicative context.") return nonmult_unit @@ -1028,9 +1105,7 @@ def _convert(self, value, src, dst, inplace=False): try: src_offset_unit = self._validate_and_extract(src) except ValueError as ex: - raise DimensionalityError( - src, dst, extra_msg=f" - In source units, {ex}" - ) + raise DimensionalityError(src, dst, extra_msg=f" - In source units, {ex}") try: dst_offset_unit = self._validate_and_extract(dst) @@ -1064,7 +1139,9 @@ def _convert(self, value, src, dst, inplace=False): # Finally convert to offset units specified in destination if dst_offset_unit: - value = self._units[dst_offset_unit].converter.from_reference(value, inplace) + value = self._units[dst_offset_unit].converter.from_reference( + value, inplace + ) return value @@ -1095,14 +1172,15 @@ def __init__(self, **kwargs): def _register_parsers(self): super()._register_parsers() - self._register_parser('@context', self._parse_context) + self._register_parser("@context", self._parse_context) def _parse_context(self, ifile): try: - self.add_context(Context.from_lines(ifile.block_iter(), - self.get_dimensionality)) + self.add_context( + Context.from_lines(ifile.block_iter(), self.get_dimensionality) + ) except KeyError as e: - raise DefinitionSyntaxError(f'unknown dimension {e} in context') + raise DefinitionSyntaxError(f"unknown dimension {e} in context") def add_context(self, context): """Add a context object to the registry. @@ -1114,13 +1192,16 @@ def add_context(self, context): if not context.name: raise ValueError("Can't add unnamed context to registry") if context.name in self._contexts: - logger.warning('The name %s was already registered for another context.', - context.name) + logger.warning( + "The name %s was already registered for another context.", context.name + ) self._contexts[context.name] = context for alias in context.aliases: if alias in self._contexts: - logger.warning('The name %s was already registered for another context', - context.name) + logger.warning( + "The name %s was already registered for another context", + context.name, + ) self._contexts[alias] = context def remove_context(self, name_or_alias): @@ -1164,7 +1245,7 @@ def enable_contexts(self, *names_or_contexts, **kwargs): # Check if the contexts have been checked first, if not we make sure # that dimensions are expressed in terms of base dimensions. for ctx in ctxs: - if getattr(ctx, '_checked', False): + if getattr(ctx, "_checked", False): continue for (src, dst), func in ctx.funcs.items(): src_ = self._get_dimensionality(src) @@ -1175,8 +1256,7 @@ def enable_contexts(self, *names_or_contexts, **kwargs): ctx._checked = True # and create a new one with the new defaults. - ctxs = tuple(Context.from_context(ctx, **kwargs) - for ctx in ctxs) + ctxs = tuple(Context.from_context(ctx, **kwargs) for ctx in ctxs) # Finally we add them to the active context. self._active_ctx.insert_contexts(*ctxs) @@ -1255,9 +1335,14 @@ def with_context(self, name, **kw): :param kwargs: keyword arguments for the contexts. :return: the wrapped function. """ + def decorator(func): - assigned = tuple(attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr)) - updated = tuple(attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr)) + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*values, **kwargs): @@ -1347,7 +1432,7 @@ def __init__(self, system=None, **kwargs): #: Map group name to group. #: :type: dict[ str | Group] self._groups = {} - self._groups['root'] = self.Group('root') + self._groups["root"] = self.Group("root") self._default_system = system def _init_dynamic_classes(self): @@ -1365,8 +1450,8 @@ def _after_init(self): super()._after_init() #: Copy units not defined in any group to the default group - if 'group' in self._defaults: - grp = self.get_group(self._defaults['group'], True) + if "group" in self._defaults: + grp = self.get_group(self._defaults["group"], True) group_units = frozenset( [ member @@ -1379,12 +1464,14 @@ def _after_init(self): grp.add_units(*(all_units - group_units)) #: System name to be used by default. - self._default_system = self._default_system or self._defaults.get('system', None) + self._default_system = self._default_system or self._defaults.get( + "system", None + ) def _register_parsers(self): super()._register_parsers() - self._register_parser('@group', self._parse_group) - self._register_parser('@system', self._parse_system) + self._register_parser("@group", self._parse_group) + self._register_parser("@system", self._parse_system) def _parse_group(self, ifile): self.Group.from_lines(ifile.block_iter(), self.define) @@ -1403,7 +1490,7 @@ def get_group(self, name, create_if_needed=True): return self._groups[name] if not create_if_needed: - raise ValueError('Unkown group %s' % name) + raise ValueError("Unkown group %s" % name) return self.Group(name) @@ -1419,7 +1506,7 @@ def default_system(self): def default_system(self, name): if name: if name not in self._systems: - raise ValueError('Unknown system %s' % name) + raise ValueError("Unknown system %s" % name) self._base_units_cache = {} @@ -1436,7 +1523,7 @@ def get_system(self, name, create_if_needed=True): return self._systems[name] if not create_if_needed: - raise ValueError('Unkown system %s' % name) + raise ValueError("Unkown system %s" % name) return self.System(name) @@ -1449,7 +1536,7 @@ def _define(self, definition): if isinstance(definition, UnitDefinition): # We add all units to the root group - self.get_group('root').add_units(definition.name) + self.get_group("root").add_units(definition.name) return definition, d, di @@ -1482,7 +1569,11 @@ def _get_base_units(self, input_units, check_nonmult=True, system=None): system = self._default_system # The cache is only done for check_nonmult=True and the current system. - if check_nonmult and system == self._default_system and input_units in self._base_units_cache: + if ( + check_nonmult + and system == self._default_system + and input_units in self._base_units_cache + ): return self._base_units_cache[input_units] factor, units = self.get_root_units(input_units, check_nonmult) @@ -1528,7 +1619,9 @@ def _get_compatible_units(self, input_units, group_or_system): elif group_or_system in self._groups: members = self._groups[group_or_system].members else: - raise ValueError("Unknown Group o System with name '%s'" % group_or_system) + raise ValueError( + "Unknown Group o System with name '%s'" % group_or_system + ) return frozenset(ret & members) return ret @@ -1554,20 +1647,27 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): or unit string """ - def __init__(self, filename='', force_ndarray=False, default_as_delta=True, - autoconvert_offset_to_baseunit=False, - on_redefinition='warn', system=None, - auto_reduce_dimensions=False, preprocessors=None): + def __init__( + self, + filename="", + force_ndarray=False, + default_as_delta=True, + autoconvert_offset_to_baseunit=False, + on_redefinition="warn", + system=None, + auto_reduce_dimensions=False, + preprocessors=None, + ): super().__init__( - filename=filename, + filename=filename, force_ndarray=force_ndarray, on_redefinition=on_redefinition, default_as_delta=default_as_delta, autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, system=system, auto_reduce_dimensions=auto_reduce_dimensions, - preprocessors=preprocessors + preprocessors=preprocessors, ) def pi_theorem(self, quantities): @@ -1585,6 +1685,7 @@ def setup_matplotlib(self, enable=True): """ # Delays importing matplotlib until it's actually requested from .matplotlib import setup_matplotlib_handlers + setup_matplotlib_handlers(self, enable) wraps = registry_helpers.wraps @@ -1593,25 +1694,24 @@ def setup_matplotlib(self, enable=True): class LazyRegistry: - def __init__(self, args=None, kwargs=None): - self.__dict__['params'] = args or (), kwargs or {} + self.__dict__["params"] = args or (), kwargs or {} def __init(self): - args, kwargs = self.__dict__['params'] - kwargs['on_redefinition'] = 'raise' + args, kwargs = self.__dict__["params"] + kwargs["on_redefinition"] = "raise" self.__class__ = UnitRegistry self.__init__(*args, **kwargs) self._after_init() def __getattr__(self, item): - if item == '_on_redefinition': - return 'raise' + if item == "_on_redefinition": + return "raise" self.__init() return getattr(self, item) def __setattr__(self, key, value): - if key == '__class__': + if key == "__class__": super().__setattr__(key, value) else: self.__init() diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 7733c49d2..9fd3f752a 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -37,8 +37,8 @@ def _to_units_container(a, registry=None): Return a tuple with the unit container and a boolean indicating if it was a reference. """ - if isinstance(a, str) and '=' in a: - return to_units_container(a.split('=', 1)[1]), True + if isinstance(a, str) and "=" in a: + return to_units_container(a.split("=", 1)[1]), True return to_units_container(a, registry), False @@ -86,8 +86,10 @@ def _parse_wrap_args(args, registry=None): if not isinstance(arg, dict): continue if not set(arg.keys()) <= defs_args: - raise ValueError('Found a missing token while wrapping a function: ' - 'Not all variable referenced in %s are defined using !' % args[ndx]) + raise ValueError( + "Found a missing token while wrapping a function: " + "Not all variable referenced in %s are defined using !" % args[ndx] + ) def _converter(ureg, values, strict): new_values = list(value for value in values) @@ -104,22 +106,28 @@ def _converter(ureg, values, strict): for ndx in dependent_args_ndx: value = values[ndx] assert _replace_units(args_as_uc[ndx][0], values_by_name) is not None - new_values[ndx] = ureg._convert(getattr(value, "_magnitude", value), - getattr(value, "_units", UnitsContainer({})), - _replace_units(args_as_uc[ndx][0], values_by_name)) + new_values[ndx] = ureg._convert( + getattr(value, "_magnitude", value), + getattr(value, "_units", UnitsContainer({})), + _replace_units(args_as_uc[ndx][0], values_by_name), + ) # third pass: convert other arguments for ndx in unit_args_ndx: if isinstance(values[ndx], ureg.Quantity): - new_values[ndx] = ureg._convert(values[ndx]._magnitude, - values[ndx]._units, - args_as_uc[ndx][0]) + new_values[ndx] = ureg._convert( + values[ndx]._magnitude, values[ndx]._units, args_as_uc[ndx][0] + ) else: if strict: - raise ValueError('A wrapped function using strict=True requires ' - 'quantity for all arguments with not None units. ' - '(error found for {}, {})'.format(args_as_uc[ndx][0], new_values[ndx])) + raise ValueError( + "A wrapped function using strict=True requires " + "quantity for all arguments with not None units. " + "(error found for {}, {})".format( + args_as_uc[ndx][0], new_values[ndx] + ) + ) return new_values, values_by_name @@ -139,7 +147,7 @@ def _apply_defaults(func, args, kwargs): if param.name not in bound_arguments.arguments: bound_arguments.arguments[param.name] = param.default args = [bound_arguments.arguments[key] for key in sig.parameters.keys()] - return args, {} + return args, {} def wraps(ureg, ret, args, strict=True): @@ -166,24 +174,31 @@ def wraps(ureg, ret, args, strict=True): """ if not isinstance(args, (list, tuple)): - args = (args, ) + args = (args,) converter = _parse_wrap_args(args) if isinstance(ret, (list, tuple)): - container, ret = True, ret.__class__([_to_units_container(arg, ureg) for arg in ret]) + container, ret = ( + True, + ret.__class__([_to_units_container(arg, ureg) for arg in ret]), + ) else: container, ret = False, _to_units_container(ret, ureg) def decorator(func): - assigned = tuple(attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr)) - updated = tuple(attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr)) + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*values, **kw): values, kw = _apply_defaults(func, values, kw) - + # In principle, the values are used as is # When then extract the magnitudes when needed. new_values, values_by_name = converter(ureg, values, strict) @@ -191,18 +206,24 @@ def wrapper(*values, **kw): result = func(*new_values, **kw) if container: - out_units = (_replace_units(r, values_by_name) if is_ref else r - for (r, is_ref) in ret) - return ret.__class__(res if unit is None else ureg.Quantity(res, unit) - for unit, res in zip_longest(out_units, result)) + out_units = ( + _replace_units(r, values_by_name) if is_ref else r + for (r, is_ref) in ret + ) + return ret.__class__( + res if unit is None else ureg.Quantity(res, unit) + for unit, res in zip_longest(out_units, result) + ) if ret[0] is None: return result - return ureg.Quantity(result, - _replace_units(ret[0], values_by_name) if ret[1] else ret[0]) + return ureg.Quantity( + result, _replace_units(ret[0], values_by_name) if ret[1] else ret[0] + ) return wrapper + return decorator @@ -220,18 +241,26 @@ def check(ureg, *args): :raises pint.DimensionalityError: if the parameters don't match dimensions """ - dimensions = [ureg.get_dimensionality(dim) if dim is not None else None for dim in args] + dimensions = [ + ureg.get_dimensionality(dim) if dim is not None else None for dim in args + ] def decorator(func): - assigned = tuple(attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr)) - updated = tuple(attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr)) + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*args, **kwargs): list_args, empty = _apply_defaults(func, args, kwargs) if len(dimensions) > len(list_args): - raise TypeError("%s takes %i parameters, but %i dimensions were passed" - % (func.__name__, len(list_args), len(dimensions))) + raise TypeError( + "%s takes %i parameters, but %i dimensions were passed" + % (func.__name__, len(list_args), len(dimensions)) + ) for dim, value in zip(dimensions, list_args): if dim is None: @@ -239,8 +268,9 @@ def wrapper(*args, **kwargs): if not ureg.Quantity(value).check(dim): val_dim = ureg.get_dimensionality(value) - raise DimensionalityError(value, 'a quantity of', - val_dim, dim) + raise DimensionalityError(value, "a quantity of", val_dim, dim) return func(*args, **kwargs) + return wrapper + return decorator diff --git a/pint/systems.py b/pint/systems.py index b9c002915..4ae836041 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -13,7 +13,13 @@ from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError, RedefinitionError -from .util import to_units_container, getattr_maybe_raise, SharedRegistryObject, SourceIterator, logger +from .util import ( + to_units_container, + getattr_maybe_raise, + SharedRegistryObject, + SourceIterator, + logger, +) from .babel_names import _babel_systems from pint.compat import Loc @@ -38,7 +44,7 @@ class Group(SharedRegistryObject): """ #: Regex to match the header parts of a definition. - _header_re = re.compile(r'@group\s+(?P\w+)\s*(using\s(?P.*))*') + _header_re = re.compile(r"@group\s+(?P\w+)\s*(using\s(?P.*))*") def __init__(self, name): """ @@ -68,16 +74,15 @@ def __init__(self, name): # Add this group to the group dictionary self._REGISTRY._groups[self.name] = self - if name != 'root': + if name != "root": # All groups are added to root group - self._REGISTRY._groups['root'].add_groups(name) + self._REGISTRY._groups["root"].add_groups(name) #: A cache of the included units. #: None indicates that the cache has been invalidated. #: :type: frozenset[str] | None self._computed_members = None - @property def members(self): """Names of the units that are members of the group. @@ -154,7 +159,10 @@ def add_groups(self, *group_names): grp = d[group_name] if grp.is_used_group(self.name): - raise ValueError('Cyclic relationship found between %s and %s' % (self.name, group_name)) + raise ValueError( + "Cyclic relationship found between %s and %s" + % (self.name, group_name) + ) self._used_groups.add(group_name) grp._used_by.add(self.name) @@ -192,23 +200,23 @@ def from_lines(cls, lines, define_func): if r is None: raise ValueError("Invalid Group header syntax: '%s'" % header) - name = r.groupdict()['name'].strip() - groups = r.groupdict()['used_groups'] + name = r.groupdict()["name"].strip() + groups = r.groupdict()["used_groups"] if groups: - group_names = tuple(a.strip() for a in groups.split(',')) + group_names = tuple(a.strip() for a in groups.split(",")) else: group_names = () unit_names = [] for lineno, line in lines: - if '=' in line: + if "=" in line: # Is a definition definition = Definition.from_string(line) if not isinstance(definition, UnitDefinition): raise DefinitionSyntaxError( - 'Only UnitDefinition are valid inside _used_groups, not ' + "Only UnitDefinition are valid inside _used_groups, not " + str(definition), - lineno=lineno + lineno=lineno, ) try: @@ -264,7 +272,7 @@ class System(SharedRegistryObject): """ #: Regex to match the header parts of a context. - _header_re = re.compile(r'@system\s+(?P\w+)\s*(using\s(?P.*))*') + _header_re = re.compile(r"@system\s+(?P\w+)\s*(using\s(?P.*))*") def __init__(self, name): """ @@ -299,7 +307,7 @@ def __dir__(self): def __getattr__(self, item): getattr_maybe_raise(self, item) - u = getattr(self._REGISTRY, self.name + '_' + item, None) + u = getattr(self._REGISTRY, self.name + "_" + item, None) if u is not None: return u return getattr(self._REGISTRY, item) @@ -314,7 +322,11 @@ def members(self): try: self._computed_members |= d[group_name].members except KeyError: - logger.warning('Could not resolve {} in System {}'.format(group_name, self.name)) + logger.warning( + "Could not resolve {} in System {}".format( + group_name, self.name + ) + ) self._computed_members = frozenset(self._computed_members) @@ -365,14 +377,14 @@ def from_lines(cls, lines, get_root_func): if r is None: raise ValueError("Invalid System header syntax '%s'" % header) - name = r.groupdict()['name'].strip() - groups = r.groupdict()['used_groups'] + name = r.groupdict()["name"].strip() + groups = r.groupdict()["used_groups"] # If the systems has no group, it automatically uses the root group. if groups: - group_names = tuple(a.strip() for a in groups.split(',')) + group_names = tuple(a.strip() for a in groups.split(",")) else: - group_names = ('root', ) + group_names = ("root",) base_unit_names = {} derived_unit_names = [] @@ -383,27 +395,30 @@ def from_lines(cls, lines, get_root_func): # - old_unit: a root unit part which is going to be removed from the system. # - new_unit: a non root unit which is going to replace the old_unit. - if ':' in line: + if ":" in line: # The syntax is new_unit:old_unit - new_unit, old_unit = line.split(':') + new_unit, old_unit = line.split(":") new_unit, old_unit = new_unit.strip(), old_unit.strip() # The old unit MUST be a root unit, if not raise an error. if old_unit != str(get_root_func(old_unit)[1]): - raise ValueError('In `%s`, the unit at the right of the `:` must be a root unit.' % line) + raise ValueError( + "In `%s`, the unit at the right of the `:` must be a root unit." + % line + ) # Here we find new_unit expanded in terms of root_units new_unit_expanded = to_units_container(get_root_func(new_unit)[1]) # We require that the old unit is present in the new_unit expanded if old_unit not in new_unit_expanded: - raise ValueError('Old unit must be a component of new unit') + raise ValueError("Old unit must be a component of new unit") # Here we invert the equation, in other words # we write old units in terms new unit and expansion new_unit_dict = { - new_unit: -1./value + new_unit: -1.0 / value for new_unit, value in new_unit_expanded.items() if new_unit != old_unit } @@ -419,11 +434,13 @@ def from_lines(cls, lines, get_root_func): old_unit_dict = to_units_container(get_root_func(line)[1]) if len(old_unit_dict) != 1: - raise ValueError('The new base must be a root dimension if not discarded unit is specified.') + raise ValueError( + "The new base must be a root dimension if not discarded unit is specified." + ) old_unit, value = dict(old_unit_dict).popitem() - base_unit_names[old_unit] = {new_unit: 1./value} + base_unit_names[old_unit] = {new_unit: 1.0 / value} system = cls(name) @@ -436,7 +453,6 @@ def from_lines(cls, lines, get_root_func): class Lister: - def __init__(self, d): self.d = d diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index b46d86f37..ffcfbf1a4 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -15,7 +15,6 @@ class TestHandler(BufferingHandler): - def __init__(self, only_warnings=False): # BufferingHandler takes a "capacity" argument # so as to know when to flush. As we're overriding @@ -60,8 +59,8 @@ def tearDown(self): if self._test_handler is not None: buf = self._test_handler.buffer l = len(buf) - msg = '\n'.join(record.get('msg', str(record)) for record in buf) - self.assertEqual(l, 0, msg='%d warnings raised.\n%s' % (l, msg)) + msg = "\n".join(record.get("msg", str(record)) for record in buf) + self.assertEqual(l, 0, msg="%d warnings raised.\n%s" % (l, msg)) class QuantityTestCase(BaseTestCase): @@ -77,15 +76,21 @@ def setUpClass(cls): def _get_comparable_magnitudes(self, first, second, msg): if isinstance(first, Quantity) and isinstance(second, Quantity): second = second.to(first) - self.assertEqual(first.units, second.units, msg=msg + ' Units are not equal.') + self.assertEqual( + first.units, second.units, msg=msg + " Units are not equal." + ) m1, m2 = first.magnitude, second.magnitude elif isinstance(first, Quantity): - self.assertTrue(first.dimensionless, msg=msg + ' The first is not dimensionless.') - first = first.to('') + self.assertTrue( + first.dimensionless, msg=msg + " The first is not dimensionless." + ) + first = first.to("") m1, m2 = first.magnitude, second elif isinstance(second, Quantity): - self.assertTrue(second.dimensionless, msg=msg + ' The second is not dimensionless.') - second = second.to('') + self.assertTrue( + second.dimensionless, msg=msg + " The second is not dimensionless." + ) + second = second.to("") m1, m2 = first, second.magnitude else: m1, m2 = first, second @@ -94,7 +99,7 @@ def _get_comparable_magnitudes(self, first, second, msg): def assertQuantityEqual(self, first, second, msg=None): if msg is None: - msg = 'Comparing %r and %r. ' % (first, second) + msg = "Comparing %r and %r. " % (first, second) m1, m2 = self._get_comparable_magnitudes(first, second, msg) @@ -105,7 +110,7 @@ def assertQuantityEqual(self, first, second, msg=None): def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None): if msg is None: - msg = 'Comparing %r and %r. ' % (first, second) + msg = "Comparing %r and %r. " % (first, second) m1, m2 = self._get_comparable_magnitudes(first, second, msg) @@ -125,6 +130,7 @@ def testsuite(): if HAS_NUMPY and HAS_UNCERTAINTIES: try: import yaml, serialize + add_docs(suite) except ImportError: pass @@ -137,7 +143,7 @@ def main(): try: unittest.main() except Exception as e: - print('Error: %s' % e) + print("Error: %s" % e) def run(): @@ -149,15 +155,14 @@ def run(): return test_runner.run(testsuite()) - import math _GLOBS = { - 'wrapping.rst': { - 'pendulum_period': lambda length: 2*math.pi*math.sqrt(length/9.806650), - 'pendulum_period2': lambda length, swing_amplitude: 1., - 'pendulum_period_maxspeed': lambda length, swing_amplitude: (1., 2.), - 'pendulum_period_error': lambda length: (1., False), + "wrapping.rst": { + "pendulum_period": lambda length: 2 * math.pi * math.sqrt(length / 9.806650), + "pendulum_period2": lambda length, swing_amplitude: 1.0, + "pendulum_period_maxspeed": lambda length, swing_amplitude: (1.0, 2.0), + "pendulum_period_error": lambda length: (1.0, False), } } @@ -167,16 +172,20 @@ def add_docs(suite): :type suite: unittest.TestSuite """ - docpath = os.path.join(os.path.dirname(__file__), '..', '..', 'docs') + docpath = os.path.join(os.path.dirname(__file__), "..", "..", "docs") docpath = os.path.abspath(docpath) if os.path.exists(docpath): checker = PintOutputChecker() - for name in (name for name in os.listdir(docpath) if name.endswith('.rst')): + for name in (name for name in os.listdir(docpath) if name.endswith(".rst")): file = os.path.join(docpath, name) - suite.addTest(doctest.DocFileSuite(file, - module_relative=False, - checker=checker, - globs=_GLOBS.get(name, None))) + suite.addTest( + doctest.DocFileSuite( + file, + module_relative=False, + checker=checker, + globs=_GLOBS.get(name, None), + ) + ) def test_docs(): @@ -184,5 +193,3 @@ def test_docs(): add_docs(suite) runner = unittest.TextTestRunner() return runner.run(suite) - - diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index cbea4c0a9..a6cb78ccb 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -5,71 +5,99 @@ import re import unittest -from ..compat import HAS_NUMPY, HAS_BABEL, HAS_UNCERTAINTIES, HAS_NUMPY_ARRAY_FUNCTION, NUMPY_VER +from ..compat import ( + HAS_NUMPY, + HAS_BABEL, + HAS_UNCERTAINTIES, + HAS_NUMPY_ARRAY_FUNCTION, + NUMPY_VER, +) def requires_array_function_protocol(): if not HAS_NUMPY: - return unittest.skip('Requires NumPy') - return unittest.skipUnless(HAS_NUMPY_ARRAY_FUNCTION, 'Requires __array_function__ protocol to be enabled') + return unittest.skip("Requires NumPy") + return unittest.skipUnless( + HAS_NUMPY_ARRAY_FUNCTION, "Requires __array_function__ protocol to be enabled" + ) def requires_not_array_function_protocol(): if not HAS_NUMPY: - return unittest.skip('Requires NumPy') - return unittest.skipIf(HAS_NUMPY_ARRAY_FUNCTION, 'Requires __array_function__ protocol to be unavailable or disabled') + return unittest.skip("Requires NumPy") + return unittest.skipIf( + HAS_NUMPY_ARRAY_FUNCTION, + "Requires __array_function__ protocol to be unavailable or disabled", + ) def requires_numpy18(): if not HAS_NUMPY: - return unittest.skip('Requires NumPy') - return unittest.skipUnless(StrictVersion(NUMPY_VER) >= StrictVersion('1.8'), 'Requires NumPy >= 1.8') + return unittest.skip("Requires NumPy") + return unittest.skipUnless( + StrictVersion(NUMPY_VER) >= StrictVersion("1.8"), "Requires NumPy >= 1.8" + ) def requires_numpy_previous_than(version): if not HAS_NUMPY: - return unittest.skip('Requires NumPy') - return unittest.skipUnless(StrictVersion(NUMPY_VER) < StrictVersion(version), 'Requires NumPy < %s' % version) + return unittest.skip("Requires NumPy") + return unittest.skipUnless( + StrictVersion(NUMPY_VER) < StrictVersion(version), + "Requires NumPy < %s" % version, + ) def requires_numpy_at_least(version): if not HAS_NUMPY: - return unittest.skip('Requires NumPy') - return unittest.skipUnless(StrictVersion(NUMPY_VER) >= StrictVersion(version), 'Requires NumPy >= %s' % version) + return unittest.skip("Requires NumPy") + return unittest.skipUnless( + StrictVersion(NUMPY_VER) >= StrictVersion(version), + "Requires NumPy >= %s" % version, + ) def requires_numpy(): - return unittest.skipUnless(HAS_NUMPY, 'Requires NumPy') + return unittest.skipUnless(HAS_NUMPY, "Requires NumPy") def requires_not_numpy(): - return unittest.skipIf(HAS_NUMPY, 'Requires NumPy is not installed.') + return unittest.skipIf(HAS_NUMPY, "Requires NumPy is not installed.") def requires_babel(): - return unittest.skipUnless(HAS_BABEL, 'Requires Babel with units support') + return unittest.skipUnless(HAS_BABEL, "Requires Babel with units support") def requires_uncertainties(): - return unittest.skipUnless(HAS_UNCERTAINTIES, 'Requires Uncertainties') + return unittest.skipUnless(HAS_UNCERTAINTIES, "Requires Uncertainties") def requires_not_uncertainties(): - return unittest.skipIf(HAS_UNCERTAINTIES, 'Requires Uncertainties is not installed.') + return unittest.skipIf( + HAS_UNCERTAINTIES, "Requires Uncertainties is not installed." + ) -_number_re = r'([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)' -_q_re = re.compile(r'%s)' % _number_re + - r'\s*,\s*' + r"'(?P.*)'" + r'\s*' + r'\)>') +_number_re = r"([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)" +_q_re = re.compile( + r"%s)" % _number_re + + r"\s*,\s*" + + r"'(?P.*)'" + + r"\s*" + + r"\)>" +) -_sq_re = re.compile(r'\s*' + r'(?P%s)' % _number_re + - r'\s' + r"(?P.*)") +_sq_re = re.compile( + r"\s*" + r"(?P%s)" % _number_re + r"\s" + r"(?P.*)" +) -_unit_re = re.compile(r'') +_unit_re = re.compile(r"") class PintOutputChecker(doctest.OutputChecker): - def check_output(self, want, got, optionflags): check = super().check_output(want, got, optionflags) if check: @@ -83,16 +111,16 @@ def check_output(self, want, got, optionflags): for regex in (_q_re, _sq_re): try: - parsed_got = regex.match(got.replace(r'\\', '')).groupdict() - parsed_want = regex.match(want.replace(r'\\', '')).groupdict() + parsed_got = regex.match(got.replace(r"\\", "")).groupdict() + parsed_want = regex.match(want.replace(r"\\", "")).groupdict() - v1 = float(parsed_got['magnitude']) - v2 = float(parsed_want['magnitude']) + v1 = float(parsed_got["magnitude"]) + v2 = float(parsed_want["magnitude"]) if abs(v1 - v2) > abs(v1) / 1000: return False - if parsed_got['unit'] != parsed_want['unit']: + if parsed_got["unit"] != parsed_want["unit"]: return False return True @@ -100,11 +128,11 @@ def check_output(self, want, got, optionflags): pass cnt = 0 - for regex in (_unit_re, ): + for regex in (_unit_re,): try: - parsed_got, tmp = regex.subn('\1', got) + parsed_got, tmp = regex.subn("\1", got) cnt += tmp - parsed_want, temp = regex.subn('\1', want) + parsed_want, temp = regex.subn("\1", want) cnt += tmp if parsed_got == parsed_want: @@ -118,4 +146,3 @@ def check_output(self, want, got, optionflags): return self.check_output(parsed_want, parsed_got, optionflags) return False - diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 0ddf29755..239610501 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -29,13 +29,18 @@ import unittest -def augment_method_docstring(method, new_class_dict, classname, - param_names, param_values, new_method): - param_assignments_str = '; '.join( - ['%s = %s' % (k, v) for (k, v) in zip(param_names, param_values)]) +def augment_method_docstring( + method, new_class_dict, classname, param_names, param_values, new_method +): + param_assignments_str = "; ".join( + ["%s = %s" % (k, v) for (k, v) in zip(param_names, param_values)] + ) extra_doc = "%s (%s.%s) [with %s] " % ( - method.__name__, new_class_dict.get('__module__', ''), - classname, param_assignments_str) + method.__name__, + new_class_dict.get("__module__", ""), + classname, + param_assignments_str, + ) try: new_method.__doc__ = extra_doc + new_method.__doc__ @@ -50,7 +55,7 @@ def __new__(meta, classname, bases, class_dict): new_class_dict = {} for attr_name, attr_value in list(class_dict.items()): - if isinstance(attr_value, Callable) and hasattr(attr_value, 'param_names'): + if isinstance(attr_value, Callable) and hasattr(attr_value, "param_names"): # print("Processing attr_name = %r; attr_value = %r" % ( # attr_name, attr_value)) @@ -60,8 +65,13 @@ def __new__(meta, classname, bases, class_dict): func_name_format = attr_value.func_name_format meta.process_method( - classname, method, param_names, data, new_class_dict, - func_name_format) + classname, + method, + param_names, + data, + new_class_dict, + func_name_format, + ) else: new_class_dict[attr_name] = attr_value @@ -69,23 +79,22 @@ def __new__(meta, classname, bases, class_dict): @classmethod def process_method( - cls, classname, method, param_names, data, new_class_dict, - func_name_format): + cls, classname, method, param_names, data, new_class_dict, func_name_format + ): method_counter = cls.method_counter for param_values in data: new_method = cls.new_method(method, param_values) - method_counter[method.__name__] = \ - method_counter.get(method.__name__, 0) + 1 + method_counter[method.__name__] = method_counter.get(method.__name__, 0) + 1 case_data = dict(list(zip(param_names, param_values))) - case_data['func_name'] = method.__name__ - case_data['case_num'] = method_counter[method.__name__] + case_data["func_name"] = method.__name__ + case_data["case_num"] = method_counter[method.__name__] new_method.__name__ = func_name_format.format(**case_data) augment_method_docstring( - method, new_class_dict, classname, - param_names, param_values, new_method) + method, new_class_dict, classname, param_names, param_values, new_method + ) new_class_dict[new_method.__name__] = new_method @classmethod @@ -99,8 +108,9 @@ def new_method(self): class ParameterizedTestMixin(metaclass=ParameterizedTestCaseMetaClass): @classmethod - def parameterize(cls, param_names, data, - func_name_format='{func_name}_{case_num:05d}'): + def parameterize( + cls, param_names, data, func_name_format="{func_name}_{case_num:05d}" + ): """Decorator for parameterizing a test method - example: @ParameterizedTestCase.parameterize( diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 6978ffc55..2210fe706 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -6,34 +6,25 @@ class TestBabel(BaseTestCase): - @helpers.requires_babel() def test_babel(self): ureg = UnitRegistry() dirname = os.path.dirname(__file__) - ureg.load_definitions(os.path.join(dirname, '../xtranslated.txt')) + ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) distance = 24.0 * ureg.meter self.assertEqual( - distance.format_babel(locale='fr_FR', length='long'), - "24.0 mètres" + distance.format_babel(locale="fr_FR", length="long"), "24.0 mètres" ) time = 8.0 * ureg.second self.assertEqual( - time.format_babel(locale='fr_FR', length='long'), - "8.0 secondes" - ) - self.assertEqual( - time.format_babel(locale='ro', length='short'), - "8.0 s" + time.format_babel(locale="fr_FR", length="long"), "8.0 secondes" ) + self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") acceleration = distance / time ** 2 self.assertEqual( - acceleration.format_babel(locale='fr_FR', length='long'), - "0.375 mètre par seconde²" - ) - mks = ureg.get_system('mks') - self.assertEqual( - mks.format_babel(locale='fr_FR'), - "métrique" + acceleration.format_babel(locale="fr_FR", length="long"), + "0.375 mètre par seconde²", ) + mks = ureg.get_system("mks") + self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 336f4dea3..1bdf3ed68 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -10,15 +10,15 @@ def add_ctxs(ureg): - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1}) - d = Context('lc') + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1}) + d = Context("lc") d.add_transformation(a, b, lambda ureg, x: ureg.speed_of_light / x) d.add_transformation(b, a, lambda ureg, x: ureg.speed_of_light / x) ureg.add_context(d) - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[current]': 1}) - d = Context('ab') + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1}) + d = Context("ab") d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x) d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x) @@ -26,15 +26,15 @@ def add_ctxs(ureg): def add_arg_ctxs(ureg): - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1}) - d = Context('lc') + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1}) + d = Context("lc") d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n) d.add_transformation(b, a, lambda ureg, x, n: ureg.speed_of_light / x / n) ureg.add_context(d) - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[current]': 1}) - d = Context('ab') + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1}) + d = Context("ab") d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x) d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x) @@ -42,8 +42,8 @@ def add_arg_ctxs(ureg): def add_argdef_ctxs(ureg): - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1}) - d = Context('lc', defaults=dict(n=1)) + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1}) + d = Context("lc", defaults=dict(n=1)) assert d.defaults == dict(n=1) d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n) @@ -51,8 +51,8 @@ def add_argdef_ctxs(ureg): ureg.add_context(d) - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[current]': 1}) - d = Context('ab') + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1}) + d = Context("ab") d.add_transformation(a, b, lambda ureg, x: ureg.ampere * ureg.meter / x) d.add_transformation(b, a, lambda ureg, x: ureg.ampere * ureg.meter / x) @@ -60,8 +60,8 @@ def add_argdef_ctxs(ureg): def add_sharedargdef_ctxs(ureg): - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1}) - d = Context('lc', defaults=dict(n=1)) + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1}) + d = Context("lc", defaults=dict(n=1)) assert d.defaults == dict(n=1) d.add_transformation(a, b, lambda ureg, x, n: ureg.speed_of_light / x / n) @@ -69,8 +69,8 @@ def add_sharedargdef_ctxs(ureg): ureg.add_context(d) - a, b = UnitsContainer({'[length]': 1}), UnitsContainer({'[current]': 1}) - d = Context('ab', defaults=dict(n=0)) + a, b = UnitsContainer({"[length]": 1}), UnitsContainer({"[current]": 1}) + d = Context("ab", defaults=dict(n=0)) d.add_transformation(a, b, lambda ureg, x, n: ureg.ampere * ureg.meter * n / x) d.add_transformation(b, a, lambda ureg, x, n: ureg.ampere * ureg.meter * n / x) @@ -78,18 +78,17 @@ def add_sharedargdef_ctxs(ureg): class TestContexts(QuantityTestCase): - def test_known_context(self): ureg = UnitRegistry() add_ctxs(ureg) - with ureg.context('lc'): + with ureg.context("lc"): self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) self.assertFalse(ureg._active_ctx) self.assertFalse(ureg._active_ctx.graph) - with ureg.context('lc', n=1): + with ureg.context("lc", n=1): self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) @@ -99,7 +98,7 @@ def test_known_context(self): def test_known_context_enable(self): ureg = UnitRegistry() add_ctxs(ureg) - ureg.enable_contexts('lc') + ureg.enable_contexts("lc") self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) ureg.disable_contexts(1) @@ -107,7 +106,7 @@ def test_known_context_enable(self): self.assertFalse(ureg._active_ctx) self.assertFalse(ureg._active_ctx.graph) - ureg.enable_contexts('lc', n=1) + ureg.enable_contexts("lc", n=1) self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) ureg.disable_contexts(1) @@ -118,9 +117,9 @@ def test_known_context_enable(self): def test_graph(self): ureg = UnitRegistry() add_ctxs(ureg) - l = UnitsContainer({'[length]': 1.}) - t = UnitsContainer({'[time]': -1.}) - c = UnitsContainer({'[current]': 1.}) + l = UnitsContainer({"[length]": 1.0}) + t = UnitsContainer({"[time]": -1.0}) + c = UnitsContainer({"[current]": 1.0}) g_sp = defaultdict(set) g_sp.update({l: {t}, t: {l}}) @@ -131,35 +130,35 @@ def test_graph(self): g = defaultdict(set) g.update({l: {t, c}, t: {l}, c: {l}}) - with ureg.context('lc'): + with ureg.context("lc"): self.assertEqual(ureg._active_ctx.graph, g_sp) - with ureg.context('lc', n=1): + with ureg.context("lc", n=1): self.assertEqual(ureg._active_ctx.graph, g_sp) - with ureg.context('ab'): + with ureg.context("ab"): self.assertEqual(ureg._active_ctx.graph, g_ab) - with ureg.context('lc'): - with ureg.context('ab'): + with ureg.context("lc"): + with ureg.context("ab"): self.assertEqual(ureg._active_ctx.graph, g) - with ureg.context('ab'): - with ureg.context('lc'): + with ureg.context("ab"): + with ureg.context("lc"): self.assertEqual(ureg._active_ctx.graph, g) - with ureg.context('lc', 'ab'): + with ureg.context("lc", "ab"): self.assertEqual(ureg._active_ctx.graph, g) - with ureg.context('ab', 'lc'): + with ureg.context("ab", "lc"): self.assertEqual(ureg._active_ctx.graph, g) def test_graph_enable(self): ureg = UnitRegistry() add_ctxs(ureg) - l = UnitsContainer({'[length]': 1.}) - t = UnitsContainer({'[time]': -1.}) - c = UnitsContainer({'[current]': 1.}) + l = UnitsContainer({"[length]": 1.0}) + t = UnitsContainer({"[time]": -1.0}) + c = UnitsContainer({"[current]": 1.0}) g_sp = defaultdict(set) g_sp.update({l: {t}, t: {l}}) @@ -170,33 +169,33 @@ def test_graph_enable(self): g = defaultdict(set) g.update({l: {t, c}, t: {l}, c: {l}}) - ureg.enable_contexts('lc') + ureg.enable_contexts("lc") self.assertEqual(ureg._active_ctx.graph, g_sp) ureg.disable_contexts(1) - ureg.enable_contexts('lc', n=1) + ureg.enable_contexts("lc", n=1) self.assertEqual(ureg._active_ctx.graph, g_sp) ureg.disable_contexts(1) - ureg.enable_contexts('ab') + ureg.enable_contexts("ab") self.assertEqual(ureg._active_ctx.graph, g_ab) ureg.disable_contexts(1) - ureg.enable_contexts('lc') - ureg.enable_contexts('ab') + ureg.enable_contexts("lc") + ureg.enable_contexts("ab") self.assertEqual(ureg._active_ctx.graph, g) ureg.disable_contexts(2) - ureg.enable_contexts('ab') - ureg.enable_contexts('lc') + ureg.enable_contexts("ab") + ureg.enable_contexts("lc") self.assertEqual(ureg._active_ctx.graph, g) ureg.disable_contexts(2) - ureg.enable_contexts('lc', 'ab') + ureg.enable_contexts("lc", "ab") self.assertEqual(ureg._active_ctx.graph, g) ureg.disable_contexts(2) - ureg.enable_contexts('ab', 'lc') + ureg.enable_contexts("ab", "lc") self.assertEqual(ureg._active_ctx.graph, g) ureg.disable_contexts(2) @@ -204,13 +203,13 @@ def test_known_nested_context(self): ureg = UnitRegistry() add_ctxs(ureg) - with ureg.context('lc'): + with ureg.context("lc"): x = dict(ureg._active_ctx) y = dict(ureg._active_ctx.graph) self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) - with ureg.context('ab'): + with ureg.context("ab"): self.assertTrue(ureg._active_ctx) self.assertTrue(ureg._active_ctx.graph) self.assertNotEqual(x, ureg._active_ctx) @@ -226,7 +225,7 @@ def test_unknown_context(self): ureg = UnitRegistry() add_ctxs(ureg) try: - with ureg.context('la'): + with ureg.context("la"): pass except KeyError as e: value = True @@ -240,11 +239,11 @@ def test_unknown_nested_context(self): ureg = UnitRegistry() add_ctxs(ureg) - with ureg.context('lc'): + with ureg.context("lc"): x = dict(ureg._active_ctx) y = dict(ureg._active_ctx.graph) try: - with ureg.context('la'): + with ureg.context("la"): pass except KeyError as e: value = True @@ -265,16 +264,16 @@ def test_one_context(self): add_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) self.assertEqual(ureg.get_compatible_units(q), meter_units | hertz_units) - self.assertRaises(DimensionalityError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, "Hz") self.assertEqual(ureg.get_compatible_units(q), meter_units) def test_multiple_context(self): @@ -283,17 +282,19 @@ def test_multiple_context(self): add_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) ampere_units = ureg.get_compatible_units(ureg.ampere) - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc', 'ab'): - self.assertEqual(q.to('Hz'), s) - self.assertEqual(ureg.get_compatible_units(q), meter_units | hertz_units | ampere_units) - self.assertRaises(DimensionalityError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc", "ab"): + self.assertEqual(q.to("Hz"), s) + self.assertEqual( + ureg.get_compatible_units(q), meter_units | hertz_units | ampere_units + ) + self.assertRaises(DimensionalityError, q.to, "Hz") self.assertEqual(ureg.get_compatible_units(q), meter_units) def test_nested_context(self): @@ -302,20 +303,20 @@ def test_nested_context(self): add_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) - with ureg.context('ab'): - self.assertEqual(q.to('Hz'), s) - self.assertEqual(q.to('Hz'), s) + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) + with ureg.context("ab"): + self.assertEqual(q.to("Hz"), s) + self.assertEqual(q.to("Hz"), s) - with ureg.context('ab'): - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) - self.assertRaises(DimensionalityError, q.to, 'Hz') + with ureg.context("ab"): + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) + self.assertRaises(DimensionalityError, q.to, "Hz") def test_context_with_arg(self): @@ -324,23 +325,23 @@ def test_context_with_arg(self): add_arg_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc', n=1): - self.assertEqual(q.to('Hz'), s) - with ureg.context('ab'): - self.assertEqual(q.to('Hz'), s) - self.assertEqual(q.to('Hz'), s) + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc", n=1): + self.assertEqual(q.to("Hz"), s) + with ureg.context("ab"): + self.assertEqual(q.to("Hz"), s) + self.assertEqual(q.to("Hz"), s) - with ureg.context('ab'): - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc', n=1): - self.assertEqual(q.to('Hz'), s) - self.assertRaises(DimensionalityError, q.to, 'Hz') + with ureg.context("ab"): + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc", n=1): + self.assertEqual(q.to("Hz"), s) + self.assertRaises(DimensionalityError, q.to, "Hz") - with ureg.context('lc'): - self.assertRaises(TypeError, q.to, 'Hz') + with ureg.context("lc"): + self.assertRaises(TypeError, q.to, "Hz") def test_enable_context_with_arg(self): @@ -349,27 +350,27 @@ def test_enable_context_with_arg(self): add_arg_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') - - self.assertRaises(DimensionalityError, q.to, 'Hz') - ureg.enable_contexts('lc', n=1) - self.assertEqual(q.to('Hz'), s) - ureg.enable_contexts('ab') - self.assertEqual(q.to('Hz'), s) - self.assertEqual(q.to('Hz'), s) + s = (ureg.speed_of_light / q).to("Hz") + + self.assertRaises(DimensionalityError, q.to, "Hz") + ureg.enable_contexts("lc", n=1) + self.assertEqual(q.to("Hz"), s) + ureg.enable_contexts("ab") + self.assertEqual(q.to("Hz"), s) + self.assertEqual(q.to("Hz"), s) ureg.disable_contexts(1) ureg.disable_contexts(1) - ureg.enable_contexts('ab') - self.assertRaises(DimensionalityError, q.to, 'Hz') - ureg.enable_contexts('lc', n=1) - self.assertEqual(q.to('Hz'), s) + ureg.enable_contexts("ab") + self.assertRaises(DimensionalityError, q.to, "Hz") + ureg.enable_contexts("lc", n=1) + self.assertEqual(q.to("Hz"), s) ureg.disable_contexts(1) - self.assertRaises(DimensionalityError, q.to, 'Hz') + self.assertRaises(DimensionalityError, q.to, "Hz") ureg.disable_contexts(1) - ureg.enable_contexts('lc') - self.assertRaises(TypeError, q.to, 'Hz') + ureg.enable_contexts("lc") + self.assertRaises(TypeError, q.to, "Hz") ureg.disable_contexts(1) def test_context_with_arg_def(self): @@ -379,33 +380,33 @@ def test_context_with_arg_def(self): add_argdef_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') - - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) - with ureg.context('ab'): - self.assertEqual(q.to('Hz'), s) - self.assertEqual(q.to('Hz'), s) - - with ureg.context('ab'): - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) - self.assertRaises(DimensionalityError, q.to, 'Hz') - - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc', n=2): - self.assertEqual(q.to('Hz'), s / 2) - with ureg.context('ab'): - self.assertEqual(q.to('Hz'), s / 2) - self.assertEqual(q.to('Hz'), s / 2) - - with ureg.context('ab'): - self.assertRaises(DimensionalityError, q.to, 'Hz') - with ureg.context('lc', n=2): - self.assertEqual(q.to('Hz'), s / 2) - self.assertRaises(DimensionalityError, q.to, 'Hz') + s = (ureg.speed_of_light / q).to("Hz") + + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) + with ureg.context("ab"): + self.assertEqual(q.to("Hz"), s) + self.assertEqual(q.to("Hz"), s) + + with ureg.context("ab"): + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) + self.assertRaises(DimensionalityError, q.to, "Hz") + + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc", n=2): + self.assertEqual(q.to("Hz"), s / 2) + with ureg.context("ab"): + self.assertEqual(q.to("Hz"), s / 2) + self.assertEqual(q.to("Hz"), s / 2) + + with ureg.context("ab"): + self.assertRaises(DimensionalityError, q.to, "Hz") + with ureg.context("lc", n=2): + self.assertEqual(q.to("Hz"), s / 2) + self.assertRaises(DimensionalityError, q.to, "Hz") def test_context_with_sharedarg_def(self): @@ -414,46 +415,43 @@ def test_context_with_sharedarg_def(self): add_sharedargdef_ctxs(ureg) q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") u = (1 / 500) * ureg.ampere - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s) - with ureg.context('ab'): - self.assertEqual(q.to('ampere'), u) + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s) + with ureg.context("ab"): + self.assertEqual(q.to("ampere"), u) - with ureg.context('ab'): - self.assertEqual(q.to('ampere'), 0 * u) - with ureg.context('lc'): - self.assertRaises(ZeroDivisionError, ureg.Quantity.to, q, 'Hz') + with ureg.context("ab"): + self.assertEqual(q.to("ampere"), 0 * u) + with ureg.context("lc"): + self.assertRaises(ZeroDivisionError, ureg.Quantity.to, q, "Hz") - with ureg.context('lc', n=2): - self.assertEqual(q.to('Hz'), s / 2) - with ureg.context('ab'): - self.assertEqual(q.to('ampere'), 2 * u) + with ureg.context("lc", n=2): + self.assertEqual(q.to("Hz"), s / 2) + with ureg.context("ab"): + self.assertEqual(q.to("ampere"), 2 * u) - with ureg.context('ab', n=3): - self.assertEqual(q.to('ampere'), 3 * u) - with ureg.context('lc'): - self.assertEqual(q.to('Hz'), s / 3) + with ureg.context("ab", n=3): + self.assertEqual(q.to("ampere"), 3 * u) + with ureg.context("lc"): + self.assertEqual(q.to("Hz"), s / 3) - with ureg.context('lc', n=2): - self.assertEqual(q.to('Hz'), s / 2) - with ureg.context('ab', n=4): - self.assertEqual(q.to('ampere'), 4 * u) + with ureg.context("lc", n=2): + self.assertEqual(q.to("Hz"), s / 2) + with ureg.context("ab", n=4): + self.assertEqual(q.to("ampere"), 4 * u) - with ureg.context('ab', n=3): - self.assertEqual(q.to('ampere'), 3 * u) - with ureg.context('lc', n=6): - self.assertEqual(q.to('Hz'), s / 6) + with ureg.context("ab", n=3): + self.assertEqual(q.to("ampere"), 3 * u) + with ureg.context("lc", n=6): + self.assertEqual(q.to("Hz"), s / 6) def test_anonymous_context(self): ureg = UnitRegistry() c = Context() - c.add_transformation( - '[length]', '[time]', - lambda ureg, x: x / ureg("5 cm/s") - ) + c.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("5 cm/s")) self.assertRaises(ValueError, ureg.add_context, c) x = ureg("10 cm") @@ -470,14 +468,8 @@ def test_anonymous_context(self): # Multiple anonymous contexts c2 = Context() - c2.add_transformation( - '[length]', '[time]', - lambda ureg, x: x / ureg("10 cm/s") - ) - c2.add_transformation( - '[mass]', '[time]', - lambda ureg, x: x / ureg("10 kg/s") - ) + c2.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("10 cm/s")) + c2.add_transformation("[mass]", "[time]", lambda ureg, x: x / ureg("10 kg/s")) with ureg.context(c2, c): self.assertQuantityEqual(x.to("s"), expect) # Transformations only in c2 are still working even if c takes priority @@ -488,7 +480,7 @@ def test_anonymous_context(self): def _test_ctx(self, ctx): ureg = UnitRegistry() q = 500 * ureg.meter - s = (ureg.speed_of_light / q).to('Hz') + s = (ureg.speed_of_light / q).to("Hz") nctx = len(ureg._contexts) @@ -499,63 +491,75 @@ def _test_ctx(self, ctx): self.assertEqual(len(ureg._contexts), nctx + 1 + len(ctx.aliases)) with ureg.context(ctx.name): - self.assertEqual(q.to('Hz'), s) - self.assertEqual(s.to('meter'), q) + self.assertEqual(q.to("Hz"), s) + self.assertEqual(s.to("meter"), q) ureg.remove_context(ctx.name) self.assertNotIn(ctx.name, ureg._contexts) self.assertEqual(len(ureg._contexts), nctx) def test_parse_invalid(self): - s = ['@context longcontextname', - '[length] = 1 / [time]: c / value', - '1 / [time] = [length]: c / value'] + s = [ + "@context longcontextname", + "[length] = 1 / [time]: c / value", + "1 / [time] = [length]: c / value", + ] self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) def test_parse_simple(self): - a = Context.__keytransform__(UnitsContainer({'[time]': -1}), UnitsContainer({'[length]': 1})) - b = Context.__keytransform__(UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1})) + a = Context.__keytransform__( + UnitsContainer({"[time]": -1}), UnitsContainer({"[length]": 1}) + ) + b = Context.__keytransform__( + UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1}) + ) - s = ['@context longcontextname', - '[length] -> 1 / [time]: c / value', - '1 / [time] -> [length]: c / value'] + s = [ + "@context longcontextname", + "[length] -> 1 / [time]: c / value", + "1 / [time] -> [length]: c / value", + ] c = Context.from_lines(s) - self.assertEqual(c.name, 'longcontextname') + self.assertEqual(c.name, "longcontextname") self.assertEqual(c.aliases, ()) self.assertEqual(c.defaults, {}) self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) - s = ['@context longcontextname = lc', - '[length] <-> 1 / [time]: c / value'] + s = ["@context longcontextname = lc", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) - self.assertEqual(c.name, 'longcontextname') - self.assertEqual(c.aliases, ('lc', )) + self.assertEqual(c.name, "longcontextname") + self.assertEqual(c.aliases, ("lc",)) self.assertEqual(c.defaults, {}) self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) - s = ['@context longcontextname = lc = lcn', - '[length] <-> 1 / [time]: c / value'] + s = [ + "@context longcontextname = lc = lcn", + "[length] <-> 1 / [time]: c / value", + ] c = Context.from_lines(s) - self.assertEqual(c.name, 'longcontextname') - self.assertEqual(c.aliases, ('lc', 'lcn', )) + self.assertEqual(c.name, "longcontextname") + self.assertEqual(c.aliases, ("lc", "lcn")) self.assertEqual(c.defaults, {}) self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) def test_parse_auto_inverse(self): - a = Context.__keytransform__(UnitsContainer({'[time]': -1.}), UnitsContainer({'[length]': 1.})) - b = Context.__keytransform__(UnitsContainer({'[length]': 1.}), UnitsContainer({'[time]': -1.})) + a = Context.__keytransform__( + UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0}) + ) + b = Context.__keytransform__( + UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0}) + ) - s = ['@context longcontextname', - '[length] <-> 1 / [time]: c / value'] + s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) self.assertEqual(c.defaults, {}) @@ -563,38 +567,45 @@ def test_parse_auto_inverse(self): self._test_ctx(c) def test_parse_define(self): - a = Context.__keytransform__(UnitsContainer({'[time]': -1}), UnitsContainer({'[length]': 1.})) - b = Context.__keytransform__(UnitsContainer({'[length]': 1}), UnitsContainer({'[time]': -1.})) + a = Context.__keytransform__( + UnitsContainer({"[time]": -1}), UnitsContainer({"[length]": 1.0}) + ) + b = Context.__keytransform__( + UnitsContainer({"[length]": 1}), UnitsContainer({"[time]": -1.0}) + ) - s = ['@context longcontextname', - '[length] <-> 1 / [time]: c / value'] + s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) self.assertEqual(c.defaults, {}) self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) def test_parse_parameterized(self): - a = Context.__keytransform__(UnitsContainer({'[time]': -1.}), UnitsContainer({'[length]': 1.})) - b = Context.__keytransform__(UnitsContainer({'[length]': 1.}), UnitsContainer({'[time]': -1.})) + a = Context.__keytransform__( + UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0}) + ) + b = Context.__keytransform__( + UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0}) + ) - s = ['@context(n=1) longcontextname', - '[length] <-> 1 / [time]: n * c / value'] + s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: n * c / value"] c = Context.from_lines(s) - self.assertEqual(c.defaults, {'n': 1}) + self.assertEqual(c.defaults, {"n": 1}) self.assertEqual(c.funcs.keys(), {a, b}) self._test_ctx(c) - s = ['@context(n=1, bla=2) longcontextname', - '[length] <-> 1 / [time]: n * c / value / bla'] + s = [ + "@context(n=1, bla=2) longcontextname", + "[length] <-> 1 / [time]: n * c / value / bla", + ] c = Context.from_lines(s) - self.assertEqual(c.defaults, {'n': 1, 'bla': 2}) + self.assertEqual(c.defaults, {"n": 1, "bla": 2}) self.assertEqual(c.funcs.keys(), {a, b}) # If the variable is not present in the definition, then raise an error - s = ['@context(n=1) longcontextname', - '[length] <-> 1 / [time]: c / value'] + s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: c / value"] self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) def test_warnings(self): @@ -604,13 +615,13 @@ def test_warnings(self): with self.capture_log() as buffer: add_ctxs(ureg) - d = Context('ab') + d = Context("ab") ureg.add_context(d) self.assertEqual(len(buffer), 1) self.assertIn("ab", str(buffer[-1])) - d = Context('ab1', aliases=('ab',)) + d = Context("ab1", aliases=("ab",)) ureg.add_context(d) self.assertEqual(len(buffer), 2) @@ -623,90 +634,96 @@ class TestDefinedContexts(QuantityTestCase): def test_defined(self): ureg = self.ureg - with ureg.context('sp'): + with ureg.context("sp"): pass - a = Context.__keytransform__(UnitsContainer({'[time]': -1.}), UnitsContainer({'[length]': 1.})) - b = Context.__keytransform__(UnitsContainer({'[length]': 1.}), UnitsContainer({'[time]': -1.})) - self.assertIn(a, ureg._contexts['sp'].funcs) - self.assertIn(b, ureg._contexts['sp'].funcs) - with ureg.context('sp'): + a = Context.__keytransform__( + UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0}) + ) + b = Context.__keytransform__( + UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0}) + ) + self.assertIn(a, ureg._contexts["sp"].funcs) + self.assertIn(b, ureg._contexts["sp"].funcs) + with ureg.context("sp"): self.assertIn(a, ureg._active_ctx) self.assertIn(b, ureg._active_ctx) def test_spectroscopy(self): ureg = self.ureg - eq = (532. * ureg.nm, 563.5 * ureg.terahertz, 2.33053 * ureg.eV) - with ureg.context('sp'): + eq = (532.0 * ureg.nm, 563.5 * ureg.terahertz, 2.33053 * ureg.eV) + with ureg.context("sp"): from pint.util import find_shortest_path + for a, b in itertools.product(eq, eq): for x in range(2): if x == 1: a = a.to_base_units() b = b.to_base_units() - da, db = Context.__keytransform__(a.dimensionality, - b.dimensionality) + da, db = Context.__keytransform__( + a.dimensionality, b.dimensionality + ) p = find_shortest_path(ureg._active_ctx.graph, da, db) self.assertTrue(p) - msg = '{} <-> {}'.format(a, b) + msg = "{} <-> {}".format(a, b) # assertAlmostEqualRelError converts second to first self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) for a, b in itertools.product(eq, eq): - self.assertQuantityAlmostEqual(a.to(b.units, 'sp'), b, rtol=0.01) + self.assertQuantityAlmostEqual(a.to(b.units, "sp"), b, rtol=0.01) def test_textile(self): ureg = self.ureg qty_direct = 1.331 * ureg.tex with self.assertRaises(DimensionalityError): - qty_indirect = qty_direct.to('Nm') + qty_indirect = qty_direct.to("Nm") - with ureg.context('textile'): + with ureg.context("textile"): from pint.util import find_shortest_path - qty_indirect = qty_direct.to('Nm') + + qty_indirect = qty_direct.to("Nm") a = qty_direct.to_base_units() b = qty_indirect.to_base_units() - da, db = Context.__keytransform__(a.dimensionality, - b.dimensionality) + da, db = Context.__keytransform__(a.dimensionality, b.dimensionality) p = find_shortest_path(ureg._active_ctx.graph, da, db) self.assertTrue(p) - msg = '{} <-> {}'.format(a, b) + msg = "{} <-> {}".format(a, b) self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) def test_decorator(self): ureg = self.ureg - a = 532. * ureg.nm - with ureg.context('sp'): - b = a.to('terahertz') + a = 532.0 * ureg.nm + with ureg.context("sp"): + b = a.to("terahertz") def f(wl): - return wl.to('terahertz') + return wl.to("terahertz") self.assertRaises(DimensionalityError, f, a) - @ureg.with_context('sp') + @ureg.with_context("sp") def g(wl): - return wl.to('terahertz') + return wl.to("terahertz") self.assertEqual(b, g(a)) def test_decorator_composition(self): ureg = self.ureg - a = 532. * ureg.nm - with ureg.context('sp'): - b = a.to('terahertz') + a = 532.0 * ureg.nm + with ureg.context("sp"): + b = a.to("terahertz") - @ureg.with_context('sp') - @ureg.check('[length]') + @ureg.with_context("sp") + @ureg.check("[length]") def f(wl): - return wl.to('terahertz') + return wl.to("terahertz") - @ureg.with_context('sp') - @ureg.check('[length]') + @ureg.with_context("sp") + @ureg.check("[length]") def g(wl): - return wl.to('terahertz') + return wl.to("terahertz") self.assertEqual(b, f(a)) self.assertEqual(b, g(a)) diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 30a7eaae3..7fadc0073 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -3,12 +3,11 @@ import itertools from pint.compat import np -from pint.converters import (ScaleConverter, OffsetConverter, Converter) +from pint.converters import ScaleConverter, OffsetConverter, Converter from pint.testsuite import helpers, BaseTestCase class TestConverter(BaseTestCase): - def test_converter(self): c = Converter() self.assertTrue(c.is_multiplicative) @@ -16,22 +15,23 @@ def test_converter(self): self.assertTrue(c.from_reference(8)) def test_multiplicative_converter(self): - c = ScaleConverter(20.) + c = ScaleConverter(20.0) self.assertEqual(c.from_reference(c.to_reference(100)), 100) self.assertEqual(c.to_reference(c.from_reference(100)), 100) def test_offset_converter(self): - c = OffsetConverter(20., 2) + c = OffsetConverter(20.0, 2) self.assertEqual(c.from_reference(c.to_reference(100)), 100) self.assertEqual(c.to_reference(c.from_reference(100)), 100) @helpers.requires_numpy() def test_converter_inplace(self): - for c in (ScaleConverter(20.), OffsetConverter(20., 2)): + for c in (ScaleConverter(20.0), OffsetConverter(20.0, 2)): fun1 = lambda x, y: c.from_reference(c.to_reference(x, y), y) fun2 = lambda x, y: c.to_reference(c.from_reference(x, y), y) - for fun, (inplace, comp) in itertools.product((fun1, fun2), - ((True, self.assertIs), (False, self.assertIsNot))): + for fun, (inplace, comp) in itertools.product( + (fun1, fun2), ((True, self.assertIs), (False, self.assertIsNot)) + ): a = np.ones((1, 10)) ac = np.ones((1, 10)) r = fun(a, inplace) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 0181f5ad9..8f30a45c5 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,92 +1,98 @@ # -*- coding: utf-8 -*- -from pint.util import (UnitsContainer) -from pint.converters import (ScaleConverter, OffsetConverter) -from pint.definitions import (Definition, PrefixDefinition, UnitDefinition, - DimensionDefinition, AliasDefinition) +from pint.util import UnitsContainer +from pint.converters import ScaleConverter, OffsetConverter +from pint.definitions import ( + Definition, + PrefixDefinition, + UnitDefinition, + DimensionDefinition, + AliasDefinition, +) from pint.testsuite import BaseTestCase class TestDefinition(BaseTestCase): - def test_invalid(self): - self.assertRaises(ValueError, Definition.from_string, 'x = [time] * meter') - self.assertRaises(ValueError, Definition.from_string, '[x] = [time] * meter') + self.assertRaises(ValueError, Definition.from_string, "x = [time] * meter") + self.assertRaises(ValueError, Definition.from_string, "[x] = [time] * meter") def test_prefix_definition(self): - for definition in ('m- = 1e-3', 'm- = 10**-3', 'm- = 0.001'): + for definition in ("m- = 1e-3", "m- = 10**-3", "m- = 0.001"): x = Definition.from_string(definition) self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, 'm') + self.assertEqual(x.name, "m") self.assertEqual(x.aliases, ()) self.assertEqual(x.converter.to_reference(1000), 1) self.assertEqual(x.converter.from_reference(0.001), 1) - self.assertEqual(str(x), 'm') + self.assertEqual(str(x), "m") - x = Definition.from_string('kilo- = 1e-3 = k-') + x = Definition.from_string("kilo- = 1e-3 = k-") self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, 'kilo') + self.assertEqual(x.name, "kilo") self.assertEqual(x.aliases, ()) - self.assertEqual(x.symbol, 'k') + self.assertEqual(x.symbol, "k") self.assertEqual(x.converter.to_reference(1000), 1) - self.assertEqual(x.converter.from_reference(.001), 1) + self.assertEqual(x.converter.from_reference(0.001), 1) - x = Definition.from_string('kilo- = 1e-3 = k- = anotherk-') + x = Definition.from_string("kilo- = 1e-3 = k- = anotherk-") self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, 'kilo') - self.assertEqual(x.aliases, ('anotherk', )) - self.assertEqual(x.symbol, 'k') + self.assertEqual(x.name, "kilo") + self.assertEqual(x.aliases, ("anotherk",)) + self.assertEqual(x.symbol, "k") self.assertEqual(x.converter.to_reference(1000), 1) - self.assertEqual(x.converter.from_reference(.001), 1) + self.assertEqual(x.converter.from_reference(0.001), 1) def test_baseunit_definition(self): - x = Definition.from_string('meter = [length]') + x = Definition.from_string("meter = [length]") self.assertIsInstance(x, UnitDefinition) self.assertTrue(x.is_base) - self.assertEqual(x.reference, UnitsContainer({'[length]': 1})) + self.assertEqual(x.reference, UnitsContainer({"[length]": 1})) def test_unit_definition(self): - x = Definition.from_string('coulomb = ampere * second') + x = Definition.from_string("coulomb = ampere * second") self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, ScaleConverter) self.assertEqual(x.converter.scale, 1) self.assertEqual(x.reference, UnitsContainer(ampere=1, second=1)) - x = Definition.from_string('faraday = 96485.3399 * coulomb') + x = Definition.from_string("faraday = 96485.3399 * coulomb") self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, ScaleConverter) - self.assertEqual(x.converter.scale, 96485.3399) + self.assertEqual(x.converter.scale, 96485.3399) self.assertEqual(x.reference, UnitsContainer(coulomb=1)) - x = Definition.from_string('degF = 9 / 5 * kelvin; offset: 255.372222') + x = Definition.from_string("degF = 9 / 5 * kelvin; offset: 255.372222") self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, OffsetConverter) - self.assertEqual(x.converter.scale, 9/5) + self.assertEqual(x.converter.scale, 9 / 5) self.assertEqual(x.converter.offset, 255.372222) self.assertEqual(x.reference, UnitsContainer(kelvin=1)) - x = Definition.from_string('turn = 6.28 * radian = _ = revolution = = cycle = _') + x = Definition.from_string( + "turn = 6.28 * radian = _ = revolution = = cycle = _" + ) self.assertIsInstance(x, UnitDefinition) - self.assertEqual(x.name, 'turn') - self.assertEqual(x.aliases, ('revolution', 'cycle')) - self.assertEqual(x.symbol, 'turn') + self.assertEqual(x.name, "turn") + self.assertEqual(x.aliases, ("revolution", "cycle")) + self.assertEqual(x.symbol, "turn") self.assertFalse(x.is_base) self.assertIsInstance(x.converter, ScaleConverter) self.assertEqual(x.converter.scale, 6.28) self.assertEqual(x.reference, UnitsContainer(radian=1)) def test_dimension_definition(self): - x = DimensionDefinition('[time]', '', (), converter='') + x = DimensionDefinition("[time]", "", (), converter="") self.assertTrue(x.is_base) - self.assertEqual(x.name, '[time]') + self.assertEqual(x.name, "[time]") - x = Definition.from_string('[speed] = [length]/[time]') + x = Definition.from_string("[speed] = [length]/[time]") self.assertIsInstance(x, DimensionDefinition) - self.assertEqual(x.reference, UnitsContainer({'[length]': 1, '[time]': -1})) + self.assertEqual(x.reference, UnitsContainer({"[length]": 1, "[time]": -1})) def test_alias_definition(self): x = Definition.from_string("@alias meter = metro = metr") diff --git a/pint/testsuite/test_formatter.py b/pint/testsuite/test_formatter.py index b471a1ce0..cf5e64b44 100644 --- a/pint/testsuite/test_formatter.py +++ b/pint/testsuite/test_formatter.py @@ -5,36 +5,45 @@ class TestFormatter(QuantityTestCase): - def test_join(self): for empty in (tuple(), []): - self.assertEqual(fmt._join('s', empty), '') - self.assertEqual(fmt._join('*', '1 2 3'.split()), '1*2*3') - self.assertEqual(fmt._join('{0}*{1}', '1 2 3'.split()), '1*2*3') + self.assertEqual(fmt._join("s", empty), "") + self.assertEqual(fmt._join("*", "1 2 3".split()), "1*2*3") + self.assertEqual(fmt._join("{0}*{1}", "1 2 3".split()), "1*2*3") def test_formatter(self): - self.assertEqual(fmt.formatter(dict().items()), '') - self.assertEqual(fmt.formatter(dict(meter=1).items()), 'meter') - self.assertEqual(fmt.formatter(dict(meter=-1).items()), '1 / meter') - self.assertEqual(fmt.formatter(dict(meter=-1).items(), as_ratio=False), 'meter ** -1') - - self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items(), as_ratio=False), - 'meter ** -1 * second ** -1') - self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items()), - '1 / meter / second') - self.assertEqual(fmt.formatter(dict(meter=-1, second=-1).items(), single_denominator=True), - '1 / (meter * second)') - self.assertEqual(fmt.formatter(dict(meter=-1, second=-2).items()), - '1 / meter / second ** 2') - self.assertEqual(fmt.formatter(dict(meter=-1, second=-2).items(), single_denominator=True), - '1 / (meter * second ** 2)') + self.assertEqual(fmt.formatter(dict().items()), "") + self.assertEqual(fmt.formatter(dict(meter=1).items()), "meter") + self.assertEqual(fmt.formatter(dict(meter=-1).items()), "1 / meter") + self.assertEqual( + fmt.formatter(dict(meter=-1).items(), as_ratio=False), "meter ** -1" + ) + + self.assertEqual( + fmt.formatter(dict(meter=-1, second=-1).items(), as_ratio=False), + "meter ** -1 * second ** -1", + ) + self.assertEqual( + fmt.formatter(dict(meter=-1, second=-1).items()), "1 / meter / second" + ) + self.assertEqual( + fmt.formatter(dict(meter=-1, second=-1).items(), single_denominator=True), + "1 / (meter * second)", + ) + self.assertEqual( + fmt.formatter(dict(meter=-1, second=-2).items()), "1 / meter / second ** 2" + ) + self.assertEqual( + fmt.formatter(dict(meter=-1, second=-2).items(), single_denominator=True), + "1 / (meter * second ** 2)", + ) def test_parse_spec(self): - self.assertEqual(fmt._parse_spec(''), '') - self.assertEqual(fmt._parse_spec(''), '') - self.assertRaises(ValueError, fmt._parse_spec, 'W') - self.assertRaises(ValueError, fmt._parse_spec, 'PL') + self.assertEqual(fmt._parse_spec(""), "") + self.assertEqual(fmt._parse_spec(""), "") + self.assertRaises(ValueError, fmt._parse_spec, "W") + self.assertRaises(ValueError, fmt._parse_spec, "PL") def test_format_unit(self): - self.assertEqual(fmt.format_unit('', 'C'), 'dimensionless') - self.assertRaises(ValueError, fmt.format_unit, 'm', 'W') + self.assertEqual(fmt.format_unit("", "C"), "dimensionless") + self.assertRaises(ValueError, fmt.format_unit, "m", "W") diff --git a/pint/testsuite/test_infer_base_unit.py b/pint/testsuite/test_infer_base_unit.py index 12507b62d..dc3bd8253 100644 --- a/pint/testsuite/test_infer_base_unit.py +++ b/pint/testsuite/test_infer_base_unit.py @@ -6,23 +6,27 @@ class TestInferBaseUnit(QuantityTestCase): def test_infer_base_unit(self): from pint.util import infer_base_unit - self.assertEqual(infer_base_unit(Q(1, 'millimeter * nanometer')), Q(1, 'meter**2').units) + + self.assertEqual( + infer_base_unit(Q(1, "millimeter * nanometer")), Q(1, "meter**2").units + ) def test_units_adding_to_zero(self): - self.assertEqual(infer_base_unit(Q(1, 'm * mm / m / um * s')), Q(1, 's').units) + self.assertEqual(infer_base_unit(Q(1, "m * mm / m / um * s")), Q(1, "s").units) def test_to_compact(self): - r = Q(1000000000, 'm') * Q(1, 'mm') / Q(1, 's') / Q(1, 'ms') + r = Q(1000000000, "m") * Q(1, "mm") / Q(1, "s") / Q(1, "ms") compact_r = r.to_compact() - expected = Q(1000., 'kilometer**2 / second**2') + expected = Q(1000.0, "kilometer**2 / second**2") self.assertQuantityAlmostEqual(compact_r, expected) - r = (Q(1, 'm') * Q(1, 'mm') / Q(1, 'm') / Q(2, 'um') * Q(2, 's')).to_compact() - self.assertQuantityAlmostEqual(r, Q(1000, 's')) + r = (Q(1, "m") * Q(1, "mm") / Q(1, "m") / Q(2, "um") * Q(2, "s")).to_compact() + self.assertQuantityAlmostEqual(r, Q(1000, "s")) def test_volts(self): from pint.util import infer_base_unit - r = Q(1, 'V') * Q(1, 'mV') / Q(1, 'kV') + + r = Q(1, "V") * Q(1, "mV") / Q(1, "kV") b = infer_base_unit(r) - self.assertEqual(b, Q(1, 'V').units) - self.assertQuantityAlmostEqual(r, Q(1, 'uV')) + self.assertEqual(b, Q(1, "V").units) + self.assertQuantityAlmostEqual(r, Q(1, "uV")) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 9f5fcd763..b61abf595 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -22,25 +22,25 @@ def setup(self): @unittest.expectedFailure def test_issue25(self): - x = ParserHelper.from_string('10 %') - self.assertEqual(x, ParserHelper(10, {'%': 1})) - x = ParserHelper.from_string('10 ‰') - self.assertEqual(x, ParserHelper(10, {'‰': 1})) - ureg = UnitRegistry() - ureg.define('percent = [fraction]; offset: 0 = %') - ureg.define('permille = percent / 10 = ‰') - x = ureg.parse_expression('10 %') - self.assertEqual(x, ureg.Quantity(10, {'%': 1})) - y = ureg.parse_expression('10 ‰') - self.assertEqual(y, ureg.Quantity(10, {'‰': 1})) - self.assertEqual(x.to('‰'), ureg.Quantity(1, {'‰': 1})) + x = ParserHelper.from_string("10 %") + self.assertEqual(x, ParserHelper(10, {"%": 1})) + x = ParserHelper.from_string("10 ‰") + self.assertEqual(x, ParserHelper(10, {"‰": 1})) + ureg = UnitRegistry() + ureg.define("percent = [fraction]; offset: 0 = %") + ureg.define("permille = percent / 10 = ‰") + x = ureg.parse_expression("10 %") + self.assertEqual(x, ureg.Quantity(10, {"%": 1})) + y = ureg.parse_expression("10 ‰") + self.assertEqual(y, ureg.Quantity(10, {"‰": 1})) + self.assertEqual(x.to("‰"), ureg.Quantity(1, {"‰": 1})) def test_issue29(self): ureg = UnitRegistry() - t = 4 * ureg('mW') + t = 4 * ureg("mW") self.assertEqual(t.magnitude, 4) self.assertEqual(t._units, UnitsContainer(milliwatt=1)) - self.assertEqual(t.to('joule / second'), 4e-3 * ureg('W')) + self.assertEqual(t.to("joule / second"), 4e-3 * ureg("W")) @unittest.expectedFailure @helpers.requires_numpy() @@ -56,7 +56,7 @@ def test_issue37(self): np.testing.assert_array_equal(q.magnitude, x) self.assertEqual(q.units, ureg.meter.units) - m = np.ma.masked_array(2 * np.ones(3,3)) + m = np.ma.masked_array(2 * np.ones(3, 3)) qq = q * m self.assertIsInstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) @@ -80,7 +80,7 @@ def test_issue39(self): np.testing.assert_array_equal(q.magnitude, x) self.assertEqual(q.units, ureg.meter.units) - m = np.matrix(2 * np.ones(3,3)) + m = np.matrix(2 * np.ones(3, 3)) qq = q * m self.assertIsInstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) @@ -93,61 +93,80 @@ def test_issue39(self): @helpers.requires_numpy() def test_issue44(self): ureg = UnitRegistry() - x = 4. * ureg.dimensionless + x = 4.0 * ureg.dimensionless np.sqrt(x) - self.assertQuantityAlmostEqual(np.sqrt([4.] * ureg.dimensionless), [2.] * ureg.dimensionless) - self.assertQuantityAlmostEqual(np.sqrt(4. * ureg.dimensionless), 2. * ureg.dimensionless) + self.assertQuantityAlmostEqual( + np.sqrt([4.0] * ureg.dimensionless), [2.0] * ureg.dimensionless + ) + self.assertQuantityAlmostEqual( + np.sqrt(4.0 * ureg.dimensionless), 2.0 * ureg.dimensionless + ) def test_issue45(self): import math + ureg = UnitRegistry() - self.assertAlmostEqual(math.sqrt(4 * ureg.m/ureg.cm), math.sqrt(4 * 100)) - self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.) + self.assertAlmostEqual(math.sqrt(4 * ureg.m / ureg.cm), math.sqrt(4 * 100)) + self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.0) @helpers.requires_numpy() def test_issue45b(self): ureg = UnitRegistry() - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.m / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless)) - self.assertAlmostEqual(np.sin([np.pi/2] * ureg.cm / ureg.m ), np.sin([np.pi/2] * ureg.dimensionless * 0.01)) + self.assertAlmostEqual( + np.sin([np.pi / 2] * ureg.m / ureg.m), + np.sin([np.pi / 2] * ureg.dimensionless), + ) + self.assertAlmostEqual( + np.sin([np.pi / 2] * ureg.cm / ureg.m), + np.sin([np.pi / 2] * ureg.dimensionless * 0.01), + ) def test_issue50(self): ureg = UnitRegistry() Q_ = ureg.Quantity self.assertEqual(Q_(100), 100 * ureg.dimensionless) - self.assertEqual(Q_('100'), 100 * ureg.dimensionless) + self.assertEqual(Q_("100"), 100 * ureg.dimensionless) def test_issue52(self): u1 = UnitRegistry() u2 = UnitRegistry() - q1 = 1*u1.meter - q2 = 1*u2.meter + q1 = 1 * u1.meter + q2 = 1 * u2.meter import operator as op - for fun in (op.add, op.iadd, - op.sub, op.isub, - op.mul, op.imul, - op.floordiv, op.ifloordiv, - op.truediv, op.itruediv): + + for fun in ( + op.add, + op.iadd, + op.sub, + op.isub, + op.mul, + op.imul, + op.floordiv, + op.ifloordiv, + op.truediv, + op.itruediv, + ): self.assertRaises(ValueError, fun, q1, q2) def test_issue54(self): ureg = UnitRegistry() - self.assertEqual((1*ureg.km/ureg.m + 1).magnitude, 1001) + self.assertEqual((1 * ureg.km / ureg.m + 1).magnitude, 1001) def test_issue54_related(self): ureg = UnitRegistry() - self.assertEqual(ureg.km/ureg.m, 1000) - self.assertEqual(1000, ureg.km/ureg.m) - self.assertLess(900, ureg.km/ureg.m) - self.assertGreater(1100, ureg.km/ureg.m) + self.assertEqual(ureg.km / ureg.m, 1000) + self.assertEqual(1000, ureg.km / ureg.m) + self.assertLess(900, ureg.km / ureg.m) + self.assertGreater(1100, ureg.km / ureg.m) def test_issue61(self): ureg = UnitRegistry() Q_ = ureg.Quantity - for value in ({}, {'a': 3}, None): + for value in ({}, {"a": 3}, None): self.assertRaises(TypeError, Q_, value) - self.assertRaises(TypeError, Q_, value, 'meter') - self.assertRaises(ValueError, Q_, '', 'meter') - self.assertRaises(ValueError, Q_, '') + self.assertRaises(TypeError, Q_, value, "meter") + self.assertRaises(ValueError, Q_, "", "meter") + self.assertRaises(ValueError, Q_, "") @helpers.requires_not_numpy() def test_issue61_notNP(self): @@ -155,39 +174,47 @@ def test_issue61_notNP(self): Q_ = ureg.Quantity for value in ([1, 2, 3], (1, 2, 3)): self.assertRaises(TypeError, Q_, value) - self.assertRaises(TypeError, Q_, value, 'meter') + self.assertRaises(TypeError, Q_, value, "meter") def test_issue62(self): ureg = UnitRegistry() - m = ureg('m**0.5') - self.assertEqual(str(m.units), 'meter ** 0.5') + m = ureg("m**0.5") + self.assertEqual(str(m.units), "meter ** 0.5") def test_issue66(self): ureg = UnitRegistry() - self.assertEqual(ureg.get_dimensionality(UnitsContainer({'[temperature]': 1})), - UnitsContainer({'[temperature]': 1})) - self.assertEqual(ureg.get_dimensionality(ureg.kelvin), - UnitsContainer({'[temperature]': 1})) - self.assertEqual(ureg.get_dimensionality(ureg.degC), - UnitsContainer({'[temperature]': 1})) + self.assertEqual( + ureg.get_dimensionality(UnitsContainer({"[temperature]": 1})), + UnitsContainer({"[temperature]": 1}), + ) + self.assertEqual( + ureg.get_dimensionality(ureg.kelvin), UnitsContainer({"[temperature]": 1}) + ) + self.assertEqual( + ureg.get_dimensionality(ureg.degC), UnitsContainer({"[temperature]": 1}) + ) def test_issue66b(self): ureg = UnitRegistry() - self.assertEqual(ureg.get_base_units(ureg.kelvin), - (1.0, ureg.Unit(UnitsContainer({'kelvin': 1})))) - self.assertEqual(ureg.get_base_units(ureg.degC), - (1.0, ureg.Unit(UnitsContainer({'kelvin': 1})))) + self.assertEqual( + ureg.get_base_units(ureg.kelvin), + (1.0, ureg.Unit(UnitsContainer({"kelvin": 1}))), + ) + self.assertEqual( + ureg.get_base_units(ureg.degC), + (1.0, ureg.Unit(UnitsContainer({"kelvin": 1}))), + ) def test_issue69(self): ureg = UnitRegistry() - q = ureg('m').to(ureg('in')) - self.assertEqual(q, ureg('m').to('in')) + q = ureg("m").to(ureg("in")) + self.assertEqual(q, ureg("m").to("in")) @helpers.requires_numpy() def test_issue74(self): ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) + v1 = np.asarray([1.0, 2.0, 3.0]) + v2 = np.asarray([3.0, 2.0, 1.0]) q1 = v1 * ureg.ms q2 = v2 * ureg.ms @@ -198,7 +225,7 @@ def test_issue74(self): np.testing.assert_array_equal(q1 >= q2, v1 >= v2) q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude + v2s = q2s.to("ms").magnitude np.testing.assert_array_equal(q1 < q2s, v1 < v2s) np.testing.assert_array_equal(q1 > q2s, v1 > v2s) @@ -209,8 +236,8 @@ def test_issue74(self): @helpers.requires_numpy() def test_issue75(self): ureg = UnitRegistry() - v1 = np.asarray([1., 2., 3.]) - v2 = np.asarray([3., 2., 1.]) + v1 = np.asarray([1.0, 2.0, 3.0]) + v2 = np.asarray([3.0, 2.0, 1.0]) q1 = v1 * ureg.ms q2 = v2 * ureg.ms @@ -218,7 +245,7 @@ def test_issue75(self): np.testing.assert_array_equal(q1 != q2, v1 != v2) q2s = np.asarray([0.003, 0.002, 0.001]) * ureg.s - v2s = q2s.to('ms').magnitude + v2s = q2s.to("ms").magnitude np.testing.assert_array_equal(q1 == q2s, v1 == v2s) np.testing.assert_array_equal(q1 != q2s, v1 != v2s) @@ -226,25 +253,27 @@ def test_issue75(self): @helpers.requires_uncertainties() def test_issue77(self): ureg = UnitRegistry() - acc = (5.0 * ureg('m/s/s')).plus_minus(0.25) - tim = (37.0 * ureg('s')).plus_minus(0.16) + acc = (5.0 * ureg("m/s/s")).plus_minus(0.25) + tim = (37.0 * ureg("s")).plus_minus(0.16) dis = acc * tim ** 2 / 2 self.assertEqual(dis.value, acc.value * tim.value ** 2 / 2) def test_issue85(self): ureg = UnitRegistry() - T = 4. * ureg.kelvin - m = 1. * ureg.amu - va = 2. * ureg.k * T / m + T = 4.0 * ureg.kelvin + m = 1.0 * ureg.amu + va = 2.0 * ureg.k * T / m try: va.to_base_units() except: - self.assertTrue(False, 'Error while trying to get base units for {}'.format(va)) + self.assertTrue( + False, "Error while trying to get base units for {}".format(va) + ) - boltmk = 1.380649e-23*ureg.J/ureg.K - vb = 2. * boltmk * T / m + boltmk = 1.380649e-23 * ureg.J / ureg.K + vb = 2.0 * boltmk * T / m self.assertQuantityAlmostEqual(va.to_base_units(), vb.to_base_units()) @@ -255,12 +284,12 @@ def test_issue86(self): def parts(q): return q.magnitude, q.units - q1 = 10. * ureg.degC - q2 = 10. * ureg.kelvin + q1 = 10.0 * ureg.degC + q2 = 10.0 * ureg.kelvin k1 = q1.to_base_units() - q3 = 3. * ureg.meter + q3 = 3.0 * ureg.meter q1m, q1u = parts(q1) q2m, q2u = parts(q2) @@ -272,9 +301,9 @@ def parts(q): self.assertEqual(parts(q2 / q3), (q2m / q3m, q2u / q3u)) self.assertEqual(parts(q3 * q2), (q3m * q2m, q3u * q2u)) self.assertEqual(parts(q3 / q2), (q3m / q2m, q3u / q2u)) - self.assertEqual(parts(q2 ** 1), (q2m ** 1, q2u ** 1)) + self.assertEqual(parts(q2 ** 1), (q2m ** 1, q2u ** 1)) self.assertEqual(parts(q2 ** -1), (q2m ** -1, q2u ** -1)) - self.assertEqual(parts(q2 ** 2), (q2m ** 2, q2u ** 2)) + self.assertEqual(parts(q2 ** 2), (q2m ** 2, q2u ** 2)) self.assertEqual(parts(q2 ** -2), (q2m ** -2, q2u ** -2)) self.assertEqual(parts(q1 * q3), (k1m * q3m, k1u * q3u)) @@ -282,13 +311,13 @@ def parts(q): self.assertEqual(parts(q3 * q1), (q3m * k1m, q3u * k1u)) self.assertEqual(parts(q3 / q1), (q3m / k1m, q3u / k1u)) self.assertEqual(parts(q1 ** -1), (k1m ** -1, k1u ** -1)) - self.assertEqual(parts(q1 ** 2), (k1m ** 2, k1u ** 2)) + self.assertEqual(parts(q1 ** 2), (k1m ** 2, k1u ** 2)) self.assertEqual(parts(q1 ** -2), (k1m ** -2, k1u ** -2)) def test_issues86b(self): ureg = self.ureg - T1 = 200. * ureg.degC + T1 = 200.0 * ureg.degC T2 = T1.to(ureg.kelvin) m = 132.9054519 * ureg.amu v1 = 2 * ureg.k * T1 / m @@ -304,8 +333,8 @@ def test_issue86c(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = True T = ureg.degC - T = 100. * T - self.assertQuantityAlmostEqual(ureg.k*2*T, ureg.k*(2*T)) + T = 100.0 * T + self.assertQuantityAlmostEqual(ureg.k * 2 * T, ureg.k * (2 * T)) def test_issue93(self): ureg = UnitRegistry() @@ -321,7 +350,7 @@ def test_issue93(self): self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - @helpers.requires_numpy_previous_than('1.10') + @helpers.requires_numpy_previous_than("1.10") def test_issue94(self): ureg = UnitRegistry() v1 = np.array([5, 5]) * ureg.meter @@ -335,8 +364,8 @@ def test_issue94(self): def test_issue104(self): ureg = UnitRegistry() - x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] - y = [ureg('1 meter')] * 3 + x = [ureg("1 meter"), ureg("1 meter"), ureg("1 meter")] + y = [ureg("1 meter")] * 3 def summer(values): if not values: @@ -347,28 +376,28 @@ def summer(values): return total - self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) - self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) - self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) + self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, "meter")) + self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, "meter")) + self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, "meter")) + self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, "meter")) def test_issue105(self): ureg = UnitRegistry() func = ureg.parse_unit_name - val = list(func('meter')) - self.assertEqual(list(func('METER')), []) - self.assertEqual(val, list(func('METER', False))) + val = list(func("meter")) + self.assertEqual(list(func("METER")), []) + self.assertEqual(val, list(func("METER", False))) for func in (ureg.get_name, ureg.parse_expression): - val = func('meter') - self.assertRaises(AttributeError, func, 'METER') - self.assertEqual(val, func('METER', False)) + val = func("meter") + self.assertRaises(AttributeError, func, "METER") + self.assertEqual(val, func("METER", False)) def test_issue121(self): sh = (2, 1) ureg = UnitRegistry() - z, v = 0, 2. + z, v = 0, 2.0 self.assertEqual(z + v * ureg.meter, v * ureg.meter) self.assertEqual(z - v * ureg.meter, -v * ureg.meter) self.assertEqual(v * ureg.meter + z, v * ureg.meter) @@ -381,7 +410,7 @@ def test_issue121b(self): sh = (2, 1) ureg = UnitRegistry() - z, v = 0, 2. + z, v = 0, 2.0 self.assertEqual(z + v * ureg.meter, v * ureg.meter) self.assertEqual(z - v * ureg.meter, -v * ureg.meter) self.assertEqual(v * ureg.meter + z, v * ureg.meter) @@ -389,17 +418,14 @@ def test_issue121b(self): self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - z, v = np.zeros(sh), 2. * np.ones(sh) + z, v = np.zeros(sh), 2.0 * np.ones(sh) self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) - z, v = np.zeros((3, 1)), 2. * np.ones(sh) - for x, y in ((z, v), - (z, v * ureg.meter), - (v * ureg.meter, z) - ): + z, v = np.zeros((3, 1)), 2.0 * np.ones(sh) + for x, y in ((z, v), (z, v * ureg.meter), (v * ureg.meter, z)): try: w = x + y self.assertTrue(False, "ValueError not raised") @@ -413,44 +439,44 @@ def test_issue121b(self): @helpers.requires_numpy() def test_issue127(self): - q = [1., 2., 3., 4.] * self.ureg.meter + q = [1.0, 2.0, 3.0, 4.0] * self.ureg.meter q[0] = np.nan - self.assertNotEqual(q[0], 1.) + self.assertNotEqual(q[0], 1.0) self.assertTrue(math.isnan(q[0].magnitude)) - q[1] = float('NaN') - self.assertNotEqual(q[1], 2.) + q[1] = float("NaN") + self.assertNotEqual(q[1], 2.0) self.assertTrue(math.isnan(q[1].magnitude)) def test_issue170(self): Q_ = UnitRegistry().Quantity - q = Q_('1 kHz')/Q_('100 Hz') + q = Q_("1 kHz") / Q_("100 Hz") iq = int(q) self.assertEqual(iq, 10) self.assertIsInstance(iq, int) def test_angstrom_creation(self): ureg = UnitRegistry() - ureg.Quantity(2, 'Å') + ureg.Quantity(2, "Å") def test_alternative_angstrom_definition(self): ureg = UnitRegistry() - ureg.Quantity(2, '\u212B') + ureg.Quantity(2, "\u212B") def test_micro_creation(self): ureg = UnitRegistry() - ureg.Quantity(2, 'µm') + ureg.Quantity(2, "µm") @helpers.requires_numpy() def test_issue171_real_imag(self): - qr = [1., 2., 3., 4.] * self.ureg.meter - qi = [4., 3., 2., 1.] * self.ureg.meter + qr = [1.0, 2.0, 3.0, 4.0] * self.ureg.meter + qi = [4.0, 3.0, 2.0, 1.0] * self.ureg.meter q = qr + 1j * qi self.assertQuantityEqual(q.real, qr) self.assertQuantityEqual(q.imag, qi) @helpers.requires_numpy() def test_issue171_T(self): - a = np.asarray([[1., 2., 3., 4.],[4., 3., 2., 1.]]) + a = np.asarray([[1.0, 2.0, 3.0, 4.0], [4.0, 3.0, 2.0, 1.0]]) q1 = a * self.ureg.meter q2 = a.T * self.ureg.meter self.assertQuantityEqual(q1.T, q2) @@ -459,11 +485,11 @@ def test_issue171_T(self): def test_issue250(self): a = self.ureg.V b = self.ureg.mV - self.assertEqual(np.float16(a/b), 1000.) - self.assertEqual(np.float32(a/b), 1000.) - self.assertEqual(np.float64(a/b), 1000.) + self.assertEqual(np.float16(a / b), 1000.0) + self.assertEqual(np.float32(a / b), 1000.0) + self.assertEqual(np.float64(a / b), 1000.0) if "float128" in dir(np): - self.assertEqual(np.float128(a/b), 1000.) + self.assertEqual(np.float128(a / b), 1000.0) def test_issue252(self): ur = UnitRegistry() @@ -474,36 +500,36 @@ def test_issue252(self): def test_issue323(self): from fractions import Fraction as F - self.assertEqual((self.Q_(F(2,3), 's')).to('ms'), self.Q_(F(2000,3), 'ms')) - self.assertEqual((self.Q_(F(2,3), 'm')).to('km'), self.Q_(F(1,1500), 'km')) + + self.assertEqual((self.Q_(F(2, 3), "s")).to("ms"), self.Q_(F(2000, 3), "ms")) + self.assertEqual((self.Q_(F(2, 3), "m")).to("km"), self.Q_(F(1, 1500), "km")) def test_issue339(self): - q1 = self.ureg('') + q1 = self.ureg("") self.assertEqual(q1.magnitude, 1) self.assertEqual(q1.units, self.ureg.dimensionless) - q2 = self.ureg('1 dimensionless') + q2 = self.ureg("1 dimensionless") self.assertEqual(q1, q2) def test_issue354_356_370(self): q = 1 * self.ureg.second / self.ureg.millisecond - self.assertEqual('{0:~}'.format(1 * self.ureg.second / self.ureg.millisecond), - '1.0 s / ms') - self.assertEqual("{0:~}".format(1 * self.ureg.count), - '1 count') - self.assertEqual('{0:~}'.format(1 * self.ureg('MiB')), - '1 MiB') + self.assertEqual( + "{0:~}".format(1 * self.ureg.second / self.ureg.millisecond), "1.0 s / ms" + ) + self.assertEqual("{0:~}".format(1 * self.ureg.count), "1 count") + self.assertEqual("{0:~}".format(1 * self.ureg("MiB")), "1 MiB") def test_issue468(self): ureg = UnitRegistry() - @ureg.wraps(('kg'), 'meter') + @ureg.wraps(("kg"), "meter") def f(x): return x - x = ureg.Quantity(1., 'meter') + x = ureg.Quantity(1.0, "meter") y = f(x) z = x * y - self.assertEqual(z, ureg.Quantity(1., 'meter * kilogram')) + self.assertEqual(z, ureg.Quantity(1.0, "meter * kilogram")) @helpers.requires_numpy() def test_issue482(self): @@ -521,8 +547,8 @@ def test_issue483(self): def test_issue523(self): ureg = UnitRegistry() - src, dst = UnitsContainer({'meter': 1}), UnitsContainer({'degF': 1}) - value = 10. + src, dst = UnitsContainer({"meter": 1}), UnitsContainer({"degF": 1}) + value = 10.0 convert = self.ureg.convert self.assertRaises(DimensionalityError, convert, value, src, dst) self.assertRaises(DimensionalityError, convert, value, dst, src) @@ -530,20 +556,20 @@ def test_issue523(self): def test_issue532(self): ureg = self.ureg - @ureg.check(ureg('')) + @ureg.check(ureg("")) def f(x): return 2 * x - self.assertEqual(f(ureg.Quantity(1, '')), 2) - self.assertRaises(DimensionalityError, f, ureg.Quantity(1, 'm')) + self.assertEqual(f(ureg.Quantity(1, "")), 2) + self.assertRaises(DimensionalityError, f, ureg.Quantity(1, "m")) def test_issue625a(self): ureg = UnitRegistry() Q_ = ureg.Quantity from math import sqrt - @ureg.wraps(ureg.second, (ureg.meters, ureg.meters/ureg.second**2)) - def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2')): + @ureg.wraps(ureg.second, (ureg.meters, ureg.meters / ureg.second ** 2)) + def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): """Calculate time to fall from a height h with a default gravity. By default, the gravity is assumed to be earth gravity, @@ -554,73 +580,73 @@ def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2')): """ return sqrt(2 * height / gravity) - lunar_module_height = Q_(10, 'm') + lunar_module_height = Q_(10, "m") t1 = calculate_time_to_fall(lunar_module_height) print(t1) - self.assertAlmostEqual(t1, Q_(1.4285714285714286, 's')) + self.assertAlmostEqual(t1, Q_(1.4285714285714286, "s")) - moon_gravity = Q_(1.625, 'm/s^2') + moon_gravity = Q_(1.625, "m/s^2") t2 = calculate_time_to_fall(lunar_module_height, moon_gravity) - self.assertAlmostEqual(t2, Q_(3.508232077228117, 's')) + self.assertAlmostEqual(t2, Q_(3.508232077228117, "s")) def test_issue625b(self): ureg = UnitRegistry() Q_ = ureg.Quantity - @ureg.wraps('=A*B', ('=A', '=B')) - def get_displacement(time, rate=Q_(1, 'm/s')): + @ureg.wraps("=A*B", ("=A", "=B")) + def get_displacement(time, rate=Q_(1, "m/s")): """Calculates displacement from a duration and default rate. """ return time * rate - d1 = get_displacement(Q_(2, 's')) - self.assertAlmostEqual(d1, Q_(2, 'm')) + d1 = get_displacement(Q_(2, "s")) + self.assertAlmostEqual(d1, Q_(2, "m")) - d2 = get_displacement(Q_(2, 's'), Q_(1, 'deg/s')) - self.assertAlmostEqual(d2, Q_(2,' deg')) + d2 = get_displacement(Q_(2, "s"), Q_(1, "deg/s")) + self.assertAlmostEqual(d2, Q_(2, " deg")) def test_issue625c(self): u = UnitRegistry() - @u.wraps('=A*B*C', ('=A', '=B', '=C')) - def get_product(a=2*u.m, b=3*u.m, c=5*u.m): - return a*b*c + @u.wraps("=A*B*C", ("=A", "=B", "=C")) + def get_product(a=2 * u.m, b=3 * u.m, c=5 * u.m): + return a * b * c - self.assertEqual(get_product(a=3*u.m), 45*u.m**3) - self.assertEqual(get_product(b=2*u.m), 20*u.m**3) - self.assertEqual(get_product(c=1*u.dimensionless), 6*u.m**2) + self.assertEqual(get_product(a=3 * u.m), 45 * u.m ** 3) + self.assertEqual(get_product(b=2 * u.m), 20 * u.m ** 3) + self.assertEqual(get_product(c=1 * u.dimensionless), 6 * u.m ** 2) def test_issue655a(self): ureg = UnitRegistry() distance = 1 * ureg.m time = 1 * ureg.s velocity = distance / time - self.assertEqual(distance.check('[length]'), True) - self.assertEqual(distance.check('[time]'), False) - self.assertEqual(velocity.check('[length] / [time]'), True) - self.assertEqual(velocity.check('1 / [time] * [length]'), True) + self.assertEqual(distance.check("[length]"), True) + self.assertEqual(distance.check("[time]"), False) + self.assertEqual(velocity.check("[length] / [time]"), True) + self.assertEqual(velocity.check("1 / [time] * [length]"), True) def test_issue655b(self): ureg = UnitRegistry() Q_ = ureg.Quantity - @ureg.check('[length]', '[length]/[time]^2') - def pendulum_period(length, G=Q_(1, 'standard_gravity')): + @ureg.check("[length]", "[length]/[time]^2") + def pendulum_period(length, G=Q_(1, "standard_gravity")): print(length) - return (2*math.pi*(length/G)**.5).to('s') + return (2 * math.pi * (length / G) ** 0.5).to("s") l = 1 * ureg.m # Assume earth gravity t = pendulum_period(l) - self.assertAlmostEqual(t, Q_('2.0064092925890407 second')) + self.assertAlmostEqual(t, Q_("2.0064092925890407 second")) # Use moon gravity - moon_gravity = Q_(1.625, 'm/s^2') + moon_gravity = Q_(1.625, "m/s^2") t = pendulum_period(l, moon_gravity) - self.assertAlmostEqual(t, Q_('4.928936075204336 second')) + self.assertAlmostEqual(t, Q_("4.928936075204336 second")) def test_issue783(self): ureg = UnitRegistry() - assert not ureg('g') == [] + assert not ureg("g") == [] def test_issue856(self): ph1 = ParserHelper(scale=123) @@ -630,17 +656,17 @@ def test_issue856(self): ureg1 = UnitRegistry() ureg2 = copy.deepcopy(ureg1) # Very basic functionality test - assert ureg2('1 t').to('kg').magnitude == 1000 + assert ureg2("1 t").to("kg").magnitude == 1000 def test_issue856b(self): # Test that, after a deepcopy(), the two UnitRegistries are # independent from each other ureg1 = UnitRegistry() ureg2 = copy.deepcopy(ureg1) - ureg1.define('test123 = 123 kg') - ureg2.define('test123 = 456 kg') - assert ureg1('1 test123').to('kg').magnitude == 123 - assert ureg2('1 test123').to('kg').magnitude == 456 + ureg1.define("test123 = 123 kg") + ureg2.define("test123 = 456 kg") + assert ureg1("1 test123").to("kg").magnitude == 123 + assert ureg2("1 test123").to("kg").magnitude == 456 def test_issue876(self): # Same hash must not imply equality. diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 931be4900..d37cf6072 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -11,7 +11,7 @@ class TestNotMeasurement(QuantityTestCase): def test_instantiate(self): M_ = self.ureg.Measurement - self.assertRaises(RuntimeError, M_, 4.0, 0.1, 's') + self.assertRaises(RuntimeError, M_, 4.0, 0.1, "s") @helpers.requires_uncertainties() @@ -21,18 +21,20 @@ class TestMeasurement(QuantityTestCase): def test_simple(self): M_ = self.ureg.Measurement - M_(4.0, 0.1, 's') + M_(4.0, 0.1, "s") def test_build(self): M_ = self.ureg.Measurement - v, u = self.Q_(4.0, 's'), self.Q_(.1, 's') - M_(v.magnitude, u.magnitude, 's') - ms = (M_(v.magnitude, u.magnitude, 's'), - M_(v, u.magnitude), - M_(v, u), - v.plus_minus(.1), - v.plus_minus(0.025, True), - v.plus_minus(u),) + v, u = self.Q_(4.0, "s"), self.Q_(0.1, "s") + M_(v.magnitude, u.magnitude, "s") + ms = ( + M_(v.magnitude, u.magnitude, "s"), + M_(v, u.magnitude), + M_(v, u), + v.plus_minus(0.1), + v.plus_minus(0.025, True), + v.plus_minus(u), + ) for m in ms: self.assertEqual(m.value, v) @@ -40,89 +42,95 @@ def test_build(self): self.assertEqual(m.rel, m.error / abs(m.value)) def test_format(self): - v, u = self.Q_(4.0, 's ** 2'), self.Q_(.1, 's ** 2') + v, u = self.Q_(4.0, "s ** 2"), self.Q_(0.1, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( - ("{}", '(4.00 +/- 0.10) second ** 2'), - ("{!r}", ''), - ('{:P}', '(4.00 ± 0.10) second²'), - ('{:L}', r'\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}'), - ('{:H}', '(4.00 ± 0.10) second^2'), - ('{:C}', '(4.00+/-0.10) second**2'), - ('{:Lx}', r'\SI[separate-uncertainty=true]{4.00(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}', '(4.0 ± 0.1) second^2'), - ('{:.1fC}', '(4.0+/-0.1) second**2'), - ('{:.1fLx}', r'\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}'), + ("{}", "(4.00 +/- 0.10) second ** 2"), + ("{!r}", ""), + ("{:P}", "(4.00 ± 0.10) second²"), + ("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), + ("{:H}", "(4.00 ± 0.10) second^2"), + ("{:C}", "(4.00+/-0.10) second**2"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(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}", "(4.0 ± 0.1) second^2"), + ("{:.1fC}", "(4.0+/-0.1) second**2"), + ("{:.1fLx}", r"\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) def test_format_paru(self): - v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') + v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( - ('{:uS}', '0.200(10) second ** 2'), - ('{:.3uS}', '0.2000(100) second ** 2'), - ('{:.3uSP}', '0.2000(100) second²'), - ('{:.3uSL}', r'0.2000\left(100\right)\ \mathrm{second}^{2}'), - ('{:.3uSH}', '0.2000(100) second^2'), - ('{:.3uSC}', '0.2000(100) second**2'), + ("{:uS}", "0.200(10) second ** 2"), + ("{:.3uS}", "0.2000(100) second ** 2"), + ("{:.3uSP}", "0.2000(100) second²"), + ("{:.3uSL}", r"0.2000\left(100\right)\ \mathrm{second}^{2}"), + ("{:.3uSH}", "0.2000(100) second^2"), + ("{:.3uSC}", "0.2000(100) second**2"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) def test_format_u(self): - v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') + v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( - ('{:.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}', '(0.2000 ± 0.0100) second^2'), - ('{:.3uC}', '(0.2000+/-0.0100) second**2'), - ('{:.3uLx}', r'\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}'), - ('{:.1uLx}', r'\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}'), + ("{:.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}", "(0.2000 ± 0.0100) second^2"), + ("{:.3uC}", "(0.2000+/-0.0100) second**2"), + ( + "{:.3uLx}", + r"\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}", + ), + ("{:.1uLx}", r"\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) def test_format_percu(self): self.test_format_perce() - v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') + v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( - ('{:.1u%}', '(20 +/- 1)% second ** 2'), - ('{:.1u%P}', '(20 ± 1)% second²'), - ('{:.1u%L}', r'\left(20 \pm 1\right) \%\ \mathrm{second}^{2}'), - ('{:.1u%H}', '(20 ± 1)% second^2'), - ('{:.1u%C}', '(20+/-1)% second**2'), + ("{:.1u%}", "(20 +/- 1)% second ** 2"), + ("{:.1u%P}", "(20 ± 1)% second²"), + ("{:.1u%L}", r"\left(20 \pm 1\right) \%\ \mathrm{second}^{2}"), + ("{:.1u%H}", "(20 ± 1)% second^2"), + ("{:.1u%C}", "(20+/-1)% second**2"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) def test_format_perce(self): - v, u = self.Q_(0.20, 's ** 2'), self.Q_(0.01, 's ** 2') + v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( - ('{:.1ue}', '(2.0 +/- 0.1)e-01 second ** 2'), - ('{:.1ueP}', '(2.0 ± 0.1)×10⁻¹ second²'), - ('{:.1ueL}', r'\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}'), - ('{:.1ueH}', '(2.0 ± 0.1)e-01 second^2'), - ('{:.1ueC}', '(2.0+/-0.1)e-01 second**2'), + ("{:.1ue}", "(2.0 +/- 0.1)e-01 second ** 2"), + ("{:.1ueP}", "(2.0 ± 0.1)×10⁻¹ second²"), + ( + "{:.1ueL}", + r"\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}", + ), + ("{:.1ueH}", "(2.0 ± 0.1)e-01 second^2"), + ("{:.1ueC}", "(2.0+/-0.1)e-01 second**2"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) def test_raise_build(self): - v, u = self.Q_(1.0, 's'), self.Q_(.1, 's') - o = self.Q_(.1, 'm') + v, u = self.Q_(1.0, "s"), self.Q_(0.1, "s") + o = self.Q_(0.1, "m") M_ = self.ureg.Measurement with self.assertRaises(DimensionalityError): @@ -134,9 +142,9 @@ def test_raise_build(self): def test_propagate_linear(self): - v1, u1 = self.Q_(8.0, 's'), self.Q_(.7, 's') - v2, u2 = self.Q_(5.0, 's'), self.Q_(.6, 's') - v2, u3 = self.Q_(-5.0, 's'), self.Q_(.6, 's') + v1, u1 = self.Q_(8.0, "s"), self.Q_(0.7, "s") + v2, u2 = self.Q_(5.0, "s"), self.Q_(0.6, "s") + v2, u3 = self.Q_(-5.0, "s"), self.Q_(0.6, "s") m1 = v1.plus_minus(u1) m2 = v2.plus_minus(u2) @@ -150,25 +158,35 @@ def test_propagate_linear(self): for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): r = ml + mr - self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude + mr.value.magnitude) - self.assertAlmostEqual(r.error.magnitude, - ml.error.magnitude + mr.error.magnitude if ml is mr else - (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** .5) + self.assertAlmostEqual( + r.value.magnitude, ml.value.magnitude + mr.value.magnitude + ) + self.assertAlmostEqual( + r.error.magnitude, + ml.error.magnitude + mr.error.magnitude + if ml is mr + else (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** 0.5, + ) self.assertEqual(r.value.units, ml.value.units) for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): r = ml - mr - self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude - mr.value.magnitude) - self.assertAlmostEqual(r.error.magnitude, - 0 if ml is mr else - (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** .5) + self.assertAlmostEqual( + r.value.magnitude, ml.value.magnitude - mr.value.magnitude + ) + self.assertAlmostEqual( + r.error.magnitude, + 0 + if ml is mr + else (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** 0.5, + ) self.assertEqual(r.value.units, ml.value.units) def test_propagate_product(self): - v1, u1 = self.Q_(8.0, 's'), self.Q_(.7, 's') - v2, u2 = self.Q_(5.0, 's'), self.Q_(.6, 's') - v2, u3 = self.Q_(-5.0, 's'), self.Q_(.6, 's') + v1, u1 = self.Q_(8.0, "s"), self.Q_(0.7, "s") + v2, u2 = self.Q_(5.0, "s"), self.Q_(0.6, "s") + v2, u3 = self.Q_(-5.0, "s"), self.Q_(0.6, "s") m1 = v1.plus_minus(u1) m2 = v2.plus_minus(u2) @@ -179,10 +197,14 @@ def test_propagate_product(self): for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): r = ml * mr - self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude * mr.value.magnitude) + self.assertAlmostEqual( + r.value.magnitude, ml.value.magnitude * mr.value.magnitude + ) self.assertEqual(r.value.units, ml.value.units * mr.value.units) for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): r = ml / mr - self.assertAlmostEqual(r.value.magnitude, ml.value.magnitude / mr.value.magnitude) + self.assertAlmostEqual( + r.value.magnitude, ml.value.magnitude / mr.value.magnitude + ) self.assertEqual(r.value.units, ml.value.units / mr.value.units) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index ead8895ee..b2097bb3d 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -18,20 +18,21 @@ class TestNumpyMethods(QuantityTestCase): @classmethod def setUpClass(cls): from pint import _DEFAULT_REGISTRY + cls.ureg = _DEFAULT_REGISTRY cls.Q_ = cls.ureg.Quantity @property def q(self): - return [[1,2],[3,4]] * self.ureg.m + return [[1, 2], [3, 4]] * self.ureg.m @property def q_nan(self): - return [[1,2],[3,np.nan]] * self.ureg.m + return [[1, 2], [3, np.nan]] * self.ureg.m @property def q_temperature(self): - return self.Q_([[1,2],[3,4]], self.ureg.degC) + return self.Q_([[1, 2], [3, 4]], self.ureg.degC) def assertNDArrayEqual(self, actual, desired): # Assert that the given arrays are equal, and are not Quantities @@ -59,8 +60,10 @@ def test_empty_like(self): @helpers.requires_array_function_protocol() def test_full_like(self): - self.assertQuantityEqual(np.full_like(self.q, self.Q_(0, self.ureg.degC)), - self.Q_([[0, 0], [0, 0]], self.ureg.degC)) + self.assertQuantityEqual( + np.full_like(self.q, self.Q_(0, self.ureg.degC)), + self.Q_([[0, 0], [0, 0]], self.ureg.degC), + ) self.assertNDArrayEqual(np.full_like(self.q, 2), np.array([[2, 2], [2, 2]])) @@ -81,7 +84,7 @@ def test_flat(self): self.assertEqual(q, v * self.ureg.m) def test_reshape(self): - self.assertQuantityEqual(self.q.reshape([1,4]), [[1, 2, 3, 4]] * self.ureg.m) + self.assertQuantityEqual(self.q.reshape([1, 4]), [[1, 2, 3, 4]] * self.ureg.m) def test_ravel(self): self.assertQuantityEqual(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) @@ -94,15 +97,21 @@ def test_ravel_numpy_func(self): @helpers.requires_array_function_protocol() def test_moveaxis(self): - self.assertQuantityEqual(np.moveaxis(self.q, 1,0), np.array([[1,2],[3,4]]).T * self.ureg.m) + self.assertQuantityEqual( + np.moveaxis(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_rollaxis(self): - self.assertQuantityEqual(np.rollaxis(self.q, 1), np.array([[1,2],[3,4]]).T * self.ureg.m) + self.assertQuantityEqual( + np.rollaxis(self.q, 1), np.array([[1, 2], [3, 4]]).T * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_swapaxes(self): - self.assertQuantityEqual(np.swapaxes(self.q, 1,0), np.array([[1,2],[3,4]]).T * self.ureg.m) + self.assertQuantityEqual( + np.swapaxes(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m + ) def test_transpose(self): self.assertQuantityEqual(self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m) @@ -113,7 +122,9 @@ def test_transpose_numpy_func(self): @helpers.requires_array_function_protocol() def test_flip_numpy_func(self): - self.assertQuantityEqual(np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m) + self.assertQuantityEqual( + np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m + ) # Changing number of dimensions @@ -128,7 +139,10 @@ def test_atleast_1d(self): @helpers.requires_array_function_protocol() def test_atleast_2d(self): actual = np.atleast_2d(self.Q_(0, self.ureg.degC), self.q.flatten()) - expected = (self.Q_(np.array([[0]]), self.ureg.degC), np.array([[1, 2, 3, 4]]) * self.ureg.m) + expected = ( + self.Q_(np.array([[0]]), self.ureg.degC), + np.array([[1, 2, 3, 4]]) * self.ureg.m, + ) for ind_actual, ind_expected in zip(actual, expected): self.assertQuantityEqual(ind_actual, ind_expected) self.assertQuantityEqual(np.atleast_2d(self.q), self.q) @@ -136,25 +150,34 @@ def test_atleast_2d(self): @helpers.requires_array_function_protocol() def test_atleast_3d(self): actual = np.atleast_3d(self.Q_(0, self.ureg.degC), self.q.flatten()) - expected = (self.Q_(np.array([[[0]]]), self.ureg.degC), np.array([[[1], [2], [3], [4]]]) * self.ureg.m) + expected = ( + self.Q_(np.array([[[0]]]), self.ureg.degC), + np.array([[[1], [2], [3], [4]]]) * self.ureg.m, + ) for ind_actual, ind_expected in zip(actual, expected): self.assertQuantityEqual(ind_actual, ind_expected) - self.assertQuantityEqual(np.atleast_3d(self.q), np.array([[[1],[2]],[[3],[4]]])* self.ureg.m) + self.assertQuantityEqual( + np.atleast_3d(self.q), np.array([[[1], [2]], [[3], [4]]]) * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_broadcast_to(self): - self.assertQuantityEqual(np.broadcast_to(self.q[:,1], (2,2)), np.array([[2,4],[2,4]]) * self.ureg.m) + self.assertQuantityEqual( + np.broadcast_to(self.q[:, 1], (2, 2)), + np.array([[2, 4], [2, 4]]) * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_expand_dims(self): - self.assertQuantityEqual(np.expand_dims(self.q, 0), np.array([[[1, 2],[3, 4]]])* self.ureg.m) + self.assertQuantityEqual( + np.expand_dims(self.q, 0), np.array([[[1, 2], [3, 4]]]) * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_squeeze(self): self.assertQuantityEqual(np.squeeze(self.q), self.q) self.assertQuantityEqual( - self.q.reshape([1,4]).squeeze(), - [1, 2, 3, 4] * self.ureg.m + self.q.reshape([1, 4]).squeeze(), [1, 2, 3, 4] * self.ureg.m ) # Changing number of dimensions @@ -162,65 +185,59 @@ def test_squeeze(self): @helpers.requires_array_function_protocol() def test_concatentate(self): self.assertQuantityEqual( - np.concatenate([self.q]*2), - self.Q_(np.concatenate([self.q.m]*2), self.ureg.m) + np.concatenate([self.q] * 2), + self.Q_(np.concatenate([self.q.m] * 2), self.ureg.m), ) @helpers.requires_array_function_protocol() def test_stack(self): self.assertQuantityEqual( - np.stack([self.q]*2), - self.Q_(np.stack([self.q.m]*2), self.ureg.m) + np.stack([self.q] * 2), self.Q_(np.stack([self.q.m] * 2), self.ureg.m) ) @helpers.requires_array_function_protocol() def test_column_stack(self): - self.assertQuantityEqual( - np.column_stack([self.q[:,0],self.q[:,1]]), - self.q - ) + self.assertQuantityEqual(np.column_stack([self.q[:, 0], self.q[:, 1]]), self.q) @helpers.requires_array_function_protocol() def test_dstack(self): self.assertQuantityEqual( - np.dstack([self.q]*2), - self.Q_(np.dstack([self.q.m]*2), self.ureg.m) + np.dstack([self.q] * 2), self.Q_(np.dstack([self.q.m] * 2), self.ureg.m) ) @helpers.requires_array_function_protocol() def test_hstack(self): self.assertQuantityEqual( - np.hstack([self.q]*2), - self.Q_(np.hstack([self.q.m]*2), self.ureg.m) + np.hstack([self.q] * 2), self.Q_(np.hstack([self.q.m] * 2), self.ureg.m) ) @helpers.requires_array_function_protocol() def test_vstack(self): self.assertQuantityEqual( - np.vstack([self.q]*2), - self.Q_(np.vstack([self.q.m]*2), self.ureg.m) + np.vstack([self.q] * 2), self.Q_(np.vstack([self.q.m] * 2), self.ureg.m) ) @helpers.requires_array_function_protocol() def test_block(self): self.assertQuantityEqual( - np.block([self.q[0, :],self.q[1, :]]), - self.Q_([1,2,3,4], self.ureg.m) + np.block([self.q[0, :], self.q[1, :]]), self.Q_([1, 2, 3, 4], self.ureg.m) ) @helpers.requires_array_function_protocol() def test_append(self): - self.assertQuantityEqual(np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), - [[1, 2], [3, 4], [0, 0]] * self.ureg.m) + self.assertQuantityEqual( + np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), + [[1, 2], [3, 4], [0, 0]] * self.ureg.m, + ) def test_astype(self): actual = self.q.astype(np.float32) - expected = self.Q_(np.array([[1., 2.], [3., 4.]], dtype=np.float32), 'm') + expected = self.Q_(np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32), "m") self.assertQuantityEqual(actual, expected) self.assertEqual(actual.m.dtype, expected.m.dtype) def test_item(self): - self.assertQuantityEqual(self.Q_([[0]], 'm').item(), 0 * self.ureg.m) + self.assertQuantityEqual(self.Q_([[0]], "m").item(), 0 * self.ureg.m) class TestNumpyMathematicalFunctions(TestNumpyMethods): @@ -228,8 +245,12 @@ class TestNumpyMathematicalFunctions(TestNumpyMethods): # Trigonometric functions @helpers.requires_array_function_protocol() def test_unwrap(self): - self.assertQuantityEqual(np.unwrap([0,3*np.pi]*self.ureg.radians), [0,np.pi]) - self.assertQuantityEqual(np.unwrap([0,540]*self.ureg.deg), [0,180]*self.ureg.deg) + self.assertQuantityEqual( + np.unwrap([0, 3 * np.pi] * self.ureg.radians), [0, np.pi] + ) + self.assertQuantityEqual( + np.unwrap([0, 540] * self.ureg.deg), [0, 180] * self.ureg.deg + ) # Rounding @@ -239,17 +260,18 @@ def test_fix(self): self.assertQuantityEqual(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) self.assertQuantityEqual( np.fix([2.1, 2.9, -2.1, -2.9] * self.ureg.m), - [2., 2., -2., -2.] * self.ureg.m + [2.0, 2.0, -2.0, -2.0] * self.ureg.m, ) + # Sums, products, differences def test_prod(self): - self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) + self.assertEqual(self.q.prod(), 24 * self.ureg.m ** 4) def test_sum(self): - self.assertEqual(self.q.sum(), 10*self.ureg.m) - self.assertQuantityEqual(self.q.sum(0), [4, 6]*self.ureg.m) - self.assertQuantityEqual(self.q.sum(1), [3, 7]*self.ureg.m) + self.assertEqual(self.q.sum(), 10 * self.ureg.m) + self.assertQuantityEqual(self.q.sum(0), [4, 6] * self.ureg.m) + self.assertQuantityEqual(self.q.sum(1), [3, 7] * self.ureg.m) @helpers.requires_array_function_protocol() def test_sum_numpy_func(self): @@ -279,70 +301,101 @@ def test_nancumprod_numpy_func(self): @helpers.requires_array_function_protocol() def test_diff(self): self.assertQuantityEqual(np.diff(self.q, 1), [[1], [1]] * self.ureg.m) - self.assertQuantityEqual(np.diff(self.q_temperature, 1), [[1], [1]] * self.ureg.delta_degC) + self.assertQuantityEqual( + np.diff(self.q_temperature, 1), [[1], [1]] * self.ureg.delta_degC + ) @helpers.requires_array_function_protocol() def test_ediff1d(self): self.assertQuantityEqual(np.ediff1d(self.q), [1, 1, 1] * self.ureg.m) - self.assertQuantityEqual(np.ediff1d(self.q_temperature), [1, 1, 1] * self.ureg.delta_degC) + self.assertQuantityEqual( + np.ediff1d(self.q_temperature), [1, 1, 1] * self.ureg.delta_degC + ) @helpers.requires_array_function_protocol() def test_gradient(self): - l = np.gradient([[1,1],[3,4]] * self.ureg.m, 1 * self.ureg.J) - self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.m / self.ureg.J) - self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.m / self.ureg.J) + l = np.gradient([[1, 1], [3, 4]] * self.ureg.m, 1 * self.ureg.J) + self.assertQuantityEqual( + l[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.m / self.ureg.J + ) + self.assertQuantityEqual( + l[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.m / self.ureg.J + ) - l = np.gradient(self.Q_([[1,1],[3,4]] , self.ureg.degC), 1 * self.ureg.J) - self.assertQuantityEqual(l[0], [[2., 3.], [2., 3.]] * self.ureg.delta_degC / self.ureg.J) - self.assertQuantityEqual(l[1], [[0., 0.], [1., 1.]] * self.ureg.delta_degC / self.ureg.J) + l = np.gradient(self.Q_([[1, 1], [3, 4]], self.ureg.degC), 1 * self.ureg.J) + self.assertQuantityEqual( + l[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.delta_degC / self.ureg.J + ) + self.assertQuantityEqual( + l[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.delta_degC / self.ureg.J + ) @helpers.requires_array_function_protocol() def test_cross(self): - a = [[3,-3, 1]] * self.ureg.kPa - b = [[4, 9, 2]] * self.ureg.m**2 - self.assertQuantityEqual(np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m**2) + a = [[3, -3, 1]] * self.ureg.kPa + b = [[4, 9, 2]] * self.ureg.m ** 2 + self.assertQuantityEqual( + np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m ** 2 + ) @helpers.requires_array_function_protocol() def test_trapz(self): - self.assertQuantityEqual(np.trapz([1. ,2., 3., 4.] * self.ureg.J, dx=1*self.ureg.m), 7.5 * self.ureg.J*self.ureg.m) + self.assertQuantityEqual( + np.trapz([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), + 7.5 * self.ureg.J * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_dot(self): - self.assertQuantityEqual(self.q.ravel().dot(np.array([1, 0, 0, 1])), 5 * self.ureg.m) + self.assertQuantityEqual( + self.q.ravel().dot(np.array([1, 0, 0, 1])), 5 * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_dot_numpy_func(self): - self.assertQuantityEqual(np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m) + self.assertQuantityEqual( + np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), + 3 * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_einsum(self): a = np.arange(25).reshape(5, 5) * self.ureg.m b = np.arange(5) * self.ureg.m - self.assertQuantityEqual(np.einsum('ii', a), 60 * self.ureg.m) - self.assertQuantityEqual(np.einsum('ii->i', a), np.array([ 0, 6, 12, 18, 24]) * self.ureg.m) - self.assertQuantityEqual(np.einsum('i,i', b, b), 30 * self.ureg.m**2) - self.assertQuantityEqual(np.einsum('ij,j', a, b), np.array([ 30, 80, 130, 180, 230]) * self.ureg.m**2) + self.assertQuantityEqual(np.einsum("ii", a), 60 * self.ureg.m) + self.assertQuantityEqual( + np.einsum("ii->i", a), np.array([0, 6, 12, 18, 24]) * self.ureg.m + ) + self.assertQuantityEqual(np.einsum("i,i", b, b), 30 * self.ureg.m ** 2) + self.assertQuantityEqual( + np.einsum("ij,j", a, b), + np.array([30, 80, 130, 180, 230]) * self.ureg.m ** 2, + ) # Arithmetic operations def test_addition_with_scalar(self): a = np.array([0, 1, 2]) - b = 10. * self.ureg('gram/kilogram') - self.assertQuantityAlmostEqual(a + b, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless)) - self.assertQuantityAlmostEqual(b + a, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless)) + b = 10.0 * self.ureg("gram/kilogram") + self.assertQuantityAlmostEqual( + a + b, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless) + ) + self.assertQuantityAlmostEqual( + b + a, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless) + ) def test_addition_with_incompatible_scalar(self): a = np.array([0, 1, 2]) - b = 1. * self.ureg.m + b = 1.0 * self.ureg.m self.assertRaises(DimensionalityError, op.add, a, b) self.assertRaises(DimensionalityError, op.add, b, a) def test_power(self): arr = np.array(range(3), dtype=np.float) - q = self.Q_(arr, 'meter') + q = self.Q_(arr, "meter") for op_ in [op.pow, op.ipow, np.power]: q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, 2., q_cp) + self.assertRaises(DimensionalityError, op_, 2.0, q_cp) arr_cp = copy.copy(arr) arr_cp = copy.copy(arr) q_cp = copy.copy(q) @@ -355,8 +408,8 @@ def test_power(self): @helpers.requires_numpy() def test_exponentiation_array_exp_2(self): arr = np.array(range(3), dtype=np.float) - #q = self.Q_(copy.copy(arr), None) - q = self.Q_(copy.copy(arr), 'meter') + # q = self.Q_(copy.copy(arr), None) + q = self.Q_(copy.copy(arr), "meter") arr_cp = copy.copy(arr) q_cp = copy.copy(q) # this fails as expected since numpy 1.8.0 but... @@ -371,7 +424,10 @@ def test_exponentiation_array_exp_2(self): class TestNumpyUnclassified(TestNumpyMethods): def test_tolist(self): - self.assertEqual(self.q.tolist(), [[1*self.ureg.m, 2*self.ureg.m], [3*self.ureg.m, 4*self.ureg.m]]) + self.assertEqual( + self.q.tolist(), + [[1 * self.ureg.m, 2 * self.ureg.m], [3 * self.ureg.m, 4 * self.ureg.m]], + ) def test_fill(self): tmp = self.q @@ -381,29 +437,33 @@ def test_fill(self): self.assertQuantityEqual(tmp, [[5, 5], [5, 5]] * self.ureg.m) def test_take(self): - self.assertQuantityEqual(self.q.take([0,1,2,3]), self.q.flatten()) + self.assertQuantityEqual(self.q.take([0, 1, 2, 3]), self.q.flatten()) def test_put(self): - q = [1., 2., 3., 4.] * self.ureg.m - q.put([0, 2], [10.,20.]*self.ureg.m) - self.assertQuantityEqual(q, [10., 2., 20., 4.]*self.ureg.m) + q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m + q.put([0, 2], [10.0, 20.0] * self.ureg.m) + self.assertQuantityEqual(q, [10.0, 2.0, 20.0, 4.0] * self.ureg.m) - q = [1., 2., 3., 4.] * self.ureg.m - q.put([0, 2], [1., 2.]*self.ureg.mm) - self.assertQuantityEqual(q, [0.001, 2., 0.002, 4.]*self.ureg.m) + q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m + q.put([0, 2], [1.0, 2.0] * self.ureg.mm) + self.assertQuantityEqual(q, [0.001, 2.0, 0.002, 4.0] * self.ureg.m) - q = [1., 2., 3., 4.] * self.ureg.m / self.ureg.mm - q.put([0, 2], [1., 2.]) - self.assertQuantityEqual(q, [0.001, 2., 0.002, 4.]*self.ureg.m/self.ureg.mm) + q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m / self.ureg.mm + q.put([0, 2], [1.0, 2.0]) + self.assertQuantityEqual( + q, [0.001, 2.0, 0.002, 4.0] * self.ureg.m / self.ureg.mm + ) - q = [1., 2., 3., 4.] * self.ureg.m + q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m with self.assertRaises(DimensionalityError): - q.put([0, 2], [4., 6.] * self.ureg.J) + q.put([0, 2], [4.0, 6.0] * self.ureg.J) with self.assertRaises(DimensionalityError): - q.put([0, 2], [4., 6.]) + q.put([0, 2], [4.0, 6.0]) def test_repeat(self): - self.assertQuantityEqual(self.q.repeat(2), [1,1,2,2,3,3,4,4]*self.ureg.m) + self.assertQuantityEqual( + self.q.repeat(2), [1, 1, 2, 2, 3, 3, 4, 4] * self.ureg.m + ) def test_sort(self): q = [4, 5, 2, 3, 1, 6] * self.ureg.m @@ -433,15 +493,18 @@ def test_diagonal_numpy_func(self): self.assertQuantityEqual(np.diagonal(q, offset=-1), [1, 2] * self.ureg.m) def test_compress(self): - self.assertQuantityEqual(self.q.compress([False, True], axis=0), - [[3, 4]] * self.ureg.m) - self.assertQuantityEqual(self.q.compress([False, True], axis=1), - [[2], [4]] * self.ureg.m) + self.assertQuantityEqual( + self.q.compress([False, True], axis=0), [[3, 4]] * self.ureg.m + ) + self.assertQuantityEqual( + self.q.compress([False, True], axis=1), [[2], [4]] * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_compress(self): - self.assertQuantityEqual(np.compress([False, True], self.q, axis=1), - [[2], [4]] * self.ureg.m) + self.assertQuantityEqual( + np.compress([False, True], self.q, axis=1), [[2], [4]] * self.ureg.m + ) def test_searchsorted(self): q = self.q.flatten() @@ -470,7 +533,7 @@ def test_count_nonzero_numpy_func(self): self.assertEqual(np.count_nonzero(q), 4) def test_max(self): - self.assertEqual(self.q.max(), 4*self.ureg.m) + self.assertEqual(self.q.max(), 4 * self.ureg.m) def test_max_numpy_func(self): self.assertEqual(np.max(self.q), 4 * self.ureg.m) @@ -481,7 +544,10 @@ def test_max_with_axis_arg(self): @helpers.requires_array_function_protocol() def test_max_with_initial_arg(self): - self.assertQuantityEqual(np.max(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m) + self.assertQuantityEqual( + np.max(self.q[..., None], axis=2, initial=3 * self.ureg.m), + [[3, 3], [3, 4]] * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_nanmax(self): @@ -499,8 +565,9 @@ def test_nanargmax_numpy_func(self): self.assertNDArrayEqual(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) def test_maximum(self): - self.assertQuantityEqual(np.maximum(self.q, self.Q_([0, 5], 'm')), - self.Q_([[1, 5], [3, 5]], 'm')) + self.assertQuantityEqual( + np.maximum(self.q, self.Q_([0, 5], "m")), self.Q_([[1, 5], [3, 5]], "m") + ) def test_min(self): self.assertEqual(self.q.min(), 1 * self.ureg.m) @@ -515,7 +582,10 @@ def test_min_with_axis_arg(self): @helpers.requires_array_function_protocol() def test_min_with_initial_arg(self): - self.assertQuantityEqual(np.min(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[1, 2], [3, 3]] * self.ureg.m) + self.assertQuantityEqual( + np.min(self.q[..., None], axis=2, initial=3 * self.ureg.m), + [[1, 2], [3, 3]] * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_nanmin(self): @@ -533,8 +603,9 @@ def test_nanargmin_numpy_func(self): self.assertNDArrayEqual(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) def test_minimum(self): - self.assertQuantityEqual(np.minimum(self.q, self.Q_([0, 5], 'm')), - self.Q_([[0, 2], [0, 4]], 'm')) + self.assertQuantityEqual( + np.minimum(self.q, self.Q_([0, 5], "m")), self.Q_([[0, 2], [0, 4]], "m") + ) def test_ptp(self): self.assertEqual(self.q.ptp(), 3 * self.ureg.m) @@ -545,23 +616,23 @@ def test_ptp_numpy_func(self): def test_clip(self): self.assertQuantityEqual( - self.q.clip(max=2*self.ureg.m), - [[1, 2], [2, 2]] * self.ureg.m + self.q.clip(max=2 * self.ureg.m), [[1, 2], [2, 2]] * self.ureg.m ) self.assertQuantityEqual( - self.q.clip(min=3*self.ureg.m), - [[3, 3], [3, 4]] * self.ureg.m + self.q.clip(min=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m ) self.assertQuantityEqual( - self.q.clip(min=2*self.ureg.m, max=3*self.ureg.m), - [[2, 2], [3, 3]] * self.ureg.m + self.q.clip(min=2 * self.ureg.m, max=3 * self.ureg.m), + [[2, 2], [3, 3]] * self.ureg.m, ) self.assertRaises(DimensionalityError, self.q.clip, self.ureg.J) self.assertRaises(DimensionalityError, self.q.clip, 1) @helpers.requires_array_function_protocol() def test_clip_numpy_func(self): - self.assertQuantityEqual(np.clip(self.q, 150 * self.ureg.cm, None), [[1.5, 2], [3, 4]] * self.ureg.m) + self.assertQuantityEqual( + np.clip(self.q, 150 * self.ureg.cm, None), [[1.5, 2], [3, 4]] * self.ureg.m + ) def test_round(self): q = [1, 1.33, 5.67, 22] * self.ureg.m @@ -571,22 +642,30 @@ def test_round(self): @helpers.requires_array_function_protocol() def test_round_numpy_func(self): - self.assertQuantityEqual(np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m) - self.assertQuantityEqual(np.round_(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m) + self.assertQuantityEqual( + np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m + ) + self.assertQuantityEqual( + np.round_(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m + ) def test_trace(self): - self.assertEqual(self.q.trace(), (1+4) * self.ureg.m) + self.assertEqual(self.q.trace(), (1 + 4) * self.ureg.m) def test_cumsum(self): self.assertQuantityEqual(self.q.cumsum(), [1, 3, 6, 10] * self.ureg.m) @helpers.requires_array_function_protocol() def test_cumsum_numpy_func(self): - self.assertQuantityEqual(np.cumsum(self.q, axis=0), [[1, 2], [4, 6]] * self.ureg.m) + self.assertQuantityEqual( + np.cumsum(self.q, axis=0), [[1, 2], [4, 6]] * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_nancumsum_numpy_func(self): - self.assertQuantityEqual(np.nancumsum(self.q_nan, axis=0), [[1, 2], [4, 2]] * self.ureg.m) + self.assertQuantityEqual( + np.nancumsum(self.q_nan, axis=0), [[1, 2], [4, 2]] * self.ureg.m + ) def test_mean(self): self.assertEqual(self.q.mean(), 2.5 * self.ureg.m) @@ -602,7 +681,11 @@ def test_nanmean_numpy_func(self): @helpers.requires_array_function_protocol() def test_average_numpy_func(self): - self.assertQuantityAlmostEqual(np.average(self.q, axis=0, weights=[1, 2]), [2.33333, 3.33333] * self.ureg.m, rtol=1e-5) + self.assertQuantityAlmostEqual( + np.average(self.q, axis=0, weights=[1, 2]), + [2.33333, 3.33333] * self.ureg.m, + rtol=1e-5, + ) @helpers.requires_array_function_protocol() def test_median_numpy_func(self): @@ -613,26 +696,28 @@ def test_nanmedian_numpy_func(self): self.assertEqual(np.nanmedian(self.q_nan), 2 * self.ureg.m) def test_var(self): - self.assertEqual(self.q.var(), 1.25*self.ureg.m**2) + self.assertEqual(self.q.var(), 1.25 * self.ureg.m ** 2) @helpers.requires_array_function_protocol() def test_var_numpy_func(self): - self.assertEqual(np.var(self.q), 1.25*self.ureg.m**2) + self.assertEqual(np.var(self.q), 1.25 * self.ureg.m ** 2) @helpers.requires_array_function_protocol() def test_nanvar_numpy_func(self): - self.assertQuantityAlmostEqual(np.nanvar(self.q_nan), 0.66667*self.ureg.m**2, rtol=1e-5) + self.assertQuantityAlmostEqual( + np.nanvar(self.q_nan), 0.66667 * self.ureg.m ** 2, rtol=1e-5 + ) def test_std(self): - self.assertQuantityAlmostEqual(self.q.std(), 1.11803*self.ureg.m, rtol=1e-5) + self.assertQuantityAlmostEqual(self.q.std(), 1.11803 * self.ureg.m, rtol=1e-5) @helpers.requires_array_function_protocol() def test_std_numpy_func(self): - self.assertQuantityAlmostEqual(np.std(self.q), 1.11803*self.ureg.m, rtol=1e-5) + self.assertQuantityAlmostEqual(np.std(self.q), 1.11803 * self.ureg.m, rtol=1e-5) self.assertRaises(OffsetUnitCalculusError, np.std, self.q_temperature) def test_prod(self): - self.assertEqual(self.q.prod(), 24 * self.ureg.m**4) + self.assertEqual(self.q.prod(), 24 * self.ureg.m ** 4) def test_cumprod(self): self.assertRaises(DimensionalityError, self.q.cumprod) @@ -640,26 +725,28 @@ def test_cumprod(self): @helpers.requires_array_function_protocol() def test_nanstd_numpy_func(self): - self.assertQuantityAlmostEqual(np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5) + self.assertQuantityAlmostEqual( + np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5 + ) - @helpers.requires_numpy_previous_than('1.10') + @helpers.requires_numpy_previous_than("1.10") def test_integer_div(self): a = [1] * self.ureg.m b = [2] * self.ureg.m - c = a/b # Should be float division + c = a / b # Should be float division self.assertEqual(c.magnitude[0], 0.5) a /= b # Should be integer division self.assertEqual(a.magnitude[0], 0) def test_conj(self): - self.assertQuantityEqual((self.q*(1+1j)).conj(), self.q*(1-1j)) - self.assertQuantityEqual((self.q*(1+1j)).conjugate(), self.q*(1-1j)) + self.assertQuantityEqual((self.q * (1 + 1j)).conj(), self.q * (1 - 1j)) + self.assertQuantityEqual((self.q * (1 + 1j)).conjugate(), self.q * (1 - 1j)) def test_getitem(self): - self.assertRaises(IndexError, self.q.__getitem__, (0,10)) - self.assertQuantityEqual(self.q[0], [1,2]*self.ureg.m) - self.assertEqual(self.q[1,1], 4*self.ureg.m) + self.assertRaises(IndexError, self.q.__getitem__, (0, 10)) + self.assertQuantityEqual(self.q[0], [1, 2] * self.ureg.m) + self.assertEqual(self.q[1, 1], 4 * self.ureg.m) def test_setitem(self): with self.assertRaises(TypeError): @@ -689,10 +776,10 @@ def test_setitem(self): q = [0, 1, 2, 3] * self.ureg.dimensionless q[0] = 1 self.assertQuantityEqual(q, np.asarray([1, 1, 2, 3])) - q[0] = self.ureg.m/self.ureg.mm + q[0] = self.ureg.m / self.ureg.mm self.assertQuantityEqual(q, np.asarray([1000, 1, 2, 3])) - q = [0., 1., 2., 3.] * self.ureg.m / self.ureg.mm + q = [0.0, 1.0, 2.0, 3.0] * self.ureg.m / self.ureg.mm q[0] = 1.0 self.assertQuantityEqual(q, [0.001, 1, 2, 3] * self.ureg.m / self.ureg.mm) @@ -722,7 +809,7 @@ def pickle_test(q): self.assertNDArrayEqual(q.magnitude, pq.magnitude) self.assertEqual(q.units, pq.units) - pickle_test([10, 20]*self.ureg.m) + pickle_test([10, 20] * self.ureg.m) def test_equal(self): x = self.q.magnitude @@ -762,12 +849,14 @@ def test_trim_zeros_numpy_func(self): @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("int64")) @helpers.requires_array_function_protocol() def test_nan_to_num_numpy_func(self): - self.assertQuantityEqual(np.nan_to_num(self.q_nan, nan=-999 * self.ureg.mm), - [[1, 2], [3, -0.999]] * self.ureg.m) + self.assertQuantityEqual( + np.nan_to_num(self.q_nan, nan=-999 * self.ureg.mm), + [[1, 2], [3, -0.999]] * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_meshgrid_numpy_func(self): @@ -780,86 +869,131 @@ def test_meshgrid_numpy_func(self): @helpers.requires_array_function_protocol() def test_isclose_numpy_func(self): q2 = [[1000.05, 2000], [3000.00007, 4001]] * self.ureg.mm - self.assertNDArrayEqual(np.isclose(self.q, q2), np.array([[False, True], [True, False]])) + self.assertNDArrayEqual( + np.isclose(self.q, q2), np.array([[False, True], [True, False]]) + ) @helpers.requires_array_function_protocol() def test_interp_numpy_func(self): x = [1, 4] * self.ureg.m xp = np.linspace(0, 3, 5) * self.ureg.m fp = self.Q_([0, 5, 10, 15, 20], self.ureg.degC) - self.assertQuantityAlmostEqual(np.interp(x, xp, fp), self.Q_([6.66667, 20.], self.ureg.degC), rtol=1e-5) + self.assertQuantityAlmostEqual( + np.interp(x, xp, fp), self.Q_([6.66667, 20.0], self.ureg.degC), rtol=1e-5 + ) def test_comparisons(self): - self.assertNDArrayEqual(self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]])) - self.assertNDArrayEqual(self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]])) + self.assertNDArrayEqual( + self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]]) + ) + self.assertNDArrayEqual( + self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]]) + ) @helpers.requires_array_function_protocol() def test_where(self): - self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 20 * self.ureg.m), - [[20, 2], [3, 4]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, 0), - [[0, 2], [3, 4]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, np.nan), - [[np.nan, 2], [3, 4]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, 0, self.q), - [[1, 2], [0, 0]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, np.nan, self.q), - [[1, 2], [np.nan, np.nan]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 2 * self.ureg.m, self.q, np.array(np.nan)), - [[np.nan, 2], [3, 4]] * self.ureg.m) - self.assertQuantityEqual(np.where(self.q >= 3 * self.ureg.m, np.array(np.nan), self.q), - [[1, 2], [np.nan, np.nan]] * self.ureg.m) - self.assertRaises(DimensionalityError, np.where, self.q < 2 * self.ureg.m, self.q, 0 * self.ureg.J) + self.assertQuantityEqual( + np.where(self.q >= 2 * self.ureg.m, self.q, 20 * self.ureg.m), + [[20, 2], [3, 4]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 2 * self.ureg.m, self.q, 0), + [[0, 2], [3, 4]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 2 * self.ureg.m, self.q, np.nan), + [[np.nan, 2], [3, 4]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 3 * self.ureg.m, 0, self.q), + [[1, 2], [0, 0]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 3 * self.ureg.m, np.nan, self.q), + [[1, 2], [np.nan, np.nan]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 2 * self.ureg.m, self.q, np.array(np.nan)), + [[np.nan, 2], [3, 4]] * self.ureg.m, + ) + self.assertQuantityEqual( + np.where(self.q >= 3 * self.ureg.m, np.array(np.nan), self.q), + [[1, 2], [np.nan, np.nan]] * self.ureg.m, + ) + self.assertRaises( + DimensionalityError, + np.where, + self.q < 2 * self.ureg.m, + self.q, + 0 * self.ureg.J, + ) @helpers.requires_array_function_protocol() def test_fabs(self): - self.assertQuantityEqual(np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], 'm')) + self.assertQuantityEqual( + np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], "m") + ) @helpers.requires_array_function_protocol() def test_isin(self): - self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'm')), - np.array([[False, True], [False, True]])) - self.assertNDArrayEqual(np.isin(self.q, self.Q_([0, 2, 4], 'J')), - np.array([[False, False], [False, False]])) - self.assertNDArrayEqual(np.isin(self.q, [self.Q_(2, 'm'), self.Q_(4, 'J')]), - np.array([[False, True], [False, False]])) - self.assertNDArrayEqual(np.isin(self.q, self.q.m), - np.array([[False, False], [False, False]])) - self.assertNDArrayEqual(np.isin(self.q / self.ureg.cm, [1, 3]), - np.array([[True, False], [True, False]])) + self.assertNDArrayEqual( + np.isin(self.q, self.Q_([0, 2, 4], "m")), + np.array([[False, True], [False, True]]), + ) + self.assertNDArrayEqual( + np.isin(self.q, self.Q_([0, 2, 4], "J")), + np.array([[False, False], [False, False]]), + ) + self.assertNDArrayEqual( + np.isin(self.q, [self.Q_(2, "m"), self.Q_(4, "J")]), + np.array([[False, True], [False, False]]), + ) + self.assertNDArrayEqual( + np.isin(self.q, self.q.m), np.array([[False, False], [False, False]]) + ) + self.assertNDArrayEqual( + np.isin(self.q / self.ureg.cm, [1, 3]), + np.array([[True, False], [True, False]]), + ) self.assertRaises(ValueError, np.isin, self.q.m, self.q) @helpers.requires_array_function_protocol() def test_percentile(self): - self.assertQuantityEqual(np.percentile(self.q, 25), self.Q_(1.75, 'm')) + self.assertQuantityEqual(np.percentile(self.q, 25), self.Q_(1.75, "m")) @helpers.requires_array_function_protocol() def test_nanpercentile(self): - self.assertQuantityEqual(np.nanpercentile(self.q_nan, 25), self.Q_(1.5, 'm')) + self.assertQuantityEqual(np.nanpercentile(self.q_nan, 25), self.Q_(1.5, "m")) @helpers.requires_array_function_protocol() def test_copyto(self): a = self.q.m q = copy.copy(self.q) np.copyto(q, 2 * q, where=[True, False]) - self.assertQuantityEqual(q, self.Q_([[2, 2], [6, 4]], 'm')) + self.assertQuantityEqual(q, self.Q_([[2, 2], [6, 4]], "m")) np.copyto(q, 0, where=[[False, False], [True, False]]) - self.assertQuantityEqual(q, self.Q_([[2, 2], [0, 4]], 'm')) + self.assertQuantityEqual(q, self.Q_([[2, 2], [0, 4]], "m")) np.copyto(a, q) self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) @helpers.requires_array_function_protocol() def test_tile(self): - self.assertQuantityEqual(np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m) + self.assertQuantityEqual( + np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_rot90(self): - self.assertQuantityEqual(np.rot90(self.q), np.array([[2, 4], [1, 3]]) * self.ureg.m) + self.assertQuantityEqual( + np.rot90(self.q), np.array([[2, 4], [1, 3]]) * self.ureg.m + ) @helpers.requires_array_function_protocol() def test_insert(self): - self.assertQuantityEqual(np.insert(self.q, 1, 0 * self.ureg.m, axis=1), - np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m) + self.assertQuantityEqual( + np.insert(self.q, 1, 0 * self.ureg.m, axis=1), + np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m, + ) @unittest.skip @@ -897,42 +1031,31 @@ def qm(self): return np.asarray([1, 2, 3, 4], dtype=np.uint8) * self.ureg.m def test_bitwise_and(self): - self._test2(np.bitwise_and, - self.q1, - (self.q2, self.qs,), - (self.qm, ), - 'same') + self._test2(np.bitwise_and, self.q1, (self.q2, self.qs), (self.qm,), "same") def test_bitwise_or(self): - self._test2(np.bitwise_or, - self.q1, - (self.q1, self.q2, self.qs, ), - (self.qm,), - 'same') + self._test2( + np.bitwise_or, self.q1, (self.q1, self.q2, self.qs), (self.qm,), "same" + ) def test_bitwise_xor(self): - self._test2(np.bitwise_xor, - self.q1, - (self.q1, self.q2, self.qs, ), - (self.qm, ), - 'same') + self._test2( + np.bitwise_xor, self.q1, (self.q1, self.q2, self.qs), (self.qm,), "same" + ) def test_invert(self): - self._test1(np.invert, - (self.q1, self.q2, self.qs, ), - (), - 'same') + self._test1(np.invert, (self.q1, self.q2, self.qs), (), "same") def test_left_shift(self): - self._test2(np.left_shift, - self.q1, - (self.qless, 2), - (self.q1, self.q2, self.qs, ), - 'same') + self._test2( + np.left_shift, self.q1, (self.qless, 2), (self.q1, self.q2, self.qs), "same" + ) def test_right_shift(self): - self._test2(np.right_shift, - self.q1, - (self.qless, 2), - (self.q1, self.q2, self.qs, ), - 'same') + self._test2( + np.right_shift, + self.q1, + (self.qless, 2), + (self.q1, self.q2, self.qs), + "same", + ) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 31772d518..573f6a462 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -5,152 +5,192 @@ from pint import DimensionalityError, OffsetUnitCalculusError from pint.compat import np from pint.testsuite.test_numpy import TestNumpyMethods -from pint.numpy_func import (_is_quantity, _is_quantity_sequence, _get_first_input_units, - convert_to_consistent_units, unwrap_and_wrap_consistent_units, - get_op_output_unit, implements, numpy_wrap) +from pint.numpy_func import ( + _is_quantity, + _is_quantity_sequence, + _get_first_input_units, + convert_to_consistent_units, + unwrap_and_wrap_consistent_units, + get_op_output_unit, + implements, + numpy_wrap, +) from unittest.mock import patch class TestNumPyFuncUtils(TestNumpyMethods): - - @patch('pint.numpy_func.HANDLED_FUNCTIONS', {}) - @patch('pint.numpy_func.HANDLED_UFUNCS', {}) + @patch("pint.numpy_func.HANDLED_FUNCTIONS", {}) + @patch("pint.numpy_func.HANDLED_UFUNCS", {}) def test_implements(self): # Test for functions - @implements('test', 'function') + @implements("test", "function") def test_function(): pass - self.assertEqual(pint.numpy_func.HANDLED_FUNCTIONS['test'], test_function) + self.assertEqual(pint.numpy_func.HANDLED_FUNCTIONS["test"], test_function) # Test for ufuncs - @implements('test', 'ufunc') + @implements("test", "ufunc") def test_ufunc(): pass - self.assertEqual(pint.numpy_func.HANDLED_UFUNCS['test'], test_ufunc) + self.assertEqual(pint.numpy_func.HANDLED_UFUNCS["test"], test_ufunc) # Test for invalid func type with self.assertRaises(ValueError): - @implements('test', 'invalid') + + @implements("test", "invalid") def test_invalid(): pass def test_is_quantity(self): self.assertTrue(_is_quantity(self.Q_(0))) self.assertTrue(_is_quantity(np.arange(4) * self.ureg.m)) - self.assertFalse(_is_quantity(1.)) + self.assertFalse(_is_quantity(1.0)) self.assertFalse(_is_quantity(np.array([1, 1, 2, 3, 5, 8]))) - self.assertFalse(_is_quantity('not-a-quantity')) + self.assertFalse(_is_quantity("not-a-quantity")) # TODO (#905 follow-up): test other duck arrays that wrap or are wrapped by Pint def test_is_quantity_sequence(self): - self.assertTrue(_is_quantity_sequence((self.Q_(0, 'm'), self.Q_(32., 'degF')))) + self.assertTrue(_is_quantity_sequence((self.Q_(0, "m"), self.Q_(32.0, "degF")))) self.assertTrue(_is_quantity_sequence(np.arange(4) * self.ureg.m)) self.assertFalse(_is_quantity_sequence((self.Q_(0), True))) self.assertFalse(_is_quantity_sequence([1, 3, 5])) self.assertFalse(_is_quantity_sequence(9 * self.ureg.m)) self.assertFalse(_is_quantity_sequence(np.arange(4))) - self.assertFalse(_is_quantity_sequence('0123')) + self.assertFalse(_is_quantity_sequence("0123")) def test_convert_to_consistent_units_with_pre_calc_units(self): args, kwargs = convert_to_consistent_units( - self.Q_(50, 'cm'), + self.Q_(50, "cm"), np.arange(4).reshape(2, 2) * self.ureg.m, [0.042] * self.ureg.km, - (self.Q_(0, 'm'), self.Q_(1, 'dam')), + (self.Q_(0, "m"), self.Q_(1, "dam")), a=6378 * self.ureg.km, - pre_calc_units=self.ureg.meter) + pre_calc_units=self.ureg.meter, + ) self.assertEqual(args[0], 0.5) self.assertNDArrayEqual(args[1], np.array([[0, 1], [2, 3]])) self.assertNDArrayEqual(args[2], np.array([42])) self.assertEqual(args[3][0], 0) self.assertEqual(args[3][1], 10) - self.assertEqual(kwargs['a'], 6.378e6) + self.assertEqual(kwargs["a"], 6.378e6) def test_convert_to_consistent_units_with_dimensionless(self): args, kwargs = convert_to_consistent_units( - np.arange(2), - pre_calc_units=self.ureg.g/self.ureg.kg) + np.arange(2), pre_calc_units=self.ureg.g / self.ureg.kg + ) self.assertNDArrayEqual(args[0], np.array([0, 1000])) self.assertEqual(kwargs, {}) def test_convert_to_consistent_units_with_dimensionality_error(self): - self.assertRaises(DimensionalityError, convert_to_consistent_units, - self.Q_(32., 'degF'), pre_calc_units=self.ureg.meter) - self.assertRaises(DimensionalityError, convert_to_consistent_units, np.arange(4), - pre_calc_units=self.ureg.meter) + self.assertRaises( + DimensionalityError, + convert_to_consistent_units, + self.Q_(32.0, "degF"), + pre_calc_units=self.ureg.meter, + ) + self.assertRaises( + DimensionalityError, + convert_to_consistent_units, + np.arange(4), + pre_calc_units=self.ureg.meter, + ) def test_convert_to_consistent_units_without_pre_calc_units(self): args, kwargs = convert_to_consistent_units( - (self.Q_(0), self.Q_(10, 'degC')), + (self.Q_(0), self.Q_(10, "degC")), [1, 2, 3, 5, 7] * self.ureg.m, - pre_calc_units=None) + pre_calc_units=None, + ) self.assertEqual(args[0][0], 0) self.assertEqual(args[0][1], 10) self.assertNDArrayEqual(args[1], np.array([1, 2, 3, 5, 7])) self.assertEqual(kwargs, {}) def test_unwrap_and_wrap_constistent_units(self): - (a, ), output_wrap_a = unwrap_and_wrap_consistent_units([2, 4, 8] * self.ureg.m) + (a,), output_wrap_a = unwrap_and_wrap_consistent_units([2, 4, 8] * self.ureg.m) (b, c), output_wrap_c = unwrap_and_wrap_consistent_units( - np.arange(4), self.Q_(1, 'g/kg')) + np.arange(4), self.Q_(1, "g/kg") + ) self.assertNDArrayEqual(a, np.array([2, 4, 8])) self.assertNDArrayEqual(b, np.array([0, 1000, 2000, 3000])) self.assertEqual(c, 1) self.assertQuantityEqual(output_wrap_a(0), 0 * self.ureg.m) - self.assertQuantityEqual(output_wrap_c(0), self.Q_(0, 'g/kg')) + self.assertQuantityEqual(output_wrap_c(0), self.Q_(0, "g/kg")) def test_op_output_unit_sum(self): - self.assertEqual(get_op_output_unit('sum', self.ureg.m), self.ureg.m) - self.assertRaises(OffsetUnitCalculusError, get_op_output_unit, 'sum', self.ureg.degC) + self.assertEqual(get_op_output_unit("sum", self.ureg.m), self.ureg.m) + self.assertRaises( + OffsetUnitCalculusError, get_op_output_unit, "sum", self.ureg.degC + ) def test_op_output_unit_mul(self): - self.assertEqual(get_op_output_unit('mul', self.ureg.s, - (self.Q_(1, 'm'), self.Q_(1, 'm**2'))), - self.ureg.m**3) + self.assertEqual( + get_op_output_unit( + "mul", self.ureg.s, (self.Q_(1, "m"), self.Q_(1, "m**2")) + ), + self.ureg.m ** 3, + ) def test_op_output_unit_delta(self): - self.assertEqual(get_op_output_unit('delta', self.ureg.m), self.ureg.m) - self.assertEqual(get_op_output_unit('delta', self.ureg.degC), self.ureg.delta_degC) + self.assertEqual(get_op_output_unit("delta", self.ureg.m), self.ureg.m) + self.assertEqual( + get_op_output_unit("delta", self.ureg.degC), self.ureg.delta_degC + ) def test_op_output_unit_delta_div(self): - self.assertEqual(get_op_output_unit('delta,div', self.ureg.m, - (self.Q_(1, 'm'), self.Q_(1, 's'))), - self.ureg.m / self.ureg.s) - self.assertEqual(get_op_output_unit('delta,div', self.ureg.degC, - (self.Q_(1, 'degC'), self.Q_(1, 'm'))), - self.ureg.delta_degC / self.ureg.m) + self.assertEqual( + get_op_output_unit( + "delta,div", self.ureg.m, (self.Q_(1, "m"), self.Q_(1, "s")) + ), + self.ureg.m / self.ureg.s, + ) + self.assertEqual( + get_op_output_unit( + "delta,div", self.ureg.degC, (self.Q_(1, "degC"), self.Q_(1, "m")) + ), + self.ureg.delta_degC / self.ureg.m, + ) def test_op_output_unit_div(self): - self.assertEqual(get_op_output_unit('div', self.ureg.m, - (self.Q_(1, 'm'), self.Q_(1, 's'), - self.Q_(1, 'K'))), - self.ureg.m / self.ureg.s / self.ureg.K) - self.assertEqual(get_op_output_unit('div', self.ureg.s, (1, self.Q_(1, 's'))), - self.ureg.s**-1) + self.assertEqual( + get_op_output_unit( + "div", self.ureg.m, (self.Q_(1, "m"), self.Q_(1, "s"), self.Q_(1, "K")) + ), + self.ureg.m / self.ureg.s / self.ureg.K, + ) + self.assertEqual( + get_op_output_unit("div", self.ureg.s, (1, self.Q_(1, "s"))), + self.ureg.s ** -1, + ) def test_op_output_unit_variance(self): - self.assertEqual(get_op_output_unit('variance', self.ureg.m), self.ureg.m**2) - self.assertRaises(OffsetUnitCalculusError, get_op_output_unit, 'variance', - self.ureg.degC) + self.assertEqual(get_op_output_unit("variance", self.ureg.m), self.ureg.m ** 2) + self.assertRaises( + OffsetUnitCalculusError, get_op_output_unit, "variance", self.ureg.degC + ) def test_op_output_unit_square(self): - self.assertEqual(get_op_output_unit('square', self.ureg.m), self.ureg.m**2) + self.assertEqual(get_op_output_unit("square", self.ureg.m), self.ureg.m ** 2) def test_op_output_unit_sqrt(self): - self.assertEqual(get_op_output_unit('sqrt', self.ureg.m), self.ureg.m**0.5) + self.assertEqual(get_op_output_unit("sqrt", self.ureg.m), self.ureg.m ** 0.5) def test_op_output_unit_reciprocal(self): - self.assertEqual(get_op_output_unit('reciprocal', self.ureg.m), self.ureg.m**-1) + self.assertEqual( + get_op_output_unit("reciprocal", self.ureg.m), self.ureg.m ** -1 + ) def test_op_output_unit_size(self): - self.assertEqual(get_op_output_unit('size', self.ureg.m, size=3), self.ureg.m**3) - self.assertRaises(ValueError, get_op_output_unit, 'size', self.ureg.m) + self.assertEqual( + get_op_output_unit("size", self.ureg.m, size=3), self.ureg.m ** 3 + ) + self.assertRaises(ValueError, get_op_output_unit, "size", self.ureg.m) def test_numpy_wrap(self): - self.assertRaises(ValueError, numpy_wrap, 'invalid', np.ones, [], {}, []) + self.assertRaises(ValueError, numpy_wrap, "invalid", np.ones, [], {}, []) # TODO (#905 follow-up): test that NotImplemented is returned when upcast types # present diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index 1f6131039..0dd782970 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -7,65 +7,64 @@ class TestPintEval(unittest.TestCase): - def _test_one(self, input_text, parsed): self.assertEqual(build_eval_tree(tokenizer(input_text)).to_string(), parsed) def test_build_eval_tree(self): - self._test_one('3', '3') - self._test_one('1 + 2', '(1 + 2)') + self._test_one("3", "3") + self._test_one("1 + 2", "(1 + 2)") # order of operations - self._test_one('2 * 3 + 4', '((2 * 3) + 4)') + self._test_one("2 * 3 + 4", "((2 * 3) + 4)") # parentheses - self._test_one('2 * (3 + 4)', '(2 * (3 + 4))') + self._test_one("2 * (3 + 4)", "(2 * (3 + 4))") # more order of operations - self._test_one('1 + 2 * 3 ** (4 + 3 / 5)', '(1 + (2 * (3 ** (4 + (3 / 5)))))') + self._test_one("1 + 2 * 3 ** (4 + 3 / 5)", "(1 + (2 * (3 ** (4 + (3 / 5)))))") # nested parentheses at beginning - self._test_one('1 * ((3 + 4) * 5)', '(1 * ((3 + 4) * 5))') + self._test_one("1 * ((3 + 4) * 5)", "(1 * ((3 + 4) * 5))") # nested parentheses at end - self._test_one('1 * (5 * (3 + 4))', '(1 * (5 * (3 + 4)))') + self._test_one("1 * (5 * (3 + 4))", "(1 * (5 * (3 + 4)))") # nested parentheses in middle - self._test_one('1 * (5 * (3 + 4) / 6)', '(1 * ((5 * (3 + 4)) / 6))') + self._test_one("1 * (5 * (3 + 4) / 6)", "(1 * ((5 * (3 + 4)) / 6))") # unary - self._test_one('-1', '(- 1)') + self._test_one("-1", "(- 1)") # unary - self._test_one('3 * -1', '(3 * (- 1))') + self._test_one("3 * -1", "(3 * (- 1))") # double unary - self._test_one('3 * --1', '(3 * (- (- 1)))') + self._test_one("3 * --1", "(3 * (- (- 1)))") # parenthetical unary - self._test_one('3 * -(2 + 4)', '(3 * (- (2 + 4)))') + self._test_one("3 * -(2 + 4)", "(3 * (- (2 + 4)))") # parenthetical unary - self._test_one('3 * -((2 + 4))', '(3 * (- (2 + 4)))') + self._test_one("3 * -((2 + 4))", "(3 * (- (2 + 4)))") # implicit op - self._test_one('3 4', '(3 4)') + self._test_one("3 4", "(3 4)") # implicit op, then parentheses - self._test_one('3 (2 + 4)', '(3 (2 + 4))') + self._test_one("3 (2 + 4)", "(3 (2 + 4))") # parentheses, then implicit - self._test_one('(3 ** 4 ) 5', '((3 ** 4) 5)') + self._test_one("(3 ** 4 ) 5", "((3 ** 4) 5)") # implicit op, then exponentiation - self._test_one('3 4 ** 5', '(3 (4 ** 5))') + self._test_one("3 4 ** 5", "(3 (4 ** 5))") # implicit op, then addition - self._test_one('3 4 + 5', '((3 4) + 5)') + self._test_one("3 4 + 5", "((3 4) + 5)") # power followed by implicit - self._test_one('3 ** 4 5', '((3 ** 4) 5)') + self._test_one("3 ** 4 5", "((3 ** 4) 5)") # implicit with parentheses - self._test_one('3 (4 ** 5)', '(3 (4 ** 5))') + self._test_one("3 (4 ** 5)", "(3 (4 ** 5))") # exponent with e - self._test_one('3e-1', '3e-1') + self._test_one("3e-1", "3e-1") # multiple units with exponents - self._test_one('kg ** 1 * s ** 2', '((kg ** 1) * (s ** 2))') + self._test_one("kg ** 1 * s ** 2", "((kg ** 1) * (s ** 2))") # multiple units with neg exponents - self._test_one('kg ** -1 * s ** -2', '((kg ** (- 1)) * (s ** (- 2)))') + self._test_one("kg ** -1 * s ** -2", "((kg ** (- 1)) * (s ** (- 2)))") # multiple units with neg exponents - self._test_one('kg^-1 * s^-2', '((kg ^ (- 1)) * (s ^ (- 2)))') + self._test_one("kg^-1 * s^-2", "((kg ^ (- 1)) * (s ^ (- 2)))") # multiple units with neg exponents, implicit op - self._test_one('kg^-1 s^-2', '((kg ^ (- 1)) (s ^ (- 2)))') + self._test_one("kg^-1 s^-2", "((kg ^ (- 1)) (s ^ (- 2)))") # nested power - self._test_one('2 ^ 3 ^ 2', '(2 ^ (3 ^ 2))') + self._test_one("2 ^ 3 ^ 2", "(2 ^ (3 ^ 2))") # nested power - self._test_one('gram * second / meter ** 2', '((gram * second) / (meter ** 2))') + self._test_one("gram * second / meter ** 2", "((gram * second) / (meter ** 2))") # nested power - self._test_one('gram / meter ** 2 / second', '((gram / (meter ** 2)) / second)') + self._test_one("gram / meter ** 2 / second", "((gram / (meter ** 2)) / second)") # units should behave like numbers, so we don't need a bunch of extra tests for them # implicit op, then addition - self._test_one('3 kg + 5', '((3 kg) + 5)') + self._test_one("3 kg + 5", "((3 kg) + 5)") diff --git a/pint/testsuite/test_pitheorem.py b/pint/testsuite/test_pitheorem.py index ce2026fca..ed6806398 100644 --- a/pint/testsuite/test_pitheorem.py +++ b/pint/testsuite/test_pitheorem.py @@ -14,18 +14,22 @@ def test_simple(self): # simple movement with self.capture_log() as buffer: - self.assertEqual(pi_theorem({'V': 'm/s', 'T': 's', 'L': 'm'}), - [{'V': 1, 'T': 1, 'L': -1}]) + self.assertEqual( + pi_theorem({"V": "m/s", "T": "s", "L": "m"}), + [{"V": 1, "T": 1, "L": -1}], + ) # pendulum - self.assertEqual(pi_theorem({'T': 's', 'M': 'grams', 'L': 'm', 'g': 'm/s**2'}), - [{'g': 1, 'T': 2, 'L': -1}]) + self.assertEqual( + pi_theorem({"T": "s", "M": "grams", "L": "m", "g": "m/s**2"}), + [{"g": 1, "T": 2, "L": -1}], + ) self.assertEqual(len(buffer), 7) def test_inputs(self): - V = 'km/hour' - T = 'ms' - L = 'cm' + V = "km/hour" + T = "ms" + L = "cm" f1 = lambda x: x f2 = lambda x: self.Q_(1, x) @@ -37,5 +41,7 @@ def test_inputs(self): qv = fv(V) qt = ft(T) ql = ft(L) - self.assertEqual(self.ureg.pi_theorem({'V': qv, 'T': qt, 'L': ql}), - [{'V': 1.0, 'T': 1.0, 'L': -1.0}]) + self.assertEqual( + self.ureg.pi_theorem({"V": qv, "T": qt, "L": ql}), + [{"V": 1.0, "T": 1.0, "L": -1.0}], + ) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 71c9e3521..3433cade5 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -19,12 +19,14 @@ class TestQuantity(QuantityTestCase): FORCE_NDARRAY = False def test_quantity_creation(self): - for args in ((4.2, 'meter'), - (4.2, UnitsContainer(meter=1)), - (4.2, self.ureg.meter), - ('4.2*meter', ), - ('4.2/meter**(-1)', ), - (self.Q_(4.2, 'meter'),)): + for args in ( + (4.2, "meter"), + (4.2, UnitsContainer(meter=1)), + (4.2, self.ureg.meter), + ("4.2*meter",), + ("4.2/meter**(-1)",), + (self.Q_(4.2, "meter"),), + ): x = self.Q_(*args) self.assertEqual(x.magnitude, 4.2) self.assertEqual(x.units, UnitsContainer(meter=1)) @@ -45,15 +47,15 @@ def test_quantity_creation(self): def test_quantity_bool(self): self.assertTrue(self.Q_(1, None)) - self.assertTrue(self.Q_(1, 'meter')) + self.assertTrue(self.Q_(1, "meter")) self.assertFalse(self.Q_(0, None)) - self.assertFalse(self.Q_(0, 'meter')) + self.assertFalse(self.Q_(0, "meter")) def test_quantity_comparison(self): - x = self.Q_(4.2, 'meter') - y = self.Q_(4.2, 'meter') - z = self.Q_(5, 'meter') - j = self.Q_(5, 'meter*meter') + x = self.Q_(4.2, "meter") + y = self.Q_(4.2, "meter") + z = self.Q_(5, "meter") + j = self.Q_(5, "meter*meter") # identity for single object self.assertTrue(x == x) @@ -75,25 +77,27 @@ def test_quantity_comparison(self): self.assertTrue(z != j) self.assertNotEqual(z, j) - self.assertEqual(self.Q_(0, 'meter'), self.Q_(0, 'centimeter')) - self.assertNotEqual(self.Q_(0, 'meter'), self.Q_(0, 'second')) + self.assertEqual(self.Q_(0, "meter"), self.Q_(0, "centimeter")) + self.assertNotEqual(self.Q_(0, "meter"), self.Q_(0, "second")) - self.assertLess(self.Q_(10, 'meter'), self.Q_(5, 'kilometer')) + self.assertLess(self.Q_(10, "meter"), self.Q_(5, "kilometer")) def test_quantity_comparison_convert(self): - self.assertEqual(self.Q_(1000, 'millimeter'), self.Q_(1, 'meter')) - self.assertEqual(self.Q_(1000, 'millimeter/min'), self.Q_(1000/60, 'millimeter/s')) + self.assertEqual(self.Q_(1000, "millimeter"), self.Q_(1, "meter")) + self.assertEqual( + self.Q_(1000, "millimeter/min"), self.Q_(1000 / 60, "millimeter/s") + ) def test_quantity_repr(self): x = self.Q_(4.2, UnitsContainer(meter=1)) - self.assertEqual(str(x), '4.2 meter') + self.assertEqual(str(x), "4.2 meter") self.assertEqual(repr(x), "") def test_quantity_hash(self): - x = self.Q_(4.2, 'meter') - x2 = self.Q_(4200, 'millimeter') - y = self.Q_(2, 'second') - z = self.Q_(0.5, 'hertz') + x = self.Q_(4.2, "meter") + x2 = self.Q_(4200, "millimeter") + y = self.Q_(2, "second") + z = self.Q_(0.5, "hertz") self.assertEqual(hash(x), hash(x2)) # Dimensionless equality @@ -101,93 +105,114 @@ def test_quantity_hash(self): # Dimensionless equality from a different unit registry ureg2 = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) - y2 = ureg2.Quantity(2, 'second') - z2 = ureg2.Quantity(0.5, 'hertz') + y2 = ureg2.Quantity(2, "second") + z2 = ureg2.Quantity(0.5, "hertz") self.assertEqual(hash(y * z), hash(y2 * z2)) def test_quantity_format(self): x = self.Q_(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( - ('{}', str(x)), - ('{!s}', str(x)), - ('{!r}', repr(x)), - ('{.magnitude}', str(x.magnitude)), - ('{.units}', str(x.units)), - ('{.magnitude!s}', str(x.magnitude)), - ('{.units!s}', str(x.units)), - ('{.magnitude!r}', repr(x.magnitude)), - ('{.units!r}', repr(x.units)), - ('{:.4f}', f'{x.magnitude:.4f} {x.units!s}'), - ('{:L}', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('{:P}', '4.12345678 kilogram·meter²/second'), - ('{:H}', '4.12345678 kilogram meter^2/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~}', '4.12345678 kg m^2/s'), - ('{:C~}', '4.12345678 kg*m**2/s'), - ('{:Lx}', r'\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}'), + ("{}", str(x)), + ("{!s}", str(x)), + ("{!r}", repr(x)), + ("{.magnitude}", str(x.magnitude)), + ("{.units}", str(x.units)), + ("{.magnitude!s}", str(x.magnitude)), + ("{.units!s}", str(x.units)), + ("{.magnitude!r}", repr(x.magnitude)), + ("{.units!r}", repr(x.units)), + ("{:.4f}", f"{x.magnitude:.4f} {x.units!s}"), + ( + "{:L}", + r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", + ), + ("{:P}", "4.12345678 kilogram·meter²/second"), + ("{:H}", "4.12345678 kilogram meter^2/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~}", "4.12345678 kg m^2/s"), + ("{:C~}", "4.12345678 kg*m**2/s"), + ("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"), ): with self.subTest(spec): self.assertEqual(spec.format(x), result) # Check the special case that prevents e.g. '3 1 / second' x = self.Q_(3, UnitsContainer(second=-1)) - self.assertEqual('{0}'.format(x), '3 / second') + self.assertEqual("{0}".format(x), "3 / second") @helpers.requires_numpy() def test_quantity_array_format(self): - x = self.Q_(np.array([1e-16, 1.0000001, 10000000.0, 1e12, np.nan, np.inf]), "kg * m ** 2") + x = self.Q_( + np.array([1e-16, 1.0000001, 10000000.0, 1e12, np.nan, np.inf]), + "kg * m ** 2", + ) for spec, result in ( - ('{}', str(x)), - ('{.magnitude}', str(x.magnitude)), - ('{:e}', "[1.000000e-16 1.000000e+00 1.000000e+07 1.000000e+12 nan inf] kilogram * meter ** 2"), - ('{:E}', "[1.000000E-16 1.000000E+00 1.000000E+07 1.000000E+12 NAN INF] kilogram * meter ** 2"), - ('{:.2f}', "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kilogram * meter ** 2"), - ('{:.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²"), + ("{}", str(x)), + ("{.magnitude}", str(x.magnitude)), + ( + "{:e}", + "[1.000000e-16 1.000000e+00 1.000000e+07 1.000000e+12 nan inf] kilogram * meter ** 2", + ), + ( + "{:E}", + "[1.000000E-16 1.000000E+00 1.000000E+07 1.000000E+12 NAN INF] kilogram * meter ** 2", + ), + ( + "{:.2f}", + "[0.00 1.00 10000000.00 1000000000000.00 nan inf] kilogram * meter ** 2", + ), + ("{:.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²"), ): with self.subTest(spec): self.assertEqual(spec.format(x), result) def test_format_compact(self): q1 = (200e-9 * self.ureg.s).to_compact() - q1b = self.Q_(200., 'nanosecond') + q1b = self.Q_(200.0, "nanosecond") self.assertAlmostEqual(q1.magnitude, q1b.magnitude) self.assertEqual(q1.units, q1b.units) - q2 = (1e-2 * self.ureg('kg m/s^2')).to_compact('N') - q2b = self.Q_(10., 'millinewton') + q2 = (1e-2 * self.ureg("kg m/s^2")).to_compact("N") + q2b = self.Q_(10.0, "millinewton") self.assertEqual(q2.magnitude, q2b.magnitude) self.assertEqual(q2.units, q2b.units) - q3 = (-1000.0 * self.ureg('meters')).to_compact() - q3b = self.Q_(-1., 'kilometer') + q3 = (-1000.0 * self.ureg("meters")).to_compact() + q3b = self.Q_(-1.0, "kilometer") self.assertEqual(q3.magnitude, q3b.magnitude) self.assertEqual(q3.units, q3b.units) - self.assertEqual('{0:#.1f}'.format(q1), '{0}'.format(q1b)) - self.assertEqual('{0:#.1f}'.format(q2), '{0}'.format(q2b)) - self.assertEqual('{0:#.1f}'.format(q3), '{0}'.format(q3b)) + self.assertEqual("{0:#.1f}".format(q1), "{0}".format(q1b)) + self.assertEqual("{0:#.1f}".format(q2), "{0}".format(q2b)) + self.assertEqual("{0:#.1f}".format(q3), "{0}".format(q3b)) def test_default_formatting(self): ureg = UnitRegistry() x = ureg.Quantity(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( - ('L', r'4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('P', '4.12345678 kilogram·meter²/second'), - ('H', '4.12345678 kilogram meter^2/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~', '4.12345678 kg m^2/s'), - ('C~', '4.12345678 kg*m**2/s'), + ( + "L", + r"4.12345678\ \frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", + ), + ("P", "4.12345678 kilogram·meter²/second"), + ("H", "4.12345678 kilogram meter^2/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~", "4.12345678 kg m^2/s"), + ("C~", "4.12345678 kg*m**2/s"), ): with self.subTest(spec): ureg.default_format = spec - self.assertEqual(f'{x}', result) + self.assertEqual(f"{x}", result) def test_exponent_formatting(self): ureg = UnitRegistry() @@ -215,42 +240,50 @@ def pretty(cls, data): ureg = UnitRegistry() x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), - "3.5 kilogram meter^2/second") - self.assertEqual(x._repr_latex_(), - r'$3.5\ \frac{\mathrm{kilogram} \cdot ' - r'\mathrm{meter}^{2}}{\mathrm{second}}$') + self.assertEqual(x._repr_html_(), "3.5 kilogram meter^2/second") + self.assertEqual( + x._repr_latex_(), + r"$3.5\ \frac{\mathrm{kilogram} \cdot " + r"\mathrm{meter}^{2}}{\mathrm{second}}$", + ) x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "3.5 kilogram·meter²/second") ureg.default_format = "~" self.assertEqual(x._repr_html_(), "3.5 kg m^2/s") - self.assertEqual(x._repr_latex_(), - r'$3.5\ \frac{\mathrm{kg} \cdot ' - r'\mathrm{m}^{2}}{\mathrm{s}}$') + self.assertEqual( + x._repr_latex_(), + r"$3.5\ \frac{\mathrm{kg} \cdot " r"\mathrm{m}^{2}}{\mathrm{s}}$", + ) alltext = [] x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "3.5 kg·m²/s") def test_to_base_units(self): - x = self.Q_('1*inch') - self.assertQuantityAlmostEqual(x.to_base_units(), self.Q_(0.0254, 'meter')) - x = self.Q_('1*inch*inch') - self.assertQuantityAlmostEqual(x.to_base_units(), self.Q_(0.0254 ** 2.0, 'meter*meter')) - x = self.Q_('1*inch/minute') - self.assertQuantityAlmostEqual(x.to_base_units(), self.Q_(0.0254 / 60., 'meter/second')) + x = self.Q_("1*inch") + self.assertQuantityAlmostEqual(x.to_base_units(), self.Q_(0.0254, "meter")) + x = self.Q_("1*inch*inch") + self.assertQuantityAlmostEqual( + x.to_base_units(), self.Q_(0.0254 ** 2.0, "meter*meter") + ) + x = self.Q_("1*inch/minute") + self.assertQuantityAlmostEqual( + x.to_base_units(), self.Q_(0.0254 / 60.0, "meter/second") + ) def test_convert(self): - x = self.Q_('2*inch') - self.assertQuantityAlmostEqual(x.to('meter'), self.Q_(2. * 0.0254, 'meter')) - x = self.Q_('2*meter') - self.assertQuantityAlmostEqual(x.to('inch'), self.Q_(2. / 0.0254, 'inch')) - x = self.Q_('2*sidereal_second') - self.assertQuantityAlmostEqual(x.to('second'), self.Q_(1.994539133 , 'second')) - x = self.Q_('2.54*centimeter/second') - self.assertQuantityAlmostEqual(x.to('inch/second'), self.Q_(1, 'inch/second')) - x = self.Q_('2.54*centimeter') - self.assertQuantityAlmostEqual(x.to('inch').magnitude, 1) - self.assertQuantityAlmostEqual(self.Q_(2, 'second').to('millisecond').magnitude, 2000) + x = self.Q_("2*inch") + self.assertQuantityAlmostEqual(x.to("meter"), self.Q_(2.0 * 0.0254, "meter")) + x = self.Q_("2*meter") + self.assertQuantityAlmostEqual(x.to("inch"), self.Q_(2.0 / 0.0254, "inch")) + x = self.Q_("2*sidereal_second") + self.assertQuantityAlmostEqual(x.to("second"), self.Q_(1.994539133, "second")) + x = self.Q_("2.54*centimeter/second") + self.assertQuantityAlmostEqual(x.to("inch/second"), self.Q_(1, "inch/second")) + x = self.Q_("2.54*centimeter") + self.assertQuantityAlmostEqual(x.to("inch").magnitude, 1) + self.assertQuantityAlmostEqual( + self.Q_(2, "second").to("millisecond").magnitude, 2000 + ) @helpers.requires_numpy() def test_convert(self): @@ -271,20 +304,24 @@ def test_convert(self): self.assertIsNot(r._magnitude, a) def test_convert_from(self): - x = self.Q_('2*inch') + x = self.Q_("2*inch") meter = self.ureg.meter # from quantity - self.assertQuantityAlmostEqual(meter.from_(x), self.Q_(2. * 0.0254, 'meter')) - self.assertQuantityAlmostEqual(meter.m_from(x), 2. * 0.0254) + self.assertQuantityAlmostEqual(meter.from_(x), self.Q_(2.0 * 0.0254, "meter")) + self.assertQuantityAlmostEqual(meter.m_from(x), 2.0 * 0.0254) # from unit - self.assertQuantityAlmostEqual(meter.from_(self.ureg.inch), self.Q_(0.0254, 'meter')) + self.assertQuantityAlmostEqual( + meter.from_(self.ureg.inch), self.Q_(0.0254, "meter") + ) self.assertQuantityAlmostEqual(meter.m_from(self.ureg.inch), 0.0254) # from number - self.assertQuantityAlmostEqual(meter.from_(2, strict=False), self.Q_(2., 'meter')) - self.assertQuantityAlmostEqual(meter.m_from(2, strict=False), 2.) + self.assertQuantityAlmostEqual( + meter.from_(2, strict=False), self.Q_(2.0, "meter") + ) + self.assertQuantityAlmostEqual(meter.m_from(2, strict=False), 2.0) # from number (strict mode) self.assertRaises(ValueError, meter.from_, 2) @@ -299,72 +336,140 @@ def test_retain_unit(self): self.assertEqual(q.u, q.reshape(2, 3).u) self.assertEqual(q.u, q.swapaxes(0, 1).u) self.assertEqual(q.u, q.mean().u) - self.assertEqual(q.u, np.compress((q==q[0,0]).any(0), q).u) + self.assertEqual(q.u, np.compress((q == q[0, 0]).any(0), q).u) def test_context_attr(self): - self.assertEqual(self.ureg.meter, self.Q_(1, 'meter')) + self.assertEqual(self.ureg.meter, self.Q_(1, "meter")) def test_both_symbol(self): - self.assertEqual(self.Q_(2, 'ms'), self.Q_(2, 'millisecond')) - self.assertEqual(self.Q_(2, 'cm'), self.Q_(2, 'centimeter')) + self.assertEqual(self.Q_(2, "ms"), self.Q_(2, "millisecond")) + self.assertEqual(self.Q_(2, "cm"), self.Q_(2, "centimeter")) def test_dimensionless_units(self): - self.assertAlmostEqual(self.Q_(360, 'degree').to('radian').magnitude, 2 * math.pi) - self.assertAlmostEqual(self.Q_(2 * math.pi, 'radian'), self.Q_(360, 'degree')) - self.assertEqual(self.Q_(1, 'radian').dimensionality, UnitsContainer()) - self.assertTrue(self.Q_(1, 'radian').dimensionless) - self.assertFalse(self.Q_(1, 'radian').unitless) - - self.assertEqual(self.Q_(1, 'meter')/self.Q_(1, 'meter'), 1) - self.assertEqual((self.Q_(1, 'meter')/self.Q_(1, 'mm')).to(''), 1000) - - self.assertEqual(self.Q_(10) // self.Q_(360, 'degree'), 1) - self.assertEqual(self.Q_(400, 'degree') // self.Q_(2 * math.pi), 1) - self.assertEqual(self.Q_(400, 'degree') // (2 * math.pi), 1) - self.assertEqual(7 // self.Q_(360, 'degree'), 1) + self.assertAlmostEqual( + self.Q_(360, "degree").to("radian").magnitude, 2 * math.pi + ) + self.assertAlmostEqual(self.Q_(2 * math.pi, "radian"), self.Q_(360, "degree")) + self.assertEqual(self.Q_(1, "radian").dimensionality, UnitsContainer()) + self.assertTrue(self.Q_(1, "radian").dimensionless) + self.assertFalse(self.Q_(1, "radian").unitless) + + self.assertEqual(self.Q_(1, "meter") / self.Q_(1, "meter"), 1) + self.assertEqual((self.Q_(1, "meter") / self.Q_(1, "mm")).to(""), 1000) + + self.assertEqual(self.Q_(10) // self.Q_(360, "degree"), 1) + self.assertEqual(self.Q_(400, "degree") // self.Q_(2 * math.pi), 1) + self.assertEqual(self.Q_(400, "degree") // (2 * math.pi), 1) + self.assertEqual(7 // self.Q_(360, "degree"), 1) def test_offset(self): - self.assertQuantityAlmostEqual(self.Q_(0, 'kelvin').to('kelvin'), self.Q_(0, 'kelvin')) - self.assertQuantityAlmostEqual(self.Q_(0, 'degC').to('kelvin'), self.Q_(273.15, 'kelvin')) - self.assertQuantityAlmostEqual(self.Q_(0, 'degF').to('kelvin'), self.Q_(255.372222, 'kelvin'), rtol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(100, 'kelvin').to('kelvin'), self.Q_(100, 'kelvin')) - self.assertQuantityAlmostEqual(self.Q_(100, 'degC').to('kelvin'), self.Q_(373.15, 'kelvin')) - self.assertQuantityAlmostEqual(self.Q_(100, 'degF').to('kelvin'), self.Q_(310.92777777, 'kelvin'), rtol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(0, 'kelvin').to('degC'), self.Q_(-273.15, 'degC')) - self.assertQuantityAlmostEqual(self.Q_(100, 'kelvin').to('degC'), self.Q_(-173.15, 'degC')) - self.assertQuantityAlmostEqual(self.Q_(0, 'kelvin').to('degF'), self.Q_(-459.67, 'degF'), rtol=0.01) - self.assertQuantityAlmostEqual(self.Q_(100, 'kelvin').to('degF'), self.Q_(-279.67, 'degF'), rtol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(32, 'degF').to('degC'), self.Q_(0, 'degC'), atol=0.01) - self.assertQuantityAlmostEqual(self.Q_(100, 'degC').to('degF'), self.Q_(212, 'degF'), atol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(54, 'degF').to('degC'), self.Q_(12.2222, 'degC'), atol=0.01) - self.assertQuantityAlmostEqual(self.Q_(12, 'degC').to('degF'), self.Q_(53.6, 'degF'), atol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(12, 'kelvin').to('degC'), self.Q_(-261.15, 'degC'), atol=0.01) - self.assertQuantityAlmostEqual(self.Q_(12, 'degC').to('kelvin'), self.Q_(285.15, 'kelvin'), atol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(12, 'kelvin').to('degR'), self.Q_(21.6, 'degR'), atol=0.01) - self.assertQuantityAlmostEqual(self.Q_(12, 'degR').to('kelvin'), self.Q_(6.66666667, 'kelvin'), atol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(12, 'degC').to('degR'), self.Q_(513.27, 'degR'), atol=0.01) - self.assertQuantityAlmostEqual(self.Q_(12, 'degR').to('degC'), self.Q_(-266.483333, 'degC'), atol=0.01) - + self.assertQuantityAlmostEqual( + self.Q_(0, "kelvin").to("kelvin"), self.Q_(0, "kelvin") + ) + self.assertQuantityAlmostEqual( + self.Q_(0, "degC").to("kelvin"), self.Q_(273.15, "kelvin") + ) + self.assertQuantityAlmostEqual( + self.Q_(0, "degF").to("kelvin"), self.Q_(255.372222, "kelvin"), rtol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(100, "kelvin").to("kelvin"), self.Q_(100, "kelvin") + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "degC").to("kelvin"), self.Q_(373.15, "kelvin") + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "degF").to("kelvin"), + self.Q_(310.92777777, "kelvin"), + rtol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.Q_(0, "kelvin").to("degC"), self.Q_(-273.15, "degC") + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "kelvin").to("degC"), self.Q_(-173.15, "degC") + ) + self.assertQuantityAlmostEqual( + self.Q_(0, "kelvin").to("degF"), self.Q_(-459.67, "degF"), rtol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "kelvin").to("degF"), self.Q_(-279.67, "degF"), rtol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(32, "degF").to("degC"), self.Q_(0, "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "degC").to("degF"), self.Q_(212, "degF"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(54, "degF").to("degC"), self.Q_(12.2222, "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(12, "degC").to("degF"), self.Q_(53.6, "degF"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(12, "kelvin").to("degC"), self.Q_(-261.15, "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(12, "degC").to("kelvin"), self.Q_(285.15, "kelvin"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(12, "kelvin").to("degR"), self.Q_(21.6, "degR"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(12, "degR").to("kelvin"), self.Q_(6.66666667, "kelvin"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(12, "degC").to("degR"), self.Q_(513.27, "degR"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.Q_(12, "degR").to("degC"), self.Q_(-266.483333, "degC"), atol=0.01 + ) def test_offset_delta(self): - self.assertQuantityAlmostEqual(self.Q_(0, 'delta_degC').to('kelvin'), self.Q_(0, 'kelvin')) - self.assertQuantityAlmostEqual(self.Q_(0, 'delta_degF').to('kelvin'), self.Q_(0, 'kelvin'), rtol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(100, 'kelvin').to('delta_degC'), self.Q_(100, 'delta_degC')) - self.assertQuantityAlmostEqual(self.Q_(100, 'kelvin').to('delta_degF'), self.Q_(180, 'delta_degF'), rtol=0.01) - self.assertQuantityAlmostEqual(self.Q_(100, 'delta_degF').to('kelvin'), self.Q_(55.55555556, 'kelvin'), rtol=0.01) - self.assertQuantityAlmostEqual(self.Q_(100, 'delta_degC').to('delta_degF'), self.Q_(180, 'delta_degF'), rtol=0.01) - self.assertQuantityAlmostEqual(self.Q_(100, 'delta_degF').to('delta_degC'), self.Q_(55.55555556, 'delta_degC'), rtol=0.01) - - self.assertQuantityAlmostEqual(self.Q_(12.3, 'delta_degC').to('delta_degF'), self.Q_(22.14, 'delta_degF'), rtol=0.01) - + self.assertQuantityAlmostEqual( + self.Q_(0, "delta_degC").to("kelvin"), self.Q_(0, "kelvin") + ) + self.assertQuantityAlmostEqual( + self.Q_(0, "delta_degF").to("kelvin"), self.Q_(0, "kelvin"), rtol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.Q_(100, "kelvin").to("delta_degC"), self.Q_(100, "delta_degC") + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "kelvin").to("delta_degF"), + self.Q_(180, "delta_degF"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "delta_degF").to("kelvin"), + self.Q_(55.55555556, "kelvin"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "delta_degC").to("delta_degF"), + self.Q_(180, "delta_degF"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.Q_(100, "delta_degF").to("delta_degC"), + self.Q_(55.55555556, "delta_degC"), + rtol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.Q_(12.3, "delta_degC").to("delta_degF"), + self.Q_(22.14, "delta_degF"), + rtol=0.01, + ) def test_pickle(self): import pickle @@ -372,17 +477,16 @@ def test_pickle(self): def pickle_test(q): self.assertEqual(q, pickle.loads(pickle.dumps(q))) - pickle_test(self.Q_(32, '')) - pickle_test(self.Q_(2.4, '')) - pickle_test(self.Q_(32, 'm/s')) - pickle_test(self.Q_(2.4, 'm/s')) - + pickle_test(self.Q_(32, "")) + pickle_test(self.Q_(2.4, "")) + pickle_test(self.Q_(32, "m/s")) + pickle_test(self.Q_(2.4, "m/s")) @helpers.requires_numpy() def test_from_sequence(self): - u_array_ref = self.Q_([200, 1000], 'g') - u_array_ref_reversed = self.Q_([1000, 200], 'g') - u_seq = [self.Q_('200g'), self.Q_('1kg')] + u_array_ref = self.Q_([200, 1000], "g") + u_array_ref_reversed = self.Q_([1000, 200], "g") + u_seq = [self.Q_("200g"), self.Q_("1kg")] u_seq_reversed = u_seq[::-1] u_array = self.Q_.from_sequence(u_seq) @@ -392,7 +496,7 @@ def test_from_sequence(self): self.assertTrue(all(u_array_2 == u_array_ref_reversed)) self.assertFalse(u_array_2.u == u_array_ref_reversed.u) - u_array_3 = self.Q_.from_sequence(u_seq_reversed, units='g') + u_array_3 = self.Q_.from_sequence(u_seq_reversed, units="g") self.assertTrue(all(u_array_3 == u_array_ref_reversed)) self.assertTrue(u_array_3.u == u_array_ref_reversed.u) @@ -405,88 +509,87 @@ def test_from_sequence(self): @helpers.requires_numpy() def test_iter(self): # Verify that iteration gives element as Quantity with same units - x = self.Q_([0, 1, 2, 3], 'm') - self.assertQuantityEqual(next(iter(x)), self.Q_(0, 'm')) + x = self.Q_([0, 1, 2, 3], "m") + self.assertQuantityEqual(next(iter(x)), self.Q_(0, "m")) def test_notiter(self): # Verify that iter() crashes immediately, without needing to draw any # element from it, if the magnitude isn't iterable - x = self.Q_(1, 'm') + x = self.Q_(1, "m") with self.assertRaises(TypeError): iter(x) @helpers.requires_array_function_protocol() - @patch('pint.quantity.SKIP_ARRAY_FUNCTION_CHANGE_WARNING', False) + @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_([]) with warnings.catch_warnings(): - warnings.filterwarnings('error') + warnings.filterwarnings("error") self.Q_([]) class TestQuantityToCompact(QuantityTestCase): - def assertQuantityAlmostIdentical(self, q1, q2): self.assertEqual(q1.units, q2.units) self.assertAlmostEqual(q1.magnitude, q2.magnitude) def compareQuantity_compact(self, q, expected_compact, unit=None): - self.assertQuantityAlmostIdentical(q.to_compact(unit=unit), - expected_compact) + self.assertQuantityAlmostIdentical(q.to_compact(unit=unit), expected_compact) def test_dimensionally_simple_units(self): ureg = self.ureg - self.compareQuantity_compact(1*ureg.m, 1*ureg.m) - self.compareQuantity_compact(1e-9*ureg.m, 1*ureg.nm) + self.compareQuantity_compact(1 * ureg.m, 1 * ureg.m) + self.compareQuantity_compact(1e-9 * ureg.m, 1 * ureg.nm) def test_power_units(self): ureg = self.ureg - self.compareQuantity_compact(900*ureg.m**2, 900*ureg.m**2) - self.compareQuantity_compact(1e7*ureg.m**2, 10*ureg.km**2) + self.compareQuantity_compact(900 * ureg.m ** 2, 900 * ureg.m ** 2) + self.compareQuantity_compact(1e7 * ureg.m ** 2, 10 * ureg.km ** 2) def test_inverse_units(self): ureg = self.ureg - self.compareQuantity_compact(1/ureg.m, 1/ureg.m) - self.compareQuantity_compact(100e9/ureg.m, 100/ureg.nm) + self.compareQuantity_compact(1 / ureg.m, 1 / ureg.m) + self.compareQuantity_compact(100e9 / ureg.m, 100 / ureg.nm) def test_inverse_square_units(self): ureg = self.ureg - self.compareQuantity_compact(1/ureg.m**2, 1/ureg.m**2) - self.compareQuantity_compact(1e11/ureg.m**2, 1e5/ureg.mm**2) + self.compareQuantity_compact(1 / ureg.m ** 2, 1 / ureg.m ** 2) + self.compareQuantity_compact(1e11 / ureg.m ** 2, 1e5 / ureg.mm ** 2) def test_fractional_units(self): ureg = self.ureg # Typing denominator first to provoke potential error - self.compareQuantity_compact(20e3*ureg('hr^(-1) m'), - 20*ureg.km/ureg.hr) + self.compareQuantity_compact(20e3 * ureg("hr^(-1) m"), 20 * ureg.km / ureg.hr) def test_fractional_exponent_units(self): ureg = self.ureg - self.compareQuantity_compact(1*ureg.m**0.5, 1*ureg.m**0.5) - self.compareQuantity_compact(1e-2*ureg.m**0.5, 10*ureg.um**0.5) + self.compareQuantity_compact(1 * ureg.m ** 0.5, 1 * ureg.m ** 0.5) + self.compareQuantity_compact(1e-2 * ureg.m ** 0.5, 10 * ureg.um ** 0.5) def test_derived_units(self): ureg = self.ureg - self.compareQuantity_compact(0.5*ureg.megabyte, 500*ureg.kilobyte) - self.compareQuantity_compact(1e-11*ureg.N, 10*ureg.pN) + self.compareQuantity_compact(0.5 * ureg.megabyte, 500 * ureg.kilobyte) + self.compareQuantity_compact(1e-11 * ureg.N, 10 * ureg.pN) def test_unit_parameter(self): ureg = self.ureg - self.compareQuantity_compact(self.Q_(100e-9, 'kg m / s^2'), - 100*ureg.nN, ureg.N) - self.compareQuantity_compact(self.Q_(101.3e3, 'kg/m/s^2'), - 101.3*ureg.kPa, ureg.Pa) + self.compareQuantity_compact( + self.Q_(100e-9, "kg m / s^2"), 100 * ureg.nN, ureg.N + ) + self.compareQuantity_compact( + self.Q_(101.3e3, "kg/m/s^2"), 101.3 * ureg.kPa, ureg.Pa + ) def test_limits_magnitudes(self): ureg = self.ureg - self.compareQuantity_compact(0*ureg.m, 0*ureg.m) - self.compareQuantity_compact(float('inf')*ureg.m, float('inf')*ureg.m) + self.compareQuantity_compact(0 * ureg.m, 0 * ureg.m) + self.compareQuantity_compact(float("inf") * ureg.m, float("inf") * ureg.m) def test_nonnumeric_magnitudes(self): ureg = self.ureg - x = "some string"*ureg.m + x = "some string" * ureg.m with self.assertWarns(RuntimeWarning): self.compareQuantity_compact(x, x) @@ -547,91 +650,91 @@ def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None self.assertNotEqual(id(result), id2) def _test_quantity_add_sub(self, unit, func): - x = self.Q_(unit, 'centimeter') - y = self.Q_(unit, 'inch') - z = self.Q_(unit, 'second') + x = self.Q_(unit, "centimeter") + y = self.Q_(unit, "inch") + z = self.Q_(unit, "second") a = self.Q_(unit, None) - func(op.add, x, x, self.Q_(unit + unit, 'centimeter')) - func(op.add, x, y, self.Q_(unit + 2.54 * unit, 'centimeter')) - func(op.add, y, x, self.Q_(unit + unit / (2.54 * unit), 'inch')) + func(op.add, x, x, self.Q_(unit + unit, "centimeter")) + func(op.add, x, y, self.Q_(unit + 2.54 * unit, "centimeter")) + func(op.add, y, x, self.Q_(unit + unit / (2.54 * unit), "inch")) func(op.add, a, unit, self.Q_(unit + unit, None)) self.assertRaises(DimensionalityError, op.add, 10, x) self.assertRaises(DimensionalityError, op.add, x, 10) self.assertRaises(DimensionalityError, op.add, x, z) - func(op.sub, x, x, self.Q_(unit - unit, 'centimeter')) - func(op.sub, x, y, self.Q_(unit - 2.54 * unit, 'centimeter')) - func(op.sub, y, x, self.Q_(unit - unit / (2.54 * unit), 'inch')) + func(op.sub, x, x, self.Q_(unit - unit, "centimeter")) + func(op.sub, x, y, self.Q_(unit - 2.54 * unit, "centimeter")) + func(op.sub, y, x, self.Q_(unit - unit / (2.54 * unit), "inch")) func(op.sub, a, unit, self.Q_(unit - unit, None)) self.assertRaises(DimensionalityError, op.sub, 10, x) self.assertRaises(DimensionalityError, op.sub, x, 10) self.assertRaises(DimensionalityError, op.sub, x, z) def _test_quantity_iadd_isub(self, unit, func): - x = self.Q_(unit, 'centimeter') - y = self.Q_(unit, 'inch') - z = self.Q_(unit, 'second') + x = self.Q_(unit, "centimeter") + y = self.Q_(unit, "inch") + z = self.Q_(unit, "second") a = self.Q_(unit, None) - func(op.iadd, x, x, self.Q_(unit + unit, 'centimeter')) - func(op.iadd, x, y, self.Q_(unit + 2.54 * unit, 'centimeter')) - func(op.iadd, y, x, self.Q_(unit + unit / 2.54, 'inch')) + func(op.iadd, x, x, self.Q_(unit + unit, "centimeter")) + func(op.iadd, x, y, self.Q_(unit + 2.54 * unit, "centimeter")) + func(op.iadd, y, x, self.Q_(unit + unit / 2.54, "inch")) func(op.iadd, a, unit, self.Q_(unit + unit, None)) self.assertRaises(DimensionalityError, op.iadd, 10, x) self.assertRaises(DimensionalityError, op.iadd, x, 10) self.assertRaises(DimensionalityError, op.iadd, x, z) - func(op.isub, x, x, self.Q_(unit - unit, 'centimeter')) - func(op.isub, x, y, self.Q_(unit - 2.54, 'centimeter')) - func(op.isub, y, x, self.Q_(unit - unit / 2.54, 'inch')) + func(op.isub, x, x, self.Q_(unit - unit, "centimeter")) + func(op.isub, x, y, self.Q_(unit - 2.54, "centimeter")) + func(op.isub, y, x, self.Q_(unit - unit / 2.54, "inch")) func(op.isub, a, unit, self.Q_(unit - unit, None)) self.assertRaises(DimensionalityError, op.sub, 10, x) self.assertRaises(DimensionalityError, op.sub, x, 10) self.assertRaises(DimensionalityError, op.sub, x, z) def _test_quantity_mul_div(self, unit, func): - func(op.mul, unit * 10.0, '4.2*meter', '42*meter', unit) - func(op.mul, '4.2*meter', unit * 10.0, '42*meter', unit) - func(op.mul, '4.2*meter', '10*inch', '42*meter*inch', unit) - func(op.truediv, unit * 42, '4.2*meter', '10/meter', unit) - func(op.truediv, '4.2*meter', unit * 10.0, '0.42*meter', unit) - func(op.truediv, '4.2*meter', '10*inch', '0.42*meter/inch', unit) + func(op.mul, unit * 10.0, "4.2*meter", "42*meter", unit) + func(op.mul, "4.2*meter", unit * 10.0, "42*meter", unit) + func(op.mul, "4.2*meter", "10*inch", "42*meter*inch", unit) + func(op.truediv, unit * 42, "4.2*meter", "10/meter", unit) + func(op.truediv, "4.2*meter", unit * 10.0, "0.42*meter", unit) + func(op.truediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit) def _test_quantity_imul_idiv(self, unit, func): - #func(op.imul, 10.0, '4.2*meter', '42*meter') - func(op.imul, '4.2*meter', 10.0, '42*meter', unit) - func(op.imul, '4.2*meter', '10*inch', '42*meter*inch', unit) - #func(op.truediv, 42, '4.2*meter', '10/meter') - func(op.itruediv, '4.2*meter', unit * 10.0, '0.42*meter', unit) - func(op.itruediv, '4.2*meter', '10*inch', '0.42*meter/inch', unit) + # func(op.imul, 10.0, '4.2*meter', '42*meter') + func(op.imul, "4.2*meter", 10.0, "42*meter", unit) + func(op.imul, "4.2*meter", "10*inch", "42*meter*inch", unit) + # func(op.truediv, 42, '4.2*meter', '10/meter') + func(op.itruediv, "4.2*meter", unit * 10.0, "0.42*meter", unit) + func(op.itruediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit) def _test_quantity_floordiv(self, unit, func): - a = self.Q_('10*meter') - b = self.Q_('3*second') + a = self.Q_("10*meter") + b = self.Q_("3*second") self.assertRaises(DimensionalityError, op.floordiv, a, b) self.assertRaises(DimensionalityError, op.floordiv, 3, b) self.assertRaises(DimensionalityError, op.floordiv, a, 3) self.assertRaises(DimensionalityError, op.ifloordiv, a, b) self.assertRaises(DimensionalityError, op.ifloordiv, 3, b) self.assertRaises(DimensionalityError, op.ifloordiv, a, 3) - func(op.floordiv, unit * 10.0, '4.2*meter/meter', 2, unit) - func(op.floordiv, '10*meter', '4.2*inch', 93, unit) + func(op.floordiv, unit * 10.0, "4.2*meter/meter", 2, unit) + func(op.floordiv, "10*meter", "4.2*inch", 93, unit) def _test_quantity_mod(self, unit, func): - a = self.Q_('10*meter') - b = self.Q_('3*second') + a = self.Q_("10*meter") + b = self.Q_("3*second") self.assertRaises(DimensionalityError, op.mod, a, b) self.assertRaises(DimensionalityError, op.mod, 3, b) self.assertRaises(DimensionalityError, op.mod, a, 3) self.assertRaises(DimensionalityError, op.imod, a, b) self.assertRaises(DimensionalityError, op.imod, 3, b) self.assertRaises(DimensionalityError, op.imod, a, 3) - func(op.mod, unit * 10.0, '4.2*meter/meter', 1.6, unit) + func(op.mod, unit * 10.0, "4.2*meter/meter", 1.6, unit) def _test_quantity_ifloordiv(self, unit, func): - func(op.ifloordiv, 10.0, '4.2*meter/meter', 2, unit) - func(op.ifloordiv, '10*meter', '4.2*inch', 93, unit) + func(op.ifloordiv, 10.0, "4.2*meter/meter", 2, unit) + func(op.ifloordiv, "10*meter", "4.2*inch", 93, unit) def _test_quantity_divmod_one(self, a, b): if isinstance(a, str): @@ -661,19 +764,19 @@ def _test_quantity_divmod_one(self, a, b): self.assertEqual(copy_a, q) def _test_quantity_divmod(self): - self._test_quantity_divmod_one('10*meter', '4.2*inch') - self._test_quantity_divmod_one('-10*meter', '4.2*inch') - self._test_quantity_divmod_one('-10*meter', '-4.2*inch') - self._test_quantity_divmod_one('10*meter', '-4.2*inch') - - self._test_quantity_divmod_one('400*degree', '3') - self._test_quantity_divmod_one('4', '180 degree') - self._test_quantity_divmod_one(4, '180 degree') - self._test_quantity_divmod_one('20', 4) - self._test_quantity_divmod_one('300*degree', '100 degree') - - a = self.Q_('10*meter') - b = self.Q_('3*second') + self._test_quantity_divmod_one("10*meter", "4.2*inch") + self._test_quantity_divmod_one("-10*meter", "4.2*inch") + self._test_quantity_divmod_one("-10*meter", "-4.2*inch") + self._test_quantity_divmod_one("10*meter", "-4.2*inch") + + self._test_quantity_divmod_one("400*degree", "3") + self._test_quantity_divmod_one("4", "180 degree") + self._test_quantity_divmod_one(4, "180 degree") + self._test_quantity_divmod_one("20", 4) + self._test_quantity_divmod_one("300*degree", "100 degree") + + a = self.Q_("10*meter") + b = self.Q_("3*second") self.assertRaises(DimensionalityError, divmod, a, b) self.assertRaises(DimensionalityError, divmod, 3, b) self.assertRaises(DimensionalityError, divmod, a, 3) @@ -686,13 +789,14 @@ def _test_numeric(self, unit, ifunc): self._test_quantity_floordiv(unit, self._test_not_inplace) self._test_quantity_mod(unit, self._test_not_inplace) self._test_quantity_divmod() - #self._test_quantity_ifloordiv(unit, ifunc) + # self._test_quantity_ifloordiv(unit, ifunc) def test_float(self): - self._test_numeric(1., self._test_not_inplace) + self._test_numeric(1.0, self._test_not_inplace) def test_fraction(self): import fractions + self._test_numeric(fractions.Fraction(1, 1), self._test_not_inplace) @helpers.requires_numpy() @@ -701,23 +805,23 @@ def test_nparray(self): def test_quantity_abs_round(self): - x = self.Q_(-4.2, 'meter') - y = self.Q_(4.2, 'meter') + x = self.Q_(-4.2, "meter") + y = self.Q_(4.2, "meter") for fun in (abs, round, op.pos, op.neg): - zx = self.Q_(fun(x.magnitude), 'meter') - zy = self.Q_(fun(y.magnitude), 'meter') + zx = self.Q_(fun(x.magnitude), "meter") + zy = self.Q_(fun(y.magnitude), "meter") rx = fun(x) ry = fun(y) - self.assertEqual(rx, zx, 'while testing {0}'.format(fun)) - self.assertEqual(ry, zy, 'while testing {0}'.format(fun)) - self.assertIsNot(rx, zx, 'while testing {0}'.format(fun)) - self.assertIsNot(ry, zy, 'while testing {0}'.format(fun)) + self.assertEqual(rx, zx, "while testing {0}".format(fun)) + self.assertEqual(ry, zy, "while testing {0}".format(fun)) + self.assertIsNot(rx, zx, "while testing {0}".format(fun)) + self.assertIsNot(ry, zy, "while testing {0}".format(fun)) def test_quantity_float_complex(self): x = self.Q_(-4.2, None) y = self.Q_(4.2, None) - z = self.Q_(1, 'meter') + z = self.Q_(1, "meter") for fun in (float, complex): self.assertEqual(fun(x), fun(x.magnitude)) self.assertEqual(fun(y), fun(y.magnitude)) @@ -730,471 +834,452 @@ class TestDimensions(QuantityTestCase): def test_get_dimensionality(self): get = self.ureg.get_dimensionality - self.assertEqual(get('[time]'), UnitsContainer({'[time]': 1})) - self.assertEqual(get(UnitsContainer({'[time]': 1})), UnitsContainer({'[time]': 1})) - self.assertEqual(get('seconds'), UnitsContainer({'[time]': 1})) - self.assertEqual(get(UnitsContainer({'seconds': 1})), UnitsContainer({'[time]': 1})) - self.assertEqual(get('[speed]'), UnitsContainer({'[length]': 1, '[time]': -1})) - self.assertEqual(get('[acceleration]'), UnitsContainer({'[length]': 1, '[time]': -2})) + self.assertEqual(get("[time]"), UnitsContainer({"[time]": 1})) + self.assertEqual( + get(UnitsContainer({"[time]": 1})), UnitsContainer({"[time]": 1}) + ) + self.assertEqual(get("seconds"), UnitsContainer({"[time]": 1})) + self.assertEqual( + get(UnitsContainer({"seconds": 1})), UnitsContainer({"[time]": 1}) + ) + self.assertEqual(get("[speed]"), UnitsContainer({"[length]": 1, "[time]": -1})) + self.assertEqual( + get("[acceleration]"), UnitsContainer({"[length]": 1, "[time]": -2}) + ) def test_dimensionality(self): - x = self.Q_(42, 'centimeter') + x = self.Q_(42, "centimeter") x.to_base_units() - x = self.Q_(42, 'meter*second') - self.assertEqual(x.dimensionality, UnitsContainer({'[length]': 1., '[time]': 1.})) - x = self.Q_(42, 'meter*second*second') - self.assertEqual(x.dimensionality, UnitsContainer({'[length]': 1., '[time]': 2.})) - x = self.Q_(42, 'inch*second*second') - self.assertEqual(x.dimensionality, UnitsContainer({'[length]': 1., '[time]': 2.})) + x = self.Q_(42, "meter*second") + self.assertEqual( + x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 1.0}) + ) + x = self.Q_(42, "meter*second*second") + self.assertEqual( + x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 2.0}) + ) + x = self.Q_(42, "inch*second*second") + self.assertEqual( + x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 2.0}) + ) self.assertTrue(self.Q_(42, None).dimensionless) - self.assertFalse(self.Q_(42, 'meter').dimensionless) - self.assertTrue((self.Q_(42, 'meter') / self.Q_(1, 'meter')).dimensionless) - self.assertFalse((self.Q_(42, 'meter') / self.Q_(1, 'second')).dimensionless) - self.assertTrue((self.Q_(42, 'meter') / self.Q_(1, 'inch')).dimensionless) + self.assertFalse(self.Q_(42, "meter").dimensionless) + self.assertTrue((self.Q_(42, "meter") / self.Q_(1, "meter")).dimensionless) + self.assertFalse((self.Q_(42, "meter") / self.Q_(1, "second")).dimensionless) + self.assertTrue((self.Q_(42, "meter") / self.Q_(1, "inch")).dimensionless) def test_inclusion(self): - dim = self.Q_(42, 'meter').dimensionality - self.assertTrue('[length]' in dim) - self.assertFalse('[time]' in dim) - dim = (self.Q_(42, 'meter') / self.Q_(11, 'second')).dimensionality - self.assertTrue('[length]' in dim) - self.assertTrue('[time]' in dim) - dim = self.Q_(20.785, 'J/(mol)').dimensionality - for dimension in ('[length]', '[mass]', '[substance]', '[time]'): + dim = self.Q_(42, "meter").dimensionality + self.assertTrue("[length]" in dim) + self.assertFalse("[time]" in dim) + dim = (self.Q_(42, "meter") / self.Q_(11, "second")).dimensionality + self.assertTrue("[length]" in dim) + self.assertTrue("[time]" in dim) + dim = self.Q_(20.785, "J/(mol)").dimensionality + for dimension in ("[length]", "[mass]", "[substance]", "[time]"): self.assertTrue(dimension in dim) - self.assertFalse('[angle]' in dim) + self.assertFalse("[angle]" in dim) class TestQuantityWithDefaultRegistry(TestDimensions): - @classmethod def setUpClass(cls): from pint import _DEFAULT_REGISTRY + cls.ureg = _DEFAULT_REGISTRY cls.Q_ = cls.ureg.Quantity class TestDimensionsWithDefaultRegistry(TestDimensions): - @classmethod def setUpClass(cls): from pint import _DEFAULT_REGISTRY + cls.ureg = _DEFAULT_REGISTRY cls.Q_ = cls.ureg.Quantity class TestOffsetUnitMath(QuantityTestCase, ParameterizedTestCase): - def setup(self): self.ureg.autoconvert_offset_to_baseunit = False self.ureg.default_as_delta = True additions = [ # --- input tuple -------------------- | -- expected result -- - (((100, 'kelvin'), (10, 'kelvin')), (110, 'kelvin')), - (((100, 'kelvin'), (10, 'degC')), 'error'), - (((100, 'kelvin'), (10, 'degF')), 'error'), - (((100, 'kelvin'), (10, 'degR')), (105.56, 'kelvin')), - (((100, 'kelvin'), (10, 'delta_degC')), (110, 'kelvin')), - (((100, 'kelvin'), (10, 'delta_degF')), (105.56, 'kelvin')), - - (((100, 'degC'), (10, 'kelvin')), 'error'), - (((100, 'degC'), (10, 'degC')), 'error'), - (((100, 'degC'), (10, 'degF')), 'error'), - (((100, 'degC'), (10, 'degR')), 'error'), - (((100, 'degC'), (10, 'delta_degC')), (110, 'degC')), - (((100, 'degC'), (10, 'delta_degF')), (105.56, 'degC')), - - (((100, 'degF'), (10, 'kelvin')), 'error'), - (((100, 'degF'), (10, 'degC')), 'error'), - (((100, 'degF'), (10, 'degF')), 'error'), - (((100, 'degF'), (10, 'degR')), 'error'), - (((100, 'degF'), (10, 'delta_degC')), (118, 'degF')), - (((100, 'degF'), (10, 'delta_degF')), (110, 'degF')), - - (((100, 'degR'), (10, 'kelvin')), (118, 'degR')), - (((100, 'degR'), (10, 'degC')), 'error'), - (((100, 'degR'), (10, 'degF')), 'error'), - (((100, 'degR'), (10, 'degR')), (110, 'degR')), - (((100, 'degR'), (10, 'delta_degC')), (118, 'degR')), - (((100, 'degR'), (10, 'delta_degF')), (110, 'degR')), - - (((100, 'delta_degC'), (10, 'kelvin')), (110, 'kelvin')), - (((100, 'delta_degC'), (10, 'degC')), (110, 'degC')), - (((100, 'delta_degC'), (10, 'degF')), (190, 'degF')), - (((100, 'delta_degC'), (10, 'degR')), (190, 'degR')), - (((100, 'delta_degC'), (10, 'delta_degC')), (110, 'delta_degC')), - (((100, 'delta_degC'), (10, 'delta_degF')), (105.56, 'delta_degC')), - - (((100, 'delta_degF'), (10, 'kelvin')), (65.56, 'kelvin')), - (((100, 'delta_degF'), (10, 'degC')), (65.56, 'degC')), - (((100, 'delta_degF'), (10, 'degF')), (110, 'degF')), - (((100, 'delta_degF'), (10, 'degR')), (110, 'degR')), - (((100, 'delta_degF'), (10, 'delta_degC')), (118, 'delta_degF')), - (((100, 'delta_degF'), (10, 'delta_degF')), (110, 'delta_degF')), - ] - - @ParameterizedTestCase.parameterize(("input", "expected_output"), - additions) + (((100, "kelvin"), (10, "kelvin")), (110, "kelvin")), + (((100, "kelvin"), (10, "degC")), "error"), + (((100, "kelvin"), (10, "degF")), "error"), + (((100, "kelvin"), (10, "degR")), (105.56, "kelvin")), + (((100, "kelvin"), (10, "delta_degC")), (110, "kelvin")), + (((100, "kelvin"), (10, "delta_degF")), (105.56, "kelvin")), + (((100, "degC"), (10, "kelvin")), "error"), + (((100, "degC"), (10, "degC")), "error"), + (((100, "degC"), (10, "degF")), "error"), + (((100, "degC"), (10, "degR")), "error"), + (((100, "degC"), (10, "delta_degC")), (110, "degC")), + (((100, "degC"), (10, "delta_degF")), (105.56, "degC")), + (((100, "degF"), (10, "kelvin")), "error"), + (((100, "degF"), (10, "degC")), "error"), + (((100, "degF"), (10, "degF")), "error"), + (((100, "degF"), (10, "degR")), "error"), + (((100, "degF"), (10, "delta_degC")), (118, "degF")), + (((100, "degF"), (10, "delta_degF")), (110, "degF")), + (((100, "degR"), (10, "kelvin")), (118, "degR")), + (((100, "degR"), (10, "degC")), "error"), + (((100, "degR"), (10, "degF")), "error"), + (((100, "degR"), (10, "degR")), (110, "degR")), + (((100, "degR"), (10, "delta_degC")), (118, "degR")), + (((100, "degR"), (10, "delta_degF")), (110, "degR")), + (((100, "delta_degC"), (10, "kelvin")), (110, "kelvin")), + (((100, "delta_degC"), (10, "degC")), (110, "degC")), + (((100, "delta_degC"), (10, "degF")), (190, "degF")), + (((100, "delta_degC"), (10, "degR")), (190, "degR")), + (((100, "delta_degC"), (10, "delta_degC")), (110, "delta_degC")), + (((100, "delta_degC"), (10, "delta_degF")), (105.56, "delta_degC")), + (((100, "delta_degF"), (10, "kelvin")), (65.56, "kelvin")), + (((100, "delta_degF"), (10, "degC")), (65.56, "degC")), + (((100, "delta_degF"), (10, "degF")), (110, "degF")), + (((100, "delta_degF"), (10, "degR")), (110, "degR")), + (((100, "delta_degF"), (10, "delta_degC")), (118, "delta_degF")), + (((100, "delta_degF"), (10, "delta_degF")), (110, "delta_degF")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) def test_addition(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) # update input tuple with new values to have correct values on failure input_tuple = q1, q2 - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.add, q1, q2) else: expected = self.Q_(*expected) self.assertEqual(op.add(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.add(q1, q2), expected, - atol=0.01) + self.assertQuantityAlmostEqual(op.add(q1, q2), expected, atol=0.01) @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), - additions) + @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) def test_inplace_addition(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple # update input tuple with new values to have correct values on failure - input_tuple = ((np.array([q1v]*2, dtype=np.float), q1u), - (np.array([q2v]*2, dtype=np.float), q2u)) + input_tuple = ( + (np.array([q1v] * 2, dtype=np.float), q1u), + (np.array([q2v] * 2, dtype=np.float), q2u), + ) Q_ = self.Q_ qin1, qin2 = input_tuple q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.iadd, q1_cp, q2) else: - expected = np.array([expected[0]]*2, dtype=np.float), expected[1] + expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] self.assertEqual(op.iadd(q1_cp, q2).units, Q_(*expected).units) q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.iadd(q1_cp, q2), Q_(*expected), - atol=0.01) + self.assertQuantityAlmostEqual(op.iadd(q1_cp, q2), Q_(*expected), atol=0.01) subtractions = [ - (((100, 'kelvin'), (10, 'kelvin')), (90, 'kelvin')), - (((100, 'kelvin'), (10, 'degC')), (-183.15, 'kelvin')), - (((100, 'kelvin'), (10, 'degF')), (-160.93, 'kelvin')), - (((100, 'kelvin'), (10, 'degR')), (94.44, 'kelvin')), - (((100, 'kelvin'), (10, 'delta_degC')), (90, 'kelvin')), - (((100, 'kelvin'), (10, 'delta_degF')), (94.44, 'kelvin')), - - (((100, 'degC'), (10, 'kelvin')), (363.15, 'delta_degC')), - (((100, 'degC'), (10, 'degC')), (90, 'delta_degC')), - (((100, 'degC'), (10, 'degF')), (112.22, 'delta_degC')), - (((100, 'degC'), (10, 'degR')), (367.59, 'delta_degC')), - (((100, 'degC'), (10, 'delta_degC')), (90, 'degC')), - (((100, 'degC'), (10, 'delta_degF')), (94.44, 'degC')), - - (((100, 'degF'), (10, 'kelvin')), (541.67, 'delta_degF')), - (((100, 'degF'), (10, 'degC')), (50, 'delta_degF')), - (((100, 'degF'), (10, 'degF')), (90, 'delta_degF')), - (((100, 'degF'), (10, 'degR')), (549.67, 'delta_degF')), - (((100, 'degF'), (10, 'delta_degC')), (82, 'degF')), - (((100, 'degF'), (10, 'delta_degF')), (90, 'degF')), - - (((100, 'degR'), (10, 'kelvin')), (82, 'degR')), - (((100, 'degR'), (10, 'degC')), (-409.67, 'degR')), - (((100, 'degR'), (10, 'degF')), (-369.67, 'degR')), - (((100, 'degR'), (10, 'degR')), (90, 'degR')), - (((100, 'degR'), (10, 'delta_degC')), (82, 'degR')), - (((100, 'degR'), (10, 'delta_degF')), (90, 'degR')), - - (((100, 'delta_degC'), (10, 'kelvin')), (90, 'kelvin')), - (((100, 'delta_degC'), (10, 'degC')), (90, 'degC')), - (((100, 'delta_degC'), (10, 'degF')), (170, 'degF')), - (((100, 'delta_degC'), (10, 'degR')), (170, 'degR')), - (((100, 'delta_degC'), (10, 'delta_degC')), (90, 'delta_degC')), - (((100, 'delta_degC'), (10, 'delta_degF')), (94.44, 'delta_degC')), - - (((100, 'delta_degF'), (10, 'kelvin')), (45.56, 'kelvin')), - (((100, 'delta_degF'), (10, 'degC')), (45.56, 'degC')), - (((100, 'delta_degF'), (10, 'degF')), (90, 'degF')), - (((100, 'delta_degF'), (10, 'degR')), (90, 'degR')), - (((100, 'delta_degF'), (10, 'delta_degC')), (82, 'delta_degF')), - (((100, 'delta_degF'), (10, 'delta_degF')), (90, 'delta_degF')), - ] - - @ParameterizedTestCase.parameterize(("input", "expected_output"), - subtractions) + (((100, "kelvin"), (10, "kelvin")), (90, "kelvin")), + (((100, "kelvin"), (10, "degC")), (-183.15, "kelvin")), + (((100, "kelvin"), (10, "degF")), (-160.93, "kelvin")), + (((100, "kelvin"), (10, "degR")), (94.44, "kelvin")), + (((100, "kelvin"), (10, "delta_degC")), (90, "kelvin")), + (((100, "kelvin"), (10, "delta_degF")), (94.44, "kelvin")), + (((100, "degC"), (10, "kelvin")), (363.15, "delta_degC")), + (((100, "degC"), (10, "degC")), (90, "delta_degC")), + (((100, "degC"), (10, "degF")), (112.22, "delta_degC")), + (((100, "degC"), (10, "degR")), (367.59, "delta_degC")), + (((100, "degC"), (10, "delta_degC")), (90, "degC")), + (((100, "degC"), (10, "delta_degF")), (94.44, "degC")), + (((100, "degF"), (10, "kelvin")), (541.67, "delta_degF")), + (((100, "degF"), (10, "degC")), (50, "delta_degF")), + (((100, "degF"), (10, "degF")), (90, "delta_degF")), + (((100, "degF"), (10, "degR")), (549.67, "delta_degF")), + (((100, "degF"), (10, "delta_degC")), (82, "degF")), + (((100, "degF"), (10, "delta_degF")), (90, "degF")), + (((100, "degR"), (10, "kelvin")), (82, "degR")), + (((100, "degR"), (10, "degC")), (-409.67, "degR")), + (((100, "degR"), (10, "degF")), (-369.67, "degR")), + (((100, "degR"), (10, "degR")), (90, "degR")), + (((100, "degR"), (10, "delta_degC")), (82, "degR")), + (((100, "degR"), (10, "delta_degF")), (90, "degR")), + (((100, "delta_degC"), (10, "kelvin")), (90, "kelvin")), + (((100, "delta_degC"), (10, "degC")), (90, "degC")), + (((100, "delta_degC"), (10, "degF")), (170, "degF")), + (((100, "delta_degC"), (10, "degR")), (170, "degR")), + (((100, "delta_degC"), (10, "delta_degC")), (90, "delta_degC")), + (((100, "delta_degC"), (10, "delta_degF")), (94.44, "delta_degC")), + (((100, "delta_degF"), (10, "kelvin")), (45.56, "kelvin")), + (((100, "delta_degF"), (10, "degC")), (45.56, "degC")), + (((100, "delta_degF"), (10, "degF")), (90, "degF")), + (((100, "delta_degF"), (10, "degR")), (90, "degR")), + (((100, "delta_degF"), (10, "delta_degC")), (82, "delta_degF")), + (((100, "delta_degF"), (10, "delta_degF")), (90, "delta_degF")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) def test_subtraction(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.sub, q1, q2) else: expected = self.Q_(*expected) self.assertEqual(op.sub(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.sub(q1, q2), expected, - atol=0.01) + self.assertQuantityAlmostEqual(op.sub(q1, q2), expected, atol=0.01) -# @unittest.expectedFailure + # @unittest.expectedFailure @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), - subtractions) + @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) def test_inplace_subtraction(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple # update input tuple with new values to have correct values on failure - input_tuple = ((np.array([q1v]*2, dtype=np.float), q1u), - (np.array([q2v]*2, dtype=np.float), q2u)) + input_tuple = ( + (np.array([q1v] * 2, dtype=np.float), q1u), + (np.array([q2v] * 2, dtype=np.float), q2u), + ) Q_ = self.Q_ qin1, qin2 = input_tuple q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.isub, q1_cp, q2) else: - expected = np.array([expected[0]]*2, dtype=np.float), expected[1] + expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] self.assertEqual(op.isub(q1_cp, q2).units, Q_(*expected).units) q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.isub(q1_cp, q2), Q_(*expected), - atol=0.01) + self.assertQuantityAlmostEqual(op.isub(q1_cp, q2), Q_(*expected), atol=0.01) multiplications = [ - (((100, 'kelvin'), (10, 'kelvin')), (1000, 'kelvin**2')), - (((100, 'kelvin'), (10, 'degC')), 'error'), - (((100, 'kelvin'), (10, 'degF')), 'error'), - (((100, 'kelvin'), (10, 'degR')), (1000, 'kelvin*degR')), - (((100, 'kelvin'), (10, 'delta_degC')), (1000, 'kelvin*delta_degC')), - (((100, 'kelvin'), (10, 'delta_degF')), (1000, 'kelvin*delta_degF')), - - (((100, 'degC'), (10, 'kelvin')), 'error'), - (((100, 'degC'), (10, 'degC')), 'error'), - (((100, 'degC'), (10, 'degF')), 'error'), - (((100, 'degC'), (10, 'degR')), 'error'), - (((100, 'degC'), (10, 'delta_degC')), 'error'), - (((100, 'degC'), (10, 'delta_degF')), 'error'), - - (((100, 'degF'), (10, 'kelvin')), 'error'), - (((100, 'degF'), (10, 'degC')), 'error'), - (((100, 'degF'), (10, 'degF')), 'error'), - (((100, 'degF'), (10, 'degR')), 'error'), - (((100, 'degF'), (10, 'delta_degC')), 'error'), - (((100, 'degF'), (10, 'delta_degF')), 'error'), - - (((100, 'degR'), (10, 'kelvin')), (1000, 'degR*kelvin')), - (((100, 'degR'), (10, 'degC')), 'error'), - (((100, 'degR'), (10, 'degF')), 'error'), - (((100, 'degR'), (10, 'degR')), (1000, 'degR**2')), - (((100, 'degR'), (10, 'delta_degC')), (1000, 'degR*delta_degC')), - (((100, 'degR'), (10, 'delta_degF')), (1000, 'degR*delta_degF')), - - (((100, 'delta_degC'), (10, 'kelvin')), (1000, 'delta_degC*kelvin')), - (((100, 'delta_degC'), (10, 'degC')), 'error'), - (((100, 'delta_degC'), (10, 'degF')), 'error'), - (((100, 'delta_degC'), (10, 'degR')), (1000, 'delta_degC*degR')), - (((100, 'delta_degC'), (10, 'delta_degC')), (1000, 'delta_degC**2')), - (((100, 'delta_degC'), (10, 'delta_degF')), (1000, 'delta_degC*delta_degF')), - - (((100, 'delta_degF'), (10, 'kelvin')), (1000, 'delta_degF*kelvin')), - (((100, 'delta_degF'), (10, 'degC')), 'error'), - (((100, 'delta_degF'), (10, 'degF')), 'error'), - (((100, 'delta_degF'), (10, 'degR')), (1000, 'delta_degF*degR')), - (((100, 'delta_degF'), (10, 'delta_degC')), (1000, 'delta_degF*delta_degC')), - (((100, 'delta_degF'), (10, 'delta_degF')), (1000, 'delta_degF**2')), - ] - - @ParameterizedTestCase.parameterize(("input", "expected_output"), - multiplications) + (((100, "kelvin"), (10, "kelvin")), (1000, "kelvin**2")), + (((100, "kelvin"), (10, "degC")), "error"), + (((100, "kelvin"), (10, "degF")), "error"), + (((100, "kelvin"), (10, "degR")), (1000, "kelvin*degR")), + (((100, "kelvin"), (10, "delta_degC")), (1000, "kelvin*delta_degC")), + (((100, "kelvin"), (10, "delta_degF")), (1000, "kelvin*delta_degF")), + (((100, "degC"), (10, "kelvin")), "error"), + (((100, "degC"), (10, "degC")), "error"), + (((100, "degC"), (10, "degF")), "error"), + (((100, "degC"), (10, "degR")), "error"), + (((100, "degC"), (10, "delta_degC")), "error"), + (((100, "degC"), (10, "delta_degF")), "error"), + (((100, "degF"), (10, "kelvin")), "error"), + (((100, "degF"), (10, "degC")), "error"), + (((100, "degF"), (10, "degF")), "error"), + (((100, "degF"), (10, "degR")), "error"), + (((100, "degF"), (10, "delta_degC")), "error"), + (((100, "degF"), (10, "delta_degF")), "error"), + (((100, "degR"), (10, "kelvin")), (1000, "degR*kelvin")), + (((100, "degR"), (10, "degC")), "error"), + (((100, "degR"), (10, "degF")), "error"), + (((100, "degR"), (10, "degR")), (1000, "degR**2")), + (((100, "degR"), (10, "delta_degC")), (1000, "degR*delta_degC")), + (((100, "degR"), (10, "delta_degF")), (1000, "degR*delta_degF")), + (((100, "delta_degC"), (10, "kelvin")), (1000, "delta_degC*kelvin")), + (((100, "delta_degC"), (10, "degC")), "error"), + (((100, "delta_degC"), (10, "degF")), "error"), + (((100, "delta_degC"), (10, "degR")), (1000, "delta_degC*degR")), + (((100, "delta_degC"), (10, "delta_degC")), (1000, "delta_degC**2")), + (((100, "delta_degC"), (10, "delta_degF")), (1000, "delta_degC*delta_degF")), + (((100, "delta_degF"), (10, "kelvin")), (1000, "delta_degF*kelvin")), + (((100, "delta_degF"), (10, "degC")), "error"), + (((100, "delta_degF"), (10, "degF")), "error"), + (((100, "delta_degF"), (10, "degR")), (1000, "delta_degF*degR")), + (((100, "delta_degF"), (10, "delta_degC")), (1000, "delta_degF*delta_degC")), + (((100, "delta_degF"), (10, "delta_degF")), (1000, "delta_degF**2")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) def test_multiplication(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) else: expected = self.Q_(*expected) self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, - atol=0.01) + self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), - multiplications) + @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) def test_inplace_multiplication(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple # update input tuple with new values to have correct values on failure - input_tuple = ((np.array([q1v]*2, dtype=np.float), q1u), - (np.array([q2v]*2, dtype=np.float), q2u)) + input_tuple = ( + (np.array([q1v] * 2, dtype=np.float), q1u), + (np.array([q2v] * 2, dtype=np.float), q2u), + ) Q_ = self.Q_ qin1, qin2 = input_tuple q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.imul, q1_cp, q2) else: - expected = np.array([expected[0]]*2, dtype=np.float), expected[1] + expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] self.assertEqual(op.imul(q1_cp, q2).units, Q_(*expected).units) q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), - atol=0.01) + self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), atol=0.01) divisions = [ - (((100, 'kelvin'), (10, 'kelvin')), (10, '')), - (((100, 'kelvin'), (10, 'degC')), 'error'), - (((100, 'kelvin'), (10, 'degF')), 'error'), - (((100, 'kelvin'), (10, 'degR')), (10, 'kelvin/degR')), - (((100, 'kelvin'), (10, 'delta_degC')), (10, 'kelvin/delta_degC')), - (((100, 'kelvin'), (10, 'delta_degF')), (10, 'kelvin/delta_degF')), - - (((100, 'degC'), (10, 'kelvin')), 'error'), - (((100, 'degC'), (10, 'degC')), 'error'), - (((100, 'degC'), (10, 'degF')), 'error'), - (((100, 'degC'), (10, 'degR')), 'error'), - (((100, 'degC'), (10, 'delta_degC')), 'error'), - (((100, 'degC'), (10, 'delta_degF')), 'error'), - - (((100, 'degF'), (10, 'kelvin')), 'error'), - (((100, 'degF'), (10, 'degC')), 'error'), - (((100, 'degF'), (10, 'degF')), 'error'), - (((100, 'degF'), (10, 'degR')), 'error'), - (((100, 'degF'), (10, 'delta_degC')), 'error'), - (((100, 'degF'), (10, 'delta_degF')), 'error'), - - (((100, 'degR'), (10, 'kelvin')), (10, 'degR/kelvin')), - (((100, 'degR'), (10, 'degC')), 'error'), - (((100, 'degR'), (10, 'degF')), 'error'), - (((100, 'degR'), (10, 'degR')), (10, '')), - (((100, 'degR'), (10, 'delta_degC')), (10, 'degR/delta_degC')), - (((100, 'degR'), (10, 'delta_degF')), (10, 'degR/delta_degF')), - - (((100, 'delta_degC'), (10, 'kelvin')), (10, 'delta_degC/kelvin')), - (((100, 'delta_degC'), (10, 'degC')), 'error'), - (((100, 'delta_degC'), (10, 'degF')), 'error'), - (((100, 'delta_degC'), (10, 'degR')), (10, 'delta_degC/degR')), - (((100, 'delta_degC'), (10, 'delta_degC')), (10, '')), - (((100, 'delta_degC'), (10, 'delta_degF')), (10, 'delta_degC/delta_degF')), - - (((100, 'delta_degF'), (10, 'kelvin')), (10, 'delta_degF/kelvin')), - (((100, 'delta_degF'), (10, 'degC')), 'error'), - (((100, 'delta_degF'), (10, 'degF')), 'error'), - (((100, 'delta_degF'), (10, 'degR')), (10, 'delta_degF/degR')), - (((100, 'delta_degF'), (10, 'delta_degC')), (10, 'delta_degF/delta_degC')), - (((100, 'delta_degF'), (10, 'delta_degF')), (10, '')), - ] - - @ParameterizedTestCase.parameterize(("input", "expected_output"), - divisions) + (((100, "kelvin"), (10, "kelvin")), (10, "")), + (((100, "kelvin"), (10, "degC")), "error"), + (((100, "kelvin"), (10, "degF")), "error"), + (((100, "kelvin"), (10, "degR")), (10, "kelvin/degR")), + (((100, "kelvin"), (10, "delta_degC")), (10, "kelvin/delta_degC")), + (((100, "kelvin"), (10, "delta_degF")), (10, "kelvin/delta_degF")), + (((100, "degC"), (10, "kelvin")), "error"), + (((100, "degC"), (10, "degC")), "error"), + (((100, "degC"), (10, "degF")), "error"), + (((100, "degC"), (10, "degR")), "error"), + (((100, "degC"), (10, "delta_degC")), "error"), + (((100, "degC"), (10, "delta_degF")), "error"), + (((100, "degF"), (10, "kelvin")), "error"), + (((100, "degF"), (10, "degC")), "error"), + (((100, "degF"), (10, "degF")), "error"), + (((100, "degF"), (10, "degR")), "error"), + (((100, "degF"), (10, "delta_degC")), "error"), + (((100, "degF"), (10, "delta_degF")), "error"), + (((100, "degR"), (10, "kelvin")), (10, "degR/kelvin")), + (((100, "degR"), (10, "degC")), "error"), + (((100, "degR"), (10, "degF")), "error"), + (((100, "degR"), (10, "degR")), (10, "")), + (((100, "degR"), (10, "delta_degC")), (10, "degR/delta_degC")), + (((100, "degR"), (10, "delta_degF")), (10, "degR/delta_degF")), + (((100, "delta_degC"), (10, "kelvin")), (10, "delta_degC/kelvin")), + (((100, "delta_degC"), (10, "degC")), "error"), + (((100, "delta_degC"), (10, "degF")), "error"), + (((100, "delta_degC"), (10, "degR")), (10, "delta_degC/degR")), + (((100, "delta_degC"), (10, "delta_degC")), (10, "")), + (((100, "delta_degC"), (10, "delta_degF")), (10, "delta_degC/delta_degF")), + (((100, "delta_degF"), (10, "kelvin")), (10, "delta_degF/kelvin")), + (((100, "delta_degF"), (10, "degC")), "error"), + (((100, "delta_degF"), (10, "degF")), "error"), + (((100, "delta_degF"), (10, "degR")), (10, "delta_degF/degR")), + (((100, "delta_degF"), (10, "delta_degC")), (10, "delta_degF/delta_degC")), + (((100, "delta_degF"), (10, "delta_degF")), (10, "")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) def test_truedivision(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.truediv, q1, q2) else: expected = self.Q_(*expected) self.assertEqual(op.truediv(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.truediv(q1, q2), expected, - atol=0.01) + self.assertQuantityAlmostEqual(op.truediv(q1, q2), expected, atol=0.01) @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), - divisions) + @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) def test_inplace_truedivision(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple # update input tuple with new values to have correct values on failure - input_tuple = ((np.array([q1v]*2, dtype=np.float), q1u), - (np.array([q2v]*2, dtype=np.float), q2u)) + input_tuple = ( + (np.array([q1v] * 2, dtype=np.float), q1u), + (np.array([q2v] * 2, dtype=np.float), q2u), + ) Q_ = self.Q_ qin1, qin2 = input_tuple q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.itruediv, q1_cp, q2) else: - expected = np.array([expected[0]]*2, dtype=np.float), expected[1] + expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] self.assertEqual(op.itruediv(q1_cp, q2).units, Q_(*expected).units) q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.itruediv(q1_cp, q2), - Q_(*expected), atol=0.01) + self.assertQuantityAlmostEqual( + op.itruediv(q1_cp, q2), Q_(*expected), atol=0.01 + ) multiplications_with_autoconvert_to_baseunit = [ - (((100, 'kelvin'), (10, 'degC')), (28315., 'kelvin**2')), - (((100, 'kelvin'), (10, 'degF')), (26092.78, 'kelvin**2')), - - (((100, 'degC'), (10, 'kelvin')), (3731.5, 'kelvin**2')), - (((100, 'degC'), (10, 'degC')), (105657.42, 'kelvin**2')), - (((100, 'degC'), (10, 'degF')), (97365.20, 'kelvin**2')), - (((100, 'degC'), (10, 'degR')), (3731.5, 'kelvin*degR')), - (((100, 'degC'), (10, 'delta_degC')), (3731.5, 'kelvin*delta_degC')), - (((100, 'degC'), (10, 'delta_degF')), (3731.5, 'kelvin*delta_degF')), - - (((100, 'degF'), (10, 'kelvin')), (3109.28, 'kelvin**2')), - (((100, 'degF'), (10, 'degC')), (88039.20, 'kelvin**2')), - (((100, 'degF'), (10, 'degF')), (81129.69, 'kelvin**2')), - (((100, 'degF'), (10, 'degR')), (3109.28, 'kelvin*degR')), - (((100, 'degF'), (10, 'delta_degC')), (3109.28, 'kelvin*delta_degC')), - (((100, 'degF'), (10, 'delta_degF')), (3109.28, 'kelvin*delta_degF')), - - (((100, 'degR'), (10, 'degC')), (28315., 'degR*kelvin')), - (((100, 'degR'), (10, 'degF')), (26092.78, 'degR*kelvin')), - - (((100, 'delta_degC'), (10, 'degC')), (28315., 'delta_degC*kelvin')), - (((100, 'delta_degC'), (10, 'degF')), (26092.78, 'delta_degC*kelvin')), - - (((100, 'delta_degF'), (10, 'degC')), (28315., 'delta_degF*kelvin')), - (((100, 'delta_degF'), (10, 'degF')), (26092.78, 'delta_degF*kelvin')), - ] + (((100, "kelvin"), (10, "degC")), (28315.0, "kelvin**2")), + (((100, "kelvin"), (10, "degF")), (26092.78, "kelvin**2")), + (((100, "degC"), (10, "kelvin")), (3731.5, "kelvin**2")), + (((100, "degC"), (10, "degC")), (105657.42, "kelvin**2")), + (((100, "degC"), (10, "degF")), (97365.20, "kelvin**2")), + (((100, "degC"), (10, "degR")), (3731.5, "kelvin*degR")), + (((100, "degC"), (10, "delta_degC")), (3731.5, "kelvin*delta_degC")), + (((100, "degC"), (10, "delta_degF")), (3731.5, "kelvin*delta_degF")), + (((100, "degF"), (10, "kelvin")), (3109.28, "kelvin**2")), + (((100, "degF"), (10, "degC")), (88039.20, "kelvin**2")), + (((100, "degF"), (10, "degF")), (81129.69, "kelvin**2")), + (((100, "degF"), (10, "degR")), (3109.28, "kelvin*degR")), + (((100, "degF"), (10, "delta_degC")), (3109.28, "kelvin*delta_degC")), + (((100, "degF"), (10, "delta_degF")), (3109.28, "kelvin*delta_degF")), + (((100, "degR"), (10, "degC")), (28315.0, "degR*kelvin")), + (((100, "degR"), (10, "degF")), (26092.78, "degR*kelvin")), + (((100, "delta_degC"), (10, "degC")), (28315.0, "delta_degC*kelvin")), + (((100, "delta_degC"), (10, "degF")), (26092.78, "delta_degC*kelvin")), + (((100, "delta_degF"), (10, "degC")), (28315.0, "delta_degF*kelvin")), + (((100, "delta_degF"), (10, "degF")), (26092.78, "delta_degF*kelvin")), + ] @ParameterizedTestCase.parameterize( - ("input", "expected_output"), - multiplications_with_autoconvert_to_baseunit) + ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit + ) def test_multiplication_with_autoconvert(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = True qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) else: expected = self.Q_(*expected) self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, - atol=0.01) + self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) @helpers.requires_numpy() @ParameterizedTestCase.parameterize( - ("input", "expected_output"), - multiplications_with_autoconvert_to_baseunit) + ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit + ) def test_inplace_multiplication_with_autoconvert(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = True (q1v, q1u), (q2v, q2u) = input_tuple # update input tuple with new values to have correct values on failure - input_tuple = ((np.array([q1v]*2, dtype=np.float), q1u), - (np.array([q2v]*2, dtype=np.float), q2u)) + input_tuple = ( + (np.array([q1v] * 2, dtype=np.float), q1u), + (np.array([q2v] * 2, dtype=np.float), q2u), + ) Q_ = self.Q_ qin1, qin2 = input_tuple q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.imul, q1_cp, q2) else: - expected = np.array([expected[0]]*2, dtype=np.float), expected[1] + expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] self.assertEqual(op.imul(q1_cp, q2).units, Q_(*expected).units) q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), - atol=0.01) + self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), atol=0.01) multiplications_with_scalar = [ - (((10, 'kelvin'), 2), (20., 'kelvin')), - (((10, 'kelvin**2'), 2), (20., 'kelvin**2')), - (((10, 'degC'), 2), (20., 'degC')), - (((10, '1/degC'), 2), 'error'), - (((10, 'degC**0.5'), 2), 'error'), - (((10, 'degC**2'), 2), 'error'), - (((10, 'degC**-2'), 2), 'error'), - ] + (((10, "kelvin"), 2), (20.0, "kelvin")), + (((10, "kelvin**2"), 2), (20.0, "kelvin**2")), + (((10, "degC"), 2), (20.0, "degC")), + (((10, "1/degC"), 2), "error"), + (((10, "degC**0.5"), 2), "error"), + (((10, "degC**2"), 2), "error"), + (((10, "degC**-2"), 2), "error"), + ] @ParameterizedTestCase.parameterize( - ("input", "expected_output"), multiplications_with_scalar) + ("input", "expected_output"), multiplications_with_scalar + ) def test_multiplication_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1203,29 +1288,28 @@ def test_multiplication_with_scalar(self, input_tuple, expected): else: in1, in2 = in1, self.Q_(*in2) input_tuple = in1, in2 # update input_tuple for better tracebacks - if expected == 'error': + if expected == "error": self.assertRaises(OffsetUnitCalculusError, op.mul, in1, in2) else: expected = self.Q_(*expected) self.assertEqual(op.mul(in1, in2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(in1, in2), expected, - atol=0.01) - - divisions_with_scalar = [ # without / with autoconvert to base unit - (((10, 'kelvin'), 2), [(5., 'kelvin'), (5., 'kelvin')]), - (((10, 'kelvin**2'), 2), [(5., 'kelvin**2'), (5., 'kelvin**2')]), - (((10, 'degC'), 2), ['error', 'error']), - (((10, 'degC**2'), 2), ['error', 'error']), - (((10, 'degC**-2'), 2), ['error', 'error']), - - ((2, (10, 'kelvin')), [(0.2, '1/kelvin'), (0.2, '1/kelvin')]), - ((2, (10, 'degC')), ['error', (2/283.15, '1/kelvin')]), - ((2, (10, 'degC**2')), ['error', 'error']), - ((2, (10, 'degC**-2')), ['error', 'error']), - ] + self.assertQuantityAlmostEqual(op.mul(in1, in2), expected, atol=0.01) + + divisions_with_scalar = [ # without / with autoconvert to base unit + (((10, "kelvin"), 2), [(5.0, "kelvin"), (5.0, "kelvin")]), + (((10, "kelvin**2"), 2), [(5.0, "kelvin**2"), (5.0, "kelvin**2")]), + (((10, "degC"), 2), ["error", "error"]), + (((10, "degC**2"), 2), ["error", "error"]), + (((10, "degC**-2"), 2), ["error", "error"]), + ((2, (10, "kelvin")), [(0.2, "1/kelvin"), (0.2, "1/kelvin")]), + ((2, (10, "degC")), ["error", (2 / 283.15, "1/kelvin")]), + ((2, (10, "degC**2")), ["error", "error"]), + ((2, (10, "degC**-2")), ["error", "error"]), + ] @ParameterizedTestCase.parameterize( - ("input", "expected_output"), divisions_with_scalar) + ("input", "expected_output"), divisions_with_scalar + ) def test_division_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1237,34 +1321,33 @@ def test_division_with_scalar(self, input_tuple, expected): expected_copy = expected[:] for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode - if expected_copy[i] == 'error': + if expected_copy[i] == "error": self.assertRaises(OffsetUnitCalculusError, op.truediv, in1, in2) else: expected = self.Q_(*expected_copy[i]) self.assertEqual(op.truediv(in1, in2).units, expected.units) self.assertQuantityAlmostEqual(op.truediv(in1, in2), expected) - exponentiation = [ # resuls without / with autoconvert - (((10, 'degC'), 1), [(10, 'degC'), (10, 'degC')]), - (((10, 'degC'), 0.5), ['error', (283.15**0.5, 'kelvin**0.5')]), - (((10, 'degC'), 0), [(1., ''), (1., '')]), - (((10, 'degC'), -1), ['error', (1/(10+273.15), 'kelvin**-1')]), - (((10, 'degC'), -2), ['error', (1/(10+273.15)**2., 'kelvin**-2')]), - ((( 0, 'degC'), -2), ['error', (1/(273.15)**2, 'kelvin**-2')]), - (((10, 'degC'), (2, '')), ['error', ((283.15)**2, 'kelvin**2')]), - (((10, 'degC'), (10, 'degK')), ['error', 'error']), - - (((10, 'kelvin'), (2, '')), [(100., 'kelvin**2'), (100., 'kelvin**2')]), - - (( 2, (2, 'kelvin')), ['error', 'error']), - (( 2, (500., 'millikelvin/kelvin')), [2**0.5, 2**0.5]), - (( 2, (0.5, 'kelvin/kelvin')), [2**0.5, 2**0.5]), - (((10, 'degC'), (500., 'millikelvin/kelvin')), - ['error', (283.15**0.5, 'kelvin**0.5')]), - ] - - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), exponentiation) + exponentiation = [ # resuls without / with autoconvert + (((10, "degC"), 1), [(10, "degC"), (10, "degC")]), + (((10, "degC"), 0.5), ["error", (283.15 ** 0.5, "kelvin**0.5")]), + (((10, "degC"), 0), [(1.0, ""), (1.0, "")]), + (((10, "degC"), -1), ["error", (1 / (10 + 273.15), "kelvin**-1")]), + (((10, "degC"), -2), ["error", (1 / (10 + 273.15) ** 2.0, "kelvin**-2")]), + (((0, "degC"), -2), ["error", (1 / (273.15) ** 2, "kelvin**-2")]), + (((10, "degC"), (2, "")), ["error", ((283.15) ** 2, "kelvin**2")]), + (((10, "degC"), (10, "degK")), ["error", "error"]), + (((10, "kelvin"), (2, "")), [(100.0, "kelvin**2"), (100.0, "kelvin**2")]), + ((2, (2, "kelvin")), ["error", "error"]), + ((2, (500.0, "millikelvin/kelvin")), [2 ** 0.5, 2 ** 0.5]), + ((2, (0.5, "kelvin/kelvin")), [2 ** 0.5, 2 ** 0.5]), + ( + ((10, "degC"), (500.0, "millikelvin/kelvin")), + ["error", (283.15 ** 0.5, "kelvin**0.5")], + ), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) def test_exponentiation(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1278,9 +1361,10 @@ def test_exponentiation(self, input_tuple, expected): expected_copy = expected[:] for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode - if expected_copy[i] == 'error': - self.assertRaises((OffsetUnitCalculusError, - DimensionalityError), op.pow, in1, in2) + if expected_copy[i] == "error": + self.assertRaises( + (OffsetUnitCalculusError, DimensionalityError), op.pow, in1, in2 + ) else: if type(expected_copy[i]) is tuple: expected = self.Q_(*expected_copy[i]) @@ -1290,14 +1374,13 @@ def test_exponentiation(self, input_tuple, expected): self.assertQuantityAlmostEqual(op.pow(in1, in2), expected) @helpers.requires_numpy() - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), exponentiation) + @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) def test_inplace_exponentiation(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple if type(in1) is tuple and type(in2) is tuple: (q1v, q1u), (q2v, q2u) = in1, in2 - in1 = self.Q_(*(np.array([q1v]*2, dtype=np.float), q1u)) + in1 = self.Q_(*(np.array([q1v] * 2, dtype=np.float), q1u)) in2 = self.Q_(q2v, q2u) elif not type(in1) is tuple and type(in2) is tuple: in2 = self.Q_(*in2) @@ -1310,30 +1393,31 @@ def test_inplace_exponentiation(self, input_tuple, expected): for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode in1_cp = copy.copy(in1) - if expected_copy[i] == 'error': - self.assertRaises((OffsetUnitCalculusError, - DimensionalityError), op.ipow, in1_cp, in2) + if expected_copy[i] == "error": + self.assertRaises( + (OffsetUnitCalculusError, DimensionalityError), op.ipow, in1_cp, in2 + ) else: if type(expected_copy[i]) is tuple: - expected = self.Q_(np.array([expected_copy[i][0]]*2, - dtype=np.float), - expected_copy[i][1]) + expected = self.Q_( + np.array([expected_copy[i][0]] * 2, dtype=np.float), + expected_copy[i][1], + ) self.assertEqual(op.ipow(in1_cp, in2).units, expected.units) else: - expected = np.array([expected_copy[i]]*2, dtype=np.float) - + expected = np.array([expected_copy[i]] * 2, dtype=np.float) in1_cp = copy.copy(in1) self.assertQuantityAlmostEqual(op.ipow(in1_cp, in2), expected) # matmul is only a ufunc since 1.16 - @helpers.requires_numpy_at_least('1.16') + @helpers.requires_numpy_at_least("1.16") def test_matmul_with_numpy(self): A = [[1, 2], [3, 4]] * self.ureg.m B = np.array([[0, -1], [-1, 0]]) b = [[1], [0]] * self.ureg.m self.assertQuantityEqual(A @ B, [[-2, -1], [-4, -3]] * self.ureg.m) - self.assertQuantityEqual(A @ b, [[1], [3]] * self.ureg.m**2) + self.assertQuantityEqual(A @ b, [[1], [3]] * self.ureg.m ** 2) self.assertQuantityEqual(B @ b, [[0], [-1]] * self.ureg.m) @@ -1344,8 +1428,8 @@ def _calc_mass(self, ureg): return density * volume def _icalc_mass(self, ureg): - res = ureg.Quantity(3.0, 'gram/liter') - res *= ureg.Quantity(32.0, 'milliliter') + res = ureg.Quantity(3.0, "gram/liter") + res *= ureg.Quantity(32.0, "milliliter") return res def test_mul_and_div_reduction(self): @@ -1378,6 +1462,7 @@ def test_nocoerce_creation(self): x = 1 * ureg.foot self.assertEqual(x.units, ureg.foot) + class TestTimedelta(QuantityTestCase): def test_add_sub(self): d = datetime.datetime(year=1968, month=1, day=10, hour=3, minute=42, second=24) @@ -1416,7 +1501,7 @@ def test_equal_zero(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False self.assertTrue(ureg.Quantity(0, ureg.J) == 0) - self.assertFalse(ureg.Quantity(0, ureg.J) == ureg.Quantity(0, '')) + self.assertFalse(ureg.Quantity(0, ureg.J) == ureg.Quantity(0, "")) self.assertFalse(ureg.Quantity(5, ureg.J) == 0) @helpers.requires_numpy() @@ -1424,44 +1509,44 @@ def test_equal_zero_NP(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False aeq = np.testing.assert_array_equal - aeq(ureg.Quantity(0, ureg.J) == np.zeros(3), - np.asarray([True, True, True])) - aeq(ureg.Quantity(5, ureg.J) == np.zeros(3), - np.asarray([False, False, False])) - aeq(ureg.Quantity(np.arange(3), ureg.J) == np.zeros(3), - np.asarray([True, False, False])) + aeq(ureg.Quantity(0, ureg.J) == np.zeros(3), np.asarray([True, True, True])) + aeq(ureg.Quantity(5, ureg.J) == np.zeros(3), np.asarray([False, False, False])) + aeq( + ureg.Quantity(np.arange(3), ureg.J) == np.zeros(3), + np.asarray([True, False, False]), + ) self.assertFalse(ureg.Quantity(np.arange(4), ureg.J) == np.zeros(3)) def test_offset_equal_zero(self): - ureg = self.ureg + ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False - q0 = ureg.Quantity(-273.15, 'degC') - q1 = ureg.Quantity(0, 'degC') - q2 = ureg.Quantity(5, 'degC') + q0 = ureg.Quantity(-273.15, "degC") + q1 = ureg.Quantity(0, "degC") + q2 = ureg.Quantity(5, "degC") self.assertRaises(OffsetUnitCalculusError, q0.__eq__, 0) self.assertRaises(OffsetUnitCalculusError, q1.__eq__, 0) self.assertRaises(OffsetUnitCalculusError, q2.__eq__, 0) - self.assertFalse(q0 == ureg.Quantity(0, '')) + self.assertFalse(q0 == ureg.Quantity(0, "")) def test_offset_autoconvert_equal_zero(self): - ureg = self.ureg + ureg = self.ureg ureg.autoconvert_offset_to_baseunit = True - q0 = ureg.Quantity(-273.15, 'degC') - q1 = ureg.Quantity(0, 'degC') - q2 = ureg.Quantity(5, 'degC') + q0 = ureg.Quantity(-273.15, "degC") + q1 = ureg.Quantity(0, "degC") + q2 = ureg.Quantity(5, "degC") self.assertTrue(q0 == 0) self.assertFalse(q1 == 0) self.assertFalse(q2 == 0) - self.assertFalse(q0 == ureg.Quantity(0, '')) + self.assertFalse(q0 == ureg.Quantity(0, "")) def test_gt_zero(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False - q0 = ureg.Quantity(0, 'J') - q0m = ureg.Quantity(0, 'm') - q0less = ureg.Quantity(0, '') - qpos = ureg.Quantity(5, 'J') - qneg = ureg.Quantity(-5, 'J') + q0 = ureg.Quantity(0, "J") + q0m = ureg.Quantity(0, "m") + q0less = ureg.Quantity(0, "") + qpos = ureg.Quantity(5, "J") + qneg = ureg.Quantity(-5, "J") self.assertTrue(qpos > q0) self.assertTrue(qpos > 0) self.assertFalse(qneg > 0) @@ -1472,39 +1557,41 @@ def test_gt_zero(self): def test_gt_zero_NP(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False - qpos = ureg.Quantity(5, 'J') - qneg = ureg.Quantity(-5, 'J') + qpos = ureg.Quantity(5, "J") + qneg = ureg.Quantity(-5, "J") aeq = np.testing.assert_array_equal aeq(qpos > np.zeros(3), np.asarray([True, True, True])) aeq(qneg > np.zeros(3), np.asarray([False, False, False])) - aeq(ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), - np.asarray([False, False, True])) - aeq(ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), - np.asarray([False, False, True])) - self.assertRaises(ValueError, - ureg.Quantity(np.arange(-1, 2), ureg.J).__gt__, - np.zeros(4)) + aeq( + ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), + np.asarray([False, False, True]), + ) + aeq( + ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), + np.asarray([False, False, True]), + ) + self.assertRaises( + ValueError, ureg.Quantity(np.arange(-1, 2), ureg.J).__gt__, np.zeros(4) + ) def test_offset_gt_zero(self): - ureg = self.ureg + ureg = self.ureg ureg.autoconvert_offset_to_baseunit = False - q0 = ureg.Quantity(-273.15, 'degC') - q1 = ureg.Quantity(0, 'degC') - q2 = ureg.Quantity(5, 'degC') + q0 = ureg.Quantity(-273.15, "degC") + q1 = ureg.Quantity(0, "degC") + q2 = ureg.Quantity(5, "degC") self.assertRaises(OffsetUnitCalculusError, q0.__gt__, 0) self.assertRaises(OffsetUnitCalculusError, q1.__gt__, 0) self.assertRaises(OffsetUnitCalculusError, q2.__gt__, 0) - self.assertRaises(DimensionalityError, q1.__gt__, - ureg.Quantity(0, '')) + self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) def test_offset_autoconvert_gt_zero(self): - ureg = self.ureg + ureg = self.ureg ureg.autoconvert_offset_to_baseunit = True - q0 = ureg.Quantity(-273.15, 'degC') - q1 = ureg.Quantity(0, 'degC') - q2 = ureg.Quantity(5, 'degC') + q0 = ureg.Quantity(-273.15, "degC") + q1 = ureg.Quantity(0, "degC") + q2 = ureg.Quantity(5, "degC") self.assertFalse(q0 > 0) self.assertTrue(q1 > 0) self.assertTrue(q2 > 0) - self.assertRaises(DimensionalityError, q1.__gt__, - ureg.Quantity(0, '')) + self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index 068b2e60b..bff359b1a 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -5,136 +5,119 @@ class TestGroup(QuantityTestCase): - def _build_empty_reg_root(self): ureg = UnitRegistry(None) - grp = ureg.get_group('root') - grp.remove_units('pi') + grp = ureg.get_group("root") + grp.remove_units("pi") grp.invalidate_members() - return ureg, ureg.get_group('root') + return ureg, ureg.get_group("root") def test_units_programatically(self): ureg, root = self._build_empty_reg_root() - d = ureg._groups + d = ureg._groups self.assertEqual(root._used_groups, set()) self.assertEqual(root._used_by, set()) - root.add_units('meter', 'second', 'meter') - self.assertEqual(root._unit_names, {'meter', 'second'}) - self.assertEqual(root.members, {'meter', 'second'}) + root.add_units("meter", "second", "meter") + self.assertEqual(root._unit_names, {"meter", "second"}) + self.assertEqual(root.members, {"meter", "second"}) - self.assertEqual(d.keys(), {'root'}) + self.assertEqual(d.keys(), {"root"}) def test_cyclic(self): ureg, root = self._build_empty_reg_root() - g2 = ureg.Group('g2') - g3 = ureg.Group('g3') - g2.add_groups('g3') + g2 = ureg.Group("g2") + g3 = ureg.Group("g3") + g2.add_groups("g3") - self.assertRaises(ValueError, g2.add_groups, 'root') - self.assertRaises(ValueError, g3.add_groups, 'g2') - self.assertRaises(ValueError, g3.add_groups, 'root') + self.assertRaises(ValueError, g2.add_groups, "root") + self.assertRaises(ValueError, g3.add_groups, "g2") + self.assertRaises(ValueError, g3.add_groups, "root") def test_groups_programatically(self): ureg, root = self._build_empty_reg_root() d = ureg._groups - g2 = ureg.Group('g2') + g2 = ureg.Group("g2") - self.assertEqual(d.keys(), {'root', 'g2'}) + self.assertEqual(d.keys(), {"root", "g2"}) - self.assertEqual(root._used_groups, {'g2'}) + self.assertEqual(root._used_groups, {"g2"}) self.assertEqual(root._used_by, set()) self.assertEqual(g2._used_groups, set()) - self.assertEqual(g2._used_by, {'root'}) - + self.assertEqual(g2._used_by, {"root"}) def test_simple(self): - lines = ['@group mygroup', - 'meter', - 'second', - ] + lines = ["@group mygroup", "meter", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(d.keys(), {'root', 'mygroup'}) + self.assertEqual(d.keys(), {"root", "mygroup"}) - self.assertEqual(grp.name, 'mygroup') - self.assertEqual(grp._unit_names, {'meter', 'second'}) + self.assertEqual(grp.name, "mygroup") + self.assertEqual(grp._unit_names, {"meter", "second"}) self.assertEqual(grp._used_groups, set()) self.assertEqual(grp._used_by, {root.name}) - self.assertEqual(grp.members, frozenset(['meter', 'second'])) + self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using1(self): - lines = ['@group mygroup using group1', - 'meter', - 'second', - ] + lines = ["@group mygroup using group1", "meter", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups - g = ureg.Group('group1') + g = ureg.Group("group1") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, 'mygroup') - self.assertEqual(grp._unit_names, {'meter', 'second'}) - self.assertEqual(grp._used_groups, {'group1'}) - self.assertEqual(grp.members, frozenset(['meter', 'second'])) + self.assertEqual(grp.name, "mygroup") + self.assertEqual(grp._unit_names, {"meter", "second"}) + self.assertEqual(grp._used_groups, {"group1"}) + self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using2(self): - lines = ['@group mygroup using group1,group2', - 'meter', - 'second', - ] + lines = ["@group mygroup using group1,group2", "meter", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups - ureg.Group('group1') - ureg.Group('group2') + ureg.Group("group1") + ureg.Group("group2") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, 'mygroup') - self.assertEqual(grp._unit_names, {'meter', 'second'}) - self.assertEqual(grp._used_groups, {'group1', 'group2'}) - self.assertEqual(grp.members, frozenset(['meter', 'second'])) + self.assertEqual(grp.name, "mygroup") + self.assertEqual(grp._unit_names, {"meter", "second"}) + self.assertEqual(grp._used_groups, {"group1", "group2"}) + self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_spaces(self): - lines = ['@group mygroup using group1 , group2', - ' meter ', - ' second ', - ] + lines = ["@group mygroup using group1 , group2", " meter ", " second "] ureg, root = self._build_empty_reg_root() d = ureg._groups - ureg.Group('group1') - ureg.Group('group2') + ureg.Group("group1") + ureg.Group("group2") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, 'mygroup') - self.assertEqual(grp._unit_names, {'meter', 'second'}) - self.assertEqual(grp._used_groups, {'group1', 'group2'}) - self.assertEqual(grp.members, frozenset(['meter', 'second'])) + self.assertEqual(grp.name, "mygroup") + self.assertEqual(grp._unit_names, {"meter", "second"}) + self.assertEqual(grp._used_groups, {"group1", "group2"}) + self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_invalidate_members(self): - lines = ['@group mygroup using group1', - 'meter', - 'second', - ] + lines = ["@group mygroup using group1", "meter", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups - g1 = ureg.Group('group1') + g1 = ureg.Group("group1") grp = ureg.Group.from_lines(lines, lambda x: None) self.assertIs(root._computed_members, None) self.assertIs(grp._computed_members, None) - self.assertEqual(grp.members, frozenset(['meter', 'second'])) + self.assertEqual(grp.members, frozenset(["meter", "second"])) self.assertIs(root._computed_members, None) self.assertIsNot(grp._computed_members, None) - self.assertEqual(root.members, frozenset(['meter', 'second'])) + self.assertEqual(root.members, frozenset(["meter", "second"])) self.assertIsNot(root._computed_members, None) self.assertIsNot(grp._computed_members, None) grp.invalidate_members() @@ -142,14 +125,15 @@ def test_invalidate_members(self): self.assertIs(grp._computed_members, None) def test_with_defintions(self): - lines = ['@group imperial', - 'inch', - 'yard', - 'kings_leg = 2 * meter', - 'kings_head = 52 * inch' - 'pint' - ] + lines = [ + "@group imperial", + "inch", + "yard", + "kings_leg = 2 * meter", + "kings_head = 52 * inch" "pint", + ] defs = [] + def define(ud): defs.append(ud.name) @@ -158,184 +142,162 @@ def define(ud): grp = ureg.Group.from_lines(lines, define) - self.assertEqual(['kings_leg', 'kings_head'], defs) + self.assertEqual(["kings_leg", "kings_head"], defs) def test_members_including(self): ureg, root = self._build_empty_reg_root() d = ureg._groups - g1 = ureg.Group('group1') + g1 = ureg.Group("group1") - g1.add_units('second', 'inch') - g2 = ureg.Group('group2') - g2.add_units('second', 'newton') + g1.add_units("second", "inch") + g2 = ureg.Group("group2") + g2.add_units("second", "newton") - g3 = ureg.Group('group3') - g3.add_units('meter', 'second') - g3.add_groups('group1', 'group2') + g3 = ureg.Group("group3") + g3.add_units("meter", "second") + g3.add_groups("group1", "group2") - self.assertEqual(root.members, frozenset(['meter', 'second', 'newton', 'inch'])) - self.assertEqual(g1.members, frozenset(['second', 'inch'])) - self.assertEqual(g2.members, frozenset(['second', 'newton'])) - self.assertEqual(g3.members, frozenset(['meter', 'second', 'newton', 'inch'])) + self.assertEqual(root.members, frozenset(["meter", "second", "newton", "inch"])) + self.assertEqual(g1.members, frozenset(["second", "inch"])) + self.assertEqual(g2.members, frozenset(["second", "newton"])) + self.assertEqual(g3.members, frozenset(["meter", "second", "newton", "inch"])) def test_get_compatible_units(self): ureg = UnitRegistry() - g = ureg.get_group('test-imperial') - g.add_units('inch', 'yard', 'pint') - c = ureg.get_compatible_units('meter', 'test-imperial') + g = ureg.get_group("test-imperial") + g.add_units("inch", "yard", "pint") + c = ureg.get_compatible_units("meter", "test-imperial") self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) - class TestSystem(QuantityTestCase): - def _build_empty_reg_root(self): ureg = UnitRegistry(None) - grp = ureg.get_group('root') - grp.remove_units('pi') + grp = ureg.get_group("root") + grp.remove_units("pi") grp.invalidate_members() - return ureg, ureg.get_group('root') + return ureg, ureg.get_group("root") def test_implicit_root(self): - lines = ['@system mks', - 'meter', - 'kilogram', - 'second', - ] + lines = ["@system mks", "meter", "kilogram", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups s = ureg.System.from_lines(lines, lambda x: x) - s._used_groups = {'root'} + s._used_groups = {"root"} def test_simple_using(self): - lines = ['@system mks using g1', - 'meter', - 'kilogram', - 'second', - ] + lines = ["@system mks using g1", "meter", "kilogram", "second"] ureg, root = self._build_empty_reg_root() d = ureg._groups s = ureg.System.from_lines(lines, lambda x: x) - s._used_groups = {'root', 'g1'} - + s._used_groups = {"root", "g1"} def test_members_group(self): - lines = ['@system mk', - 'meter', - 'kilogram', - ] + lines = ["@system mk", "meter", "kilogram"] ureg, root = self._build_empty_reg_root() d = ureg._groups - root.add_units('second') + root.add_units("second") s = ureg.System.from_lines(lines, lambda x: x) - self.assertEqual(s.members, frozenset(['second'])) + self.assertEqual(s.members, frozenset(["second"])) def test_get_compatible_units(self): - sysname = 'mysys1' + sysname = "mysys1" ureg = UnitRegistry() - g = ureg.get_group('test-imperial') + g = ureg.get_group("test-imperial") - g.add_units('inch', 'yard', 'pint') - c = ureg.get_compatible_units('meter', 'test-imperial') + g.add_units("inch", "yard", "pint") + c = ureg.get_compatible_units("meter", "test-imperial") self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) - lines = ['@system %s using test-imperial' % sysname, - 'inch', - ] + lines = ["@system %s using test-imperial" % sysname, "inch"] s = ureg.System.from_lines(lines, lambda x: x) - c = ureg.get_compatible_units('meter', sysname) + c = ureg.get_compatible_units("meter", sysname) self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) def test_get_base_units(self): - sysname = 'mysys2' + sysname = "mysys2" ureg = UnitRegistry() - g = ureg.get_group('test-imperial') - g.add_units('inch', 'yard', 'pint') + g = ureg.get_group("test-imperial") + g.add_units("inch", "yard", "pint") - lines = ['@system %s using test-imperial' % sysname, - 'inch', - ] + lines = ["@system %s using test-imperial" % sysname, "inch"] s = ureg.System.from_lines(lines, ureg.get_base_units) ureg._systems[s.name] = s # base_factor, destination_units - c = ureg.get_base_units('inch', system=sysname) + c = ureg.get_base_units("inch", system=sysname) self.assertAlmostEqual(c[0], 1) - self.assertEqual(c[1], {'inch': 1}) + self.assertEqual(c[1], {"inch": 1}) - c = ureg.get_base_units('cm', system=sysname) - self.assertAlmostEqual(c[0], 1./2.54) - self.assertEqual(c[1], {'inch': 1}) + c = ureg.get_base_units("cm", system=sysname) + self.assertAlmostEqual(c[0], 1.0 / 2.54) + self.assertEqual(c[1], {"inch": 1}) def test_get_base_units_different_exponent(self): - sysname = 'mysys3' + sysname = "mysys3" ureg = UnitRegistry() - g = ureg.get_group('test-imperial') - g.add_units('inch', 'yard', 'pint') - c = ureg.get_compatible_units('meter', 'test-imperial') + g = ureg.get_group("test-imperial") + g.add_units("inch", "yard", "pint") + c = ureg.get_compatible_units("meter", "test-imperial") - lines = ['@system %s using test-imperial' % sysname, - 'pint:meter', - ] + lines = ["@system %s using test-imperial" % sysname, "pint:meter"] s = ureg.System.from_lines(lines, ureg.get_base_units) ureg._systems[s.name] = s # base_factor, destination_units - c = ureg.get_base_units('inch', system=sysname) + c = ureg.get_base_units("inch", system=sysname) self.assertAlmostEqual(c[0], 0.326, places=3) - self.assertEqual(c[1], {'pint': 1./3}) + self.assertEqual(c[1], {"pint": 1.0 / 3}) - c = ureg.get_base_units('cm', system=sysname) + c = ureg.get_base_units("cm", system=sysname) self.assertAlmostEqual(c[0], 0.1283, places=3) - self.assertEqual(c[1], {'pint': 1./3}) + self.assertEqual(c[1], {"pint": 1.0 / 3}) - c = ureg.get_base_units('inch**2', system=sysname) - self.assertAlmostEqual(c[0], 0.326**2, places=3) - self.assertEqual(c[1], {'pint': 2./3}) + c = ureg.get_base_units("inch**2", system=sysname) + self.assertAlmostEqual(c[0], 0.326 ** 2, places=3) + self.assertEqual(c[1], {"pint": 2.0 / 3}) - c = ureg.get_base_units('cm**2', system=sysname) - self.assertAlmostEqual(c[0], 0.1283**2, places=3) - self.assertEqual(c[1], {'pint': 2./3}) + c = ureg.get_base_units("cm**2", system=sysname) + self.assertAlmostEqual(c[0], 0.1283 ** 2, places=3) + self.assertEqual(c[1], {"pint": 2.0 / 3}) def test_get_base_units_relation(self): - sysname = 'mysys4' + sysname = "mysys4" ureg = UnitRegistry() - g = ureg.get_group('test-imperial') - g.add_units('inch', 'yard', 'pint') + g = ureg.get_group("test-imperial") + g.add_units("inch", "yard", "pint") - lines = ['@system %s using test-imperial' % sysname, - 'mph:meter', - ] + lines = ["@system %s using test-imperial" % sysname, "mph:meter"] s = ureg.System.from_lines(lines, ureg.get_base_units) ureg._systems[s.name] = s # base_factor, destination_units - c = ureg.get_base_units('inch', system=sysname) + c = ureg.get_base_units("inch", system=sysname) self.assertAlmostEqual(c[0], 0.056, places=2) - self.assertEqual(c[1], {'mph': 1, 'second': 1}) + self.assertEqual(c[1], {"mph": 1, "second": 1}) - c = ureg.get_base_units('kph', system=sysname) + c = ureg.get_base_units("kph", system=sysname) self.assertAlmostEqual(c[0], 0.6213, places=3) - self.assertEqual(c[1], {'mph': 1}) + self.assertEqual(c[1], {"mph": 1}) def test_members_nowarning(self): ureg = self.ureg diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index f9bfdcc12..7b917ae88 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -17,7 +17,7 @@ class TestUFuncs(QuantityTestCase): @property def qless(self): - return np.asarray([1., 2., 3., 4.]) * self.ureg.dimensionless + return np.asarray([1.0, 2.0, 3.0, 4.0]) * self.ureg.dimensionless @property def qs(self): @@ -25,7 +25,7 @@ def qs(self): @property def q1(self): - return np.asarray([1., 2., 3., 4.]) * self.ureg.J + return np.asarray([1.0, 2.0, 3.0, 4.0]) * self.ureg.J @property def q2(self): @@ -33,13 +33,15 @@ def q2(self): @property def qm(self): - return np.asarray([1., 2., 3., 4.]) * self.ureg.m + return np.asarray([1.0, 2.0, 3.0, 4.0]) * self.ureg.m @property def qi(self): return np.asarray([1 + 1j, 2 + 2j, 3 + 3j, 4 + 4j]) * self.ureg.m - def _test1(self, func, ok_with, raise_with=(), output_units='same', results=None, rtol=1e-6): + def _test1( + self, func, ok_with, raise_with=(), output_units="same", results=None, rtol=1e-6 + ): """Test function that takes a single argument and returns Quantity. :param func: function callable. @@ -56,10 +58,10 @@ def _test1(self, func, ok_with, raise_with=(), output_units='same', results=Non :param rtol: relative tolerance. """ if results is None: - results = [None, ] * len(ok_with) + results = [None] * len(ok_with) for x1, res in zip(ok_with, results): - err_msg = 'At {} with {}'.format(func.__name__, x1) - if output_units == 'same': + err_msg = "At {} with {}".format(func.__name__, x1) + if output_units == "same": ou = x1.units elif isinstance(output_units, (int, float)): ou = x1.units ** output_units @@ -76,11 +78,11 @@ def _test1(self, func, ok_with, raise_with=(), output_units='same', results=Non for x1 in raise_with: with self.assertRaises( - DimensionalityError, msg=f'At {func.__name__} with {x1}' + DimensionalityError, msg=f"At {func.__name__} with {x1}" ): func(x1) - def _testn(self, func, ok_with, raise_with=(), results=None): + def _testn(self, func, ok_with, raise_with=(), results=None): """Test function that takes a single argument and returns and ndarray (not a Quantity) :param func: function callable. @@ -92,8 +94,15 @@ def _testn(self, func, ok_with, raise_with=(), results=None): """ self._test1(func, ok_with, raise_with, output_units=None, results=results) - def _test1_2o(self, func, ok_with, raise_with=(), output_units=('same', 'same'), - results=None, rtol=1e-6): + def _test1_2o( + self, + func, + ok_with, + raise_with=(), + output_units=("same", "same"), + results=None, + rtol=1e-6, + ): """Test functions that takes a single argument and return two Quantities. :param func: function callable. @@ -111,15 +120,15 @@ def _test1_2o(self, func, ok_with, raise_with=(), output_units=('same', 'same'), """ if results is None: - results = [None, ] * len(ok_with) + results = [None] * len(ok_with) for x1, res in zip(ok_with, results): - err_msg = 'At {} with {}'.format(func.__name__, x1) + err_msg = "At {} with {}".format(func.__name__, x1) qms = func(x1) if res is None: res = func(x1.magnitude) for ndx, (qm, re, ou) in enumerate(zip(qms, res, output_units)): - if ou == 'same': + if ou == "same": ou = x1.units elif isinstance(ou, (int, float)): ou = x1.units ** ou @@ -130,10 +139,19 @@ def _test1_2o(self, func, ok_with, raise_with=(), output_units=('same', 'same'), self.assertQuantityAlmostEqual(qm, re, rtol=rtol, msg=err_msg) for x1 in raise_with: - with self.assertRaises(ValueError, msg=f'At {func.__name__} with {x1}'): + with self.assertRaises(ValueError, msg=f"At {func.__name__} with {x1}"): func(x1) - def _test2(self, func, x1, ok_with, raise_with=(), output_units='same', rtol=1e-6, convert2=True): + def _test2( + self, + func, + x1, + ok_with, + raise_with=(), + output_units="same", + rtol=1e-6, + convert2=True, + ): """Test function that takes two arguments and return a Quantity. :param func: function callable. @@ -151,24 +169,24 @@ def _test2(self, func, x1, ok_with, raise_with=(), output_units='same', rtol=1e- :param convert2: if the ok_with[n] should be converted to x1.units. """ for x2 in ok_with: - err_msg = 'At {} with {} and {}'.format(func.__name__, x1, x2) - if output_units == 'same': + err_msg = "At {} with {} and {}".format(func.__name__, x1, x2) + if output_units == "same": ou = x1.units - elif output_units == 'prod': + elif output_units == "prod": ou = x1.units * x2.units - elif output_units == 'div': + elif output_units == "div": ou = x1.units / x2.units - elif output_units == 'second': + elif output_units == "second": ou = x1.units ** x2 else: ou = output_units qm = func(x1, x2) - if convert2 and hasattr(x2, 'magnitude'): - m2 = x2.to(getattr(x1, 'units', '')).magnitude + if convert2 and hasattr(x2, "magnitude"): + m2 = x2.to(getattr(x1, "units", "")).magnitude else: - m2 = getattr(x2, 'magnitude', x2) + m2 = getattr(x2, "magnitude", x2) res = func(x1.magnitude, m2) if ou is not None: @@ -178,7 +196,7 @@ def _test2(self, func, x1, ok_with, raise_with=(), output_units='same', rtol=1e- for x2 in raise_with: with self.assertRaises( - DimensionalityError, msg=f'At {func.__name__} with {x1} and {x2}' + DimensionalityError, msg=f"At {func.__name__} with {x1} and {x2}" ): func(x1, x2) @@ -228,157 +246,110 @@ class TestMathUfuncs(TestUFuncs): reciprocal(x[, out]) Return the reciprocal of the argument, element-wise. ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array. """ + def test_add(self): - self._test2(np.add, - self.q1, - (self.q2, self.qs), - (self.qm, )) + self._test2(np.add, self.q1, (self.q2, self.qs), (self.qm,)) def test_subtract(self): - self._test2(np.subtract, - self.q1, - (self.q2, self.qs), - (self.qm, )) + self._test2(np.subtract, self.q1, (self.q2, self.qs), (self.qm,)) def test_multiply(self): - self._test2(np.multiply, - self.q1, - (self.q2, self.qs), (), - 'prod') + self._test2(np.multiply, self.q1, (self.q2, self.qs), (), "prod") def test_divide(self): - self._test2(np.divide, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'div', convert2=False) + self._test2( + np.divide, + self.q1, + (self.q2, self.qs, self.qless), + (), + "div", + convert2=False, + ) def test_logaddexp(self): - self._test2(np.logaddexp, - self.qless, - (self.qless, ), - (self.q1, ), - '') + self._test2(np.logaddexp, self.qless, (self.qless,), (self.q1,), "") def test_logaddexp2(self): - self._test2(np.logaddexp2, - self.qless, - (self.qless, ), - (self.q1, ), - 'div') + self._test2(np.logaddexp2, self.qless, (self.qless,), (self.q1,), "div") def test_true_divide(self): - self._test2(np.true_divide, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'div', convert2=False) + self._test2( + np.true_divide, + self.q1, + (self.q2, self.qs, self.qless), + (), + "div", + convert2=False, + ) def test_floor_divide(self): - self._test2(np.floor_divide, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'div', convert2=False) - + self._test2( + np.floor_divide, + self.q1, + (self.q2, self.qs, self.qless), + (), + "div", + convert2=False, + ) def test_negative(self): - self._test1(np.negative, - (self.qless, self.q1), - ()) + self._test1(np.negative, (self.qless, self.q1), ()) def test_remainder(self): - self._test2(np.remainder, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'same', convert2=False) + self._test2( + np.remainder, + self.q1, + (self.q2, self.qs, self.qless), + (), + "same", + convert2=False, + ) def test_mod(self): - self._test2(np.mod, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'same', convert2=False) + self._test2( + np.mod, self.q1, (self.q2, self.qs, self.qless), (), "same", convert2=False + ) def test_fmod(self): - self._test2(np.fmod, - self.q1, - (self.q2, self.qs, self.qless), - (), - 'same', convert2=False) + self._test2( + np.fmod, self.q1, (self.q2, self.qs, self.qless), (), "same", convert2=False + ) def test_absolute(self): - self._test1(np.absolute, - (self.q2, self.qs, self.qless, self.qi), - (), - 'same') + self._test1(np.absolute, (self.q2, self.qs, self.qless, self.qi), (), "same") def test_rint(self): - self._test1(np.rint, - (self.q2, self.qs, self.qless, self.qi), - (), - 'same') + self._test1(np.rint, (self.q2, self.qs, self.qless, self.qi), (), "same") def test_conj(self): - self._test1(np.conj, - (self.q2, self.qs, self.qless, self.qi), - (), - 'same') + self._test1(np.conj, (self.q2, self.qs, self.qless, self.qi), (), "same") def test_exp(self): - self._test1(np.exp, - (self.qless, ), - (self.q1, ), - '') + self._test1(np.exp, (self.qless,), (self.q1,), "") def test_exp2(self): - self._test1(np.exp2, - (self.qless,), - (self.q1, ), - '') + self._test1(np.exp2, (self.qless,), (self.q1,), "") def test_log(self): - self._test1(np.log, - (self.qless,), - (self.q1, ), - '') + self._test1(np.log, (self.qless,), (self.q1,), "") def test_log2(self): - self._test1(np.log2, - (self.qless,), - (self.q1, ), - '') + self._test1(np.log2, (self.qless,), (self.q1,), "") def test_log10(self): - self._test1(np.log10, - (self.qless,), - (self.q1, ), - '') + self._test1(np.log10, (self.qless,), (self.q1,), "") def test_expm1(self): - self._test1(np.expm1, - (self.qless,), - (self.q1, ), - '') + self._test1(np.expm1, (self.qless,), (self.q1,), "") def test_sqrt(self): - self._test1(np.sqrt, - (self.q2, self.qs, self.qless, self.qi), - (), - 0.5) + self._test1(np.sqrt, (self.q2, self.qs, self.qless, self.qi), (), 0.5) def test_square(self): - self._test1(np.square, - (self.q2, self.qs, self.qless, self.qi), - (), - 2) + self._test1(np.square, (self.q2, self.qs, self.qless, self.qi), (), 2) def test_reciprocal(self): - self._test1(np.reciprocal, - (self.q2, self.qs, self.qless, self.qi), - (), - -1) + self._test1(np.reciprocal, (self.q2, self.qs, self.qless, self.qi), (), -1) @helpers.requires_numpy() @@ -406,125 +377,231 @@ class TestTrigUfuncs(TestUFuncs): """ def test_sin(self): - self._test1(np.sin, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m - ), (1*self.ureg.m, ), '', results=(None, None, np.sin(np.arange(0, pi/2, pi/4)*0.001))) - self._test1(np.sin, (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), results=(np.sin(np.arange(0, pi/2, pi/4)), )) + self._test1( + np.sin, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.sin(np.arange(0, pi / 2, pi / 4) * 0.001)), + ) + self._test1( + np.sin, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.sin(np.arange(0, pi / 2, pi / 4)),), + ) def test_cos(self): - self._test1(np.cos, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m, - ), (1*self.ureg.m, ), '', - results=(None, - None, - np.cos(np.arange(0, pi/2, pi/4)*0.001), - ) + self._test1( + np.cos, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.cos(np.arange(0, pi / 2, pi / 4) * 0.001)), ) - self._test1(np.cos, - (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), - results=(np.cos(np.arange(0, pi/2, pi/4)), ) + self._test1( + np.cos, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.cos(np.arange(0, pi / 2, pi / 4)),), ) def test_tan(self): - self._test1(np.tan, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m - ), (1*self.ureg.m, ), '', results=(None, None, np.tan(np.arange(0, pi/2, pi/4)*0.001))) - self._test1(np.tan, (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), results=(np.tan(np.arange(0, pi/2, pi/4)), )) + self._test1( + np.tan, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.tan(np.arange(0, pi / 2, pi / 4) * 0.001)), + ) + self._test1( + np.tan, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.tan(np.arange(0, pi / 2, pi / 4)),), + ) def test_arcsin(self): - self._test1(np.arcsin, (np.arange(0, .9, .1) * self.ureg.dimensionless, - np.arange(0, .9, .1) * self.ureg.m / self.ureg.m - ), (1*self.ureg.m, ), 'radian') + self._test1( + np.arcsin, + ( + np.arange(0, 0.9, 0.1) * self.ureg.dimensionless, + np.arange(0, 0.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (1 * self.ureg.m,), + "radian", + ) def test_arccos(self): - x = np.arange(0, .9, .1) * self.ureg.m - self._test1(np.arccos, (np.arange(0, .9, .1) * self.ureg.dimensionless, - np.arange(0, .9, .1) * self.ureg.m / self.ureg.m - ), (1*self.ureg.m, ), 'radian') + x = np.arange(0, 0.9, 0.1) * self.ureg.m + self._test1( + np.arccos, + ( + np.arange(0, 0.9, 0.1) * self.ureg.dimensionless, + np.arange(0, 0.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (1 * self.ureg.m,), + "radian", + ) def test_arctan(self): - self._test1(np.arctan, (np.arange(0, .9, .1) * self.ureg.dimensionless, - np.arange(0, .9, .1) * self.ureg.m / self.ureg.m - ), (1*self.ureg.m, ), 'radian') + self._test1( + np.arctan, + ( + np.arange(0, 0.9, 0.1) * self.ureg.dimensionless, + np.arange(0, 0.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (1 * self.ureg.m,), + "radian", + ) def test_arctan2(self): m = self.ureg.m j = self.ureg.J km = self.ureg.km - self._test2(np.arctan2, np.arange(0, .9, .1) * m, - (np.arange(0, .9, .1) * m, np.arange(.9, 0., -.1) * m, - np.arange(0, .9, .1) * km, np.arange(.9, 0., -.1) * km, - ), - raise_with=np.arange(0, .9, .1) * j, - output_units='radian') + self._test2( + np.arctan2, + np.arange(0, 0.9, 0.1) * m, + ( + np.arange(0, 0.9, 0.1) * m, + np.arange(0.9, 0.0, -0.1) * m, + np.arange(0, 0.9, 0.1) * km, + np.arange(0.9, 0.0, -0.1) * km, + ), + raise_with=np.arange(0, 0.9, 0.1) * j, + output_units="radian", + ) def test_hypot(self): - self.assertTrue(np.hypot(3. * self.ureg.m, 4. * self.ureg.m) == 5. * self.ureg.m) - self.assertTrue(np.hypot(3. * self.ureg.m, 400. * self.ureg.cm) == 5. * self.ureg.m) + self.assertTrue( + np.hypot(3.0 * self.ureg.m, 4.0 * self.ureg.m) == 5.0 * self.ureg.m + ) + self.assertTrue( + np.hypot(3.0 * self.ureg.m, 400.0 * self.ureg.cm) == 5.0 * self.ureg.m + ) with self.assertRaises(DimensionalityError): - np.hypot(1. * self.ureg.m, 2. * self.ureg.J) + np.hypot(1.0 * self.ureg.m, 2.0 * self.ureg.J) def test_sinh(self): - self._test1(np.sinh, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m - ), (1*self.ureg.m, ), '', results=(None, None, np.sinh(np.arange(0, pi/2, pi/4)*0.001))) - self._test1(np.sinh, (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), results=(np.sinh(np.arange(0, pi/2, pi/4)), )) + self._test1( + np.sinh, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.sinh(np.arange(0, pi / 2, pi / 4) * 0.001)), + ) + self._test1( + np.sinh, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.sinh(np.arange(0, pi / 2, pi / 4)),), + ) def test_cosh(self): - self._test1(np.cosh, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m - ), (1*self.ureg.m, ), '', results=(None, None, np.cosh(np.arange(0, pi/2, pi/4)*0.001))) - self._test1(np.cosh, (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), results=(np.cosh(np.arange(0, pi/2, pi/4)), )) + self._test1( + np.cosh, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.cosh(np.arange(0, pi / 2, pi / 4) * 0.001)), + ) + self._test1( + np.cosh, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.cosh(np.arange(0, pi / 2, pi / 4)),), + ) def test_tanh(self): - self._test1(np.tanh, (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m - ), (1*self.ureg.m, ), '', results=(None, None, np.tanh(np.arange(0, pi/2, pi/4)*0.001))) - self._test1(np.tanh, (np.rad2deg(np.arange(0, pi/2, pi/4)) * self.ureg.degrees, - ), results=(np.tanh(np.arange(0, pi/2, pi/4)), )) + self._test1( + np.tanh, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "", + results=(None, None, np.tanh(np.arange(0, pi / 2, pi / 4) * 0.001)), + ) + self._test1( + np.tanh, + (np.rad2deg(np.arange(0, pi / 2, pi / 4)) * self.ureg.degrees,), + results=(np.tanh(np.arange(0, pi / 2, pi / 4)),), + ) def test_arcsinh(self): - self._test1(np.arcsinh, (np.arange(0, .9, .1) * self.ureg.dimensionless, - np.arange(0, .9, .1) * self.ureg.m / self.ureg.m - ), (1*self.ureg.m, ), 'radian') + self._test1( + np.arcsinh, + ( + np.arange(0, 0.9, 0.1) * self.ureg.dimensionless, + np.arange(0, 0.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (1 * self.ureg.m,), + "radian", + ) def test_arccosh(self): - self._test1(np.arccosh, (np.arange(1., 1.9, .1) * self.ureg.dimensionless, - np.arange(1., 1.9, .1) * self.ureg.m / self.ureg.m - ), (1*self.ureg.m, ), 'radian') + self._test1( + np.arccosh, + ( + np.arange(1.0, 1.9, 0.1) * self.ureg.dimensionless, + np.arange(1.0, 1.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (1 * self.ureg.m,), + "radian", + ) def test_arctanh(self): - self._test1(np.arctanh, (np.arange(0, .9, .1) * self.ureg.dimensionless, - np.arange(0, .9, .1) * self.ureg.m / self.ureg.m - ), (.1 * self.ureg.m, ), 'radian') + self._test1( + np.arctanh, + ( + np.arange(0, 0.9, 0.1) * self.ureg.dimensionless, + np.arange(0, 0.9, 0.1) * self.ureg.m / self.ureg.m, + ), + (0.1 * self.ureg.m,), + "radian", + ) def test_deg2rad(self): - self._test1(np.deg2rad, (np.arange(0, pi/2, pi/4) * self.ureg.degrees, - ), (1*self.ureg.m, ), 'radians') + self._test1( + np.deg2rad, + (np.arange(0, pi / 2, pi / 4) * self.ureg.degrees,), + (1 * self.ureg.m,), + "radians", + ) def test_rad2deg(self): - self._test1(np.rad2deg, - (np.arange(0, pi/2, pi/4) * self.ureg.dimensionless, - np.arange(0, pi/2, pi/4) * self.ureg.radian, - np.arange(0, pi/2, pi/4) * self.ureg.mm / self.ureg.m, - ), - (1*self.ureg.m, ), 'degree', - results=(None, - None, - np.rad2deg(np.arange(0, pi/2, pi/4)*0.001) * self.ureg.degree, - )) - + self._test1( + np.rad2deg, + ( + np.arange(0, pi / 2, pi / 4) * self.ureg.dimensionless, + np.arange(0, pi / 2, pi / 4) * self.ureg.radian, + np.arange(0, pi / 2, pi / 4) * self.ureg.mm / self.ureg.m, + ), + (1 * self.ureg.m,), + "degree", + results=( + None, + None, + np.rad2deg(np.arange(0, pi / 2, pi / 4) * 0.001) * self.ureg.degree, + ), + ) class TestComparisonUfuncs(TestUFuncs): @@ -541,40 +618,22 @@ class TestComparisonUfuncs(TestUFuncs): """ def test_greater(self): - self._testn2(np.greater, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.greater, self.q1, (self.q2,), (self.qm,)) def test_greater_equal(self): - self._testn2(np.greater_equal, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.greater_equal, self.q1, (self.q2,), (self.qm,)) def test_less(self): - self._testn2(np.less, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.less, self.q1, (self.q2,), (self.qm,)) def test_less_equal(self): - self._testn2(np.less_equal, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.less_equal, self.q1, (self.q2,), (self.qm,)) def test_not_equal(self): - self._testn2(np.not_equal, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.not_equal, self.q1, (self.q2,), (self.qm,)) def test_equal(self): - self._testn2(np.equal, - self.q1, - (self.q2, ), - (self.qm, )) + self._testn2(np.equal, self.q1, (self.q2,), (self.qm,)) class TestFloatingUfuncs(TestUFuncs): @@ -600,69 +659,48 @@ class TestFloatingUfuncs(TestUFuncs): """ def test_isreal(self): - self._testn(np.isreal, - (self.q1, self.qm, self.qless)) + self._testn(np.isreal, (self.q1, self.qm, self.qless)) def test_iscomplex(self): - self._testn(np.iscomplex, - (self.q1, self.qm, self.qless)) + self._testn(np.iscomplex, (self.q1, self.qm, self.qless)) def test_isfinite(self): - self._testn(np.isfinite, - (self.q1, self.qm, self.qless)) + self._testn(np.isfinite, (self.q1, self.qm, self.qless)) def test_isinf(self): - self._testn(np.isinf, - (self.q1, self.qm, self.qless)) + self._testn(np.isinf, (self.q1, self.qm, self.qless)) def test_isnan(self): - self._testn(np.isnan, - (self.q1, self.qm, self.qless)) + self._testn(np.isnan, (self.q1, self.qm, self.qless)) def test_signbit(self): - self._testn(np.signbit, - (self.q1, self.qm, self.qless)) + self._testn(np.signbit, (self.q1, self.qm, self.qless)) def test_copysign(self): - self._test2(np.copysign, - self.q1, - (self.q2, self.qs), - (self.qm, )) + self._test2(np.copysign, self.q1, (self.q2, self.qs), (self.qm,)) def test_nextafter(self): - self._test2(np.nextafter, - self.q1, - (self.q2, self.qs), - (self.qm, )) + self._test2(np.nextafter, self.q1, (self.q2, self.qs), (self.qm,)) def test_modf(self): - self._test1_2o(np.modf, - (self.q2, self.qs), - ) + self._test1_2o(np.modf, (self.q2, self.qs)) def test_ldexp(self): x1, x2 = np.frexp(self.q2) - self._test2(np.ldexp, - x1, - (x2, )) + self._test2(np.ldexp, x1, (x2,)) def test_frexp(self): - self._test1_2o(np.frexp, - (self.q2, self.qs), - output_units=('same', None)) + self._test1_2o(np.frexp, (self.q2, self.qs), output_units=("same", None)) def test_fmod(self): # See TestMathUfuncs.test_fmod pass def test_floor(self): - self._test1(np.floor, - (self.q1, self.qm, self.qless)) + self._test1(np.floor, (self.q1, self.qm, self.qless)) def test_ceil(self): - self._test1(np.ceil, - (self.q1, self.qm, self.qless)) + self._test1(np.ceil, (self.q1, self.qm, self.qless)) def test_trunc(self): - self._test1(np.trunc, - (self.q1, self.qm, self.qless)) + self._test1(np.trunc, (self.q1, self.qm, self.qless)) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 30ccec0ee..e1bcf57e5 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -6,19 +6,21 @@ import re from pint.compat import np -from pint.registry import (UnitRegistry, LazyRegistry) +from pint.registry import UnitRegistry, LazyRegistry from pint import ( - DefinitionSyntaxError, DimensionalityError, RedefinitionError, UndefinedUnitError + DefinitionSyntaxError, + DimensionalityError, + RedefinitionError, + UndefinedUnitError, ) from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase -from pint.util import (UnitsContainer, ParserHelper) +from pint.util import UnitsContainer, ParserHelper class TestUnit(QuantityTestCase): - def test_creation(self): - for arg in ('meter', UnitsContainer(meter=1), self.U_('m')): + for arg in ("meter", UnitsContainer(meter=1), self.U_("m")): self.assertEqual(self.U_(arg)._units, UnitsContainer(meter=1)) self.assertRaises(TypeError, self.U_, 1) @@ -28,25 +30,28 @@ def test_deepcopy(self): def test_unit_repr(self): x = self.U_(UnitsContainer(meter=1)) - self.assertEqual(str(x), 'meter') + self.assertEqual(str(x), "meter") self.assertEqual(repr(x), "") def test_unit_formatting(self): x = self.U_(UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( - ('{}', str(x)), - ('{!s}', str(x)), - ('{!r}', repr(x)), - ('{:L}', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('{:P}', 'kilogram·meter²/second'), - ('{:H}', 'kilogram meter^2/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~}', 'kg m^2/s'), - ('{:C~}', 'kg*m**2/s'), + ("{}", str(x)), + ("{!s}", str(x)), + ("{!r}", repr(x)), + ( + "{:L}", + r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", + ), + ("{:P}", "kilogram·meter²/second"), + ("{:H}", "kilogram meter^2/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~}", "kg m^2/s"), + ("{:C~}", "kg*m**2/s"), ): with self.subTest(spec): self.assertEqual(spec.format(x), result) @@ -55,38 +60,41 @@ def test_unit_default_formatting(self): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( - ('L', r'\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}'), - ('P', 'kilogram·meter²/second'), - ('H', 'kilogram meter^2/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~', 'kg m^2/s'), - ('C~', 'kg*m**2/s'), + ( + "L", + r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", + ), + ("P", "kilogram·meter²/second"), + ("H", "kilogram meter^2/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~", "kg m^2/s"), + ("C~", "kg*m**2/s"), ): with self.subTest(spec): ureg.default_format = spec - self.assertEqual(f'{x}', result, f'Failed for {spec}, {result}') + self.assertEqual(f"{x}", result, f"Failed for {spec}, {result}") def test_unit_formatting_snake_case(self): # Test that snake_case units are escaped where appropriate ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(oil_barrel=1)) for spec, result in ( - ('L', r'\mathrm{oil\_barrel}'), - ('P', 'oil_barrel'), - ('H', 'oil\_barrel'), - ('C', 'oil_barrel'), - ('~', 'oil_bbl'), - ('L~', r'\mathrm{oil\_bbl}'), - ('P~', 'oil_bbl'), - ('H~', 'oil\_bbl'), - ('C~', 'oil_bbl'), + ("L", r"\mathrm{oil\_barrel}"), + ("P", "oil_barrel"), + ("H", "oil\_barrel"), + ("C", "oil_barrel"), + ("~", "oil_bbl"), + ("L~", r"\mathrm{oil\_bbl}"), + ("P~", "oil_bbl"), + ("H~", "oil\_bbl"), + ("C~", "oil_bbl"), ): with self.subTest(spec): ureg.default_format = spec - self.assertEqual(f'{x}', result, f'Failed for {spec}, {result}') + self.assertEqual(f"{x}", result, f"Failed for {spec}, {result}") def test_ipython(self): alltext = [] @@ -99,94 +107,97 @@ def text(text): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) self.assertEqual(x._repr_html_(), "kilogram meter^2/second") - self.assertEqual(x._repr_latex_(), r'$\frac{\mathrm{kilogram} \cdot ' - r'\mathrm{meter}^{2}}{\mathrm{second}}$') + self.assertEqual( + x._repr_latex_(), + r"$\frac{\mathrm{kilogram} \cdot " r"\mathrm{meter}^{2}}{\mathrm{second}}$", + ) x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "kilogram·meter²/second") ureg.default_format = "~" self.assertEqual(x._repr_html_(), "kg m^2/s") - self.assertEqual(x._repr_latex_(), - r'$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$') + self.assertEqual( + x._repr_latex_(), r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" + ) alltext = [] x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "kg·m²/s") def test_unit_mul(self): - x = self.U_('m') - self.assertEqual(x*1, self.Q_(1, 'm')) - self.assertEqual(x*0.5, self.Q_(0.5, 'm')) - self.assertEqual(x*self.Q_(1, 'm'), self.Q_(1, 'm**2')) - self.assertEqual(1*x, self.Q_(1, 'm')) + x = self.U_("m") + self.assertEqual(x * 1, self.Q_(1, "m")) + self.assertEqual(x * 0.5, self.Q_(0.5, "m")) + self.assertEqual(x * self.Q_(1, "m"), self.Q_(1, "m**2")) + self.assertEqual(1 * x, self.Q_(1, "m")) def test_unit_div(self): - x = self.U_('m') - self.assertEqual(x/1, self.Q_(1, 'm')) - self.assertEqual(x/0.5, self.Q_(2.0, 'm')) - self.assertEqual(x/self.Q_(1, 'm'), self.Q_(1)) + x = self.U_("m") + self.assertEqual(x / 1, self.Q_(1, "m")) + self.assertEqual(x / 0.5, self.Q_(2.0, "m")) + self.assertEqual(x / self.Q_(1, "m"), self.Q_(1)) def test_unit_rdiv(self): - x = self.U_('m') - self.assertEqual(1/x, self.Q_(1, '1/m')) + x = self.U_("m") + self.assertEqual(1 / x, self.Q_(1, "1/m")) def test_unit_pow(self): - x = self.U_('m') - self.assertEqual(x**2, self.U_('m**2')) + x = self.U_("m") + self.assertEqual(x ** 2, self.U_("m**2")) def test_unit_hash(self): - x = self.U_('m') + x = self.U_("m") self.assertEqual(hash(x), hash(x._units)) def test_unit_eqs(self): - x = self.U_('m') - self.assertEqual(x, self.U_('m')) - self.assertNotEqual(x, self.U_('cm')) + x = self.U_("m") + self.assertEqual(x, self.U_("m")) + self.assertNotEqual(x, self.U_("cm")) - self.assertEqual(x, self.Q_(1, 'm')) - self.assertNotEqual(x, self.Q_(2, 'm')) + self.assertEqual(x, self.Q_(1, "m")) + self.assertNotEqual(x, self.Q_(2, "m")) - self.assertEqual(x, UnitsContainer({'meter': 1})) + self.assertEqual(x, UnitsContainer({"meter": 1})) - y = self.U_('cm/m') + y = self.U_("cm/m") self.assertEqual(y, 0.01) - self.assertEqual(self.U_('byte') == self.U_('byte'), True) - self.assertEqual(self.U_('byte') != self.U_('byte'), False) + self.assertEqual(self.U_("byte") == self.U_("byte"), True) + self.assertEqual(self.U_("byte") != self.U_("byte"), False) def test_unit_cmp(self): - x = self.U_('m') - self.assertLess(x, self.U_('km')) - self.assertGreater(x, self.U_('mm')) + x = self.U_("m") + self.assertLess(x, self.U_("km")) + self.assertGreater(x, self.U_("mm")) - y = self.U_('m/mm') + y = self.U_("m/mm") self.assertGreater(y, 1) self.assertLess(y, 1e6) def test_dimensionality(self): - x = self.U_('m') - self.assertEqual(x.dimensionality, UnitsContainer({'[length]': 1})) + x = self.U_("m") + self.assertEqual(x.dimensionality, UnitsContainer({"[length]": 1})) def test_dimensionless(self): - self.assertTrue(self.U_('m/mm').dimensionless) - self.assertFalse(self.U_('m').dimensionless) + self.assertTrue(self.U_("m/mm").dimensionless) + self.assertFalse(self.U_("m").dimensionless) def test_unit_casting(self): - self.assertEqual(int(self.U_('m/mm')), 1000) - self.assertEqual(float(self.U_('mm/m')), 1e-3) - self.assertEqual(complex(self.U_('mm/mm')), 1+0j) + self.assertEqual(int(self.U_("m/mm")), 1000) + self.assertEqual(float(self.U_("mm/m")), 1e-3) + self.assertEqual(complex(self.U_("mm/mm")), 1 + 0j) @helpers.requires_numpy() def test_array_interface(self): import numpy as np - x = self.U_('m') + x = self.U_("m") arr = np.ones(10) - self.assertQuantityEqual(arr*x, self.Q_(arr, 'm')) - self.assertQuantityEqual(arr/x, self.Q_(arr, '1/m')) - self.assertQuantityEqual(x/arr, self.Q_(arr, 'm')) + self.assertQuantityEqual(arr * x, self.Q_(arr, "m")) + self.assertQuantityEqual(arr / x, self.Q_(arr, "1/m")) + self.assertQuantityEqual(x / arr, self.Q_(arr, "m")) class TestRegistry(QuantityTestCase): @@ -198,10 +209,10 @@ def setup(self): def test_base(self): ureg = UnitRegistry(None) - ureg.define('meter = [length]') - self.assertRaises(DefinitionSyntaxError, ureg.define, 'meter = [length]') + ureg.define("meter = [length]") + self.assertRaises(DefinitionSyntaxError, ureg.define, "meter = [length]") self.assertRaises(TypeError, ureg.define, list()) - x = ureg.define('degC = kelvin; offset: 273.15') + x = ureg.define("degC = kelvin; offset: 273.15") def test_define(self): ureg = UnitRegistry(None) @@ -211,129 +222,209 @@ def test_define(self): def test_load(self): import pkg_resources from pint import unit - data = pkg_resources.resource_filename(unit.__name__, 'default_en.txt') + + data = pkg_resources.resource_filename(unit.__name__, "default_en.txt") ureg1 = UnitRegistry() ureg2 = UnitRegistry(data) self.assertEqual(dir(ureg1), dir(ureg2)) - self.assertRaises(ValueError, UnitRegistry(None).load_definitions, 'notexisting') + self.assertRaises( + ValueError, UnitRegistry(None).load_definitions, "notexisting" + ) def test_default_format(self): ureg = UnitRegistry() q = ureg.meter - s1 = '{0}'.format(q) - s2 = '{0:~}'.format(q) - ureg.default_format = '~' - s3 = '{0}'.format(q) + s1 = "{0}".format(q) + s2 = "{0:~}".format(q) + ureg.default_format = "~" + s3 = "{0}".format(q) self.assertEqual(s2, s3) self.assertNotEqual(s1, s3) - self.assertEqual(ureg.default_format, '~') + self.assertEqual(ureg.default_format, "~") def test_parse_number(self): - self.assertEqual(self.ureg.parse_expression('pi'), math.pi) - self.assertEqual(self.ureg.parse_expression('x', x=2), 2) - self.assertEqual(self.ureg.parse_expression('x', x=2.3), 2.3) - self.assertEqual(self.ureg.parse_expression('x * y', x=2.3, y=3), 2.3 * 3) - self.assertEqual(self.ureg.parse_expression('x', x=(1+1j)), (1+1j)) + self.assertEqual(self.ureg.parse_expression("pi"), math.pi) + self.assertEqual(self.ureg.parse_expression("x", x=2), 2) + self.assertEqual(self.ureg.parse_expression("x", x=2.3), 2.3) + self.assertEqual(self.ureg.parse_expression("x * y", x=2.3, y=3), 2.3 * 3) + self.assertEqual(self.ureg.parse_expression("x", x=(1 + 1j)), (1 + 1j)) def test_parse_single(self): - self.assertEqual(self.ureg.parse_expression('meter'), self.Q_(1, UnitsContainer(meter=1.))) - self.assertEqual(self.ureg.parse_expression('second'), self.Q_(1, UnitsContainer(second=1.))) + self.assertEqual( + self.ureg.parse_expression("meter"), self.Q_(1, UnitsContainer(meter=1.0)) + ) + self.assertEqual( + self.ureg.parse_expression("second"), self.Q_(1, UnitsContainer(second=1.0)) + ) def test_parse_alias(self): - self.assertEqual(self.ureg.parse_expression('metre'), self.Q_(1, UnitsContainer(meter=1.))) + self.assertEqual( + self.ureg.parse_expression("metre"), self.Q_(1, UnitsContainer(meter=1.0)) + ) def test_parse_plural(self): - self.assertEqual(self.ureg.parse_expression('meters'), self.Q_(1, UnitsContainer(meter=1.))) + self.assertEqual( + self.ureg.parse_expression("meters"), self.Q_(1, UnitsContainer(meter=1.0)) + ) def test_parse_prefix(self): - self.assertEqual(self.ureg.parse_expression('kilometer'), self.Q_(1, UnitsContainer(kilometer=1.))) - #self.assertEqual(self.ureg._units['kilometer'], self.Q_(1000., UnitsContainer(meter=1.))) + self.assertEqual( + self.ureg.parse_expression("kilometer"), + self.Q_(1, UnitsContainer(kilometer=1.0)), + ) + # self.assertEqual(self.ureg._units['kilometer'], self.Q_(1000., UnitsContainer(meter=1.))) def test_parse_complex(self): - self.assertEqual(self.ureg.parse_expression('kilometre'), self.Q_(1, UnitsContainer(kilometer=1.))) - self.assertEqual(self.ureg.parse_expression('kilometres'), self.Q_(1, UnitsContainer(kilometer=1.))) + self.assertEqual( + self.ureg.parse_expression("kilometre"), + self.Q_(1, UnitsContainer(kilometer=1.0)), + ) + self.assertEqual( + self.ureg.parse_expression("kilometres"), + self.Q_(1, UnitsContainer(kilometer=1.0)), + ) def test_parse_mul_div(self): - self.assertEqual(self.ureg.parse_expression('meter*meter'), self.Q_(1, UnitsContainer(meter=2.))) - self.assertEqual(self.ureg.parse_expression('meter**2'), self.Q_(1, UnitsContainer(meter=2.))) - self.assertEqual(self.ureg.parse_expression('meter*second'), self.Q_(1, UnitsContainer(meter=1., second=1))) - self.assertEqual(self.ureg.parse_expression('meter/second'), self.Q_(1, UnitsContainer(meter=1., second=-1))) - self.assertEqual(self.ureg.parse_expression('meter/second**2'), self.Q_(1, UnitsContainer(meter=1., second=-2))) + self.assertEqual( + self.ureg.parse_expression("meter*meter"), + self.Q_(1, UnitsContainer(meter=2.0)), + ) + self.assertEqual( + self.ureg.parse_expression("meter**2"), + self.Q_(1, UnitsContainer(meter=2.0)), + ) + self.assertEqual( + self.ureg.parse_expression("meter*second"), + self.Q_(1, UnitsContainer(meter=1.0, second=1)), + ) + self.assertEqual( + self.ureg.parse_expression("meter/second"), + self.Q_(1, UnitsContainer(meter=1.0, second=-1)), + ) + self.assertEqual( + self.ureg.parse_expression("meter/second**2"), + self.Q_(1, UnitsContainer(meter=1.0, second=-2)), + ) def test_parse_pretty(self): - self.assertEqual(self.ureg.parse_expression('meter/second²'), - self.Q_(1, UnitsContainer(meter=1., second=-2))) - self.assertEqual(self.ureg.parse_expression('m³/s³'), - self.Q_(1, UnitsContainer(meter=3., second=-3))) - self.assertEqual(self.ureg.parse_expression('meter² · second'), - self.Q_(1, UnitsContainer(meter=2., second=1))) - self.assertEqual(self.ureg.parse_expression('meter⁰.⁵·second'), - self.Q_(1, UnitsContainer(meter=0.5, second=1))) - self.assertEqual(self.ureg.parse_expression('meter³⁷/second⁴.³²¹'), - self.Q_(1, UnitsContainer(meter=37, second=-4.321))) + self.assertEqual( + self.ureg.parse_expression("meter/second²"), + self.Q_(1, UnitsContainer(meter=1.0, second=-2)), + ) + self.assertEqual( + self.ureg.parse_expression("m³/s³"), + self.Q_(1, UnitsContainer(meter=3.0, second=-3)), + ) + self.assertEqual( + self.ureg.parse_expression("meter² · second"), + self.Q_(1, UnitsContainer(meter=2.0, second=1)), + ) + self.assertEqual( + self.ureg.parse_expression("meter⁰.⁵·second"), + self.Q_(1, UnitsContainer(meter=0.5, second=1)), + ) + self.assertEqual( + self.ureg.parse_expression("meter³⁷/second⁴.³²¹"), + self.Q_(1, UnitsContainer(meter=37, second=-4.321)), + ) def test_parse_factor(self): - self.assertEqual(self.ureg.parse_expression('42*meter'), self.Q_(42, UnitsContainer(meter=1.))) - self.assertEqual(self.ureg.parse_expression('meter*42'), self.Q_(42, UnitsContainer(meter=1.))) + self.assertEqual( + self.ureg.parse_expression("42*meter"), + self.Q_(42, UnitsContainer(meter=1.0)), + ) + self.assertEqual( + self.ureg.parse_expression("meter*42"), + self.Q_(42, UnitsContainer(meter=1.0)), + ) def test_rep_and_parse(self): - q = self.Q_(1, 'g/(m**2*s)') + q = self.Q_(1, "g/(m**2*s)") self.assertEqual(self.Q_(q.magnitude, str(q.units)), q) def test_as_delta(self): parse = self.ureg.parse_units - self.assertEqual(parse('kelvin', as_delta=True), UnitsContainer(kelvin=1)) - self.assertEqual(parse('kelvin', as_delta=False), UnitsContainer(kelvin=1)) - self.assertEqual(parse('kelvin**(-1)', as_delta=True), UnitsContainer(kelvin=-1)) - self.assertEqual(parse('kelvin**(-1)', as_delta=False), UnitsContainer(kelvin=-1)) - self.assertEqual(parse('kelvin**2', as_delta=True), UnitsContainer(kelvin=2)) - self.assertEqual(parse('kelvin**2', as_delta=False), UnitsContainer(kelvin=2)) - self.assertEqual(parse('kelvin*meter', as_delta=True), UnitsContainer(kelvin=1, meter=1)) - self.assertEqual(parse('kelvin*meter', as_delta=False), UnitsContainer(kelvin=1, meter=1)) + self.assertEqual(parse("kelvin", as_delta=True), UnitsContainer(kelvin=1)) + self.assertEqual(parse("kelvin", as_delta=False), UnitsContainer(kelvin=1)) + self.assertEqual( + parse("kelvin**(-1)", as_delta=True), UnitsContainer(kelvin=-1) + ) + self.assertEqual( + parse("kelvin**(-1)", as_delta=False), UnitsContainer(kelvin=-1) + ) + self.assertEqual(parse("kelvin**2", as_delta=True), UnitsContainer(kelvin=2)) + self.assertEqual(parse("kelvin**2", as_delta=False), UnitsContainer(kelvin=2)) + self.assertEqual( + parse("kelvin*meter", as_delta=True), UnitsContainer(kelvin=1, meter=1) + ) + self.assertEqual( + parse("kelvin*meter", as_delta=False), UnitsContainer(kelvin=1, meter=1) + ) def test_parse_expression_with_preprocessor(self): # Add parsing of UDUNITS-style power - self.ureg.preprocessors.append(functools.partial( - re.sub, r'(?<=[A-Za-z])(?![A-Za-z])(?') + self.assertEqual(str(x), "dimensionless") + self.assertEqual(repr(x), "") x = UnitsContainer(meter=1, second=2) - self.assertEqual(str(x), 'meter * second ** 2') - self.assertEqual(repr(x), - "") + self.assertEqual(str(x), "meter * second ** 2") + self.assertEqual(repr(x), "") x = UnitsContainer(meter=1, second=2.5) - self.assertEqual(str(x), 'meter * second ** 2.5') - self.assertEqual(repr(x), - "") + self.assertEqual(str(x), "meter * second ** 2.5") + self.assertEqual(repr(x), "") def test_unitcontainer_bool(self): self.assertTrue(UnitsContainer(meter=1, second=2)) @@ -72,7 +79,7 @@ def test_unitcontainer_bool(self): def test_unitcontainer_comp(self): x = UnitsContainer(meter=1, second=2) - y = UnitsContainer(meter=1., second=2) + y = UnitsContainer(meter=1.0, second=2) z = UnitsContainer(meter=1, second=3) self.assertTrue(x == y) self.assertFalse(x != y) @@ -98,17 +105,17 @@ def test_string_comparison(self): x = UnitsContainer(meter=1) y = UnitsContainer(second=1) z = UnitsContainer(meter=1, second=-2) - self.assertEqual(x, 'meter') - self.assertEqual('meter', x) - self.assertNotEqual(x, 'meter ** 2') - self.assertNotEqual(x, 'meter * meter') - self.assertNotEqual(x, 'second') - self.assertEqual(y, 'second') - self.assertEqual(z, 'meter/second/second') + self.assertEqual(x, "meter") + self.assertEqual("meter", x) + self.assertNotEqual(x, "meter ** 2") + self.assertNotEqual(x, "meter * meter") + self.assertNotEqual(x, "second") + self.assertEqual(y, "second") + self.assertEqual(z, "meter/second/second") def test_invalid(self): self.assertRaises(TypeError, UnitsContainer, {1: 2}) - self.assertRaises(TypeError, UnitsContainer, {'1': '2'}) + self.assertRaises(TypeError, UnitsContainer, {"1": "2"}) d = UnitsContainer() self.assertRaises(TypeError, d.__mul__, list()) self.assertRaises(TypeError, d.__pow__, list()) @@ -117,9 +124,8 @@ def test_invalid(self): class TestToUnitsContainer(BaseTestCase): - def test_str_conversion(self): - self.assertEqual(to_units_container('m'), UnitsContainer(m=1)) + self.assertEqual(to_units_container("m"), UnitsContainer(m=1)) def test_uc_conversion(self): a = UnitsContainer(m=1) @@ -127,22 +133,25 @@ def test_uc_conversion(self): def test_quantity_conversion(self): from pint.registry import UnitRegistry + ureg = UnitRegistry() - self.assertEqual(to_units_container(ureg.Quantity(1, UnitsContainer(m=1))), - UnitsContainer(m=1)) + self.assertEqual( + to_units_container(ureg.Quantity(1, UnitsContainer(m=1))), + UnitsContainer(m=1), + ) def test_unit_conversion(self): from pint import Unit - self.assertEqual(to_units_container(Unit(UnitsContainer(m=1))), - UnitsContainer(m=1)) + self.assertEqual( + to_units_container(Unit(UnitsContainer(m=1))), UnitsContainer(m=1) + ) def test_dict_conversion(self): self.assertEqual(to_units_container(dict(m=1)), UnitsContainer(m=1)) class TestParseHelper(BaseTestCase): - def test_basic(self): # Parse Helper ar mutables, so we build one everytime x = lambda: ParserHelper(1, meter=2) @@ -151,37 +160,35 @@ def test_basic(self): self.assertEqual(x(), xp()) self.assertNotEqual(x(), y()) - self.assertEqual(ParserHelper.from_string(''), ParserHelper()) + self.assertEqual(ParserHelper.from_string(""), ParserHelper()) self.assertEqual(repr(x()), "") self.assertEqual(ParserHelper(2), 2) self.assertEqual(x(), dict(meter=2)) - self.assertEqual(x(), 'meter ** 2') + self.assertEqual(x(), "meter ** 2") self.assertNotEqual(y(), dict(meter=2)) - self.assertNotEqual(y(), 'meter ** 2') + self.assertNotEqual(y(), "meter ** 2") self.assertNotEqual(xp(), object()) def test_calculate(self): # Parse Helper ar mutables, so we build one everytime - x = lambda: ParserHelper(1., meter=2) - y = lambda: ParserHelper(2., meter=-2) - z = lambda: ParserHelper(2., meter=2) + x = lambda: ParserHelper(1.0, meter=2) + y = lambda: ParserHelper(2.0, meter=-2) + z = lambda: ParserHelper(2.0, meter=2) - self.assertEqual(x() * 4., ParserHelper(4., meter=2)) - self.assertEqual(x() * y(), ParserHelper(2.)) - self.assertEqual(x() * 'second', ParserHelper(1., meter=2, second=1)) + self.assertEqual(x() * 4.0, ParserHelper(4.0, meter=2)) + self.assertEqual(x() * y(), ParserHelper(2.0)) + self.assertEqual(x() * "second", ParserHelper(1.0, meter=2, second=1)) - self.assertEqual(x() / 4., ParserHelper(0.25, meter=2)) - self.assertEqual(x() / 'second', ParserHelper(1., meter=2, second=-1)) + self.assertEqual(x() / 4.0, ParserHelper(0.25, meter=2)) + self.assertEqual(x() / "second", ParserHelper(1.0, meter=2, second=-1)) self.assertEqual(x() / z(), ParserHelper(0.5)) - self.assertEqual(4. / z(), ParserHelper(2., meter=-2)) - self.assertEqual('seconds' / z(), - ParserHelper(0.5, seconds=1, meter=-2)) - self.assertEqual(dict(seconds=1) / z(), - ParserHelper(0.5, seconds=1, meter=-2)) + self.assertEqual(4.0 / z(), ParserHelper(2.0, meter=-2)) + self.assertEqual("seconds" / z(), ParserHelper(0.5, seconds=1, meter=-2)) + self.assertEqual(dict(seconds=1) / z(), ParserHelper(0.5, seconds=1, meter=-2)) def _test_eval_token(self, expected, expression, use_decimal=False): token = next(tokenizer(expression)) @@ -189,78 +196,75 @@ def _test_eval_token(self, expected, expression, use_decimal=False): self.assertEqual(expected, actual) self.assertEqual(type(expected), type(actual)) - def test_eval_token(self): - self._test_eval_token(1000.0, '1e3') - self._test_eval_token(1000.0, '1E3') - self._test_eval_token(Decimal(1000), '1e3', use_decimal=True) - self._test_eval_token(1000, '1000') + self._test_eval_token(1000.0, "1e3") + self._test_eval_token(1000.0, "1E3") + self._test_eval_token(Decimal(1000), "1e3", use_decimal=True) + self._test_eval_token(1000, "1000") # integer numbers are represented as ints, not Decimals - self._test_eval_token(1000, '1000', use_decimal=True) + self._test_eval_token(1000, "1000", use_decimal=True) class TestStringProcessor(BaseTestCase): - def _test(self, bef, aft): - for pattern in ('{0}', '+{0}+'): + for pattern in ("{0}", "+{0}+"): b = pattern.format(bef) a = pattern.format(aft) self.assertEqual(string_preprocessor(b), a) def test_square_cube(self): - self._test('bcd^3', 'bcd**3') - self._test('bcd^ 3', 'bcd** 3') - self._test('bcd ^3', 'bcd **3') - self._test('bcd squared', 'bcd**2') - self._test('bcd squared', 'bcd**2') - self._test('bcd cubed', 'bcd**3') - self._test('sq bcd', 'bcd**2') - self._test('square bcd', 'bcd**2') - self._test('cubic bcd', 'bcd**3') - self._test('bcd efg', 'bcd*efg') + self._test("bcd^3", "bcd**3") + self._test("bcd^ 3", "bcd** 3") + self._test("bcd ^3", "bcd **3") + self._test("bcd squared", "bcd**2") + self._test("bcd squared", "bcd**2") + self._test("bcd cubed", "bcd**3") + self._test("sq bcd", "bcd**2") + self._test("square bcd", "bcd**2") + self._test("cubic bcd", "bcd**3") + self._test("bcd efg", "bcd*efg") def test_per(self): - self._test('miles per hour', 'miles/hour') + self._test("miles per hour", "miles/hour") def test_numbers(self): - self._test('1,234,567', '1234567') - self._test('1e-24', '1e-24') - self._test('1e+24', '1e+24') - self._test('1e24', '1e24') - self._test('1E-24', '1E-24') - self._test('1E+24', '1E+24') - self._test('1E24', '1E24') + self._test("1,234,567", "1234567") + self._test("1e-24", "1e-24") + self._test("1e+24", "1e+24") + self._test("1e24", "1e24") + self._test("1E-24", "1E-24") + self._test("1E+24", "1E+24") + self._test("1E24", "1E24") def test_space_multiplication(self): - self._test('bcd efg', 'bcd*efg') - self._test('bcd efg', 'bcd*efg') - self._test('1 hour', '1*hour') - self._test('1. hour', '1.*hour') - self._test('1.1 hour', '1.1*hour') - self._test('1E24 hour', '1E24*hour') - self._test('1E-24 hour', '1E-24*hour') - self._test('1E+24 hour', '1E+24*hour') - self._test('1.2E24 hour', '1.2E24*hour') - self._test('1.2E-24 hour', '1.2E-24*hour') - self._test('1.2E+24 hour', '1.2E+24*hour') + self._test("bcd efg", "bcd*efg") + self._test("bcd efg", "bcd*efg") + self._test("1 hour", "1*hour") + self._test("1. hour", "1.*hour") + self._test("1.1 hour", "1.1*hour") + self._test("1E24 hour", "1E24*hour") + self._test("1E-24 hour", "1E-24*hour") + self._test("1E+24 hour", "1E+24*hour") + self._test("1.2E24 hour", "1.2E24*hour") + self._test("1.2E-24 hour", "1.2E-24*hour") + self._test("1.2E+24 hour", "1.2E+24*hour") def test_joined_multiplication(self): - self._test('1hour', '1*hour') - self._test('1.hour', '1.*hour') - self._test('1.1hour', '1.1*hour') - self._test('1h', '1*h') - self._test('1.h', '1.*h') - self._test('1.1h', '1.1*h') + self._test("1hour", "1*hour") + self._test("1.hour", "1.*hour") + self._test("1.1hour", "1.1*hour") + self._test("1h", "1*h") + self._test("1.h", "1.*h") + self._test("1.1h", "1.1*h") def test_names(self): - self._test('g_0', 'g_0') - self._test('g0', 'g0') - self._test('g', 'g') - self._test('water_60F', 'water_60F') + self._test("g_0", "g_0") + self._test("g0", "g0") + self._test("g", "g") + self._test("water_60F", "water_60F") class TestGraph(BaseTestCase): - def test_start_not_in_graph(self): g = collections.defaultdict(set) g[1] = {2} @@ -293,41 +297,43 @@ def test_shortest_path(self): class TestMatrix(BaseTestCase): - def test_matrix_to_string(self): - self.assertEqual(matrix_to_string([[1, 2], [3, 4]], - row_headers=None, - col_headers=None), - '1\t2\n' - '3\t4') - - self.assertEqual(matrix_to_string([[1, 2], [3, 4]], - row_headers=None, - col_headers=None, - fmtfun=lambda x: '{0:.2f}'.format(x)), - '1.00\t2.00\n' - '3.00\t4.00') - - self.assertEqual(matrix_to_string([[1, 2], [3, 4]], - row_headers=['c', 'd'], - col_headers=None), - 'c\t1\t2\n' - 'd\t3\t4') - - self.assertEqual(matrix_to_string([[1, 2], [3, 4]], - row_headers=None, - col_headers=['a', 'b']), - 'a\tb\n' - '1\t2\n' - '3\t4') - - self.assertEqual(matrix_to_string([[1, 2], [3, 4]], - row_headers=['c', 'd'], - col_headers=['a', 'b']), - '\ta\tb\n' - 'c\t1\t2\n' - 'd\t3\t4') + self.assertEqual( + matrix_to_string([[1, 2], [3, 4]], row_headers=None, col_headers=None), + "1\t2\n" "3\t4", + ) + + self.assertEqual( + matrix_to_string( + [[1, 2], [3, 4]], + row_headers=None, + col_headers=None, + fmtfun=lambda x: "{0:.2f}".format(x), + ), + "1.00\t2.00\n" "3.00\t4.00", + ) + + self.assertEqual( + matrix_to_string( + [[1, 2], [3, 4]], row_headers=["c", "d"], col_headers=None + ), + "c\t1\t2\n" "d\t3\t4", + ) + + self.assertEqual( + matrix_to_string( + [[1, 2], [3, 4]], row_headers=None, col_headers=["a", "b"] + ), + "a\tb\n" "1\t2\n" "3\t4", + ) + + self.assertEqual( + matrix_to_string( + [[1, 2], [3, 4]], row_headers=["c", "d"], col_headers=["a", "b"] + ), + "\ta\tb\n" "c\t1\t2\n" "d\t3\t4", + ) def test_transpose(self): @@ -335,12 +341,11 @@ def test_transpose(self): class TestOtherUtils(BaseTestCase): - def test_iterable(self): # Test with list, string, generator, and scalar self.assertTrue(iterable([0, 1, 2, 3])) - self.assertTrue(iterable('test')) + self.assertTrue(iterable("test")) self.assertTrue(iterable((i for i in range(5)))) self.assertFalse(iterable(0)) @@ -348,6 +353,6 @@ def test_sized(self): # Test with list, string, generator, and scalar self.assertTrue(sized([0, 1, 2, 3])) - self.assertTrue(sized('test')) + self.assertTrue(sized("test")) self.assertFalse(sized((i for i in range(5)))) self.assertFalse(sized(0)) diff --git a/pint/unit.py b/pint/unit.py index eda92d6fa..f96e549ff 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -29,7 +29,7 @@ class Unit(PrettyIPython, SharedRegistryObject): """ #: Default formatting string. - default_format = '' + default_format = "" def __reduce__(self): # See notes in Quantity.__reduce__ @@ -46,8 +46,10 @@ def __init__(self, units): elif isinstance(units, Unit): self._units = units._units else: - raise TypeError('units must be of type str, Unit or ' - 'UnitsContainer; not {}.'.format(type(units))) + raise TypeError( + "units must be of type str, Unit or " + "UnitsContainer; not {}.".format(type(units)) + ) self.__used = False self.__handling = None @@ -62,9 +64,9 @@ def __copy__(self): return ret def __deepcopy__(self, memo): - ret = self.__class__(copy.deepcopy(self._units, memo)) - ret.__used = self.__used - return ret + ret = self.__class__(copy.deepcopy(self._units, memo)) + ret.__used = self.__used + return ret def __str__(self): return format(self) @@ -78,35 +80,41 @@ def __repr__(self): def __format__(self, spec): spec = spec or self.default_format # special cases - if 'Lx' in spec: # the LaTeX siunitx code - return r'\si[]{%s}' % siunitx_format_unit(self) + if "Lx" in spec: # the LaTeX siunitx code + return r"\si[]{%s}" % siunitx_format_unit(self) - if '~' in spec: + if "~" in spec: if not self._units: - return '' - units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), - value) - for key, value in self._units.items())) - spec = spec.replace('~', '') + return "" + units = UnitsContainer( + dict( + (self._REGISTRY._get_symbol(key), value) + for key, value in self._units.items() + ) + ) + spec = spec.replace("~", "") else: units = self._units - return '%s' % (format(units, spec)) + return "%s" % (format(units, spec)) - def format_babel(self, spec='', **kwspec): + def format_babel(self, spec="", **kwspec): spec = spec or self.default_format - if '~' in spec: + if "~" in spec: if self.dimensionless: - return '' - units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), - value) - for key, value in self._units.items())) - spec = spec.replace('~', '') + return "" + units = UnitsContainer( + dict( + (self._REGISTRY._get_symbol(key), value) + for key, value in self._units.items() + ) + ) + spec = spec.replace("~", "") else: units = self._units - return '%s' % (units.format_babel(spec, **kwspec)) + return "%s" % (units.format_babel(spec, **kwspec)) @property def dimensionless(self): @@ -138,7 +146,7 @@ def compatible_units(self, *contexts): def __mul__(self, other): if self._check(other): if isinstance(other, self.__class__): - return self.__class__(self._units*other._units) + return self.__class__(self._units * other._units) else: qself = self._REGISTRY.Quantity(1.0, self._units) return qself * other @@ -153,20 +161,20 @@ def __mul__(self, other): def __truediv__(self, other): if self._check(other): if isinstance(other, self.__class__): - return self.__class__(self._units/other._units) + return self.__class__(self._units / other._units) else: qself = 1.0 * self return qself / other - return self._REGISTRY.Quantity(1/other, self._units) + return self._REGISTRY.Quantity(1 / other, self._units) def __rtruediv__(self, other): # As Unit and Quantity both handle truediv with each other rtruediv can # only be called for something different. if isinstance(other, NUMERIC_TYPES): - return self._REGISTRY.Quantity(other, 1/self._units) + return self._REGISTRY.Quantity(other, 1 / self._units) elif isinstance(other, UnitsContainer): - return self.__class__(other/self._units) + return self.__class__(other / self._units) else: return NotImplemented @@ -175,10 +183,10 @@ def __rtruediv__(self, other): def __pow__(self, other): if isinstance(other, NUMERIC_TYPES): - return self.__class__(self._units**other) + return self.__class__(self._units ** other) else: - mess = 'Cannot power Unit by {}'.format(type(other)) + mess = "Cannot power Unit by {}".format(type(other)) raise TypeError(mess) def __hash__(self): @@ -234,12 +242,12 @@ def __array_prepare__(self, array, context=None): def __array_wrap__(self, array, context=None): uf, objs, huh = context - if uf.__name__ in ('true_divide', 'divide', 'floor_divide'): - return self._REGISTRY.Quantity(array, 1/self._units) - elif uf.__name__ in ('multiply',): + if uf.__name__ in ("true_divide", "divide", "floor_divide"): + return self._REGISTRY.Quantity(array, 1 / self._units) + elif uf.__name__ in ("multiply",): return self._REGISTRY.Quantity(array, self._units) else: - raise ValueError('Unsupproted operation for Unit') + raise ValueError("Unsupproted operation for Unit") @property def systems(self): @@ -250,7 +258,7 @@ def systems(self): out.add(sname) return frozenset(out) - def from_(self, value, strict=True, name='value'): + def from_(self, value, strict=True, name="value"): """Converts a numerical value or quantity to this unit :param value: a Quantity (or numerical value if strict=False) to convert @@ -269,7 +277,7 @@ def from_(self, value, strict=True, name='value'): else: return value * self - def m_from(self, value, strict=True, name='value'): + def m_from(self, value, strict=True, name="value"): """Converts a numerical value or quantity to this unit, then returns the magnitude of the converted value diff --git a/pint/util.py b/pint/util.py index cc66d861d..d20778e22 100644 --- a/pint/util.py +++ b/pint/util.py @@ -29,20 +29,23 @@ logger.addHandler(NullHandler()) -def matrix_to_string(matrix, row_headers=None, col_headers=None, fmtfun=lambda x: str(int(x))): +def matrix_to_string( + matrix, row_headers=None, col_headers=None, fmtfun=lambda x: str(int(x)) +): """Takes a 2D matrix (as nested list) and returns a string. """ ret = [] if col_headers: - ret.append(('\t' if row_headers else '') + '\t'.join(col_headers)) + ret.append(("\t" if row_headers else "") + "\t".join(col_headers)) if row_headers: - ret += [rh + '\t' + '\t'.join(fmtfun(f) for f in row) - for rh, row in zip(row_headers, matrix)] + ret += [ + rh + "\t" + "\t".join(fmtfun(f) for f in row) + for rh, row in zip(row_headers, matrix) + ] else: - ret += ['\t'.join(fmtfun(f) for f in row) - for row in matrix] + ret += ["\t".join(fmtfun(f) for f in row) for row in matrix] - return '\n'.join(ret) + return "\n".join(ret) def transpose(matrix): @@ -79,7 +82,7 @@ def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): new_M.append(r) M = new_M -# M = [[ntype(x) for x in row] for row in M] + # M = [[ntype(x) for x in row] for row in M] I = [[ntype(1) if n == nc else ntype(0) for nc in range(rows)] for n in range(rows)] swapped = [] @@ -108,8 +111,8 @@ def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): if i == r: continue lv = M[i][lead] - M[i] = [iv - lv*rv for rv, iv in zip(M[r], M[i])] - I[i] = [iv - lv*rv for rv, iv in zip(I[r], I[i])] + M[i] = [iv - lv * rv for rv, iv in zip(M[r], M[i])] + I[i] = [iv - lv * rv for rv, iv in zip(I[r], I[i])] lead += 1 @@ -138,14 +141,16 @@ def pi_theorem(quantities, registry=None): value = ParserHelper.from_string(value) if isinstance(value, dict): dims = getdim(UnitsContainer(value)) - elif not hasattr(value, 'dimensionality'): + elif not hasattr(value, "dimensionality"): dims = getdim(value) else: dims = value.dimensionality - if not registry and any(not key.startswith('[') for key in dims): - logger.warning('A non dimension was found and a registry was not provided. ' - 'Assuming that it is a dimension name: {}.'.format(dims)) + if not registry and any(not key.startswith("[") for key in dims): + logger.warning( + "A non dimension was found and a registry was not provided. " + "Assuming that it is a dimension name: {}.".format(dims) + ) quant.append((name, dims)) dimensions = dimensions.union(dims.keys()) @@ -153,8 +158,10 @@ def pi_theorem(quantities, registry=None): dimensions = list(dimensions) # Calculate dimensionless quantities - M = [[dimensionality[dimension] for name, dimensionality in quant] - for dimension in dimensions] + M = [ + [dimensionality[dimension] for name, dimensionality in quant] + for dimension in dimensions + ] M, identity, pivot = column_echelon_form(M, transpose_result=False) @@ -167,8 +174,13 @@ def pi_theorem(quantities, registry=None): continue max_den = max(f.denominator for f in rowi) neg = -1 if sum(f < 0 for f in rowi) > sum(f > 0 for f in rowi) else 1 - results.append(dict((q[0], neg * f.numerator * max_den / f.denominator) - for q, f in zip(quant, rowi) if f.numerator != 0)) + results.append( + dict( + (q[0], neg * f.numerator * max_den / f.denominator) + for q, f in zip(quant, rowi) + if f.numerator != 0 + ) + ) return results @@ -189,8 +201,9 @@ def solve_dependencies(dependencies): # can be done right away if not t: raise ValueError( - 'Cyclic dependencies exist among these items: {}' - .format(', '.join(repr(x) for x in dependencies.items())) + "Cyclic dependencies exist among these items: {}".format( + ", ".join(repr(x) for x in dependencies.items()) + ) ) r.append(t) # and cleaned up @@ -232,8 +245,9 @@ class udict(dict): """ Custom dict implementing __missing__. """ + def __missing__(self, key): - return 0. + return 0.0 def copy(self): return udict(self) @@ -247,16 +261,17 @@ class UnitsContainer(Mapping): return new instances. """ - __slots__ = ('_d', '_hash') + + __slots__ = ("_d", "_hash") def __init__(self, *args, **kwargs): d = udict(*args, **kwargs) self._d = d for key, value in d.items(): if not isinstance(key, str): - raise TypeError('key must be a str, not {}'.format(type(key))) + raise TypeError("key must be a str, not {}".format(type(key))) if not isinstance(value, Number): - raise TypeError('value must be a number, not {}'.format(type(value))) + raise TypeError("value must be a number, not {}".format(type(value))) if not isinstance(value, float): d[key] = float(value) self._hash = None @@ -328,12 +343,13 @@ def __eq__(self, other): return dict.__eq__(self._d, other) def __str__(self): - return self.__format__('') + return self.__format__("") def __repr__(self): - tmp = '{%s}' % ', '.join(["'{}': {}".format(key, value) - for key, value in sorted(self._d.items())]) - return ''.format(tmp) + tmp = "{%s}" % ", ".join( + ["'{}': {}".format(key, value) for key, value in sorted(self._d.items())] + ) + return "".format(tmp) def __format__(self, spec): return format_unit(self, spec) @@ -350,7 +366,7 @@ def __copy__(self): def __mul__(self, other): if not isinstance(other, self.__class__): - err = 'Cannot multiply UnitsContainer by {}' + err = "Cannot multiply UnitsContainer by {}" raise TypeError(err.format(type(other))) new = self.copy() @@ -366,7 +382,7 @@ def __mul__(self, other): def __pow__(self, other): if not isinstance(other, NUMERIC_TYPES): - err = 'Cannot power UnitsContainer by {}' + err = "Cannot power UnitsContainer by {}" raise TypeError(err.format(type(other))) new = self.copy() @@ -377,7 +393,7 @@ def __pow__(self, other): def __truediv__(self, other): if not isinstance(other, self.__class__): - err = 'Cannot divide UnitsContainer by {}' + err = "Cannot divide UnitsContainer by {}" raise TypeError(err.format(type(other))) new = self.copy() @@ -391,10 +407,10 @@ def __truediv__(self, other): def __rtruediv__(self, other): if not isinstance(other, self.__class__) and other != 1: - err = 'Cannot divide {} by UnitsContainer' + err = "Cannot divide {} by UnitsContainer" raise TypeError(err.format(type(other))) - return self**-1 + return self ** -1 class ParserHelper(UnitsContainer): @@ -410,7 +426,7 @@ class ParserHelper(UnitsContainer): """ - __slots__ = ('scale', ) + __slots__ = ("scale",) def __init__(self, scale=1, *args, **kwargs): super().__init__(*args, **kwargs) @@ -443,7 +459,7 @@ def eval_token(cls, token, use_decimal=False): elif token_type == NAME: return ParserHelper.from_word(token_text) else: - raise Exception('unknown token type') + raise Exception("unknown token type") @classmethod @lru_cache() @@ -455,8 +471,10 @@ def _from_string(cls, input_string): return cls() input_string = string_preprocessor(input_string) - if '[' in input_string: - input_string = input_string.replace('[', '__obra__').replace(']', '__cbra__') + if "[" in input_string: + input_string = input_string.replace("[", "__obra__").replace( + "]", "__cbra__" + ) reps = True else: reps = False @@ -473,9 +491,9 @@ def _from_string(cls, input_string): return ParserHelper( ret.scale, { - key.replace('__obra__', '[').replace('__cbra__', ']'): value + key.replace("__obra__", "[").replace("__cbra__", "]"): value for key, value in ret.items() - } + }, ) def __copy__(self): @@ -488,22 +506,19 @@ def copy(self): def __hash__(self): if self.scale != 1.0: - mess = 'Only scale 1.0 ParserHelper instance should be considered hashable' + mess = "Only scale 1.0 ParserHelper instance should be considered hashable" raise ValueError(mess) return super().__hash__() def __eq__(self, other): if isinstance(other, ParserHelper): - return ( - self.scale == other.scale and - super().__eq__(other) - ) + return self.scale == other.scale and super().__eq__(other) elif isinstance(other, str): return self == ParserHelper.from_string(other) elif isinstance(other, Number): return self.scale == other and not len(self._d) else: - return self.scale == 1. and super().__eq__(other) + return self.scale == 1.0 and super().__eq__(other) def operate(self, items, op=operator.iadd, cleanup=True): d = udict(self._d) @@ -518,14 +533,16 @@ def operate(self, items, op=operator.iadd, cleanup=True): return self.__class__(self.scale, d) def __str__(self): - tmp = '{%s}' % ', '.join(["'{}': {}".format(key, value) - for key, value in sorted(self._d.items())]) - return '{} {}'.format(self.scale, tmp) + tmp = "{%s}" % ", ".join( + ["'{}': {}".format(key, value) for key, value in sorted(self._d.items())] + ) + return "{} {}".format(self.scale, tmp) def __repr__(self): - tmp = '{%s}' % ', '.join(["'{}': {}".format(key, value) - for key, value in sorted(self._d.items())]) - return ''.format(self.scale, tmp) + tmp = "{%s}" % ", ".join( + ["'{}': {}".format(key, value) for key, value in sorted(self._d.items())] + ) + return "".format(self.scale, tmp) def __mul__(self, other): if isinstance(other, str): @@ -546,7 +563,7 @@ def __pow__(self, other): d = self._d.copy() for key in self._d: d[key] *= other - return self.__class__(self.scale**other, d) + return self.__class__(self.scale ** other, d) def __truediv__(self, other): if isinstance(other, str): @@ -578,20 +595,24 @@ def __rtruediv__(self, other): #: List of regex substitution pairs. -_subs_re = [('\N{DEGREE SIGN}', " degree"), - (r"([\w\.\-\+\*\\\^])\s+", r"\1 "), # merge multiple spaces - (r"({}) squared", r"\1**2"), # Handle square and cube - (r"({}) cubed", r"\1**3"), - (r"cubic ({})", r"\1**3"), - (r"square ({})", r"\1**2"), - (r"sq ({})", r"\1**2"), - (r"\b([0-9]+\.?[0-9]*)(?=[e|E][a-zA-Z]|[a-df-zA-DF-Z])", r"\1*"), # Handle numberLetter for multiplication - (r"([\w\.\-])\s+(?=\w)", r"\1*"), # Handle space for multiplication - ] +_subs_re = [ + ("\N{DEGREE SIGN}", " degree"), + (r"([\w\.\-\+\*\\\^])\s+", r"\1 "), # merge multiple spaces + (r"({}) squared", r"\1**2"), # Handle square and cube + (r"({}) cubed", r"\1**3"), + (r"cubic ({})", r"\1**3"), + (r"square ({})", r"\1**2"), + (r"sq ({})", r"\1**2"), + ( + r"\b([0-9]+\.?[0-9]*)(?=[e|E][a-zA-Z]|[a-df-zA-DF-Z])", + r"\1*", + ), # Handle numberLetter for multiplication + (r"([\w\.\-])\s+(?=\w)", r"\1*"), # Handle space for multiplication +] #: Compiles the regex and replace {} by a regex that matches an identifier. _subs_re = [(re.compile(a.format(r"[_a-zA-Z][_a-zA-Z0-9]*")), b) for a, b in _subs_re] -_pretty_table = str.maketrans('⁰¹²³⁴⁵⁶⁷⁸⁹·⁻', '0123456789*-') +_pretty_table = str.maketrans("⁰¹²³⁴⁵⁶⁷⁸⁹·⁻", "0123456789*-") _pretty_exp_re = re.compile(r"⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+(?:\.[⁰¹²³⁴⁵⁶⁷⁸⁹]*)?") @@ -605,7 +626,7 @@ def string_preprocessor(input_string): # Replace pretty format characters for pretty_exp in _pretty_exp_re.findall(input_string): - exp = '**' + pretty_exp.translate(_pretty_table) + exp = "**" + pretty_exp.translate(_pretty_table) input_string = input_string.replace(pretty_exp, exp) input_string = input_string.translate(_pretty_table) @@ -615,7 +636,7 @@ def string_preprocessor(input_string): def _is_dim(name): - return name[0] == '[' and name[-1] == ']' + return name[0] == "[" and name[-1] == "]" class SharedRegistryObject: @@ -625,6 +646,7 @@ class SharedRegistryObject: that an object from this class has a '_units' attribute. """ + def __new__(cls, *args, **kwargs): inst = object.__new__(cls) if not hasattr(cls, "_REGISTRY"): @@ -644,13 +666,14 @@ def _check(self, other): same unit registry. """ - if self._REGISTRY is getattr(other, '_REGISTRY', None): + if self._REGISTRY is getattr(other, "_REGISTRY", None): return True elif isinstance(other, SharedRegistryObject): - mess = 'Cannot operate with {} and {} of different registries.' - raise ValueError(mess.format(self.__class__.__name__, - other.__class__.__name__)) + mess = "Cannot operate with {} and {} of different registries." + raise ValueError( + mess.format(self.__class__.__name__, other.__class__.__name__) + ) else: return False @@ -715,7 +738,7 @@ 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.startswith("_") or item.endswith("__"): raise AttributeError("%r object has no attribute %r" % (self, item)) @@ -748,10 +771,10 @@ def __iter__(self): return self def __next__(self): - line = '' - while not line or line.startswith('#'): + line = "" + while not line or line.startswith("#"): lineno, line = next(self.internal) - line = line.split('#', 1)[0].strip() + line = line.split("#", 1)[0].strip() self.last = lineno, line return lineno, line @@ -782,10 +805,10 @@ def __next__(self): return self.last lineno, line = SourceIterator.__next__(self) - if line.startswith('@end'): + if line.startswith("@end"): raise StopIteration - elif line.startswith('@'): - raise DefinitionSyntaxError('cannot nest @ directives', lineno=lineno) + elif line.startswith("@"): + raise DefinitionSyntaxError("cannot nest @ directives", lineno=lineno) return lineno, line diff --git a/setup.py b/setup.py index 71d5bc9fb..4aac94044 100644 --- a/setup.py +++ b/setup.py @@ -11,59 +11,54 @@ try: from setuptools import setup except ImportError: - print('Please install or upgrade setuptools or pip to continue') + print("Please install or upgrade setuptools or pip to continue") sys.exit(1) import codecs def read(filename): - return codecs.open(filename, encoding='utf-8').read() + return codecs.open(filename, encoding="utf-8").read() -long_description = '\n\n'.join([read('README'), - read('AUTHORS'), - read('CHANGES')]) +long_description = "\n\n".join([read("README"), read("AUTHORS"), read("CHANGES")]) __doc__ = long_description setup( - name='Pint', - version='0.10.dev0', - description='Physical quantities module', + name="Pint", + version="0.10.dev0", + description="Physical quantities module", long_description=long_description, - keywords='physical quantities unit conversion science', - author='Hernan E. Grecco', - author_email='hernan.grecco@gmail.com', - url='https://github.com/hgrecco/pint', - test_suite='pint.testsuite.testsuite', + keywords="physical quantities unit conversion science", + author="Hernan E. Grecco", + author_email="hernan.grecco@gmail.com", + url="https://github.com/hgrecco/pint", + test_suite="pint.testsuite.testsuite", zip_safe=True, - packages=['pint'], - package_data={ - 'pint': ['default_en.txt', - 'constants_en.txt'] - }, + packages=["pint"], + package_data={"pint": ["default_en.txt", "constants_en.txt"]}, include_package_data=True, - license='BSD', + license="BSD", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering', - 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], - python_requires='>=3.6', - install_requires=['setuptools'], + python_requires=">=3.6", + install_requires=["setuptools"], extras_require={ - 'numpy': ['numpy >= 1.14'], - 'uncertainties': ['uncertainties >= 3.0'], + "numpy": ["numpy >= 1.14"], + "uncertainties": ["uncertainties >= 3.0"], }, ) From 32f43110f107264d86ba630b8ec7b104abe8647c Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 11:49:54 +0000 Subject: [PATCH 182/612] flake8 fixes --- docs/conf.py | 8 +++--- pint/__init__.py | 11 ++++++--- pint/babel_names.py | 4 +-- pint/compat.py | 5 ++-- pint/context.py | 15 ++++++------ pint/formatting.py | 10 ++++---- pint/numpy_func.py | 59 ++++++++++++++++++++++++--------------------- pint/pint_eval.py | 21 +++++++++------- pint/quantity.py | 12 +++------ pint/registry.py | 4 +-- pint/util.py | 10 +++++--- setup.cfg | 24 ++++++++++++++++++ setup.py | 14 +---------- 13 files changed, 110 insertions(+), 87 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2d740f327..1b851a29d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ try: # pragma: no cover version = pkg_resources.get_distribution(project).version -except: # pragma: no cover +except Exception: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown version = "unknown" @@ -200,11 +200,11 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', + # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - "preamble": "".join(("\DeclareUnicodeCharacter{2212}{-}",)) # MINUS + "preamble": "".join((r"\DeclareUnicodeCharacter{2212}{-}",)) # MINUS } # Grouping the document tree into LaTeX files. List of tuples diff --git a/pint/__init__.py b/pint/__init__.py index 7e27f55b2..4f525c756 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -34,16 +34,19 @@ import sys try: - from pintpandas import PintType, PintArray + from pintpandas import PintArray, PintType + + del PintType + del PintArray _HAS_PINTPANDAS = True -except Exception: +except ImportError: _HAS_PINTPANDAS = False _, _pintpandas_error, _ = sys.exc_info() try: # pragma: no cover __version__ = pkg_resources.get_distribution("pint").version -except: # pragma: no cover +except Exception: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown __version__ = "unknown" @@ -124,7 +127,9 @@ def test(): "RedefinitionError", "UndefinedUnitError", "UnitStrippedWarning", + "formatter", "get_application_registry", "set_application_registry", + "pi_theorem", "__version__", ) diff --git a/pint/babel_names.py b/pint/babel_names.py index e4bd1a002..43c69d2d7 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -57,7 +57,7 @@ millimeter="length-millimeter", metric_horsepower="power-horsepower", gibibyte="digital-gigabyte", - ## 'temperature-generic', + # 'temperature-generic', liter="volume-liter", turn="angle-revolution", microsecond="duration-microsecond", @@ -111,7 +111,7 @@ century="duration-century", cubic_foot="volume-cubic-foot", deciliter="volume-deciliter", - ##pint='volume-pint-metric', + # pint='volume-pint-metric', cubic_meter="volume-cubic-meter", cubic_kilometer="volume-cubic-kilometer", quart="volume-quart", diff --git a/pint/compat.py b/pint/compat.py index 3f7e49fb5..4c20d3ed8 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ import tokenize -import warnings from io import BytesIO from numbers import Number from decimal import Decimal @@ -129,10 +128,12 @@ def _to_magnitude(value, force_ndarray=False): HAS_BABEL = False if not HAS_BABEL: - Loc = babel_units = None + Loc = babel_units = None # noqa: F811 # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and # downcast/wrappable types + + def is_upcast_type(other): # Check if class name is in preset list return other.__class__.__name__ in ("PintArray", "Series", "DataArray") diff --git a/pint/context.py b/pint/context.py index e027864b1..777affba4 100644 --- a/pint/context.py +++ b/pint/context.py @@ -13,7 +13,7 @@ import weakref from collections import ChainMap, defaultdict -from .util import ParserHelper, UnitsContainer, to_units_container, SourceIterator +from .util import ParserHelper, to_units_container, SourceIterator from .errors import DefinitionSyntaxError #: Regex to match the header parts of a context. @@ -37,6 +37,7 @@ class Context: dimension to another. Each Dimension are specified using a UnitsContainer. Simple transformation are given with a function taking a single parameter. + >>> from pint.util import UnitsContainer >>> timedim = UnitsContainer({'[time]': 1}) >>> spacedim = UnitsContainer({'[length]': 1}) >>> def f(time): @@ -106,10 +107,10 @@ def from_lines(cls, lines, to_base_func=None): else: aliases = () defaults = r.groupdict()["defaults"] - except: + except Exception as exc: raise DefinitionSyntaxError( "Could not parse the Context header '%s'" % header, lineno=lineno - ) + ) from exc if defaults: @@ -123,11 +124,11 @@ def to_num(val): try: defaults = (part.split("=") for part in defaults.strip("()").split(",")) defaults = {str(k).strip(): to_num(v) for k, v in defaults} - except (ValueError, TypeError): + except (ValueError, TypeError) as exc: raise DefinitionSyntaxError( f"Could not parse Context definition defaults: '{txt}'", lineno=lineno, - ) + ) from exc ctx = cls(name, aliases, defaults) else: @@ -156,11 +157,11 @@ def to_num(val): ctx.add_transformation(src, dst, func) else: raise Exception - except: + except Exception as exc: raise DefinitionSyntaxError( "Could not parse Context %s relation '%s'" % (name, line), lineno=lineno, - ) + ) from exc if defaults: missing_pars = defaults.keys() - set(names) diff --git a/pint/formatting.py b/pint/formatting.py index 931e15414..75f374e7c 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -266,7 +266,7 @@ def _tothe(power): # remove unit prefix if it exists # siunitx supports \prefix commands - l = lpos if power >= 0 else lneg + lpick = lpos if power >= 0 else lneg prefix = None for p in registry._prefixes.values(): p = str(p) @@ -275,11 +275,11 @@ def _tothe(power): unit = unit.replace(prefix, "", 1) if power < 0: - l.append(r"\per") + lpick.append(r"\per") if prefix is not None: - l.append(r"\{}".format(prefix)) - l.append(r"\{}".format(unit)) - l.append(r"{}".format(_tothe(abs(power)))) + lpick.append(r"\{}".format(prefix)) + lpick.append(r"\{}".format(unit)) + lpick.append(r"{}".format(_tothe(abs(power)))) return "".join(lpos) + "".join(lneg) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 52d791dea..9ebb4df68 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -11,8 +11,8 @@ from itertools import chain import warnings -from .compat import NP_NO_VALUE, is_upcast_type, np, eq -from .errors import DimensionalityError, UnitStrippedWarning +from .compat import is_upcast_type, np, eq +from .errors import DimensionalityError from .util import iterable, sized HANDLED_UFUNCS = {} @@ -92,8 +92,8 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): def unwrap_and_wrap_consistent_units(*args): """Strip units from args while providing a rewrapping function. - Returns the given args as parsed by convert_to_consistent_units assuming units of first - arg with units, along with a wrapper to restore that unit to the output. + Returns the given args as parsed by convert_to_consistent_units assuming units of + first arg with units, along with a wrapper to restore that unit to the output. """ first_input_units = _get_first_input_units(args) args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) @@ -167,7 +167,9 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): def implements(numpy_func_string, func_type): - """Register an __array_function__/__array_ufunc__ implementation for Quantity objects.""" + """Register an __array_function__/__array_ufunc__ implementation for Quantity + objects. + """ def decorator(func): if func_type == "function": @@ -198,8 +200,8 @@ def implementation(*args, **kwargs): *args, pre_calc_units=first_input_units, **kwargs ) else: - # Match all input args/kwargs to input_units, or if input_units is None, simply - # strip units + # Match all input args/kwargs to input_units, or if input_units is None, + # simply strip units stripped_args, stripped_kwargs = convert_to_consistent_units( *args, pre_calc_units=input_units, **kwargs ) @@ -237,19 +239,19 @@ def implementation(*args, **kwargs): Define ufunc behavior collections. - `strip_unit_input_output_ufuncs`: units should be ignored on both input and output -- `matching_input_bare_output_ufuncs`: inputs are converted to matching units, but outputs are - returned as-is -- `matching_input_set_units_output_ufuncs`: inputs are converted to matching units, and the - output units are as set by the dict value -- `set_units_ufuncs`: dict values are specified as (in_unit, out_unit), so that inputs are - converted to in_unit before having magnitude passed to NumPy ufunc, and outputs are set to - have out_unit +- `matching_input_bare_output_ufuncs`: inputs are converted to matching units, but + outputs are returned as-is +- `matching_input_set_units_output_ufuncs`: inputs are converted to matching units, and + the output units are as set by the dict value +- `set_units_ufuncs`: dict values are specified as (in_unit, out_unit), so that inputs + are converted to in_unit before having magnitude passed to NumPy ufunc, and outputs + are set to have out_unit - `matching_input_copy_units_output_ufuncs`: inputs are converted to matching units, and outputs are set to that unit -- `copy_units_output_ufuncs`: input units (except the first) are ignored, and output is set to - that of the first input unit -- `op_units_output_ufuncs`: determine output unit from input unit as determined by operation - (see `get_op_output_unit`) +- `copy_units_output_ufuncs`: input units (except the first) are ignored, and output is + set to that of the first input unit +- `op_units_output_ufuncs`: determine output unit from input unit as determined by + operation (see `get_op_output_unit`) """ strip_unit_input_output_ufuncs = ["isnan", "isinf", "isfinite", "signbit"] matching_input_bare_output_ufuncs = [ @@ -289,9 +291,10 @@ def implementation(*args, **kwargs): "logaddexp": ("", ""), "logaddexp2": ("", ""), } -# TODO (#905 follow-up): while this matches previous behavior, some of these have optional -# arguments that should not be Quantities. This should be fixed, and tests using these -# optional arguments should be added. +# TODO (#905 follow-up): +# while this matches previous behavior, some of these have optional arguments that +# should not be Quantities. This should be fixed, and tests using these optional +# arguments should be added. matching_input_copy_units_output_ufuncs = [ "compress", "conj", @@ -398,9 +401,9 @@ def _power(x1, x2): def _add_subtract_handle_non_quantity_zero(x1, x2): - # As in #121/#122, if a value is 0 (but not Quantity 0) do the operation without checking - # units. We do the calculation instead of just returning the same value to enforce any - # shape checking and type casting due to the operation. + # As in #121/#122, if a value is 0 (but not Quantity 0) do the operation without + # checking units. We do the calculation instead of just returning the same value to + # enforce any shape checking and type casting due to the operation. if eq(x1, 0, True): (x2,), output_wrap = unwrap_and_wrap_consistent_units(x2) elif eq(x2, 0, True): @@ -546,8 +549,8 @@ def _isin(element, test_elements, assume_unique=False, invert=False): try: compatible_test_elements.append(test_element.m_as(element.units)) except DimensionalityError: - # Incompatible unit test elements cannot be in element, but others in sequence - # may + # Incompatible unit test elements cannot be in element, but others in + # sequence may pass test_elements = compatible_test_elements else: @@ -679,8 +682,8 @@ def implementation(*arrays): "function", func_str, input_units="all_consistent", output_unit="match_input" ) -# Handle cumulative products (which must be dimensionless for consistent units across output -# array) +# Handle cumulative products (which must be dimensionless for consistent units across +# output array) for func_str in ["cumprod", "cumproduct", "nancumprod"]: implement_func( "function", func_str, input_units="dimensionless", output_unit="match_input" diff --git a/pint/pint_eval.py b/pint/pint_eval.py index f7e08a218..2c2e02ec9 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -95,10 +95,11 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= Params: Index, depth, and prev_op used recursively, so don't touch. Tokens is an iterable of tokens from an expression to be evaluated. - - Transform the tokens from an expression into a recursive parse tree, following order of operations. - Operations can include binary ops (3 + 4), implicit ops (3 kg), or unary ops (-1). - + + Transform the tokens from an expression into a recursive parse tree, following order + of operations. Operations can include binary ops (3 + 4), implicit ops (3 kg), or + unary ops (-1). + General Strategy: 1) Get left side of operator 2) If no tokens left, return final result @@ -109,7 +110,7 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= 6) Go back to step #2 """ - if depth == 0 and prev_op == None: + if depth == 0 and prev_op is None: # ensure tokens is list so we can access by index tokens = list(tokens) @@ -147,9 +148,10 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= result = right elif token_text in op_priority: if result: - # equal-priority operators are grouped in a left-to-right order, unless they're - # exponentiation, in which case they're grouped right-to-left - # this allows us to get the expected behavior for multiple exponents + # equal-priority operators are grouped in a left-to-right order, + # unless they're exponentiation, in which case they're grouped + # right-to-left this allows us to get the expected behavior for + # multiple exponents # (2^3^4) --> (2^(3^4)) # (2 * 3 / 4) --> ((2 * 3) / 4) if op_priority[token_text] <= op_priority.get( @@ -174,7 +176,8 @@ def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op= if result: # tokens with an implicit operation i.e. "1 kg" if op_priority[""] <= op_priority.get(prev_op, -1): - # previous operator is higher priority than implicit, so end previous binary op + # previous operator is higher priority than implicit, so end + # previous binary op return result, index - 1 right, index = build_eval_tree( tokens, op_priority, index, depth + 1, "" diff --git a/pint/quantity.py b/pint/quantity.py index 1aff1c238..4d345b4cf 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -31,14 +31,12 @@ DimensionalityError, OffsetUnitCalculusError, PintTypeError, - UndefinedUnitError, UnitStrippedWarning, ) from .definitions import UnitDefinition from .compat import ( Loc, NUMPY_VER, - SKIP_ARRAY_FUNCTION_CHANGE_WARNING, BehaviorChangeWarning, ndarray, np, @@ -47,6 +45,7 @@ eq, array_function_change_msg, ) +from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 from .util import ( PrettyIPython, logger, @@ -54,14 +53,11 @@ SharedRegistryObject, to_units_container, infer_base_unit, - iterable, - sized, ) from .numpy_func import ( HANDLED_UFUNCS, copy_units_output_ufuncs, get_op_output_unit, - matching_input_bare_output_ufuncs, matching_input_copy_units_output_ufuncs, matching_input_set_units_output_ufuncs, numpy_wrap, @@ -404,7 +400,7 @@ def check(self, dimension): @classmethod def from_list(cls, quant_list, units=None): - """Transforms a list of Quantities into an numpy.array quantity. + """Transforms a list of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. Same as from_sequence. @@ -420,7 +416,7 @@ def from_list(cls, quant_list, units=None): @classmethod def from_sequence(cls, seq, units=None): - """Transforms a sequence of Quantities into an numpy.array quantity. + """Transforms a sequence of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. If units is not specified and sequence is empty, the unit cannot be determined @@ -616,7 +612,7 @@ def to_compact(self, unit=None): log10_scale = int(math.log10(scale)) if log10_scale == math.log10(scale): SI_prefixes[log10_scale] = prefix.name - except: + except Exception: SI_prefixes[0] = "" SI_prefixes = sorted(SI_prefixes.items()) diff --git a/pint/registry.py b/pint/registry.py index 0cb585b54..d42010202 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1048,9 +1048,9 @@ def _is_multiplicative(self, u): # If the unit is not in the registry might be because it is not # registered with its prefixed version. # TODO: Might be better to register them. - l = self._dedup_candidates(self.parse_unit_name(u)) + candidates = self._dedup_candidates(self.parse_unit_name(u)) try: - u = l[0][1] + u = candidates[0][1] return self._units[u].is_multiplicative except KeyError: raise UndefinedUnitError(u) diff --git a/pint/util.py b/pint/util.py index d20778e22..7175a038c 100644 --- a/pint/util.py +++ b/pint/util.py @@ -82,8 +82,10 @@ def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): new_M.append(r) M = new_M - # M = [[ntype(x) for x in row] for row in M] - I = [[ntype(1) if n == nc else ntype(0) for nc in range(rows)] for n in range(rows)] + # M = [[ntype(x) for x in row] for row in M] + I = [ # noqa: E741 + [ntype(1) if n == nc else ntype(0) for nc in range(rows)] for n in range(rows) + ] swapped = [] for r in range(rows): @@ -215,7 +217,7 @@ def find_shortest_path(graph, start, end, path=None): path = (path or []) + [start] if start == end: return path - if not start in graph: + if start not in graph: return None shortest = None for node in graph[start]: @@ -228,7 +230,7 @@ def find_shortest_path(graph, start, end, path=None): def find_connected_nodes(graph, start, visited=None): - if not start in graph: + if start not in graph: return None visited = visited or set() diff --git a/setup.cfg b/setup.cfg index 6499fe786..0f2b7edc9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,27 @@ ignore = [bdist_wheel] universal = 1 + +[flake8] +ignore= + # whitespace before ':' - doesn't work well with black + E203 + E402 + # line too long - let black worry about that + E501 + # do not assign a lambda expression, use a def + E731 + # line break before binary operator + W503 +exclude= + build + + +[isort] +default_section=THIRDPARTY +known_first_party=xarray +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 diff --git a/setup.py b/setup.py index 4aac94044..dc70354ff 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import sys - -try: - reload(sys).setdefaultencoding("UTF-8") -except: - pass - -try: - from setuptools import setup -except ImportError: - print("Please install or upgrade setuptools or pip to continue") - sys.exit(1) - import codecs +from setuptools import setup def read(filename): From 5548f5ef543546a66863afc7adc4a6bef815722d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 11:53:11 +0000 Subject: [PATCH 183/612] isort --- bench/bench.py | 2 +- docs/_themes/flask_theme_support.py | 14 ++--- docs/conf.py | 3 +- pint/__init__.py | 9 ++- pint/compat.py | 2 +- pint/context.py | 2 +- pint/definitions.py | 4 +- pint/formatting.py | 2 +- pint/numpy_func.py | 4 +- pint/pint_eval.py | 1 - pint/quantity.py | 50 ++++++++--------- pint/registry.py | 62 ++++++++++----------- pint/registry_helpers.py | 2 +- pint/systems.py | 9 +-- pint/testsuite/__init__.py | 8 +-- pint/testsuite/helpers.py | 6 +- pint/testsuite/parameterized.py | 3 +- pint/testsuite/test_application_registry.py | 1 - pint/testsuite/test_babel.py | 5 +- pint/testsuite/test_contexts.py | 4 +- pint/testsuite/test_converters.py | 4 +- pint/testsuite/test_definitions.py | 9 ++- pint/testsuite/test_errors.py | 2 +- pint/testsuite/test_issues.py | 7 +-- pint/testsuite/test_numpy_func.py | 10 ++-- pint/testsuite/test_quantity.py | 4 +- pint/testsuite/test_unit.py | 6 +- pint/testsuite/test_util.py | 15 ++--- pint/unit.py | 5 +- pint/util.py | 8 +-- setup.py | 1 + 31 files changed, 129 insertions(+), 135 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index 6593376be..e7c3a97e0 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -1,6 +1,6 @@ +import copy import fnmatch import os -import copy from timeit import Timer import yaml diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 0dcf53b75..64e249963 100644 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,18 +1,18 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import ( - Keyword, - Name, Comment, - String, Error, + Generic, + Keyword, + Literal, + Name, Number, Operator, - Generic, - Whitespace, - Punctuation, Other, - Literal, + Punctuation, + String, + Whitespace, ) diff --git a/docs/conf.py b/docs/conf.py index 1b851a29d..925f79d71 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,9 +12,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import pkg_resources import datetime +import pkg_resources + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. diff --git a/pint/__init__.py b/pint/__init__.py index 4f525c756..b3d4393b2 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -12,6 +12,8 @@ :license: BSD, see LICENSE for more details. """ +import sys + import pkg_resources from .context import Context @@ -26,12 +28,9 @@ from .formatting import formatter from .measurement import Measurement from .quantity import Quantity -from .registry import UnitRegistry, LazyRegistry +from .registry import LazyRegistry, UnitRegistry from .unit import Unit -from .util import pi_theorem, logger - - -import sys +from .util import logger, pi_theorem try: from pintpandas import PintArray, PintType diff --git a/pint/compat.py b/pint/compat.py index 4c20d3ed8..93d1a654d 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -9,9 +9,9 @@ :license: BSD, see LICENSE for more details. """ import tokenize +from decimal import Decimal from io import BytesIO from numbers import Number -from decimal import Decimal def tokenizer(input_string): diff --git a/pint/context.py b/pint/context.py index 777affba4..e4fff6a40 100644 --- a/pint/context.py +++ b/pint/context.py @@ -13,8 +13,8 @@ import weakref from collections import ChainMap, defaultdict -from .util import ParserHelper, to_units_container, SourceIterator from .errors import DefinitionSyntaxError +from .util import ParserHelper, SourceIterator, to_units_container #: Regex to match the header parts of a context. _header_re = re.compile( diff --git a/pint/definitions.py b/pint/definitions.py index e6ad101cb..7cd479963 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -9,8 +9,8 @@ :license: BSD, see LICENSE for more details. """ -from .converters import ScaleConverter, OffsetConverter -from .util import UnitsContainer, _is_dim, ParserHelper +from .converters import OffsetConverter, ScaleConverter +from .util import ParserHelper, UnitsContainer, _is_dim class Definition: diff --git a/pint/formatting.py b/pint/formatting.py index 75f374e7c..224e0a2f4 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -11,7 +11,7 @@ import re -from .babel_names import _babel_units, _babel_lengths +from .babel_names import _babel_lengths, _babel_units from .compat import Loc __JOIN_REG_EXP = re.compile(r"\{\d*\}") diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 9ebb4df68..6025e273e 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -7,11 +7,11 @@ :license: BSD, see LICENSE for more details. """ +import warnings from inspect import signature from itertools import chain -import warnings -from .compat import is_upcast_type, np, eq +from .compat import eq, is_upcast_type, np from .errors import DimensionalityError from .util import iterable, sized diff --git a/pint/pint_eval.py b/pint/pint_eval.py index 2c2e02ec9..f768d6ca3 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -10,7 +10,6 @@ """ import operator - import token as tokenlib from .errors import DefinitionSyntaxError diff --git a/pint/quantity.py b/pint/quantity.py index 4d345b4cf..3561e0754 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -21,38 +21,30 @@ from pkg_resources.extern.packaging import version -from .formatting import ( - remove_custom_flags, - siunitx_format_unit, - ndarray_to_latex, - ndarray_to_latex_parts, +from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 +from .compat import ( + NUMPY_VER, + BehaviorChangeWarning, + Loc, + _to_magnitude, + array_function_change_msg, + eq, + is_upcast_type, + ndarray, + np, ) +from .definitions import UnitDefinition from .errors import ( DimensionalityError, OffsetUnitCalculusError, PintTypeError, UnitStrippedWarning, ) -from .definitions import UnitDefinition -from .compat import ( - Loc, - NUMPY_VER, - BehaviorChangeWarning, - ndarray, - np, - _to_magnitude, - is_upcast_type, - eq, - array_function_change_msg, -) -from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 -from .util import ( - PrettyIPython, - logger, - UnitsContainer, - SharedRegistryObject, - to_units_container, - infer_base_unit, +from .formatting import ( + ndarray_to_latex, + ndarray_to_latex_parts, + remove_custom_flags, + siunitx_format_unit, ) from .numpy_func import ( HANDLED_UFUNCS, @@ -64,6 +56,14 @@ op_units_output_ufuncs, set_units_ufuncs, ) +from .util import ( + PrettyIPython, + SharedRegistryObject, + UnitsContainer, + infer_base_unit, + logger, + to_units_container, +) class _Exception(Exception): # pragma: no cover diff --git a/pint/registry.py b/pint/registry.py index d42010202..68a17f8b7 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -33,54 +33,52 @@ """ import copy -import os -import re -import math import functools import itertools -import pkg_resources +import math +import os +import re +from collections import defaultdict +from contextlib import closing, contextmanager from decimal import Decimal from fractions import Fraction -from contextlib import contextmanager, closing -from io import open, StringIO -from collections import defaultdict -from tokenize import NUMBER, NAME +from io import StringIO, open +from tokenize import NAME, NUMBER -from . import registry_helpers -from .context import Context, ContextChain -from .util import ( - getattr_maybe_raise, - logger, - pi_theorem, - solve_dependencies, - ParserHelper, - string_preprocessor, - find_connected_nodes, - find_shortest_path, - UnitsContainer, - _is_dim, - to_units_container, - SourceIterator, -) +import pkg_resources +from . import registry_helpers, systems from .compat import tokenizer +from .context import Context, ContextChain +from .converters import ScaleConverter from .definitions import ( + AliasDefinition, Definition, - UnitDefinition, - PrefixDefinition, DimensionDefinition, - AliasDefinition, + PrefixDefinition, + UnitDefinition, ) -from .converters import ScaleConverter from .errors import ( - DimensionalityError, - UndefinedUnitError, DefinitionSyntaxError, + DimensionalityError, RedefinitionError, + UndefinedUnitError, ) - from .pint_eval import build_eval_tree -from . import systems +from .util import ( + ParserHelper, + SourceIterator, + UnitsContainer, + _is_dim, + find_connected_nodes, + find_shortest_path, + getattr_maybe_raise, + logger, + pi_theorem, + solve_dependencies, + string_preprocessor, + to_units_container, +) _BLOCK_RE = re.compile(r" |\(") diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 9fd3f752a..db2ba91a9 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -14,7 +14,7 @@ from itertools import zip_longest from .errors import DimensionalityError -from .util import to_units_container, UnitsContainer +from .util import UnitsContainer, to_units_container def _replace_units(original_units, values_by_name): diff --git a/pint/systems.py b/pint/systems.py index 4ae836041..134f57018 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -11,17 +11,18 @@ import re +from pint.compat import Loc + +from .babel_names import _babel_systems from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError, RedefinitionError from .util import ( - to_units_container, - getattr_maybe_raise, SharedRegistryObject, SourceIterator, + getattr_maybe_raise, logger, + to_units_container, ) -from .babel_names import _babel_systems -from pint.compat import Loc class Group(SharedRegistryObject): diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index ffcfbf1a4..1d09c7fef 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -2,16 +2,15 @@ import doctest import logging +import math import os import unittest - from contextlib import contextmanager +from logging.handlers import BufferingHandler +from pint import Quantity, UnitRegistry, logger from pint.compat import ndarray, np - -from pint import logger, UnitRegistry, Quantity from pint.testsuite.helpers import PintOutputChecker -from logging.handlers import BufferingHandler class TestHandler(BufferingHandler): @@ -155,7 +154,6 @@ def run(): return test_runner.run(testsuite()) -import math _GLOBS = { "wrapping.rst": { diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index a6cb78ccb..952f0596f 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- import doctest -from distutils.version import StrictVersion import re import unittest +from distutils.version import StrictVersion from ..compat import ( - HAS_NUMPY, HAS_BABEL, - HAS_UNCERTAINTIES, + HAS_NUMPY, HAS_NUMPY_ARRAY_FUNCTION, + HAS_UNCERTAINTIES, NUMPY_VER, ) diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 239610501..366b4be5a 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -23,11 +23,10 @@ # def test_eval(self, input, expected_output): # self.assertEqual(eval(input), expected_output) +import unittest from collections.abc import Callable from functools import wraps -import unittest - def augment_method_docstring( method, new_class_dict, classname, param_names, param_values, new_method diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index b0f77ebc1..923304c68 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -11,7 +11,6 @@ get_application_registry, set_application_registry, ) - from pint.testsuite import BaseTestCase from pint.testsuite.helpers import requires_uncertainties diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 2210fe706..e85ec4038 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from pint.testsuite import helpers, BaseTestCase -from pint import UnitRegistry import os +from pint import UnitRegistry +from pint.testsuite import BaseTestCase, helpers + class TestBabel(BaseTestCase): @helpers.requires_babel() diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 1bdf3ed68..80103d066 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -3,10 +3,10 @@ import itertools from collections import defaultdict -from pint import UnitRegistry, DefinitionSyntaxError, DimensionalityError +from pint import DefinitionSyntaxError, DimensionalityError, UnitRegistry from pint.context import Context -from pint.util import UnitsContainer from pint.testsuite import QuantityTestCase +from pint.util import UnitsContainer def add_ctxs(ureg): diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 7fadc0073..53d37b51c 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -3,8 +3,8 @@ import itertools from pint.compat import np -from pint.converters import ScaleConverter, OffsetConverter, Converter -from pint.testsuite import helpers, BaseTestCase +from pint.converters import Converter, OffsetConverter, ScaleConverter +from pint.testsuite import BaseTestCase, helpers class TestConverter(BaseTestCase): diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 8f30a45c5..25197a8c0 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,16 +1,15 @@ # -*- coding: utf-8 -*- -from pint.util import UnitsContainer -from pint.converters import ScaleConverter, OffsetConverter +from pint.converters import OffsetConverter, ScaleConverter from pint.definitions import ( + AliasDefinition, Definition, + DimensionDefinition, PrefixDefinition, UnitDefinition, - DimensionDefinition, - AliasDefinition, ) - from pint.testsuite import BaseTestCase +from pint.util import UnitsContainer class TestDefinition(BaseTestCase): diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index 56ae7528b..920ed2e53 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -5,8 +5,8 @@ from pint import ( DefinitionSyntaxError, DimensionalityError, - Quantity, OffsetUnitCalculusError, + Quantity, RedefinitionError, UndefinedUnitError, UnitRegistry, diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index b61abf595..4913b01d0 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -2,15 +2,14 @@ import copy import math -import unittest import pprint +import unittest from pint import DimensionalityError, UnitRegistry -from pint.unit import UnitsContainer -from pint.util import ParserHelper - from pint.compat import np from pint.testsuite import QuantityTestCase, helpers +from pint.unit import UnitsContainer +from pint.util import ParserHelper class TestIssues(QuantityTestCase): diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 573f6a462..262a2ddcd 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- -import pint.numpy_func +from unittest.mock import patch +import pint.numpy_func from pint import DimensionalityError, OffsetUnitCalculusError from pint.compat import np -from pint.testsuite.test_numpy import TestNumpyMethods from pint.numpy_func import ( + _get_first_input_units, _is_quantity, _is_quantity_sequence, - _get_first_input_units, convert_to_consistent_units, - unwrap_and_wrap_consistent_units, get_op_output_unit, implements, numpy_wrap, + unwrap_and_wrap_consistent_units, ) -from unittest.mock import patch +from pint.testsuite.test_numpy import TestNumpyMethods class TestNumPyFuncUtils(TestNumpyMethods): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 3433cade5..070d7f042 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -5,13 +5,13 @@ import math import operator as op import warnings +from unittest.mock import patch from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry -from pint.unit import UnitsContainer from pint.compat import BehaviorChangeWarning, np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase -from unittest.mock import patch +from pint.unit import UnitsContainer class TestQuantity(QuantityTestCase): diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index e1bcf57e5..632714393 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -5,17 +5,17 @@ import math import re -from pint.compat import np -from pint.registry import UnitRegistry, LazyRegistry from pint import ( DefinitionSyntaxError, DimensionalityError, RedefinitionError, UndefinedUnitError, ) +from pint.compat import np +from pint.registry import LazyRegistry, UnitRegistry from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase -from pint.util import UnitsContainer, ParserHelper +from pint.util import ParserHelper, UnitsContainer class TestUnit(QuantityTestCase): diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 2d2ccd12e..29848d3ad 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -4,19 +4,20 @@ import copy import operator as op from decimal import Decimal + from pint.testsuite import BaseTestCase, QuantityTestCase from pint.util import ( - string_preprocessor, - find_shortest_path, - matrix_to_string, - transpose, - tokenizer, - find_connected_nodes, ParserHelper, UnitsContainer, - to_units_container, + find_connected_nodes, + find_shortest_path, iterable, + matrix_to_string, sized, + string_preprocessor, + to_units_container, + tokenizer, + transpose, ) diff --git a/pint/unit.py b/pint/unit.py index f96e549ff..733208d2c 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -14,11 +14,10 @@ import operator from numbers import Number -from .util import PrettyIPython, UnitsContainer, SharedRegistryObject - from .compat import NUMERIC_TYPES -from .formatting import siunitx_format_unit from .definitions import UnitDefinition +from .formatting import siunitx_format_unit +from .util import PrettyIPython, SharedRegistryObject, UnitsContainer class Unit(PrettyIPython, SharedRegistryObject): diff --git a/pint/util.py b/pint/util.py index 7175a038c..ca0f01164 100644 --- a/pint/util.py +++ b/pint/util.py @@ -10,20 +10,20 @@ """ import logging -from logging import NullHandler import operator import re from collections.abc import Mapping from decimal import Decimal -from numbers import Number from fractions import Fraction from functools import lru_cache +from logging import NullHandler +from numbers import Number from token import NAME, NUMBER -from .compat import tokenizer, NUMERIC_TYPES +from .compat import NUMERIC_TYPES, tokenizer +from .errors import DefinitionSyntaxError from .formatting import format_unit from .pint_eval import build_eval_tree -from .errors import DefinitionSyntaxError logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) diff --git a/setup.py b/setup.py index dc70354ff..9c6eed47f 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import codecs + from setuptools import setup From 599b2e77757ed6e4a1b1865b5863f849b90a471d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 11:56:26 +0000 Subject: [PATCH 184/612] Manual tweaks --- pint/registry.py | 37 ++++++++++++++++++++++--------------- pint/testsuite/__init__.py | 1 - 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 68a17f8b7..1fe973ca2 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -42,7 +42,7 @@ from contextlib import closing, contextmanager from decimal import Decimal from fractions import Fraction -from io import StringIO, open +from io import StringIO from tokenize import NAME, NUMBER import pkg_resources @@ -113,6 +113,7 @@ class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. Capabilities: + - Register units, prefixes, and dimensions, and their relations. - Convert between units. - Find dimensionality of a unit. @@ -121,17 +122,22 @@ class BaseRegistry(metaclass=RegistryMeta): - Parse a definition file. - Allow extending the definition file parser by registering @ directives. - :param filename: path of the units definition file to load or line iterable object. - Empty to load the default definition file. - None to leave the UnitRegistry empty. - :type filename: str or None - :param force_ndarray: convert any input, scalar or not to a numpy.ndarray. - :param on_redefinition: action to take in case a unit is redefined. - 'warn', 'raise', 'ignore' - :type on_redefinition: str - :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. - :param preprocessors: list of callables which are iteratively ran on any input expression - or unit string + :param filename: + path of the units definition file to load or line iterable object. Empty to load + the default definition file. None to leave the UnitRegistry empty. + :type filename: + str or None + :param force_ndarray: + convert any input, scalar or not to a numpy.ndarray. + :param on_redefinition: + action to take in case a unit is redefined: 'warn', 'raise', 'ignore' + :type on_redefinition: + str + :param auto_reduce_dimensions: + If True, reduce dimensionality on appropriate operations. + :param preprocessors: + list of callables which are iteratively ran on any input expression or unit + string """ #: Map context prefix to function @@ -165,7 +171,6 @@ def __init__( auto_reduce_dimensions=False, preprocessors=None, ): - self._register_parsers() self._init_dynamic_classes() @@ -179,7 +184,8 @@ def __init__( #: Determines if dimensionality should be reduced on appropriate operations. self.auto_reduce_dimensions = auto_reduce_dimensions - #: Map between name (string) and value (string) of defaults stored in the definitions file. + #: Map between name (string) and value (string) of defaults stored in the + #: definitions file. self._defaults = {} #: Map dimension name (string) to its definition (DimensionDefinition). @@ -189,7 +195,8 @@ def __init__( #: Might contain prefixed units. self._units = {} - #: Map unit name in lower case (string) to a set of unit names with the right case. + #: Map unit name in lower case (string) to a set of unit names with the right + #: case. #: Does not contain prefixed units. #: e.g: 'hz' - > set('Hz', ) self._units_casei = defaultdict(set) diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 1d09c7fef..1cfaa22a3 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -154,7 +154,6 @@ def run(): return test_runner.run(testsuite()) - _GLOBS = { "wrapping.rst": { "pendulum_period": lambda length: 2 * math.pi * math.sqrt(length / 9.806650), From e856866a512eb18c59dcb8c7715c4f6624cc567a Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 12:09:22 +0000 Subject: [PATCH 185/612] More polish --- pint/testsuite/__init__.py | 10 +++++----- pint/testsuite/helpers.py | 6 +++--- pint/testsuite/test_umath.py | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 1cfaa22a3..6c126cde2 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -42,10 +42,10 @@ def capture_log(self, level=logging.DEBUG): th.setLevel(level) logger.addHandler(th) if self._test_handler is not None: - l = len(self._test_handler.buffer) + buflen = len(self._test_handler.buffer) yield th.buffer if self._test_handler is not None: - self._test_handler.buffer = self._test_handler.buffer[:l] + self._test_handler.buffer = self._test_handler.buffer[:buflen] def setUp(self): self._test_handler = None @@ -57,9 +57,8 @@ def setUp(self): def tearDown(self): if self._test_handler is not None: buf = self._test_handler.buffer - l = len(buf) msg = "\n".join(record.get("msg", str(record)) for record in buf) - self.assertEqual(l, 0, msg="%d warnings raised.\n%s" % (l, msg)) + self.assertEqual(len(buf), 0, msg=f"{len(buf)} warnings raised.\n{msg}") class QuantityTestCase(BaseTestCase): @@ -128,7 +127,8 @@ def testsuite(): # TESTING THE DOCUMENTATION requires pyyaml, serialize, numpy and uncertainties if HAS_NUMPY and HAS_UNCERTAINTIES: try: - import yaml, serialize + import serialize # noqa: F401 + import yaml # noqa: F401 add_docs(suite) except ImportError: diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 952f0596f..4bc8b3452 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -106,7 +106,7 @@ def check_output(self, want, got, optionflags): try: if eval(want) == eval(got): return True - except: + except Exception: pass for regex in (_q_re, _sq_re): @@ -124,7 +124,7 @@ def check_output(self, want, got, optionflags): return False return True - except: + except Exception: pass cnt = 0 @@ -138,7 +138,7 @@ def check_output(self, want, got, optionflags): if parsed_got == parsed_want: return True - except: + except Exception: pass if cnt: diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index 7b917ae88..cc7e4dc0d 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -442,7 +442,6 @@ def test_arcsin(self): ) def test_arccos(self): - x = np.arange(0, 0.9, 0.1) * self.ureg.m self._test1( np.arccos, ( From ca4bd8ea5bf46d9fae30758b9e3d52e21ccf8a7c Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 12:25:16 +0000 Subject: [PATCH 186/612] More polish --- pint/testsuite/test_numpy.py | 16 ++++++++-------- pint/testsuite/test_numpy_func.py | 1 - pint/testsuite/test_quantity.py | 30 ++++++++++++++++-------------- pint/testsuite/test_unit.py | 16 ++++++---------- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index b2097bb3d..affc8a560 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -4,7 +4,7 @@ import operator as op import unittest -from pint import DimensionalityError, OffsetUnitCalculusError, set_application_registry +from pint import DimensionalityError, OffsetUnitCalculusError from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.test_umath import TestUFuncs @@ -314,20 +314,20 @@ def test_ediff1d(self): @helpers.requires_array_function_protocol() def test_gradient(self): - l = np.gradient([[1, 1], [3, 4]] * self.ureg.m, 1 * self.ureg.J) + grad = np.gradient([[1, 1], [3, 4]] * self.ureg.m, 1 * self.ureg.J) self.assertQuantityEqual( - l[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.m / self.ureg.J + grad[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.m / self.ureg.J ) self.assertQuantityEqual( - l[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.m / self.ureg.J + grad[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.m / self.ureg.J ) - l = np.gradient(self.Q_([[1, 1], [3, 4]], self.ureg.degC), 1 * self.ureg.J) + grad = np.gradient(self.Q_([[1, 1], [3, 4]], self.ureg.degC), 1 * self.ureg.J) self.assertQuantityEqual( - l[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.delta_degC / self.ureg.J + grad[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.delta_degC / self.ureg.J ) self.assertQuantityEqual( - l[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.delta_degC / self.ureg.J + grad[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.delta_degC / self.ureg.J ) @helpers.requires_array_function_protocol() @@ -501,7 +501,7 @@ def test_compress(self): ) @helpers.requires_array_function_protocol() - def test_compress(self): + def test_compress_nep18(self): self.assertQuantityEqual( np.compress([False, True], self.q, axis=1), [[2], [4]] * self.ureg.m ) diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 262a2ddcd..b2c81efa7 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -6,7 +6,6 @@ from pint import DimensionalityError, OffsetUnitCalculusError from pint.compat import np from pint.numpy_func import ( - _get_first_input_units, _is_quantity, _is_quantity_sequence, convert_to_consistent_units, diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 070d7f042..33a7eae83 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -271,22 +271,24 @@ def test_to_base_units(self): ) def test_convert(self): - x = self.Q_("2*inch") - self.assertQuantityAlmostEqual(x.to("meter"), self.Q_(2.0 * 0.0254, "meter")) - x = self.Q_("2*meter") - self.assertQuantityAlmostEqual(x.to("inch"), self.Q_(2.0 / 0.0254, "inch")) - x = self.Q_("2*sidereal_second") - self.assertQuantityAlmostEqual(x.to("second"), self.Q_(1.994539133, "second")) - x = self.Q_("2.54*centimeter/second") - self.assertQuantityAlmostEqual(x.to("inch/second"), self.Q_(1, "inch/second")) - x = self.Q_("2.54*centimeter") - self.assertQuantityAlmostEqual(x.to("inch").magnitude, 1) self.assertQuantityAlmostEqual( - self.Q_(2, "second").to("millisecond").magnitude, 2000 + self.Q_("2 inch").to("meter"), self.Q_(2.0 * 0.0254, "meter") + ) + self.assertQuantityAlmostEqual( + self.Q_("2 meter").to("inch"), self.Q_(2.0 / 0.0254, "inch") + ) + self.assertQuantityAlmostEqual( + self.Q_("2 sidereal_year").to("second"), self.Q_(63116297.5325, "second") + ) + self.assertQuantityAlmostEqual( + self.Q_("2.54 centimeter/second").to("inch/second"), + self.Q_("1 inch/second"), ) + self.assertAlmostEqual(self.Q_("2.54 centimeter").to("inch").magnitude, 1) + self.assertAlmostEqual(self.Q_("2 second").to("millisecond").magnitude, 2000) @helpers.requires_numpy() - def test_convert(self): + def test_convert_numpy(self): # Conversions with single units take a different codepath than # Conversions with more than one unit. @@ -606,7 +608,7 @@ def _test_inplace(self, operator, value1, value2, expected_result, unit=None): if isinstance(expected_result, str): expected_result = self.Q_(expected_result) - if not unit is None: + if unit is not None: value1 = value1 * unit value2 = value2 * unit expected_result = expected_result * unit @@ -630,7 +632,7 @@ def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None if isinstance(expected_result, str): expected_result = self.Q_(expected_result) - if not unit is None: + if unit is not None: value1 = value1 * unit value2 = value2 * unit expected_result = expected_result * unit diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 632714393..2b3cc407f 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -84,12 +84,12 @@ def test_unit_formatting_snake_case(self): for spec, result in ( ("L", r"\mathrm{oil\_barrel}"), ("P", "oil_barrel"), - ("H", "oil\_barrel"), + ("H", r"oil\_barrel"), ("C", "oil_barrel"), ("~", "oil_bbl"), ("L~", r"\mathrm{oil\_bbl}"), ("P~", "oil_bbl"), - ("H~", "oil\_bbl"), + ("H~", r"oil\_bbl"), ("C~", "oil_bbl"), ): with self.subTest(spec): @@ -212,7 +212,7 @@ def test_base(self): ureg.define("meter = [length]") self.assertRaises(DefinitionSyntaxError, ureg.define, "meter = [length]") self.assertRaises(TypeError, ureg.define, list()) - x = ureg.define("degC = kelvin; offset: 273.15") + ureg.define("degC = kelvin; offset: 273.15") def test_define(self): ureg = UnitRegistry(None) @@ -272,7 +272,6 @@ def test_parse_prefix(self): self.ureg.parse_expression("kilometer"), self.Q_(1, UnitsContainer(kilometer=1.0)), ) - # self.assertEqual(self.ureg._units['kilometer'], self.Q_(1000., UnitsContainer(meter=1.))) def test_parse_complex(self): self.assertEqual( @@ -427,11 +426,8 @@ def test_imperial_symbol(self): self.assertEqual(self.ureg.get_symbol("international_inch"), "in") def test_pint(self): - p = self.ureg.pint - l = self.ureg.liter - ip = self.ureg.imperial_pint - self.assertLess(p, l) - self.assertLess(p, ip) + self.assertLess(self.ureg.pint, self.ureg.liter) + self.assertLess(self.ureg.pint, self.ureg.imperial_pint) def test_wraps(self): def func(x): @@ -742,7 +738,7 @@ def test_lazy(self): x.test = "test" self.assertIsInstance(x, UnitRegistry) y = LazyRegistry() - q = y("meter") + y("meter") self.assertIsInstance(y, UnitRegistry) def test_redefinition(self): From 1e4f410d3736f8698b34cbcb7a4c5a43d38bfa05 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:06:02 +0000 Subject: [PATCH 187/612] Package version automatically increases at every commit --- setup.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index 71d5bc9fb..e4989d734 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import sys - -try: - reload(sys).setdefaultencoding("UTF-8") -except: - pass - -try: - from setuptools import setup -except ImportError: - print('Please install or upgrade setuptools or pip to continue') - sys.exit(1) - import codecs +import subprocess + +from setuptools import setup def read(filename): @@ -27,9 +17,28 @@ def read(filename): __doc__ = long_description + +version = "0.10" +RELEASE_VERSION = False + +if not RELEASE_VERSION: + # Append incremental version number from git + try: + version += ( + ".dev" + + subprocess.check_output(["git", "rev-list", "--count", "HEAD"]) + .decode() + .strip() + ) + except (subprocess.CalledProcessError, FileNotFoundError): + # The .git directory has been removed (likely by setup.py sdist), + # and/or git is not installed + version += ".dev0" + + setup( name='Pint', - version='0.10.dev0', + version=version, description='Physical quantities module', long_description=long_description, keywords='physical quantities unit conversion science', From c0da869390c3d0759c7cd4e4eb92d2ee5af3bccc Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:18:12 +0000 Subject: [PATCH 188/612] CHANGES --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 0909b1d03..623712ca9 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Auto-increase package version at every commit when pint is installed from the git tip, + e.g. ``pip install git+https://github.com/hgrecco/pint.git``. + (Issue #930, Thanks Guido Imperiale) - Fix HTML (Jupyter Notebook) and LateX representation of some units (Issue #927, Thanks Guido Imperiale) - **BREAKING CHANGE**: From a3936857b809f8593a385c560c335b0f1b612fb4 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:18:23 +0000 Subject: [PATCH 189/612] CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 623712ca9..5b3052009 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,7 @@ Pint Changelog ----------------- - Auto-increase package version at every commit when pint is installed from the git tip, - e.g. ``pip install git+https://github.com/hgrecco/pint.git``. + e.g. pip install git+https://github.com/hgrecco/pint.git. (Issue #930, Thanks Guido Imperiale) - Fix HTML (Jupyter Notebook) and LateX representation of some units (Issue #927, Thanks Guido Imperiale) From bf3beebc8c4a34a70c41d350b65d5b99efe97e62 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:33:44 +0000 Subject: [PATCH 190/612] Remove redundant encoding information (utf8 is the default) --- docs/conf.py | 1 - pint/__init__.py | 1 - pint/babel_names.py | 1 - pint/compat.py | 1 - pint/context.py | 1 - pint/converters.py | 1 - pint/definitions.py | 1 - pint/errors.py | 1 - pint/formatting.py | 1 - pint/matplotlib.py | 1 - pint/measurement.py | 1 - pint/numpy_func.py | 1 - pint/pint_eval.py | 1 - pint/quantity.py | 1 - pint/registry.py | 1 - pint/registry_helpers.py | 1 - pint/systems.py | 1 - pint/testsuite/__init__.py | 2 -- pint/testsuite/helpers.py | 2 -- pint/testsuite/parameterized.py | 2 -- pint/testsuite/test_babel.py | 2 -- pint/testsuite/test_contexts.py | 2 -- pint/testsuite/test_converters.py | 2 -- pint/testsuite/test_definitions.py | 2 -- pint/testsuite/test_errors.py | 2 -- pint/testsuite/test_formatter.py | 2 -- pint/testsuite/test_issues.py | 2 -- pint/testsuite/test_measurement.py | 2 -- pint/testsuite/test_numpy.py | 2 -- pint/testsuite/test_numpy_func.py | 2 -- pint/testsuite/test_pint_eval.py | 2 -- pint/testsuite/test_pitheorem.py | 2 -- pint/testsuite/test_quantity.py | 2 -- pint/testsuite/test_systems.py | 2 -- pint/testsuite/test_umath.py | 2 -- pint/testsuite/test_unit.py | 2 -- pint/testsuite/test_util.py | 2 -- pint/unit.py | 1 - pint/util.py | 1 - setup.py | 1 - 40 files changed, 60 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 925f79d71..211ff0b10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # pint documentation build configuration file, created by # sphinx-quickstart on Thu Mar 1 13:33:14 2012. diff --git a/pint/__init__.py b/pint/__init__.py index b3d4393b2..682e4e717 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint ~~~~ diff --git a/pint/babel_names.py b/pint/babel_names.py index 43c69d2d7..9f23b8366 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.babel ~~~~~~~~~~ diff --git a/pint/compat.py b/pint/compat.py index 93d1a654d..6f64acbfb 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.compat ~~~~~~~~~~~ diff --git a/pint/context.py b/pint/context.py index e4fff6a40..db57336e5 100644 --- a/pint/context.py +++ b/pint/context.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.context ~~~~~~~~~~~~ diff --git a/pint/converters.py b/pint/converters.py index 62572f8ad..ff1624ce3 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.converters ~~~~~~~~~~~~~~~ diff --git a/pint/definitions.py b/pint/definitions.py index 7cd479963..0ca7ed4fc 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.definitions ~~~~~~~~~~~~~~~~ diff --git a/pint/errors.py b/pint/errors.py index d5ae803f0..c0d9ab7ae 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.errors ~~~~~~~~~~~ diff --git a/pint/formatting.py b/pint/formatting.py index 224e0a2f4..254ce1635 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.formatter ~~~~~~~~~~~~~~ diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 2390677f0..3b7b0af90 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.matplotlib ~~~~~~~~~~~~~~~ diff --git a/pint/measurement.py b/pint/measurement.py index 54df1d802..175a4bfc0 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.measurement ~~~~~~~~~~~~~~~~ diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 6025e273e..75a4de6a6 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.numpy_func ~~~~~~~~~~~~~~~ diff --git a/pint/pint_eval.py b/pint/pint_eval.py index f768d6ca3..afa3877f9 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.pint_eval ~~~~~~~~~~~~~~ diff --git a/pint/quantity.py b/pint/quantity.py index 3561e0754..9dd16596d 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.quantity ~~~~~~~~~~~~~ diff --git a/pint/registry.py b/pint/registry.py index 1fe973ca2..444b3631a 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.registry ~~~~~~~~~~~~~ diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index db2ba91a9..07877ba4a 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.registry_helpers ~~~~~~~~~~~~~~~~~~~~~ diff --git a/pint/systems.py b/pint/systems.py index 134f57018..35752687c 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.systems ~~~~~~~~~~~~ diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 6c126cde2..a1e51d92e 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import doctest import logging import math diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 4bc8b3452..5a9139146 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import doctest import re import unittest diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index 366b4be5a..ca0a72442 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Adds Parameterized tests for Python's unittest module # # Code from: parameterizedtestcase, version: 0.1.0 diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index e85ec4038..67bc22ed4 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from pint import UnitRegistry diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 80103d066..f73cfef0c 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import itertools from collections import defaultdict diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 53d37b51c..061b05da8 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import itertools from pint.compat import np diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 25197a8c0..421bdb8d8 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pint.converters import OffsetConverter, ScaleConverter from pint.definitions import ( AliasDefinition, diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index 920ed2e53..cb75dd7ca 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import pickle from pint import ( diff --git a/pint/testsuite/test_formatter.py b/pint/testsuite/test_formatter.py index cf5e64b44..c4a4d5766 100644 --- a/pint/testsuite/test_formatter.py +++ b/pint/testsuite/test_formatter.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pint import formatting as fmt from pint.testsuite import QuantityTestCase diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 4913b01d0..7bc4427c3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import copy import math import pprint diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index d37cf6072..ff8cbc881 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pint import DimensionalityError from pint.testsuite import QuantityTestCase, helpers diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index affc8a560..8fd39d842 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import copy import operator as op import unittest diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index b2c81efa7..59f0a2c13 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from unittest.mock import patch import pint.numpy_func diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index 0dd782970..20f9b46da 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import unittest from pint.compat import tokenizer diff --git a/pint/testsuite/test_pitheorem.py b/pint/testsuite/test_pitheorem.py index ed6806398..40abd8c2e 100644 --- a/pint/testsuite/test_pitheorem.py +++ b/pint/testsuite/test_pitheorem.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import itertools from pint import pi_theorem diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 33a7eae83..44713d0fb 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import copy import datetime import math diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index bff359b1a..fd7697312 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pint import UnitRegistry from pint.testsuite import QuantityTestCase diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index cc7e4dc0d..a1accc3ac 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pint import DimensionalityError from pint.compat import np from pint.testsuite import QuantityTestCase, helpers diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 2b3cc407f..5e52ac9c7 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import copy import functools import math diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 29848d3ad..dc03080b9 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import collections import copy import operator as op diff --git a/pint/unit.py b/pint/unit.py index 733208d2c..c64f96694 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.unit ~~~~~~~~~ diff --git a/pint/util.py b/pint/util.py index ca0f01164..3b6c4672b 100644 --- a/pint/util.py +++ b/pint/util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ pint.util ~~~~~~~~~ diff --git a/setup.py b/setup.py index 9c6eed47f..f13e0d76f 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import codecs From 965fcbeb6db2a8306121d405d15115ba7a33dd95 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:34:49 +0000 Subject: [PATCH 191/612] Unneeded use of codecs --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index f13e0d76f..59cf64619 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,11 @@ #!/usr/bin/env python -import codecs - from setuptools import setup def read(filename): - return codecs.open(filename, encoding="utf-8").read() + with open(filename) as fh: + return fh.read() long_description = "\n\n".join([read("README"), read("AUTHORS"), read("CHANGES")]) From 3b9e047201cddd0410933b77d8889a7ebc59e341 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:36:10 +0000 Subject: [PATCH 192/612] Unneeded use of codecs --- setup.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index e4989d734..c8d29b5b1 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -import codecs import subprocess from setuptools import setup def read(filename): - return codecs.open(filename, encoding='utf-8').read() + with open(filename) as fh: + return fh.read() -long_description = '\n\n'.join([read('README'), - read('AUTHORS'), - read('CHANGES')]) - +long_description = '\n\n'.join([read('README'), read('AUTHORS'), read('CHANGES')]) __doc__ = long_description From 840977b58855faeeaf3d86acb55d7ff7da971db1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:48:05 +0000 Subject: [PATCH 193/612] Preformatted strings --- pint/formatting.py | 4 ++-- pint/measurement.py | 2 +- pint/testsuite/test_issues.py | 32 ++++++++++---------------------- pint/testsuite/test_quantity.py | 8 ++++---- pint/testsuite/test_unit.py | 6 +++--- pint/testsuite/test_util.py | 4 ++-- 6 files changed, 22 insertions(+), 34 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index 254ce1635..2564030de 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -42,7 +42,7 @@ def _pretty_fmt_exponent(num): """Format an number into a pretty printed exponent. """ # TODO: Will not work for decimals - ret = "{0:n}".format(num).replace("-", "⁻") + ret = f"{num:n}".replace("-", "⁻") for n in range(10): ret = ret.replace(str(n), _PRETTY_EXPONENTS[n]) return ret @@ -103,7 +103,7 @@ def formatter( division_fmt=" / ", power_fmt="{} ** {}", parentheses_fmt="({0})", - exp_call=lambda x: "{0:n}".format(x), + exp_call=lambda x: f"{x:n}", locale=None, babel_length="long", babel_plural_form="one", diff --git a/pint/measurement.py b/pint/measurement.py index 175a4bfc0..98ba7a575 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -75,7 +75,7 @@ def __repr__(self): ) def __str__(self): - return "{0}".format(self) + return "{}".format(self) def __format__(self, spec): # special cases diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 7bc4427c3..833ee1c34 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -262,12 +262,7 @@ def test_issue85(self): m = 1.0 * ureg.amu va = 2.0 * ureg.k * T / m - try: - va.to_base_units() - except: - self.assertTrue( - False, "Error while trying to get base units for {}".format(va) - ) + va.to_base_units() boltmk = 1.380649e-23 * ureg.J / ureg.K vb = 2.0 * boltmk * T / m @@ -388,11 +383,11 @@ def test_issue105(self): for func in (ureg.get_name, ureg.parse_expression): val = func("meter") - self.assertRaises(AttributeError, func, "METER") + with self.assertRaises(AttributeError): + func("METER") self.assertEqual(val, func("METER", False)) def test_issue121(self): - sh = (2, 1) ureg = UnitRegistry() z, v = 0, 2.0 self.assertEqual(z + v * ureg.meter, v * ureg.meter) @@ -423,16 +418,10 @@ def test_issue121b(self): z, v = np.zeros((3, 1)), 2.0 * np.ones(sh) for x, y in ((z, v), (z, v * ureg.meter), (v * ureg.meter, z)): - try: - w = x + y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass - try: - w = x - y - self.assertTrue(False, "ValueError not raised") - except ValueError: - pass + with self.assertRaises(ValueError): + x + y + with self.assertRaises(ValueError): + x - y @helpers.requires_numpy() def test_issue127(self): @@ -509,12 +498,11 @@ def test_issue339(self): self.assertEqual(q1, q2) def test_issue354_356_370(self): - q = 1 * self.ureg.second / self.ureg.millisecond self.assertEqual( - "{0:~}".format(1 * self.ureg.second / self.ureg.millisecond), "1.0 s / ms" + "{:~}".format(1 * self.ureg.second / self.ureg.millisecond), "1.0 s / ms" ) - self.assertEqual("{0:~}".format(1 * self.ureg.count), "1 count") - self.assertEqual("{0:~}".format(1 * self.ureg("MiB")), "1 MiB") + self.assertEqual("{:~}".format(1 * self.ureg.count), "1 count") + self.assertEqual("{:~}".format(1 * self.ureg("MiB")), "1 MiB") def test_issue468(self): ureg = UnitRegistry() diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 44713d0fb..f54e59f3b 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -142,7 +142,7 @@ def test_quantity_format(self): # Check the special case that prevents e.g. '3 1 / second' x = self.Q_(3, UnitsContainer(second=-1)) - self.assertEqual("{0}".format(x), "3 / second") + self.assertEqual(f"{x}", "3 / second") @helpers.requires_numpy() def test_quantity_array_format(self): @@ -187,9 +187,9 @@ def test_format_compact(self): self.assertEqual(q3.magnitude, q3b.magnitude) self.assertEqual(q3.units, q3b.units) - self.assertEqual("{0:#.1f}".format(q1), "{0}".format(q1b)) - self.assertEqual("{0:#.1f}".format(q2), "{0}".format(q2b)) - self.assertEqual("{0:#.1f}".format(q3), "{0}".format(q3b)) + self.assertEqual(f"{q1:#.1f}", f"{q1b}") + self.assertEqual(f"{q2:#.1f}", f"{q2b}") + self.assertEqual(f"{q3:#.1f}", f"{q3b}") def test_default_formatting(self): ureg = UnitRegistry() diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 5e52ac9c7..16b8929d3 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -232,10 +232,10 @@ def test_load(self): def test_default_format(self): ureg = UnitRegistry() q = ureg.meter - s1 = "{0}".format(q) - s2 = "{0:~}".format(q) + s1 = f"{q}" + s2 = f"{q:~}" ureg.default_format = "~" - s3 = "{0}".format(q) + s3 = f"{q}" self.assertEqual(s2, s3) self.assertNotEqual(s1, s3) self.assertEqual(ureg.default_format, "~") diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index dc03080b9..d27a4ebe2 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -206,7 +206,7 @@ def test_eval_token(self): class TestStringProcessor(BaseTestCase): def _test(self, bef, aft): - for pattern in ("{0}", "+{0}+"): + for pattern in ("{}", "+{}+"): b = pattern.format(bef) a = pattern.format(aft) self.assertEqual(string_preprocessor(b), a) @@ -308,7 +308,7 @@ def test_matrix_to_string(self): [[1, 2], [3, 4]], row_headers=None, col_headers=None, - fmtfun=lambda x: "{0:.2f}".format(x), + fmtfun=lambda x: f"{x:.2f}", ), "1.00\t2.00\n" "3.00\t4.00", ) From 8c89b44bc3086af26044078a1d5df668437744d3 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 14:55:49 +0000 Subject: [PATCH 194/612] Polish --- pint/testsuite/test_contexts.py | 19 ++++--------------- pint/testsuite/test_issues.py | 7 +++---- pint/testsuite/test_systems.py | 20 +++++++++----------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index f73cfef0c..590eb503b 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -115,7 +115,7 @@ def test_known_context_enable(self): def test_graph(self): ureg = UnitRegistry() add_ctxs(ureg) - l = UnitsContainer({"[length]": 1.0}) + l = UnitsContainer({"[length]": 1.0}) # noqa: E741 t = UnitsContainer({"[time]": -1.0}) c = UnitsContainer({"[current]": 1.0}) @@ -154,7 +154,7 @@ def test_graph(self): def test_graph_enable(self): ureg = UnitRegistry() add_ctxs(ureg) - l = UnitsContainer({"[length]": 1.0}) + l = UnitsContainer({"[length]": 1.0}) # noqa: E741 t = UnitsContainer({"[time]": -1.0}) c = UnitsContainer({"[current]": 1.0}) @@ -222,14 +222,9 @@ def test_known_nested_context(self): def test_unknown_context(self): ureg = UnitRegistry() add_ctxs(ureg) - try: + with self.assertRaises(KeyError): with ureg.context("la"): pass - except KeyError as e: - value = True - except Exception as e: - value = False - self.assertTrue(value) self.assertFalse(ureg._active_ctx) self.assertFalse(ureg._active_ctx.graph) @@ -240,15 +235,9 @@ def test_unknown_nested_context(self): with ureg.context("lc"): x = dict(ureg._active_ctx) y = dict(ureg._active_ctx.graph) - try: + with self.assertRaises(KeyError): with ureg.context("la"): pass - except KeyError as e: - value = True - except Exception as e: - value = False - - self.assertTrue(value) self.assertEqual(x, ureg._active_ctx) self.assertEqual(y, ureg._active_ctx.graph) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 833ee1c34..145d0cc9b 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -531,7 +531,6 @@ def test_issue483(self): np.testing.assert_array_equal(p, a ** a) def test_issue523(self): - ureg = UnitRegistry() src, dst = UnitsContainer({"meter": 1}), UnitsContainer({"degF": 1}) value = 10.0 convert = self.ureg.convert @@ -620,13 +619,13 @@ def pendulum_period(length, G=Q_(1, "standard_gravity")): print(length) return (2 * math.pi * (length / G) ** 0.5).to("s") - l = 1 * ureg.m + length = Q_(1, ureg.m) # Assume earth gravity - t = pendulum_period(l) + t = pendulum_period(length) self.assertAlmostEqual(t, Q_("2.0064092925890407 second")) # Use moon gravity moon_gravity = Q_(1.625, "m/s^2") - t = pendulum_period(l, moon_gravity) + t = pendulum_period(length, moon_gravity) self.assertAlmostEqual(t, Q_("4.928936075204336 second")) def test_issue783(self): diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index fd7697312..a97449133 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -62,27 +62,25 @@ def test_simple(self): self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using1(self): - lines = ["@group mygroup using group1", "meter", "second"] - ureg, root = self._build_empty_reg_root() - d = ureg._groups - - g = ureg.Group("group1") - grp = ureg.Group.from_lines(lines, lambda x: None) + ureg._groups + ureg.Group("group1") + grp = ureg.Group.from_lines( + ["@group mygroup using group1", "meter", "second"], lambda x: None + ) self.assertEqual(grp.name, "mygroup") self.assertEqual(grp._unit_names, {"meter", "second"}) self.assertEqual(grp._used_groups, {"group1"}) self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using2(self): - lines = ["@group mygroup using group1,group2", "meter", "second"] - ureg, root = self._build_empty_reg_root() - d = ureg._groups - + ureg._groups ureg.Group("group1") ureg.Group("group2") - grp = ureg.Group.from_lines(lines, lambda x: None) + grp = ureg.Group.from_lines( + ["@group mygroup using group1,group2", "meter", "second"], lambda x: None + ) self.assertEqual(grp.name, "mygroup") self.assertEqual(grp._unit_names, {"meter", "second"}) self.assertEqual(grp._used_groups, {"group1", "group2"}) From c36b26d3e76d4156ad294729b64d2779cee4d68b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 15:01:30 +0000 Subject: [PATCH 195/612] All flake8 tests successful --- pint/testsuite/test_systems.py | 43 ++++++++++------------------------ 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index a97449133..008d299f5 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -62,25 +62,23 @@ def test_simple(self): self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using1(self): + lines = ["@group mygroup using group1", "meter", "second"] + ureg, root = self._build_empty_reg_root() - ureg._groups ureg.Group("group1") - grp = ureg.Group.from_lines( - ["@group mygroup using group1", "meter", "second"], lambda x: None - ) + grp = ureg.Group.from_lines(lines, lambda x: None) self.assertEqual(grp.name, "mygroup") self.assertEqual(grp._unit_names, {"meter", "second"}) self.assertEqual(grp._used_groups, {"group1"}) self.assertEqual(grp.members, frozenset(["meter", "second"])) def test_using2(self): + lines = ["@group mygroup using group1,group2", "meter", "second"] + ureg, root = self._build_empty_reg_root() - ureg._groups ureg.Group("group1") ureg.Group("group2") - grp = ureg.Group.from_lines( - ["@group mygroup using group1,group2", "meter", "second"], lambda x: None - ) + grp = ureg.Group.from_lines(lines, lambda x: None) self.assertEqual(grp.name, "mygroup") self.assertEqual(grp._unit_names, {"meter", "second"}) self.assertEqual(grp._used_groups, {"group1", "group2"}) @@ -90,8 +88,6 @@ def test_spaces(self): lines = ["@group mygroup using group1 , group2", " meter ", " second "] ureg, root = self._build_empty_reg_root() - d = ureg._groups - ureg.Group("group1") ureg.Group("group2") grp = ureg.Group.from_lines(lines, lambda x: None) @@ -104,9 +100,7 @@ def test_invalidate_members(self): lines = ["@group mygroup using group1", "meter", "second"] ureg, root = self._build_empty_reg_root() - d = ureg._groups - - g1 = ureg.Group("group1") + ureg.Group("group1") grp = ureg.Group.from_lines(lines, lambda x: None) self.assertIs(root._computed_members, None) self.assertIs(grp._computed_members, None) @@ -134,20 +128,16 @@ def define(ud): defs.append(ud.name) ureg, root = self._build_empty_reg_root() - d = ureg._groups - - grp = ureg.Group.from_lines(lines, define) + ureg.Group.from_lines(lines, define) self.assertEqual(["kings_leg", "kings_head"], defs) def test_members_including(self): - ureg, root = self._build_empty_reg_root() - d = ureg._groups g1 = ureg.Group("group1") - g1.add_units("second", "inch") + g2 = ureg.Group("group2") g2.add_units("second", "newton") @@ -181,8 +171,6 @@ def test_implicit_root(self): lines = ["@system mks", "meter", "kilogram", "second"] ureg, root = self._build_empty_reg_root() - d = ureg._groups - s = ureg.System.from_lines(lines, lambda x: x) s._used_groups = {"root"} @@ -190,8 +178,6 @@ def test_simple_using(self): lines = ["@system mks using g1", "meter", "kilogram", "second"] ureg, root = self._build_empty_reg_root() - d = ureg._groups - s = ureg.System.from_lines(lines, lambda x: x) s._used_groups = {"root", "g1"} @@ -199,8 +185,6 @@ def test_members_group(self): lines = ["@system mk", "meter", "kilogram"] ureg, root = self._build_empty_reg_root() - d = ureg._groups - root.add_units("second") s = ureg.System.from_lines(lines, lambda x: x) self.assertEqual(s.members, frozenset(["second"])) @@ -217,13 +201,12 @@ def test_get_compatible_units(self): lines = ["@system %s using test-imperial" % sysname, "inch"] - s = ureg.System.from_lines(lines, lambda x: x) + ureg.System.from_lines(lines, lambda x: x) c = ureg.get_compatible_units("meter", sysname) self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) def test_get_base_units(self): sysname = "mysys2" - ureg = UnitRegistry() g = ureg.get_group("test-imperial") @@ -245,12 +228,11 @@ def test_get_base_units(self): def test_get_base_units_different_exponent(self): sysname = "mysys3" - ureg = UnitRegistry() g = ureg.get_group("test-imperial") g.add_units("inch", "yard", "pint") - c = ureg.get_compatible_units("meter", "test-imperial") + ureg.get_compatible_units("meter", "test-imperial") lines = ["@system %s using test-imperial" % sysname, "pint:meter"] @@ -276,7 +258,6 @@ def test_get_base_units_different_exponent(self): def test_get_base_units_relation(self): sysname = "mysys4" - ureg = UnitRegistry() g = ureg.get_group("test-imperial") @@ -298,4 +279,4 @@ def test_get_base_units_relation(self): def test_members_nowarning(self): ureg = self.ureg for name in dir(ureg.sys): - s = dir(getattr(ureg.sys, name)) + dir(getattr(ureg.sys, name)) From f6abd2b68103b4919254ed2b529be55b384e07fa Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Dec 2019 15:08:16 +0000 Subject: [PATCH 196/612] Avoid collisions with builtin min and max --- pint/quantity.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 9dd16596d..627e3f216 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1437,30 +1437,30 @@ def _numpy_method_wrap(self, func, *args, **kwargs): return value def clip(self, first=None, second=None, out=None, **kwargs): - min = kwargs.get("min", first) - max = kwargs.get("max", second) + minimum = kwargs.get("min", first) + maximum = kwargs.get("max", second) - if min is None and max is None: + if minimum is None and maximum is None: raise TypeError("clip() takes at least 3 arguments (2 given)") - if max is None and "min" not in kwargs: - min, max = max, min + if maximum is None and "min" not in kwargs: + minimum, maximum = maximum, minimum kwargs = {"out": out} - if min is not None: - if isinstance(min, self.__class__): - kwargs["min"] = min.to(self).magnitude + if minimum is not None: + if isinstance(minimum, self.__class__): + kwargs["min"] = minimum.to(self).magnitude elif self.dimensionless: - kwargs["min"] = min + kwargs["min"] = minimum else: raise DimensionalityError("dimensionless", self._units) - if max is not None: - if isinstance(max, self.__class__): - kwargs["max"] = max.to(self).magnitude + if maximum is not None: + if isinstance(maximum, self.__class__): + kwargs["max"] = maximum.to(self).magnitude elif self.dimensionless: - kwargs["max"] = max + kwargs["max"] = maximum else: raise DimensionalityError("dimensionless", self._units) From e3e025582363d5c12f5f40584dcf9dec86e2921a Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Dec 2019 11:35:08 +0000 Subject: [PATCH 197/612] Merge from master --- pint/testsuite/test_contexts.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index c11021e18..5fd6598da 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -692,9 +692,13 @@ def test_textile(self): # Check RKM <-> cN/tex conversion self.assertQuantityAlmostEqual(1 * ureg.RKM, 0.980665 * ureg.cN / ureg.tex) - self.assertQuantityAlmostEqual((1/0.980665) * ureg.RKM, 1 * ureg.cN / ureg.tex) + self.assertQuantityAlmostEqual( + (1 / 0.980665) * ureg.RKM, 1 * ureg.cN / ureg.tex + ) self.assertAlmostEqual((1 * ureg.RKM).to(ureg.cN / ureg.tex).m, 0.980665) - self.assertAlmostEqual((1 * ureg.cN / ureg.tex).to(ureg.RKM).m, 1/0.980665) + self.assertAlmostEqual( + (1 * ureg.cN / ureg.tex).to(ureg.RKM).m, 1 / 0.980665 + ) def test_decorator(self): ureg = self.ureg From fa90f28a784e39e4c5310e110d4b4713a348686d Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Sat, 14 Dec 2019 18:16:04 +0000 Subject: [PATCH 198/612] Now we setuptools_scm to get version from git instead of calling subprocess on own. --- setup.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index 8f0336448..85de856c8 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -import subprocess - from setuptools import setup @@ -13,28 +11,9 @@ def read(filename): long_description = "\n\n".join([read("README"), read("AUTHORS"), read("CHANGES")]) __doc__ = long_description - -version = "0.10" -RELEASE_VERSION = False - -if not RELEASE_VERSION: - # Append incremental version number from git - try: - version += ( - ".dev" - + subprocess.check_output(["git", "rev-list", "--count", "HEAD"]) - .decode() - .strip() - ) - except (subprocess.CalledProcessError, FileNotFoundError): - # The .git directory has been removed (likely by setup.py sdist), - # and/or git is not installed - version += ".dev0" - - setup( name="Pint", - version=version, + use_scm_version=True, description="Physical quantities module", long_description=long_description, keywords="physical quantities unit conversion science", @@ -62,8 +41,9 @@ def read(filename): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - python_requires=">=3.6", + python_requires='>=3.6', install_requires=["setuptools"], + setup_requires=["setuptools", "setuptools_scm"], extras_require={ "numpy": ["numpy >= 1.14"], "uncertainties": ["uncertainties >= 3.0"], From 3b4c77e454822366b16fe844eecec5f698603441 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Dec 2019 18:17:27 +0000 Subject: [PATCH 199/612] # Conflicts: # pint/formatting.py # pint/measurement.py # pint/quantity.py # pint/testsuite/test_measurement.py # pint/testsuite/test_quantity.py # pint/testsuite/test_unit.py # pint/unit.py --- CHANGES | 2 +- pint/formatting.py | 2 +- pint/measurement.py | 29 +++++++++++++----- pint/quantity.py | 20 +++++++++---- pint/testsuite/test_measurement.py | 47 +++++++++++++++++++++++++----- pint/testsuite/test_quantity.py | 25 +++++++++------- pint/testsuite/test_unit.py | 16 +++++----- pint/unit.py | 6 +++- 8 files changed, 105 insertions(+), 42 deletions(-) diff --git a/CHANGES b/CHANGES index 5b3052009..7b4d7c208 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,7 @@ Pint Changelog e.g. pip install git+https://github.com/hgrecco/pint.git. (Issue #930, Thanks Guido Imperiale) - Fix HTML (Jupyter Notebook) and LateX representation of some units - (Issue #927, Thanks Guido Imperiale) + (Issues #927 / #928 / #933, Thanks Guido Imperiale) - **BREAKING CHANGE**: Implement NEP-18 for Pint Quantities. Most NumPy functions that previously stripped units when applied to diff --git a/pint/formatting.py b/pint/formatting.py index 931e15414..7e98c2dbe 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -104,7 +104,7 @@ def formatter( division_fmt=" / ", power_fmt="{} ** {}", parentheses_fmt="({0})", - exp_call=lambda x: "{0:n}".format(x), + exp_call=lambda x: "{:n}".format(x), locale=None, babel_length="long", babel_plural_form="one", diff --git a/pint/measurement.py b/pint/measurement.py index 54df1d802..9e71c4dd2 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -6,6 +6,7 @@ :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +import re from .compat import ufloat from .formatting import _FORMATS, siunitx_format_unit @@ -71,12 +72,12 @@ def __reduce__(self): return _unpickle, (Measurement, self.magnitude, self._units) def __repr__(self): - return "".format( + return "".format( self.magnitude.nominal_value, self.magnitude.std_dev, self.units ) def __str__(self): - return "{0}".format(self) + return "{}".format(self) def __format__(self, spec): # special cases @@ -90,8 +91,7 @@ def __format__(self, spec): opts = "separate-uncertainty=true" mstr = format(self.magnitude, spec) ustr = siunitx_format_unit(self.units) - ret = r"\SI[%s]{%s}{%s}" % (opts, mstr, ustr) - return ret + return r"\SI[%s]{%s}{%s}" % (opts, mstr, ustr) # standard cases if "L" in spec: @@ -118,19 +118,32 @@ def __format__(self, spec): pars = _FORMATS["H"]["parentheses_fmt"] mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) + if "(" in mag: + # Exponential format has its own parentheses + pars = "{}" if "L" in newspec and "S" in newspec: mag = mag.replace("(", r"\left(").replace(")", r"\right)") - if "L" in newspec: + if "L" in newspec or "H" in spec: space = r"\ " else: space = " " - if "uS" in newspec or "ue" in newspec or "u%" in newspec: - return mag + space + format(self.units, spec) + ustr = format(self.units, spec) + if not ("uS" in newspec or "ue" in newspec or "u%" in newspec): + mag = pars.format(mag) + + 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) + + assert ustr[:2] == r"\[" + assert ustr[-2:] == r"\]" + return r"\[" + mag + space + ustr[2:] else: - return pars.format(mag) + space + format(self.units, spec) + return mag + space + ustr _Measurement = Measurement diff --git a/pint/quantity.py b/pint/quantity.py index 1aff1c238..5f39fc93f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -26,6 +26,7 @@ siunitx_format_unit, ndarray_to_latex, ndarray_to_latex_parts, + _pretty_fmt_exponent, ) from .errors import ( DimensionalityError, @@ -260,8 +261,6 @@ def __format__(self, spec): else: allf = plain_allf = "{} {}" - mstr, ustr = None, None - # If Compact is selected, do it at the beginning if "#" in spec: spec = spec.replace("#", "") @@ -272,10 +271,16 @@ def __format__(self, spec): # the LaTeX siunitx code if "Lx" in spec: spec = spec.replace("Lx", "") - # todo: add support for extracting options + # 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: ustr = format(obj.units, spec) @@ -284,8 +289,8 @@ def __format__(self, spec): if "L" in spec: mstr = ndarray_to_latex(obj.magnitude, mspec) elif "H" in spec: - # this is required to have the magnitude and unit in the same line 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: @@ -302,7 +307,12 @@ 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) + mstr = self._exp_pattern.sub(r"\1×10^{\2\3}", mstr) + elif "P" in spec: + m = self._exp_pattern.match(mstr) + if m: + exp = int(m.group(2) + m.group(3)) + mstr = self._exp_pattern.sub(r"\1×10" + _pretty_fmt_exponent(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 d37cf6072..4d97e5f53 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -47,16 +47,16 @@ def test_format(self): for spec, result in ( ("{}", "(4.00 +/- 0.10) second ** 2"), - ("{!r}", ""), + ("{!r}", ""), ("{:P}", "(4.00 ± 0.10) second²"), ("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), - ("{:H}", "(4.00 ± 0.10) second^2"), + ("{:H}", r"\[(4.00 ± 0.10)\ second^2\]"), ("{:C}", "(4.00+/-0.10) second**2"), ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(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}", "(4.0 ± 0.1) second^2"), + ("{:.1fH}", r"\[(4.0 ± 0.1)\ second^2\]"), ("{:.1fC}", "(4.0+/-0.1) second**2"), ("{:.1fLx}", r"\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}"), ): @@ -72,7 +72,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}", "0.2000(100) second^2"), + ("{:.3uSH}", r"\[0.2000(100)\ second^2\]"), ("{:.3uSC}", "0.2000(100) second**2"), ): with self.subTest(spec): @@ -86,7 +86,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}", "(0.2000 ± 0.0100) second^2"), + ("{:.3uH}", r"\[(0.2000 ± 0.0100)\ second^2\]"), ("{:.3uC}", "(0.2000+/-0.0100) second**2"), ( "{:.3uLx}", @@ -106,7 +106,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}", "(20 ± 1)% second^2"), + ("{:.1u%H}", r"\[(20 ± 1)%\ second^2\]"), ("{:.1u%C}", "(20+/-1)% second**2"), ): with self.subTest(spec): @@ -122,12 +122,45 @@ def test_format_perce(self): "{:.1ueL}", r"\left(2.0 \pm 0.1\right) \times 10^{-1}\ \mathrm{second}^{2}", ), - ("{:.1ueH}", "(2.0 ± 0.1)e-01 second^2"), + ("{:.1ueH}", r"\[(2.0 ± 0.1)×10^{-1}\ second^2\]"), ("{:.1ueC}", "(2.0+/-0.1)e-01 second**2"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) + def test_format_exponential_pos(self): + # Quantities in exponential format come with their own parenthesis, don't wrap + # them twice + m = self.ureg.Quantity(4e20, "s^2").plus_minus(1e19) + for spec, result in ( + ("{}", "(4.00 +/- 0.10)e+20 second ** 2"), + ("{!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\]"), + ("{:C}", "(4.00+/-0.10)e+20 second**2"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(10)e+20}{\second\squared}"), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) + + def test_format_exponential_neg(self): + m = self.ureg.Quantity(4e-20, "s^2").plus_minus(1e-21) + for spec, result in ( + ("{}", "(4.00 +/- 0.10)e-20 second ** 2"), + ("{!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\]"), + ("{:C}", "(4.00+/-0.10)e-20 second**2"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(10)e-20}{\second\squared}"), + ): + with self.subTest(spec): + self.assertEqual(spec.format(m), result) + def test_raise_build(self): v, u = self.Q_(1.0, "s"), self.Q_(0.1, "s") o = self.Q_(0.1, "m") diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 3433cade5..e7272de63 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -127,7 +127,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}", "4.12345678 kilogram meter^2/second"), + ("{:H}", r"\[4.12345678\ kilogram\ meter^2/second\]"), ("{:C}", "4.12345678 kilogram*meter**2/second"), ("{:~}", "4.12345678 kg * m ** 2 / s"), ( @@ -135,7 +135,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~}", "4.12345678 kg m^2/s"), + ("{:H~}", r"\[4.12345678\ kg\ m^2/s\]"), ("{:C~}", "4.12345678 kg*m**2/s"), ("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"), ): @@ -202,12 +202,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", "4.12345678 kilogram meter^2/second"), + ("H", r"\[4.12345678\ kilogram\ meter^2/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~", "4.12345678 kg m^2/s"), + ("H~", r"\[4.12345678\ kg\ m^2/s\]"), ("C~", "4.12345678 kg*m**2/s"), ): with self.subTest(spec): @@ -216,12 +216,15 @@ def test_default_formatting(self): def test_exponent_formatting(self): ureg = UnitRegistry() - x = ureg.Quantity(1e20, UnitsContainer(meter=1)) - self.assertEqual("{:~H}".format(x), "1×1020 m") - self.assertEqual("{:~L}".format(x), r"1\times 10^{20}\ \mathrm{m}") + x = ureg.Quantity(1e20, "meter") + 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") + x /= 1e40 - self.assertEqual("{:~H}".format(x), "1×10-20 m") - self.assertEqual("{:~L}".format(x), r"1\times 10^{-20}\ \mathrm{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") def test_ipython(self): alltext = [] @@ -240,7 +243,7 @@ def pretty(cls, data): ureg = UnitRegistry() x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), "3.5 kilogram meter^2/second") + self.assertEqual(x._repr_html_(), r"\[3.5\ kilogram\ meter^2/second\]") self.assertEqual( x._repr_latex_(), r"$3.5\ \frac{\mathrm{kilogram} \cdot " @@ -249,7 +252,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_(), "3.5 kg m^2/s") + self.assertEqual(x._repr_html_(), r"\[3.5\ kg\ m^2/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 e1bcf57e5..a02e3585b 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -44,13 +44,13 @@ def test_unit_formatting(self): r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("{:P}", "kilogram·meter²/second"), - ("{:H}", "kilogram meter^2/second"), + ("{:H}", r"\[kilogram\ meter^2/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~}", "kg m^2/s"), + ("{:H~}", r"\[kg\ m^2/s\]"), ("{:C~}", "kg*m**2/s"), ): with self.subTest(spec): @@ -65,12 +65,12 @@ def test_unit_default_formatting(self): r"\frac{\mathrm{kilogram} \cdot \mathrm{meter}^{2}}{\mathrm{second}}", ), ("P", "kilogram·meter²/second"), - ("H", "kilogram meter^2/second"), + ("H", r"\[kilogram\ meter^2/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~", "kg m^2/s"), + ("H~", r"\[kg\ m^2/s\]"), ("C~", "kg*m**2/s"), ): with self.subTest(spec): @@ -84,12 +84,12 @@ def test_unit_formatting_snake_case(self): for spec, result in ( ("L", r"\mathrm{oil\_barrel}"), ("P", "oil_barrel"), - ("H", "oil\_barrel"), + ("H", r"\[oil\_barrel\]"), ("C", "oil_barrel"), ("~", "oil_bbl"), ("L~", r"\mathrm{oil\_bbl}"), ("P~", "oil_bbl"), - ("H~", "oil\_bbl"), + ("H~", r"\[oil\_bbl\]"), ("C~", "oil_bbl"), ): with self.subTest(spec): @@ -106,7 +106,7 @@ def text(text): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), "kilogram meter^2/second") + self.assertEqual(x._repr_html_(), r"\[kilogram\ meter^2/second\]") self.assertEqual( x._repr_latex_(), r"$\frac{\mathrm{kilogram} \cdot " r"\mathrm{meter}^{2}}{\mathrm{second}}$", @@ -114,7 +114,7 @@ def text(text): x._repr_pretty_(Pretty, False) self.assertEqual("".join(alltext), "kilogram·meter²/second") ureg.default_format = "~" - self.assertEqual(x._repr_html_(), "kg m^2/s") + self.assertEqual(x._repr_html_(), r"\[kg\ m^2/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 f96e549ff..3f37aa6fc 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -96,7 +96,11 @@ def __format__(self, spec): else: units = self._units - return "%s" % (format(units, spec)) + 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): spec = spec or self.default_format From 6554479084834fda18548e24a7220c958a470aa3 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Dec 2019 18:25:22 +0000 Subject: [PATCH 200/612] # Conflicts: # pint/registry.py --- pint/context.py | 7 ++++--- pint/testsuite/test_issues.py | 13 ++++++++++++- pint/util.py | 21 ++++++++++----------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/pint/context.py b/pint/context.py index e027864b1..a9efff533 100644 --- a/pint/context.py +++ b/pint/context.py @@ -215,6 +215,7 @@ class ContextChain(ChainMap): def __init__(self): super().__init__() + self.maps.clear() # Remove default empty map self._graph = None self._contexts = [] @@ -229,11 +230,11 @@ def insert_contexts(self, *contexts): self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None - def remove_contexts(self, n): + def remove_contexts(self, n: int = None): """Remove the last n inserted contexts from the chain. """ - self._contexts = self._contexts[n:] - self.maps = self.maps[n:] + del self._contexts[:n] + del self.maps[:n] self._graph = None @property diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index b61abf595..939e7caa0 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -5,7 +5,7 @@ import unittest import pprint -from pint import DimensionalityError, UnitRegistry +from pint import Context, DimensionalityError, UnitRegistry from pint.unit import UnitsContainer from pint.util import ParserHelper @@ -695,3 +695,14 @@ def test_issue912(self): meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) pprint.pformat(meter_units | hertz_units) + + def test_issue932(self): + ureg = UnitRegistry() + q = ureg.Quantity("1 kg") + with self.assertRaises(DimensionalityError): + q.to("joule") + ureg.enable_contexts("energy", *(Context() for _ in range(20))) + q.to("joule") + ureg.disable_contexts() + with self.assertRaises(DimensionalityError): + q.to("joule") diff --git a/pint/util.py b/pint/util.py index d20778e22..1efdf28ee 100644 --- a/pint/util.py +++ b/pint/util.py @@ -187,12 +187,13 @@ def pi_theorem(quantities, registry=None): def solve_dependencies(dependencies): """Solve a dependency graph. - :param dependencies: dependency dictionary. For each key, the value is - an iterable indicating its dependencies. - :return: list of sets, each containing keys of independents tasks dependent - only of the previous tasks in the list. + :param dependencies: + dependency dictionary. For each key, the value is an iterable indicating its + dependencies. + :return: + iterator of sets, each containing keys of independents tasks dependent only of + the previous tasks in the list. """ - r = [] while dependencies: # values not in keys (items without dep) t = {i for v in dependencies.values() for i in v} - dependencies.keys() @@ -205,10 +206,9 @@ def solve_dependencies(dependencies): ", ".join(repr(x) for x in dependencies.items()) ) ) - r.append(t) # and cleaned up dependencies = {k: v - t for k, v in dependencies.items() if v} - return r + yield t def find_shortest_path(graph, start, end, path=None): @@ -721,11 +721,10 @@ def to_units_container(unit_like, registry=None): def infer_base_unit(q): """Return UnitsContainer of q with all prefixes stripped.""" d = udict() - parse = q._REGISTRY.parse_unit_name for unit_name, power in q._units.items(): - completely_parsed_unit = list(parse(unit_name))[-1] - - _, base_unit, __ = completely_parsed_unit + candidates = q._REGISTRY.parse_unit_name(unit_name) + assert len(candidates) == 1 + _, base_unit, _ = candidates[0] d[base_unit] += power # remove values that resulted in a power of 0 From 97f5d0fa5830cd955c6fcdecefe638da5f9349ab Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Dec 2019 18:30:11 +0000 Subject: [PATCH 201/612] Merge from context_define --- pint/registry.py | 111 +++++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 43 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 0cb585b54..38a1363c3 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -43,7 +43,7 @@ from fractions import Fraction from contextlib import contextmanager, closing from io import open, StringIO -from collections import defaultdict +from collections import ChainMap, defaultdict from tokenize import NUMBER, NAME from . import registry_helpers @@ -111,6 +111,19 @@ def __init__(self): self.parse_unit = {} +class ContextCacheOverlay: + """Layer on top of the base UnitRegistry cache specific to a combination of + active contexts + """ + + def __init__(self, registry_cache): + # TODO: Use ChainMap to define context-specific caches + self.dimensional_equivalents = registry_cache.dimensional_equivalents + self.root_units = registry_cache.root_units + self.dimensionality = registry_cache.dimensionality + self.parse_unit = registry_cache.parse_unit + + class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. @@ -200,7 +213,7 @@ def __init__( self._prefixes = {"": PrefixDefinition("", "", (), 1)} #: Map suffix name (string) to canonical , and unit alias to canonical unit name - self._suffixes = {"": None, "s": ""} + self._suffixes = {"": "", "s": ""} #: Map contexts to RegistryCache self._cache = RegistryCache() @@ -510,13 +523,13 @@ def _build_cache(self): for unit_name in unit_names: if "[" in unit_name: continue - parsed_names = tuple(self.parse_unit_name(unit_name)) - _prefix = None + parsed_names = self.parse_unit_name(unit_name) + prefix = None if parsed_names: - _prefix, base_name, _suffix = parsed_names[0] + prefix, base_name, _suffix = parsed_names[0] else: base_name = unit_name - prefixed = True if _prefix else False + try: uc = ParserHelper.from_word(base_name) @@ -526,34 +539,14 @@ def _build_cache(self): self._cache.root_units[uc] = bu self._cache.dimensionality[uc] = di - if not prefixed: + if not prefix: dimeq_set = self._cache.dimensional_equivalents.setdefault( di, set() ) dimeq_set.add(self._units[base_name]._name) - except Exception as e: - logger.warning("Could not resolve {0}: {1!r}".format(unit_name, e)) - - def _dedup_candidates(self, candidates): - """Given a list of unit triplets (prefix, name, suffix), - remove those with different names but equal value. - - e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') - """ - candidates = tuple(candidates) - if len(candidates) < 2: - return candidates - - unique = [candidates[0]] - for c in candidates[2:]: - for u in unique: - if c == u: - break - else: - unique.append(c) - - return tuple(unique) + except Exception as exc: + logger.warning(f"Could not resolve {unit_name}: {exc!r}") def get_name(self, name_or_alias, case_sensitive=True): """Return the canonical name of a unit. @@ -567,9 +560,7 @@ def get_name(self, name_or_alias, case_sensitive=True): except KeyError: pass - candidates = self._dedup_candidates( - self.parse_unit_name(name_or_alias, case_sensitive) - ) + candidates = self.parse_unit_name(name_or_alias, case_sensitive) if not candidates: raise UndefinedUnitError(name_or_alias) elif len(candidates) == 1: @@ -595,7 +586,7 @@ def get_name(self, name_or_alias, case_sensitive=True): def get_symbol(self, name_or_alias): """Return the preferred alias for a unit """ - candidates = self._dedup_candidates(self.parse_unit_name(name_or_alias)) + candidates = self.parse_unit_name(name_or_alias) if not candidates: raise UndefinedUnitError(name_or_alias) elif len(candidates) == 1: @@ -856,8 +847,20 @@ def _convert(self, value, src, dst, inplace=False, check_dimensionality=True): def parse_unit_name(self, unit_name, case_sensitive=True): """Parse a unit to identify prefix, unit name and suffix by walking the list of prefix and suffix. + In case of equivalent combinations (e.g. ('kilo', 'gram', '') and + ('', 'kilogram', ''), prefer those with prefix. - :rtype: (str, str, str) + :returns: + all non-equivalent combinations of (prefix, unit name, suffix) + :rtype: + ((str, str, str), ...) + """ + return self._dedup_candidates( + self._parse_unit_name(unit_name, case_sensitive=case_sensitive) + ) + + def _parse_unit_name(self, unit_name, case_sensitive=True): + """Helper of parse_unit_name """ stw = unit_name.startswith edw = unit_name.endswith @@ -883,6 +886,24 @@ def parse_unit_name(self, unit_name, case_sensitive=True): self._suffixes[suffix], ) + @staticmethod + def _dedup_candidates(candidates): + """Helper of parse_unit_name. + + Given an iterable of unit triplets (prefix, name, suffix), remove those with + different names but equal value, preferring those with a prefix. + + e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') + """ + candidates = dict.fromkeys(candidates) # ordered set + for cp, cu, cs in list(candidates): + assert isinstance(cp, str) + assert isinstance(cu, str) + assert cs == "", "not empty suffix is not supported" + if cp: + candidates.pop(("", cp + cu, ""), None) + return tuple(candidates) + def parse_units(self, input_string, as_delta=None): """Parse a units expression and returns a UnitContainer with the canonical names. @@ -1048,10 +1069,11 @@ def _is_multiplicative(self, u): # If the unit is not in the registry might be because it is not # registered with its prefixed version. # TODO: Might be better to register them. - l = self._dedup_candidates(self.parse_unit_name(u)) + names = self.parse_unit_name(u) + assert len(names) == 1 + _, base_name, _ = names[0] try: - u = l[0][1] - return self._units[u].is_multiplicative + return self._units[base_name].is_multiplicative except KeyError: raise UndefinedUnitError(u) @@ -1218,12 +1240,16 @@ def remove_context(self, name_or_alias): return context def _build_cache(self): + super()._build_cache() + self._caches[()] = self._cache + + def _switch_context_cache(self): key = self._active_ctx.hashable() try: self._cache = self._caches[key] except KeyError: - super()._build_cache() - self._caches[key] = self._cache + base_cache = self._caches[()] + self._caches[key] = ContextCacheOverlay(base_cache) def enable_contexts(self, *names_or_contexts, **kwargs): """Enable contexts provided by name or by object. @@ -1260,15 +1286,13 @@ def enable_contexts(self, *names_or_contexts, **kwargs): # Finally we add them to the active context. self._active_ctx.insert_contexts(*ctxs) - self._build_cache() + self._switch_context_cache() def disable_contexts(self, n=None): """Disable the last n enabled contexts. """ - if n is None: - n = len(self._contexts) self._active_ctx.remove_contexts(n) - self._build_cache() + self._switch_context_cache() @contextmanager def context(self, *names, **kwargs): @@ -1396,6 +1420,7 @@ def _get_compatible_units(self, input_units, group_or_system): ret = super()._get_compatible_units(input_units, group_or_system) if self._active_ctx: + ret = ret.copy() nodes = find_connected_nodes(self._active_ctx.graph, src_dim) if nodes: for node in nodes: From 0d15f54d9ff1f469b6854efea6682521597a0747 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 16 Dec 2019 18:32:47 +0000 Subject: [PATCH 202/612] typo --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0f2b7edc9..b2de1ede9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ exclude= [isort] default_section=THIRDPARTY -known_first_party=xarray +known_first_party=pint multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 From c7736704fb00bdeb0247c2048c76024516dfd40b Mon Sep 17 00:00:00 2001 From: acox37 Date: Mon, 16 Dec 2019 17:37:21 -0500 Subject: [PATCH 203/612] add slinch to Avoirdupois group Fixes #511 --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index c860a301f..a7bbca0c8 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -553,6 +553,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N ton = 2e3 * pound = _ = short_ton long_ton = 2240 * pound slug = g_0 * pound * second ** 2 / foot + slinch = g_0 * pound * second ** 2 / inch = blob = slugette force_ounce = g_0 * ounce = ozf = ounce_force force_pound = g_0 * pound = lbf = pound_force From 797cf2fa652efaa33c5d46cbc9f7e669b9bb8be0 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 10:36:37 +0000 Subject: [PATCH 204/612] Lint --- pint/quantity.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 090c79946..23fed4b6d 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -40,11 +40,11 @@ UnitStrippedWarning, ) from .formatting import ( + _pretty_fmt_exponent, ndarray_to_latex, ndarray_to_latex_parts, remove_custom_flags, siunitx_format_unit, - _pretty_fmt_exponent, ) from .numpy_func import ( HANDLED_UFUNCS, diff --git a/setup.py b/setup.py index 85de856c8..03ac0e927 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def read(filename): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - python_requires='>=3.6', + python_requires=">=3.6", install_requires=["setuptools"], setup_requires=["setuptools", "setuptools_scm"], extras_require={ From 7343b916c4ee52349d9369e76bd67523d034c19e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 10:40:40 +0000 Subject: [PATCH 205/612] Black --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85de856c8..03ac0e927 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def read(filename): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - python_requires='>=3.6', + python_requires=">=3.6", install_requires=["setuptools"], setup_requires=["setuptools", "setuptools_scm"], extras_require={ From 4760144f2c41cf257b67418ffc97891b9e1ac5ef Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:11:55 +0000 Subject: [PATCH 206/612] flake8/black/isort in CI --- .travis.yml | 5 ++++- CHANGES | 8 +++++++- pull_request_template.md | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 pull_request_template.md diff --git a/.travis.yml b/.travis.yml index ea25ab36c..b47d947d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ env: - PKGS="python=3.6 numpy uncertainties" - PKGS="python=3.7 numpy uncertainties" - PKGS="python=3.8 numpy uncertainties" + - PKGS="python=3.7 flake8 black isort" # TODO: pandas tests # - PKGS="python=3.7 numpy pandas uncertainties pandas" @@ -53,6 +54,7 @@ install: - conda create -n travis $PKGS coveralls - source activate travis - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi + - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list @@ -63,7 +65,8 @@ script: # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi # test notebooks too if pandas available # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - - if [[ $PANDAS == '0' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi + - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi + - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - coverage combine - coverage report -m diff --git a/CHANGES b/CHANGES index 5b3052009..26ebe641c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,11 +4,17 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Added slinch to Avoirdupois group + (Issue #936, Thanks awcox21) +- Use black, flake8, and isort on the project + (Issues #929, #931, and TODO, Thanks Guido Imperiale) - Auto-increase package version at every commit when pint is installed from the git tip, e.g. pip install git+https://github.com/hgrecco/pint.git. - (Issue #930, Thanks Guido Imperiale) + (Issues #930 and #934, Thanks Guido Imperiale and KOLANICH) - Fix HTML (Jupyter Notebook) and LateX representation of some units (Issue #927, Thanks Guido Imperiale) +- Fixed the definition of RKM unit as gf / tex + (Issue #921, Thanks Giuseppe Corbelli) - **BREAKING CHANGE**: Implement NEP-18 for Pint Quantities. Most NumPy functions that previously stripped units when applied to diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 000000000..9122a2534 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,4 @@ +- [ ] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors +- [ ] The change is fully covered by automated unit tests +- [ ] Documented in docs/ as appropriate +- [ ] Documented in CHANGES From 544c525584296500eec3fc162cf86150d50f1c45 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:14:29 +0000 Subject: [PATCH 207/612] contributing guide --- docs/contributing.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index c3cb6ca15..d64970d5e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -22,6 +22,9 @@ the changes using a pull request against the **master** branch. - If you are fixing a bug, add a test to test_issues.py Also add "Close # as described in the `github docs`_. - If you are submitting new code, add tests and documentation. +- If adding or modifying a feature, make sure it's documented in docs/. +- Log the change in the CHANGES file. +- Execute ``black -t py36 . && isort -rc . && flake8`` and resolve any issues Pint uses `bors-ng` as a merge bot and therefore every PR is tested before merging. From 13cb282b6674afdf16ab9b38f561e3fb2a3b4919 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:24:31 +0000 Subject: [PATCH 208/612] Polish --- .travis.yml | 2 +- CHANGES | 2 +- docs/contributing.rst | 7 +++---- pull_request_template.md | 2 +- setup.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index b47d947d1..7bb48099a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ env: # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py # for min/max Python versions supported by uncertainties + - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -28,7 +29,6 @@ env: - PKGS="python=3.6 numpy uncertainties" - PKGS="python=3.7 numpy uncertainties" - PKGS="python=3.8 numpy uncertainties" - - PKGS="python=3.7 flake8 black isort" # TODO: pandas tests # - PKGS="python=3.7 numpy pandas uncertainties pandas" diff --git a/CHANGES b/CHANGES index 26ebe641c..651be4273 100644 --- a/CHANGES +++ b/CHANGES @@ -7,7 +7,7 @@ Pint Changelog - Added slinch to Avoirdupois group (Issue #936, Thanks awcox21) - Use black, flake8, and isort on the project - (Issues #929, #931, and TODO, Thanks Guido Imperiale) + (Issues #929, #931, and #937, Thanks Guido Imperiale) - Auto-increase package version at every commit when pint is installed from the git tip, e.g. pip install git+https://github.com/hgrecco/pint.git. (Issues #930 and #934, Thanks Guido Imperiale and KOLANICH) diff --git a/docs/contributing.rst b/docs/contributing.rst index d64970d5e..cb3634ce7 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -19,12 +19,11 @@ Contribute code To contribute fixes, code or documentation to Pint, fork Pint in github_ and submit the changes using a pull request against the **master** branch. -- If you are fixing a bug, add a test to test_issues.py - Also add "Close # as described in the `github docs`_. +- If you are fixing a bug, add a test to test_issues.py; + also add "Closes # as described in the `github docs`_. - If you are submitting new code, add tests and documentation. -- If adding or modifying a feature, make sure it's documented in docs/. - Log the change in the CHANGES file. -- Execute ``black -t py36 . && isort -rc . && flake8`` and resolve any issues +- Execute ``black -t py36 . && isort -rc . && flake8`` and resolve any issues. Pint uses `bors-ng` as a merge bot and therefore every PR is tested before merging. diff --git a/pull_request_template.md b/pull_request_template.md index 9122a2534..b90a49561 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,4 +1,4 @@ - [ ] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors - [ ] The change is fully covered by automated unit tests - [ ] Documented in docs/ as appropriate -- [ ] Documented in CHANGES +- [ ] Added an entry to the CHANGES file diff --git a/setup.py b/setup.py index 85de856c8..03ac0e927 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def read(filename): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - python_requires='>=3.6', + python_requires=">=3.6", install_requires=["setuptools"], setup_requires=["setuptools", "setuptools_scm"], extras_require={ From 4031c7ef08f128f1792de13d9e263d44f362f358 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:36:30 +0000 Subject: [PATCH 209/612] Fix travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bb48099a..4bcfb5c7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,8 +67,8 @@ script: # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - coverage combine - - coverage report -m + - if [[ $LINT == 0 ]]; coverage combine; fi + - if [[ $LINT == 0 ]]; coverage report -m; fi after_success: - coveralls --verbose From 4ca48a42b8ce916d7ccba6263f7e0fc167fde7ed Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:46:52 +0000 Subject: [PATCH 210/612] Fix travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4bcfb5c7a..eaacb73f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,8 +67,8 @@ script: # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - if [[ $LINT == 0 ]]; coverage combine; fi - - if [[ $LINT == 0 ]]; coverage report -m; fi + - if [[ $LINT == 0 ]]; then coverage combine; fi + - if [[ $LINT == 0 ]]; then coverage report -m; fi after_success: - coveralls --verbose From 3426ceeba7fa729a1f570e9d42187e7b37ed382b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 11:55:08 +0000 Subject: [PATCH 211/612] Trivial --- docs/contributing.rst | 6 ++++-- pull_request_template.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index cb3634ce7..a7c7e41c0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -19,9 +19,11 @@ Contribute code To contribute fixes, code or documentation to Pint, fork Pint in github_ and submit the changes using a pull request against the **master** branch. -- If you are fixing a bug, add a test to test_issues.py; - also add "Closes # as described in the `github docs`_. +- If you are fixing a bug, add a test to test_issues.py, or amend/enrich the general + test suite to cover the use case. - If you are submitting new code, add tests and documentation. +- Write "Closes #" in the PR description or a comment, as described in the + `github docs`_. - Log the change in the CHANGES file. - Execute ``black -t py36 . && isort -rc . && flake8`` and resolve any issues. diff --git a/pull_request_template.md b/pull_request_template.md index b90a49561..d3fdd1693 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,3 +1,4 @@ +- [ ] Closes # (insert issue number) - [ ] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors - [ ] The change is fully covered by automated unit tests - [ ] Documented in docs/ as appropriate From 1ea0031caf26915e0c223edcdb9bb2a47c886423 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 16:08:20 +0000 Subject: [PATCH 212/612] Cleanup --- pint/registry.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 76fc63d03..82253e53d 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -717,13 +717,11 @@ def _get_root_units(self, input_units, check_nonmult=True): if not input_units: return 1.0, UnitsContainer() - # The cache is only done for check_nonmult=True cache = self._cache.root_units - if check_nonmult: - try: - return cache[input_units] - except KeyError: - pass + try: + return cache[input_units] + except KeyError: + pass accumulators = [1.0, defaultdict(float)] self._get_root_units_recurse(input_units, 1.0, accumulators) @@ -733,13 +731,10 @@ def _get_root_units(self, input_units, check_nonmult=True): # Check if any of the final units is non multiplicative and return None instead. if check_nonmult: - for unit in units: - if not self._units[unit].converter.is_multiplicative: - return None, units - - if check_nonmult: - cache[input_units] = factor, units + if any(not self._units[unit].converter.is_multiplicative for unit in units): + factor = None + cache[input_units] = factor, units return factor, units def get_base_units(self, input_units, check_nonmult=True, system=None): @@ -832,7 +827,7 @@ def _convert(self, value, src, dst, inplace=False, check_dimensionality=True): # Here src and dst have only multiplicative units left. Thus we can # convert with a factor. - factor, units = self._get_root_units(src / dst) + factor, _ = self._get_root_units(src / dst) # factor is type float and if our magnitude is type Decimal then # must first convert to Decimal before we can '*' the values From 42aca09739a301603b51fb59880f6b6ce1f94668 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 17 Dec 2019 16:22:33 +0000 Subject: [PATCH 213/612] Cleanup --- pint/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 82253e53d..c5ce1a4dd 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -754,7 +754,7 @@ def get_base_units(self, input_units, check_nonmult=True, system=None): return self.get_root_units(input_units, check_nonmult) def _get_root_units_recurse(self, ref, exp, accumulators): - for key in sorted(ref): + for key in ref: exp2 = exp * ref[key] key = self.get_name(key) reg = self._units[key] From 8ee10f4c5591ca65540a782476e9555391a11ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Sun, 8 Dec 2019 17:04:41 +0100 Subject: [PATCH 214/612] Fix autoreducedimension conversion * use iterable in function * Raise an error when trying to remove non existing key * Add test in test_issues.py * Update registry.py to remove unit when unit exists Closes #902 --- pint/quantity.py | 2 +- pint/registry.py | 6 +++--- pint/testsuite/test_issues.py | 7 +++++++ pint/util.py | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 627e3f216..b83a05395 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -559,7 +559,7 @@ def ito_reduced_units(self): if unit1 != unit2: power = self._REGISTRY._get_dimensionality_ratio(unit1, unit2) if power: - newunits = newunits.add(unit2, exp / power).remove(unit1) + newunits = newunits.add(unit2, exp / power).remove([unit1]) break return self.ito(newunits) diff --git a/pint/registry.py b/pint/registry.py index 444b3631a..e0c1d3669 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1132,11 +1132,11 @@ def _convert(self, value, src, dst, inplace=False): # clean src from offset units by converting to reference if src_offset_unit: value = self._units[src_offset_unit].converter.to_reference(value, inplace) - - src = src.remove([src_offset_unit]) + src = src.remove([src_offset_unit]) # clean dst units from offset units - dst = dst.remove([dst_offset_unit]) + if dst_offset_unit: + dst = dst.remove([dst_offset_unit]) # Convert non multiplicative units to the dst. value = super()._convert(value, src, dst, inplace, False) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 145d0cc9b..ddbe4b3cc 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -670,6 +670,13 @@ def test_issue876(self): assert a != b assert a != c + def test_issue902(self): + ureg = UnitRegistry(auto_reduce_dimensions=True) + velocity = 1 * ureg.m / ureg.s + cross_section = 1 * ureg.um ** 2 + result = cross_section / velocity + assert result == 1e-12 * ureg.m * ureg.s + def test_issue912(self): """pprint.pformat() invokes sorted() on large sets and frozensets and graciously handles TypeError, but not generic Exceptions. This test will fail if diff --git a/pint/util.py b/pint/util.py index 3b6c4672b..d8d8c090a 100644 --- a/pint/util.py +++ b/pint/util.py @@ -286,7 +286,7 @@ def add(self, key, value): if newval: new._d[key] = newval else: - new._d.pop(key, None) + new._d.pop(key) new._hash = None return new @@ -296,7 +296,7 @@ def remove(self, keys): """ new = self.copy() for k in keys: - new._d.pop(k, None) + new._d.pop(k) new._hash = None return new From 085b5ea1633d1ab0703ac2357e14c6cb3b29f970 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 18 Dec 2019 10:12:42 +0000 Subject: [PATCH 215/612] Context-specific redefinitions (untested) --- pint/context.py | 42 +++++++++++++++++-- pint/registry.py | 103 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/pint/context.py b/pint/context.py index 6d60a11bc..1331e6817 100644 --- a/pint/context.py +++ b/pint/context.py @@ -12,6 +12,7 @@ import weakref from collections import ChainMap, defaultdict +from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError from .util import ParserHelper, SourceIterator, to_units_container @@ -58,6 +59,13 @@ class Context: >>> c.transform(timedim, spacedim, 2) 2 + Additionally, a context may host redefinitions: + + >>> c.redefine("pound = 0.5 kg") + + A redefinition must be performed among units that already exist in the registry. It + cannot change the dimensionality of a unit. The symbol and aliases are automatically + inherited from the registry. """ def __init__(self, name=None, aliases=(), defaults=None): @@ -71,6 +79,12 @@ def __init__(self, name=None, aliases=(), defaults=None): #: Maps defaults variable names to values self.defaults = defaults or {} + # Store Definition objects that are context-specific + self.redefinitions = [] + + # Flag set to True by the Registry the first time the context is enabled + self.checked = False + #: Maps (src, dst) -> self #: Used as a convenience dictionary to be composed by ContextChain self.relation_to_context = weakref.WeakValueDictionary() @@ -135,6 +149,10 @@ def to_num(val): names = set() for lineno, line in lines: + if "=" in line: + ctx.redefine(line) + continue + try: rel, eq = line.split(":") names.update(_varname_re.findall(eq)) @@ -195,6 +213,21 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) + def redefine(self, definition): + for line in definition.splitlines(): + d = Definition.from_string(line) + if not isinstance(d, UnitDefinition): + raise DefinitionSyntaxError( + "Expected = ; got %s" % line.strip() + ) + if d.symbol or d.aliases: + raise DefinitionSyntaxError( + "Can't change a unit's symbol or aliases within a context" + ) + if d.base: + raise DefinitionSyntaxError("Cannot define base units within a context") + self.redefinitions.append(d) + def hashable(self): """Generate a unique hashable and comparable representation of self, which can be used as a key in a dict. This class cannot define ``__hash__`` because it is @@ -205,6 +238,7 @@ def hashable(self): tuple(self.aliases), frozenset((k, id(v)) for k, v in self.funcs.items()), frozenset(self.defaults.items()), + tuple(self.redefinitions), ) @@ -217,7 +251,7 @@ def __init__(self): super().__init__() self.maps.clear() # Remove default empty map self._graph = None - self._contexts = [] + self.contexts = [] def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. @@ -226,14 +260,14 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ - self._contexts = list(reversed(contexts)) + self._contexts + self.contexts = list(reversed(contexts)) + self.contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None def remove_contexts(self, n: int = None): """Remove the last n inserted contexts from the chain. """ - del self._contexts[:n] + del self.contexts[:n] del self.maps[:n] self._graph = None @@ -266,4 +300,4 @@ def hashable(self): be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. """ - return tuple(ctx.hashable() for ctx in self._contexts) + return tuple(ctx.hashable() for ctx in self.contexts) diff --git a/pint/registry.py b/pint/registry.py index c5ce1a4dd..2c4fd5e8c 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -37,7 +37,7 @@ import math import os import re -from collections import defaultdict +from collections import ChainMap, defaultdict from contextlib import closing, contextmanager from decimal import Decimal from fractions import Fraction @@ -113,10 +113,9 @@ class ContextCacheOverlay: active contexts """ - def __init__(self, registry_cache): - # TODO: Use ChainMap to define context-specific caches + def __init__(self, registry_cache: RegistryCache, units_overlay: bool = False): self.dimensional_equivalents = registry_cache.dimensional_equivalents - self.root_units = registry_cache.root_units + self.root_units = {} if units_overlay else registry_cache.root_units self.dimensionality = registry_cache.dimensionality self.parse_unit = registry_cache.parse_unit @@ -1182,15 +1181,20 @@ class ContextRegistry(BaseRegistry): """ def __init__(self, **kwargs): - #: Map context name (string) or abbreviation to context. + # Map context name (string) or abbreviation to context. self._contexts = {} - #: Stores active contexts. + # Stores active contexts. self._active_ctx = ContextChain() - #: Map context chain to cache + # Map context chain to cache self._caches = {} + # Map context chain to units override + self._context_units = {} super().__init__(**kwargs) + # Allow contexts to add override layers to the units + self._units = ChainMap(self._units) + def _register_parsers(self): super()._register_parsers() self._register_parser("@context", self._parse_context) @@ -1203,7 +1207,7 @@ def _parse_context(self, ifile): except KeyError as e: raise DefinitionSyntaxError(f"unknown dimension {e} in context") - def add_context(self, context): + def add_context(self, context: Context) -> None: """Add a context object to the registry. The context will be accessible by its name and aliases. @@ -1225,7 +1229,7 @@ def add_context(self, context): ) self._contexts[alias] = context - def remove_context(self, name_or_alias): + def remove_context(self, name_or_alias: str) -> Context: """Remove a context from the registry and return it. Notice that this methods will not disable the context. Use `disable_contexts`. @@ -1238,19 +1242,82 @@ def remove_context(self, name_or_alias): return context - def _build_cache(self): + def _build_cache(self) -> None: super()._build_cache() self._caches[()] = self._cache - def _switch_context_cache(self): + def _switch_context_cache_and_units(self) -> None: key = self._active_ctx.hashable() + units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) + try: self._cache = self._caches[key] except KeyError: base_cache = self._caches[()] - self._caches[key] = ContextCacheOverlay(base_cache) + self._caches[key] = ContextCacheOverlay( + base_cache, units_overlay=units_overlay + ) + + # Disable previous units overlay + del self._units.maps[:-1] + if not self._active_ctx.contexts or not units_overlay: + # No active contexts, or contexts don't redefine units + return + + try: + self._units.maps.insert(0, self._context_units[key]) + return + except KeyError: + pass + + # First time activating a context chain with units redefinitions; inject them + # into the context + self._context_units[key] = units_overlay = {} + self._units.maps.insert(0, units_overlay) + for ctx in self._active_ctx.contexts: + for definition in ctx.redefinitions: + self._redefine(definition) + + def _redefine(self, definition: UnitDefinition) -> None: + """Redefine a unit from a context + """ + # Find original definition in the UnitRegistry + candidates = self.parse_unit_name(definition.name) + assert candidates + candidates_no_prefix = [c for c in candidates if not c[0]] + if len(candidates_no_prefix) > 1: + raise ValueError( + f"Ambiguous redefinition {definition.name}; " + f"candidates: {candidates_no_prefix}" + ) + if not candidates_no_prefix: + raise ValueError( + f"Can't redefine a unit with a prefix {definition.name}" + ) + _, name, _ = candidates_no_prefix[0] + try: + basedef = self._units[name] + except KeyError: + raise UndefinedUnitError(name) + + # Rebuild definition as a variant of the base + if basedef.is_base: + raise ValueError("Can't redefine a base unit to a derived one") + # Do not modify in place the original definition, as (1) the context may + # be shared by other registries, and (2) it would alter the cache key + definition = UnitDefinition( + name=basedef.name, + symbol=basedef.symbol, + aliases=basedef.aliases, + is_base=False, + reference=definition.reference, + converter=definition.converter, + ) + + # Write into the context-specific self._units.maps[0] and self._cache.root_units + self.define(definition) - def enable_contexts(self, *names_or_contexts, **kwargs): + def enable_contexts(self, *names_or_contexts, **kwargs) -> None: """Enable contexts provided by name or by object. :param names_or_contexts: sequence of the contexts or contexts names/alias @@ -1270,7 +1337,7 @@ def enable_contexts(self, *names_or_contexts, **kwargs): # Check if the contexts have been checked first, if not we make sure # that dimensions are expressed in terms of base dimensions. for ctx in ctxs: - if getattr(ctx, "_checked", False): + if ctx.checked: continue for (src, dst), func in ctx.funcs.items(): src_ = self._get_dimensionality(src) @@ -1278,20 +1345,20 @@ def enable_contexts(self, *names_or_contexts, **kwargs): if src != src_ or dst != dst_: ctx.remove_transformation(src, dst) ctx.add_transformation(src_, dst_, func) - ctx._checked = True + ctx.checked = True # and create a new one with the new defaults. ctxs = tuple(Context.from_context(ctx, **kwargs) for ctx in ctxs) # Finally we add them to the active context. self._active_ctx.insert_contexts(*ctxs) - self._switch_context_cache() + self._switch_context_cache_and_units() - def disable_contexts(self, n=None): + def disable_contexts(self, n: int = None) -> None: """Disable the last n enabled contexts. """ self._active_ctx.remove_contexts(n) - self._switch_context_cache() + self._switch_context_cache_and_units() @contextmanager def context(self, *names, **kwargs): From 9ca858f2880c520f9a4120a5260563d87d384571 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 18 Dec 2019 13:52:48 +0000 Subject: [PATCH 216/612] Polish --- pint/context.py | 4 ++-- pint/registry.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pint/context.py b/pint/context.py index 1331e6817..3b5521d5c 100644 --- a/pint/context.py +++ b/pint/context.py @@ -213,7 +213,7 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) - def redefine(self, definition): + def redefine(self, definition: str): for line in definition.splitlines(): d = Definition.from_string(line) if not isinstance(d, UnitDefinition): @@ -249,9 +249,9 @@ class ContextChain(ChainMap): def __init__(self): super().__init__() + self.contexts = [] self.maps.clear() # Remove default empty map self._graph = None - self.contexts = [] def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. diff --git a/pint/registry.py b/pint/registry.py index 2c4fd5e8c..9b6ffdc7c 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -527,11 +527,10 @@ def _build_cache(self): if "[" in unit_name: continue parsed_names = self.parse_unit_name(unit_name) - prefix = None if parsed_names: - prefix, base_name, _suffix = parsed_names[0] + prefix, base_name, _ = parsed_names[0] else: - base_name = unit_name + prefix, base_name = '', unit_name try: uc = ParserHelper.from_word(base_name) @@ -897,7 +896,8 @@ def _dedup_candidates(candidates): for cp, cu, cs in list(candidates): assert isinstance(cp, str) assert isinstance(cu, str) - assert cs == "", "not empty suffix is not supported" + if cs != "": + raise NotImplementedError("non-empty suffix") if cp: candidates.pop(("", cp + cu, ""), None) return tuple(candidates) @@ -1486,7 +1486,7 @@ def _get_compatible_units(self, input_units, group_or_system): ret = super()._get_compatible_units(input_units, group_or_system) if self._active_ctx: - ret = ret.copy() + ret = ret.copy() # Do not alter self._cache nodes = find_connected_nodes(self._active_ctx.graph, src_dim) if nodes: for node in nodes: From 63ee7951abd03721a63bf331f8191fd851e5c3a9 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 18 Dec 2019 14:06:11 +0000 Subject: [PATCH 217/612] Bugfix --- pint/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/context.py b/pint/context.py index 3b5521d5c..ec07a18bf 100644 --- a/pint/context.py +++ b/pint/context.py @@ -220,11 +220,11 @@ def redefine(self, definition: str): raise DefinitionSyntaxError( "Expected = ; got %s" % line.strip() ) - if d.symbol or d.aliases: + if d.symbol != d.name or d.aliases: raise DefinitionSyntaxError( "Can't change a unit's symbol or aliases within a context" ) - if d.base: + if d.is_base: raise DefinitionSyntaxError("Cannot define base units within a context") self.redefinitions.append(d) From a9f773bb363154cd9093453d23395fc8ea580e96 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 18 Dec 2019 14:34:35 +0000 Subject: [PATCH 218/612] Bugfix --- pint/context.py | 1 + pint/registry.py | 37 ++++++++++++++++++------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pint/context.py b/pint/context.py index ec07a18bf..5d5ed1f81 100644 --- a/pint/context.py +++ b/pint/context.py @@ -101,6 +101,7 @@ def from_context(cls, context, **defaults): newdef = dict(context.defaults, **defaults) c = cls(context.name, context.aliases, newdef) c.funcs = context.funcs + c.redefinitions = context.redefinitions for edge in context.funcs: c.relation_to_context[edge] = c return c diff --git a/pint/registry.py b/pint/registry.py index 9b6ffdc7c..6c98a99af 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -113,9 +113,9 @@ class ContextCacheOverlay: active contexts """ - def __init__(self, registry_cache: RegistryCache, units_overlay: bool = False): + def __init__(self, registry_cache: RegistryCache): self.dimensional_equivalents = registry_cache.dimensional_equivalents - self.root_units = {} if units_overlay else registry_cache.root_units + self.root_units = {} self.dimensionality = registry_cache.dimensionality self.parse_unit = registry_cache.parse_unit @@ -1247,31 +1247,30 @@ def _build_cache(self) -> None: self._caches[()] = self._cache def _switch_context_cache_and_units(self) -> None: - key = self._active_ctx.hashable() - units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) - - try: - self._cache = self._caches[key] - except KeyError: - base_cache = self._caches[()] - self._caches[key] = ContextCacheOverlay( - base_cache, units_overlay=units_overlay - ) - - # Disable previous units overlay + """If any of the active contexts redefine units, create variant self._cache + and self._units specific to the combination of active contexts. + The next time this method is invoked with the same combination of contexts, + reuse the same variant self._cache and self._units as in the previous time. + """ del self._units.maps[:-1] - if not self._active_ctx.contexts or not units_overlay: - # No active contexts, or contexts don't redefine units + units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) + if not units_overlay: + # Use the default _cache and _units + self._cache = self._caches[()] return + key = self._active_ctx.hashable() try: + self._cache = self._caches[key] self._units.maps.insert(0, self._context_units[key]) - return except KeyError: pass - # First time activating a context chain with units redefinitions; inject them - # into the context + # First time using this specific combination of contexts and it contains + # unit redefinitions + base_cache = self._caches[()] + self._caches[key] = self._cache = ContextCacheOverlay(base_cache) + self._context_units[key] = units_overlay = {} self._units.maps.insert(0, units_overlay) for ctx in self._active_ctx.contexts: From 78d4547c7d119d662a5ee35e5972e33b701f74ac Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 18 Dec 2019 15:00:33 +0000 Subject: [PATCH 219/612] Polish --- pint/context.py | 7 ++++++- pint/registry.py | 16 +++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pint/context.py b/pint/context.py index 5d5ed1f81..0ffdefb1e 100644 --- a/pint/context.py +++ b/pint/context.py @@ -214,7 +214,12 @@ def transform(self, src, dst, registry, value): _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) - def redefine(self, definition: str): + def redefine(self, definition: str) -> None: + """Override the definition of a unit in the registry. + + :param definition: + `` = ``, e.g. ``pound = 0.5 kg`` + """ for line in definition.splitlines(): d = Definition.from_string(line) if not isinstance(d, UnitDefinition): diff --git a/pint/registry.py b/pint/registry.py index 6c98a99af..8f6f957e1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -109,8 +109,8 @@ def __init__(self): class ContextCacheOverlay: - """Layer on top of the base UnitRegistry cache specific to a combination of - active contexts + """Layer on top of the base UnitRegistry cache, specific to a combination of + active contexts which contain unit redefinitions. """ def __init__(self, registry_cache: RegistryCache): @@ -1273,9 +1273,15 @@ def _switch_context_cache_and_units(self) -> None: self._context_units[key] = units_overlay = {} self._units.maps.insert(0, units_overlay) - for ctx in self._active_ctx.contexts: - for definition in ctx.redefinitions: - self._redefine(definition) + + on_redefinition_backup = self._on_redefinition + self._on_redefinition = "ignore" + try: + for ctx in self._active_ctx.contexts: + for definition in ctx.redefinitions: + self._redefine(definition) + finally: + self._on_redefinition = on_redefinition_backup def _redefine(self, definition: UnitDefinition) -> None: """Redefine a unit from a context From fe53ca90195938f9e3c9ecfee725a2ecf29dbaa7 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 18 Dec 2019 20:04:24 -0600 Subject: [PATCH 220/612] convert_to_consistent_units should only take Unit or None --- pint/numpy_func.py | 11 +++++++++-- pint/testsuite/test_numpy.py | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 75a4de6a6..0ac55e9de 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -51,7 +51,8 @@ def _get_first_input_units(args, kwargs={}): def convert_arg(arg, pre_calc_units): """Convert quantities and sequences of quantities to pre_calc_units and strip units. - Helper function for convert_to_consistent_units. + Helper function for convert_to_consistent_units. pre_calc_units must be given as a pint + Unit or None. """ if pre_calc_units is not None: if _is_quantity(arg): @@ -199,10 +200,16 @@ def implementation(*args, **kwargs): *args, pre_calc_units=first_input_units, **kwargs ) else: + if isinstance(input_units, str): + # Conversion requires Unit, not str + pre_calc_units = first_input_units._REGISTRY.parse_units(input_units) + else: + pre_calc_units = input_units + # Match all input args/kwargs to input_units, or if input_units is None, # simply strip units stripped_args, stripped_kwargs = convert_to_consistent_units( - *args, pre_calc_units=input_units, **kwargs + *args, pre_calc_units=pre_calc_units, **kwargs ) # Determine result through base numpy function on stripped arguments diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 8fd39d842..399886d5f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -290,6 +290,9 @@ def test_cumprod_numpy_func(self): self.assertRaises(DimensionalityError, np.cumproduct, self.q) self.assertQuantityEqual(np.cumprod(self.q / self.ureg.m), [1, 2, 6, 24]) self.assertQuantityEqual(np.cumproduct(self.q / self.ureg.m), [1, 2, 6, 24]) + self.assertQuantityEqual( + np.cumprod(self.q / self.ureg.m, axis=1), [[1, 2], [3, 12]] + ) @helpers.requires_array_function_protocol() def test_nancumprod_numpy_func(self): From b5978d7c9ba43e5d781130e31c80bdc146062ab4 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 18 Dec 2019 20:14:25 -0600 Subject: [PATCH 221/612] Cumululative products should only have unit handling on first argument, not all --- pint/numpy_func.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 0ac55e9de..cac03d20c 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -682,17 +682,31 @@ def implementation(*arrays): for func_str in ["atleast_1d", "atleast_2d", "atleast_3d"]: implement_atleast_nd(func_str) -# Handle single-argument consistent unit functions -for func_str in ["block", "hstack", "vstack", "dstack", "column_stack"]: - implement_func( - "function", func_str, input_units="all_consistent", output_unit="match_input" - ) # Handle cumulative products (which must be dimensionless for consistent units across # output array) +def implement_single_dimensionless_argument_func(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(a, *args, **kwargs): + (a_stripped,), _ = convert_to_consistent_units( + a, pre_calc_units=a._REGISTRY.parse_units("dimensionless") + ) + return a._REGISTRY.Quantity(func(a_stripped, *args, **kwargs)) + + for func_str in ["cumprod", "cumproduct", "nancumprod"]: + implement_single_dimensionless_argument_func(func_str) + +# Handle single-argument consistent unit functions +for func_str in ["block", "hstack", "vstack", "dstack", "column_stack"]: implement_func( - "function", func_str, input_units="dimensionless", output_unit="match_input" + "function", func_str, input_units="all_consistent", output_unit="match_input" ) # Handle functions that ignore units on input and output From a08cd4a3bf43b9da5d2c5c62c34b9b638bee778a Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 18 Dec 2019 20:44:30 -0600 Subject: [PATCH 222/612] Fix infinite recursion with array raised to scalar quantity power --- pint/numpy_func.py | 5 ++++- pint/quantity.py | 9 ++++++--- pint/testsuite/test_numpy.py | 8 ++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 75a4de6a6..311e75981 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -396,7 +396,10 @@ def _frexp(x, *args, **kwargs): @implements("power", "ufunc") def _power(x1, x2): - return x1 ** x2 + if _is_quantity(x1): + return x1 ** x2 + else: + return x2.__rpow__(x1) def _add_subtract_handle_non_quantity_zero(x1, x2): diff --git a/pint/quantity.py b/pint/quantity.py index 23fed4b6d..c4b1598f9 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1258,6 +1258,7 @@ def __pow__(self, other): if other == 1: return self elif other == 0: + exponent = 0 units = UnitsContainer() else: if not self._is_multiplicative: @@ -1267,13 +1268,15 @@ def __pow__(self, other): raise OffsetUnitCalculusError(self._units) if getattr(other, "dimensionless", False): - units = new_self._units ** other.to_root_units().magnitude + exponent = other.to_root_units().magnitude + units = new_self._units ** exponent elif not getattr(other, "dimensionless", True): raise DimensionalityError(other._units, "dimensionless") else: - units = new_self._units ** other + exponent = _to_magnitude(other, self.force_ndarray) + units = new_self._units ** exponent - magnitude = new_self._magnitude ** _to_magnitude(other, self.force_ndarray) + magnitude = new_self._magnitude ** exponent return self.__class__(magnitude, units) @check_implemented diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 8fd39d842..0058dd69a 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -402,6 +402,14 @@ def test_power(self): q2_cp = copy.copy(q) self.assertRaises(DimensionalityError, op_, q_cp, q2_cp) + self.assertQuantityEqual( + np.power(self.q, self.Q_(2)), self.Q_([[1, 4], [9, 16]], "m**2") + ) + self.assertQuantityEqual( + self.q ** self.Q_(2), self.Q_([[1, 4], [9, 16]], "m**2") + ) + self.assertNDArrayEqual(arr ** self.Q_(2), np.array([0, 1, 4])) + @unittest.expectedFailure @helpers.requires_numpy() def test_exponentiation_array_exp_2(self): From bf12356ebda975388d99240fe985363a6628e06d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 09:22:36 +0000 Subject: [PATCH 223/612] Fix failing tests --- pint/definitions.py | 5 +++-- pint/registry.py | 6 ++---- pint/testsuite/test_contexts.py | 12 +++++++----- pint/testsuite/test_definitions.py | 7 +++++-- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 0ca7ed4fc..4188124b4 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -9,6 +9,7 @@ """ from .converters import OffsetConverter, ScaleConverter +from .errors import DefinitionSyntaxError from .util import ParserHelper, UnitsContainer, _is_dim @@ -126,7 +127,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal elif all(_is_dim(key) for key in converter.keys()): self.is_base = True else: - raise ValueError( + raise DefinitionSyntaxError( "Cannot mix dimensions and units in the same definition. " "Base units must be referenced only to dimensions. " "Derived units must be referenced only to units." @@ -154,7 +155,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal elif all(_is_dim(key) for key in converter.keys()): self.is_base = False else: - raise ValueError( + raise DefinitionSyntaxError( "Base dimensions must be referenced to None. " "Derived dimensions must only be referenced " "to dimensions." diff --git a/pint/registry.py b/pint/registry.py index 8f6f957e1..3c9923e55 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -530,7 +530,7 @@ def _build_cache(self): if parsed_names: prefix, base_name, _ = parsed_names[0] else: - prefix, base_name = '', unit_name + prefix, base_name = "", unit_name try: uc = ParserHelper.from_word(base_name) @@ -1296,9 +1296,7 @@ def _redefine(self, definition: UnitDefinition) -> None: f"candidates: {candidates_no_prefix}" ) if not candidates_no_prefix: - raise ValueError( - f"Can't redefine a unit with a prefix {definition.name}" - ) + raise ValueError(f"Can't redefine a unit with a prefix {definition.name}") _, name, _ = candidates_no_prefix[0] try: basedef = self._units[name] diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 1b072757f..5107a14d0 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -486,13 +486,15 @@ def _test_ctx(self, ctx): self.assertEqual(len(ureg._contexts), nctx) def test_parse_invalid(self): - s = [ - "@context longcontextname", + for badrow in ( "[length] = 1 / [time]: c / value", "1 / [time] = [length]: c / value", - ] - - self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) + "[length] <- [time] = c / value", + "[length] - [time] = c / value", + ): + with self.subTest(badrow): + with self.assertRaises(DefinitionSyntaxError): + Context.from_lines(["@context c", badrow]) def test_parse_simple(self): diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 421bdb8d8..e80622f93 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -6,14 +6,17 @@ PrefixDefinition, UnitDefinition, ) +from pint.errors import DefinitionSyntaxError from pint.testsuite import BaseTestCase from pint.util import UnitsContainer class TestDefinition(BaseTestCase): def test_invalid(self): - self.assertRaises(ValueError, Definition.from_string, "x = [time] * meter") - self.assertRaises(ValueError, Definition.from_string, "[x] = [time] * meter") + with self.assertRaises(DefinitionSyntaxError): + Definition.from_string("x = [time] * meter") + with self.assertRaises(DefinitionSyntaxError): + Definition.from_string("[x] = [time] * meter") def test_prefix_definition(self): for definition in ("m- = 1e-3", "m- = 10**-3", "m- = 0.001"): From ec7b3cc5ce2ec42d0e68b5c5e2fcb8dca280d8b1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 10:11:41 +0000 Subject: [PATCH 224/612] Unit tests --- pint/testsuite/test_contexts.py | 74 ++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 5107a14d0..7d2c5a53f 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -1,7 +1,7 @@ import itertools from collections import defaultdict -from pint import DefinitionSyntaxError, DimensionalityError, UnitRegistry +from pint import DefinitionSyntaxError, DimensionalityError, UndefinedUnitError, UnitRegistry from pint.context import Context from pint.testsuite import QuantityTestCase from pint.util import UnitsContainer @@ -726,3 +726,75 @@ def g(wl): self.assertEqual(b, f(a)) self.assertEqual(b, g(a)) + + +class TestContextRedefinitions(QuantityTestCase): + def test_redefine(self): + ureg = UnitRegistry( + """ + foo = [d] = f = foo_alias + bar = 2 foo = b = bar_alias + baz = 3 bar = _ = baz_alias + asd = 4 baz + + @context c + # Note how we're redefining a symbol, not the base name, as a + # function of another name + b = 5 f + """.splitlines() + ) + # Units that are somehow directly or indirectly defined as a function of the + # overridden unit are also affected + foo = ureg.Quantity(1, "foo") + bar = ureg.Quantity(1, "bar") + asd = ureg.Quantity(1, "asd") + + # Test without context before and after, to verify that the cache and units have + # not been polluted + for enable_ctx in (False, True, False): + with self.subTest(enable_ctx): + if enable_ctx: + ureg.enable_contexts("c") + k = 5 + else: + k = 2 + + self.assertEqual(foo.to("b").magnitude, 1 / k) + self.assertEqual(foo.to("bar").magnitude, 1 / k) + self.assertEqual(foo.to("bar_alias").magnitude, 1 / k) + self.assertEqual(foo.to("baz").magnitude, 1 / k / 3) + self.assertEqual(bar.to("foo").magnitude, k) + self.assertEqual(bar.to("baz").magnitude, 1 / 3) + self.assertEqual(asd.to("foo").magnitude, 4 * 3 * k) + self.assertEqual(asd.to("bar").magnitude, 4 * 3) + self.assertEqual(asd.to("baz").magnitude, 4) + + ureg.disable_contexts() + + def test_define_undef(self): + ureg = UnitRegistry( + """ + USD = [currency] + EUR = undef USD + GBP = undef USD + + @context c + EUR = 1.11 USD + # Note that we're redefining which unit GBP is defined against + GBP = 1.18 EUR + @end + """.splitlines() + ) + + q = ureg.Quantity("10 GBP") + self.assertEquals(q.magnitude, 10) + self.assertEquals(q.units.dimensionality, {"[currency]": 1}) + self.assertEquals(q.to("GBP").magnitude, 10) + + with self.assertRaises(UndefinedUnitError) as ex: + q.to("USD") + self.assertEquals( + str(ex.exception), "'undef' is not defined in the unit registry" + ) + + self.assertAlmostEqual(q.to("USD", "c").magnitude, 1.18 * 1.11) From 9ca0c7bd3dd16fd19f2f944a3aedf5c9e5f33044 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 10:14:30 +0000 Subject: [PATCH 225/612] Code quality --- pint/util.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pint/util.py b/pint/util.py index b19fc4633..7ee6eb58a 100644 --- a/pint/util.py +++ b/pint/util.py @@ -442,10 +442,6 @@ def from_word(cls, input_word): """ return cls(1, [(input_word, 1)]) - @classmethod - def from_string(cls, input_string): - return cls._from_string(input_string) - @classmethod def eval_token(cls, token, use_decimal=False): token_type = token.type @@ -464,9 +460,8 @@ def eval_token(cls, token, use_decimal=False): @classmethod @lru_cache() - def _from_string(cls, input_string): + def from_string(cls, input_string): """Parse linear expression mathematical units and return a quantity object. - """ if not input_string: return cls() @@ -618,9 +613,7 @@ def __rtruediv__(self, other): def string_preprocessor(input_string): - - input_string = input_string.replace(",", "") - input_string = input_string.replace(" per ", "/") + input_string = input_string.strip().replace(",", "").replace(" per ", "/") for a, b in _subs_re: input_string = a.sub(b, input_string) From fe160245a4714e021b1615a8809268a386e4822b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 11:11:25 +0000 Subject: [PATCH 226/612] Parse NaN as a number --- pint/compat.py | 5 ++--- pint/testsuite/test_contexts.py | 27 ++++++++++++++------------- pint/testsuite/test_util.py | 8 ++++++++ pint/util.py | 27 +++++++++++++++++---------- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 6f64acbfb..4ec363368 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -15,9 +15,8 @@ def tokenizer(input_string): for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline): - if tokinfo.type == tokenize.ENCODING: - continue - yield tokinfo + if tokinfo.type != tokenize.ENCODING: + yield tokinfo # TODO: remove this warning after v0.10 diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 7d2c5a53f..12e43629e 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -1,7 +1,8 @@ import itertools +import math from collections import defaultdict -from pint import DefinitionSyntaxError, DimensionalityError, UndefinedUnitError, UnitRegistry +from pint import DefinitionSyntaxError, DimensionalityError, UnitRegistry from pint.context import Context from pint.testsuite import QuantityTestCase from pint.util import UnitsContainer @@ -734,7 +735,7 @@ def test_redefine(self): """ foo = [d] = f = foo_alias bar = 2 foo = b = bar_alias - baz = 3 bar = _ = baz_alias + baz = 3 bar = _ = baz_alias asd = 4 baz @context c @@ -771,16 +772,16 @@ def test_redefine(self): ureg.disable_contexts() - def test_define_undef(self): + def test_define_nan(self): ureg = UnitRegistry( """ USD = [currency] - EUR = undef USD - GBP = undef USD - + EUR = nan USD + GBP = nan USD + @context c EUR = 1.11 USD - # Note that we're redefining which unit GBP is defined against + # Note that we're changing which unit GBP is defined against GBP = 1.18 EUR @end """.splitlines() @@ -790,11 +791,11 @@ def test_define_undef(self): self.assertEquals(q.magnitude, 10) self.assertEquals(q.units.dimensionality, {"[currency]": 1}) self.assertEquals(q.to("GBP").magnitude, 10) + self.assertTrue(math.isnan(q.to("USD").magnitude)) + self.assertAlmostEqual(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11) - with self.assertRaises(UndefinedUnitError) as ex: - q.to("USD") - self.assertEquals( - str(ex.exception), "'undef' is not defined in the unit registry" - ) + def test_non_multiplicative(self): + raise NotImplementedError("TODO") - self.assertAlmostEqual(q.to("USD", "c").magnitude, 1.18 * 1.11) + def test_invalid(self): + raise NotImplementedError("TODO") diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index d27a4ebe2..52c7c8239 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -1,5 +1,6 @@ import collections import copy +import math import operator as op from decimal import Decimal @@ -203,6 +204,13 @@ def test_eval_token(self): # integer numbers are represented as ints, not Decimals self._test_eval_token(1000, "1000", use_decimal=True) + def test_nan(self): + for s in ("nan", "NAN", "NaN", "123 NaN nan NAN 456"): + with self.subTest(s): + p = ParserHelper.from_string(s + " kg") + assert math.isnan(p.scale) + self.assertEqual(dict(p), {"kg": 1}) + class TestStringProcessor(BaseTestCase): def _test(self, bef, aft): diff --git a/pint/util.py b/pint/util.py index 7ee6eb58a..8d4f0e3e6 100644 --- a/pint/util.py +++ b/pint/util.py @@ -9,6 +9,7 @@ """ import logging +import math import operator import re from collections.abc import Mapping @@ -481,16 +482,21 @@ def from_string(cls, input_string): if isinstance(ret, Number): return ParserHelper(ret) - if not reps: - return ret + if reps: + ret = ParserHelper( + ret.scale, + { + key.replace("__obra__", "[").replace("__cbra__", "]"): value + for key, value in ret.items() + }, + ) - return ParserHelper( - ret.scale, - { - key.replace("__obra__", "[").replace("__cbra__", "]"): value - for key, value in ret.items() - }, - ) + for k in list(ret): + if k.lower() == "nan": + del ret._d[k] + ret.scale = math.nan + + return ret def __copy__(self): new = super().__copy__() @@ -613,7 +619,8 @@ def __rtruediv__(self, other): def string_preprocessor(input_string): - input_string = input_string.strip().replace(",", "").replace(" per ", "/") + input_string = input_string.replace(",", "") + input_string = input_string.replace(" per ", "/") for a, b in _subs_re: input_string = a.sub(b, input_string) From d8e37778271d7bfc58b69b13d5bdfed0b4de9dab Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 14:50:44 +0000 Subject: [PATCH 227/612] Unit tests --- pint/context.py | 2 +- pint/registry.py | 2 +- pint/testsuite/test_contexts.py | 136 +++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/pint/context.py b/pint/context.py index 0ffdefb1e..dce298d1d 100644 --- a/pint/context.py +++ b/pint/context.py @@ -231,7 +231,7 @@ def redefine(self, definition: str) -> None: "Can't change a unit's symbol or aliases within a context" ) if d.is_base: - raise DefinitionSyntaxError("Cannot define base units within a context") + raise DefinitionSyntaxError("Can't define base units within a context") self.redefinitions.append(d) def hashable(self): diff --git a/pint/registry.py b/pint/registry.py index 3c9923e55..ab39d7bbf 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1296,7 +1296,7 @@ def _redefine(self, definition: UnitDefinition) -> None: f"candidates: {candidates_no_prefix}" ) if not candidates_no_prefix: - raise ValueError(f"Can't redefine a unit with a prefix {definition.name}") + raise ValueError(f"Can't redefine a unit with a prefix: {definition.name}") _, name, _ = candidates_no_prefix[0] try: basedef = self._units[name] diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 12e43629e..a66b60d60 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -795,7 +795,137 @@ def test_define_nan(self): self.assertAlmostEqual(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11) def test_non_multiplicative(self): - raise NotImplementedError("TODO") + ureg = UnitRegistry( + """ + kelvin = [temperature] + fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 + bogokelvin = 10 * kelvin + + @context nonmult_to_nonmult + fahrenheit = 10 * kelvin; offset: 123 + @end + @context nonmult_to_mult + fahrenheit = 123 * kelvin + @end + @context mult_to_nonmult + bogodegrees = kelvin; 5 * kelvin; offset: 123 + @end + """.splitlines() + ) + k = ureg.Quantity(100, "kelvin") + + with self.subTest("nonmult_to_nonmult"): + with ureg.context("nonmult_to_nonmult"): + self.assertEqual(k.to("fahrenheit").magnitude, 100 * 10 + 123) + + with self.subTest("nonmult_to_mult"): + with ureg.context("nonmult_to_mult"): + self.assertEqual(k.to("fahrenheit").magnitude, 100 * 123) + + with self.subTest("mult_to_nonmult"): + with ureg.context("mult_to_nonmult"): + self.assertEqual(k.to("bogodegrees").magnitude, 5 * 100 + 123) + + def test_err_to_base_unit(self): + with self.assertRaises(DefinitionSyntaxError) as e: + Context.from_lines(["@context c", "x = [d]"]) + self.assertEquals(str(e.exception), "Can't define base units within a context") + + def test_err_change_base_unit(self): + ureg = UnitRegistry( + """ + foo = [d1] + bar = [d2] + + @context c + bar = foo + @end + """.splitlines() + ) + + with self.assertRaises(ValueError) as e: + ureg.enable_contexts("c") + self.assertEquals( + str(e.exception), "Can't redefine a base unit to a derived one" + ) + + def test_change_dimensionality(self): + ureg = UnitRegistry( + """ + foo = [d1] + bar = [d2] + baz = foo - def test_invalid(self): - raise NotImplementedError("TODO") + @context c + baz = bar + @end + """.splitlines() + ) + with self.assertRaises(ValueError) as e: + ureg.enable_contexts("c") + self.assertEquals( + str(e.exception), + "Can't change dimensionality of baz from [d1] to [d2] in a context", + ) + + def test_err_cyclic_dependency(self): + ureg = UnitRegistry( + """ + foo = [d] + bar = foo + baz = bar + + @context c + bar = baz + @end + """.splitlines() + ) + # TODO align this exception and the one you get when you implement a cyclic + # dependency within the base registry. Ideally this exception should be + # raised by enable_contexts. + ureg.enable_contexts("c") + q = ureg.Quantity("bar") + with self.assertRaises(RecursionError): + q.to("foo") + + def test_err_dimension_redefinition(self): + with self.assertRaises(DefinitionSyntaxError) as e: + Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) + self.assertEquals( + str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" + ) + + def test_err_prefix_redefinition(self): + with self.assertRaises(DefinitionSyntaxError) as e: + Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) + self.assertEquals( + str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" + ) + + def test_err_redefine_alias(self): + for s in ("foo = bar = f", "foo = bar = _ = baz"): + with self.subTest(s): + with self.assertRaises(DefinitionSyntaxError) as e: + Context.from_lines(["@context c", s]) + self.assertEquals( + str(e.exception), + "Can't change a unit's symbol or aliases within a context", + ) + + def test_err_redefine_with_prefix(self): + ureg = UnitRegistry( + """ + kilo- = 1000 + gram = [mass] + pound = 454 gram + + @context c + kilopound = 500000 gram + @end + """.splitlines() + ) + with self.assertRaises(ValueError) as e: + ureg.enable_contexts("c") + self.assertEquals( + str(e.exception), "Can't redefine a unit with a prefix: kilopound" + ) From 0d6a4360c222683327655ec9b1d698fa8e5b738a Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 19 Dec 2019 10:41:42 -0600 Subject: [PATCH 228/612] Add documentation for implement_func parameters --- pint/numpy_func.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index cac03d20c..24bb8d532 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -184,7 +184,30 @@ def decorator(func): def implement_func(func_type, func_str, input_units=None, output_unit=None): - """Add default-behavior NumPy function/ufunc to the handled list.""" + """Add default-behavior NumPy function/ufunc to the handled list. + + Parameters + ---------- + func_type : str + "function" for NumPy functions, "ufunc" for NumPy ufuncs + func_str : str + String representing the name of the NumPy function/ufunc to add + input_units : pint.Unit or str or None + Parameter to control how the function downcasts to magnitudes of arguments. If + `pint.Unit`, converts all args and kwargs to this unit before downcasting to + magnitude. If "all_consistent", converts all args and kwargs to the unit of the + first Quantity in args and kwargs before downcasting to magnitude. If some + other string, the string is parsed as a unit, and all args and kwargs are + converted to that unit. If None, units are stripped without conversion. + output_unit : pint.Unit or str or None + Parameter to control the unit of the output. If `pint.Unit`, output is wrapped + with that unit. If "match_input", output is wrapped with the unit of the first + Quantity in args and kwargs. If a string representing a unit operation defined + in `get_op_output_unit`, output is wrapped by the unit determined by + `get_op_output_unit`. If some other string, the string is parsed as a unit, + which becomes the unit of the output. If None, the bare magnitude is returned. + + """ # If NumPy is not available, do not attempt implement that which does not exist if np is None: return From 8d740ab0170577d54ac9d56e27937841079ab4b0 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 18:11:50 +0000 Subject: [PATCH 229/612] Unit tests and bugfixes --- pint/registry.py | 9 +++----- pint/testsuite/test_contexts.py | 40 +++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index ab39d7bbf..8de3f246b 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1288,15 +1288,12 @@ def _redefine(self, definition: UnitDefinition) -> None: """ # Find original definition in the UnitRegistry candidates = self.parse_unit_name(definition.name) - assert candidates + if not candidates: + raise UndefinedUnitError(definition.name) candidates_no_prefix = [c for c in candidates if not c[0]] - if len(candidates_no_prefix) > 1: - raise ValueError( - f"Ambiguous redefinition {definition.name}; " - f"candidates: {candidates_no_prefix}" - ) if not candidates_no_prefix: raise ValueError(f"Can't redefine a unit with a prefix: {definition.name}") + assert len(candidates_no_prefix) == 1 _, name, _ = candidates_no_prefix[0] try: basedef = self._units[name] diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index a66b60d60..bc575ed4f 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -2,7 +2,12 @@ import math from collections import defaultdict -from pint import DefinitionSyntaxError, DimensionalityError, UnitRegistry +from pint import ( + DefinitionSyntaxError, + DimensionalityError, + UndefinedUnitError, + UnitRegistry, +) from pint.context import Context from pint.testsuite import QuantityTestCase from pint.util import UnitsContainer @@ -798,33 +803,37 @@ def test_non_multiplicative(self): ureg = UnitRegistry( """ kelvin = [temperature] - fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 - bogokelvin = 10 * kelvin + fahrenheit = 5 / 9 * kelvin; offset: 255 + bogodegrees = 9 * kelvin @context nonmult_to_nonmult - fahrenheit = 10 * kelvin; offset: 123 + fahrenheit = 7 * kelvin; offset: 123 @end @context nonmult_to_mult fahrenheit = 123 * kelvin @end @context mult_to_nonmult - bogodegrees = kelvin; 5 * kelvin; offset: 123 + bogodegrees = 5 * kelvin; offset: 123 @end """.splitlines() ) k = ureg.Quantity(100, "kelvin") + with self.subTest("baseline"): + self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 255) * 9 / 5) + self.assertAlmostEqual(k.to("bogodegrees").magnitude, 100 / 9) + with self.subTest("nonmult_to_nonmult"): with ureg.context("nonmult_to_nonmult"): - self.assertEqual(k.to("fahrenheit").magnitude, 100 * 10 + 123) + self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 123) / 7) with self.subTest("nonmult_to_mult"): with ureg.context("nonmult_to_mult"): - self.assertEqual(k.to("fahrenheit").magnitude, 100 * 123) + self.assertAlmostEqual(k.to("fahrenheit").magnitude, 100 / 123) with self.subTest("mult_to_nonmult"): with ureg.context("mult_to_nonmult"): - self.assertEqual(k.to("bogodegrees").magnitude, 5 * 100 + 123) + self.assertAlmostEqual(k.to("bogodegrees").magnitude, (100 - 123) / 5) def test_err_to_base_unit(self): with self.assertRaises(DefinitionSyntaxError) as e: @@ -849,7 +858,7 @@ def test_err_change_base_unit(self): str(e.exception), "Can't redefine a base unit to a derived one" ) - def test_change_dimensionality(self): + def test_err_change_dimensionality(self): ureg = UnitRegistry( """ foo = [d1] @@ -929,3 +938,16 @@ def test_err_redefine_with_prefix(self): self.assertEquals( str(e.exception), "Can't redefine a unit with a prefix: kilopound" ) + + def test_err_new_unit(self): + ureg = UnitRegistry( + """ + foo = [d] + @context c + bar = foo + @end + """.splitlines() + ) + with self.assertRaises(UndefinedUnitError) as e: + ureg.enable_contexts("c") + self.assertEquals(str(e.exception), "'bar' is not defined in the unit registry") From eb127c1551dd24077cc1200de7bf79bfb203b145 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 18:32:21 +0000 Subject: [PATCH 230/612] Dimensionality check --- pint/registry.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pint/registry.py b/pint/registry.py index 8de3f246b..dc85dfe53 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1303,6 +1303,15 @@ def _redefine(self, definition: UnitDefinition) -> None: # Rebuild definition as a variant of the base if basedef.is_base: raise ValueError("Can't redefine a base unit to a derived one") + + dims_old = self._get_dimensionality(basedef.reference) + dims_new = self._get_dimensionality(definition.reference) + if dims_old != dims_new: + raise ValueError( + f"Can't change dimensionality of {basedef.name} " + f"from {dims_old} to {dims_new} in a context" + ) + # Do not modify in place the original definition, as (1) the context may # be shared by other registries, and (2) it would alter the cache key definition = UnitDefinition( From 3416cb8522307bd6c2675fb6dea9fd7bfc7d8346 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 19 Dec 2019 20:38:52 +0000 Subject: [PATCH 231/612] Documentation --- CHANGES | 16 +++++- docs/contexts.rst | 123 ++++++++++++++++++++++++++++++++++++++++++++ docs/currencies.rst | 86 +++++++++++++++++++++++++++++++ docs/index.rst | 1 + 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 docs/currencies.rst diff --git a/CHANGES b/CHANGES index 95c26c82e..b74f5bfd1 100644 --- a/CHANGES +++ b/CHANGES @@ -4,8 +4,19 @@ Pint Changelog 0.10 (unreleased) ----------------- +- It is now possible to redefine units within a context, and use pint for currency + conversions. Read + + - https://pint.readthedocs.io/en/latest/contexts.html + - https://pint.readthedocs.io/en/latest/currencies.html + + (Issue #938, Thanks Guido Imperiale) +- NaN (any capitalization) in a definitions file is now treated as a number + (Issue #938, Thanks Guido Imperiale) - Added slinch to Avoirdupois group (Issue #936, Thanks awcox21) +- Fix bug where ureg.disable_contexts() would fail to fully disable throwaway contexts + (Issue #932, Thanks Guido Imperiale) - Use black, flake8, and isort on the project (Issues #929, #931, and #937, Thanks Guido Imperiale) - Auto-increase package version at every commit when pint is installed from the git tip, @@ -40,8 +51,9 @@ Pint Changelog (Issue #915, Thanks Guido Imperiale) - TODO #913 - TODO #911 -- Prevent 1500x slowdown when using .to() with a context - (Issues #909 and #923, Thanks Guido Imperiale) +- Context activation and deactivation is now instantaneous; drastically reduced memory + footprint of a context (it used to be ~1.6MB per context; now it's a few bytes) + (Issues #909 / #923 / #938, Thanks Guido Imperiale) - **BREAKING CHANGE**: Drop support for Python < 3.6, numpy < 1.14, and uncertainties < 3.0; if you still need them, please install pint 0.9. diff --git a/docs/contexts.rst b/docs/contexts.rst index 4221fe718..27cb6abc2 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -202,3 +202,126 @@ It is also possible to create anonymous contexts without invoking add_context: ... >>> ureg("1 s").to("km", c) 299792.458 kilometer + +Using contexts for unit redefinition +------------------------------------ + +The exact definition of a unit of measure can change slightly depending on the country, +year, and more in general convention. For example, the ISO board released over the years +several revisions of its whitepapers, which subtly change the value of some of the more +obscure units. And as soon as one steps out of the SI system and starts wandering into +imperial and colonial measuring systems, the same unit may start being defined slightly +differently every time - with no clear 'right' or 'wrong' definition. + +The default pint definitions file (default_en.txt) tries to mitigate the problem by +offering multiple variants of the same unit by calling them with different names; for +example, one will find multiple definitions of a "BTU":: + + british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso + international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it + thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th + +That's sometimes insufficient, as Wikipedia reports `no less than 6 different +definitions `_ for BTU, and it's +entirely possible that some companies in the energy sector, or even individual energy +contracts, may redefine it to something new entirely, e.g. with a different rounding. + +Pint allows changing the definition of a unit within the scope of a context. +This allows layering; in the example above, a company may use the global definition +of BTU from default_en.txt above, then override it with a customer-specific one in +a context, and then override it again with a contract-specific one on top of it. + +A redefinition follows the following syntax:: + + = + +where can be the base unit name or one of its aliases. +For example:: + + BTU = 1055 J + + +Programmatically: + +.. code-block:: python + + >>> ureg = pint.UnitRegistry() + >>> q = ureg.Quantity("1 BTU") + >>> q.to("J") + 1055.056 joule + >>> ctx = pint.Context() + >>> ctx.redefine("BTU = 1055 J") + >>> q.to("J", ctx) + 1055.0 joule + # When the context is disabled, pint reverts to the base definition + >>> q.to("J") + 1055.056 joule + +Or with a definitions file:: + + @context somecontract + BTU = 1055 J + @end + +.. code-block:: python + + >>> ureg = pint.UnitRegistry() + >>> ureg.load_definitions("somefile.txt") + >>> q = ureg.Quantity("1 BTU") + >>> q.to("J") + 1055.056 joule + >>> q.to("J", "somecontract") + 1055.0 joule + + +.. note:: + Redefinitions are transitive; if the registry defines B as a function of A + and C as a function of B, redefining B will also impact the conversion from C to A. + +**Limitations** + +- You can't create brand new units ; all units must be defined outside of the context + first. +- You can't change the dimensionality of a unit within a context. For example, you + can't define a context that redefines grams as a force instead of a mass (but see + the unit ``force_gram`` in default_en.txt). +- You can't redefine a unit with a prefix; e.g. you can redefine a liter, but not a + decaliter. +- You can't redefine a base unit, such as grams. +- You can't add or remove aliases, or change the symbol. Symbol and aliases are + automatically inherited from the UnitRegistry. +- You can't redefine dimensions or prefixes. + +Working without a default definition +------------------------------------ + +In some cases, the definition of a certain unit may be so volatile to make it unwise to +define a default conversion rate in the UnitRegistry. + +This can be solved by using 'NaN' (any capitalization) instead of a conversion rate rate +in the UnitRegistry, and then override it in contexts:: + + truckload = nan kg + + @context Euro_TIR + truckload = 2000 kg + @end + + @context British_grocer + truckload = 500 lb + @end + +This allows you, before any context is activated, to define quantities and perform +dimensional analysis: + +.. code-block:: python + + >>> ureg.truckload.dimensionality + [mass] + >>> q = ureg.Quantity("2 truckloads") + >>> q.to("kg") + nan kg + >>> q.to("kg", "Euro_TIR") + 4000 kilogram + >>> q.to("kg", "British_grocer") + 453.59237 kilogram diff --git a/docs/currencies.rst b/docs/currencies.rst new file mode 100644 index 000000000..26b66b531 --- /dev/null +++ b/docs/currencies.rst @@ -0,0 +1,86 @@ +.. _currencies: + +Using Pint for currency conversions +=================================== +Currency conversion tends to be substantially more complex than physical units. +The exact exchange rate between two currencies: + +- changes every minute, +- changes depending on the place, +- changes depending on who you are and who changes the money, +- may be not reversible, e.g. EUR->USD may not be the same as 1/(USD->EUR), +- different rates may apply to different amounts of money, e.g. in a BUY/SELL ledger, +- frequently involves fees, whose calculation can be more or less sophisticated. + For example, a typical credit card contract may state that the bank will charge you a + fee on all purchases in foreign currency of 1 USD or 2%, whichever is higher, for all + amounts less than 1000 USD, and then 1.5% for anything in excess. + +You may implement currencies in two ways, both of which require you to be familiar +with :ref:`contexts`. + +Simplified model +---------------- + +This model implies a few strong assumptions: + +- There are no conversion fees +- All exchange rates are reversible +- Any amount of money can be exchanged at the same rate +- All exchanges can happen at the same time, between the same actors. + +In this simplified scenario, you can perform any round-trip across currencies +and always come back with the original money; e.g. +1 USD -> EUR -> JPY -> GBP -> USD will always give you 1 USD. + +In reality, these assumptions almost never happen but can be a reasonable approximation, +for example in the case of large financial institutions, which can use interbank +exchange rates and have nearly-limitless liquidity and sub-second trading systems. + +This can be implemented by putting all currencies on the same dimension, with a +default conversion rate of NaN, and then setting the rate within contexts:: + + USD = [currency] + EUR = nan USD + JPY = nan USD + GBP = nan USD + + @context FX + EUR = 1.11254 USD + GBP = 1.16956 EUR + @end + +Note how, in the example above: + +- USD is our *base currency*. It is arbitrary, only matters for the purpose + of invoking ``to_base_units()``, and can be changed with :ref:`systems`. +- We did not set a value for JPY - maybe because the trader has no availability, or + because the data source was for some reason missing up-to-date data. + Any conversion involving JPY will return NaN. +- We redefined GBP to be a function of EUR instead of USD. This is fine as long as there + is a path between two currencies. + +Full model +---------- + +If any of the assumptions of the simplified model fails, one can resort to putting each +currency on its own dimension, and then implement transformations:: + + EUR = [currency_EUR] + GBP = [currency_GBP] + + @context FX + GBP -> EUR: value * 1.11108 EUR/GBP + EUR -> GBP: value * 0.81227 GBP/EUR + @end + +.. code-block:: python + + >>> q = ureg.Quantity("1 EUR") + >>> with ureg.context("FX"): + ... q = q.to("GBP").to("EUR") + >>> q + 0.9024969516 EUR + +More sophisticated formulas, e.g. dealing with flat fees and thresholds, can be +implemented with arbitrary python code by programmatically defining a context (see +:ref:`contexts`). diff --git a/docs/index.rst b/docs/index.rst index bb76a7249..b3245abf3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -127,6 +127,7 @@ User Guide defining performance systems + currencies pint-pandas.ipynb More information From df1d2fb26e99ea742d707b524afffe5c8806ee5a Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 00:57:21 -0300 Subject: [PATCH 232/612] Improved babel experience 1. UnitRegistry now has an optional keyword argument (`locale`) that can be used to define the default value for `Quantity.format_babel` locale argument. 2. When Babel is not installed, `Quantity.format_babel` display a nicer and more informative exception. Closes #899, #904 --- pint/compat.py | 16 +++++++++++++++- pint/formatting.py | 4 ++-- pint/quantity.py | 4 ++-- pint/registry.py | 7 +++++++ pint/systems.py | 4 ++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 4ec363368..24fa7699f 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -13,6 +13,18 @@ from numbers import Number +def missing_dependency(package, display_name=None): + display_name = display_name or package + + def _inner(*args, **kwargs): + raise Exception( + "This feature requires %s. Please install it by running:\n" + "pip install %s" % (display_name, package) + ) + + return _inner + + def tokenizer(input_string): for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline): if tokinfo.type != tokenize.ENCODING: @@ -121,12 +133,14 @@ def _to_magnitude(value, force_ndarray=False): from babel import Locale as Loc from babel import units as babel_units + babel_parse = Loc.parse + HAS_BABEL = hasattr(babel_units, "format_unit") except ImportError: HAS_BABEL = False if not HAS_BABEL: - Loc = babel_units = None # noqa: F811 + babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and # downcast/wrappable types diff --git a/pint/formatting.py b/pint/formatting.py index 2564030de..002925df2 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -11,7 +11,7 @@ import re from .babel_names import _babel_lengths, _babel_units -from .compat import Loc +from .compat import babel_parse __JOIN_REG_EXP = re.compile(r"\{\d*\}") @@ -138,7 +138,7 @@ def formatter( for key, value in sorted(items): if locale and babel_length and babel_plural_form and key in _babel_units: _key = _babel_units[key] - locale = Loc.parse(locale) + locale = babel_parse(locale) unit_patterns = locale._data["unit_patterns"] compound_unit_patterns = locale._data["compound_unit_patterns"] plural = "one" if abs(value) <= 0 else babel_plural_form diff --git a/pint/quantity.py b/pint/quantity.py index 9fddf2fc6..e771ca564 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -24,9 +24,9 @@ from .compat import ( NUMPY_VER, BehaviorChangeWarning, - Loc, _to_magnitude, array_function_change_msg, + babel_parse, eq, is_upcast_type, ndarray, @@ -334,7 +334,7 @@ def format_babel(self, spec="", **kwspec): kwspec = dict(kwspec) if "length" in kwspec: kwspec["babel_length"] = kwspec.pop("length") - kwspec["locale"] = Loc.parse(kwspec["locale"]) + kwspec["locale"] = babel_parse(kwspec.get("locale", self._REGISTRY.locale)) kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) return "{} {}".format( format(obj.magnitude, remove_custom_flags(spec)), diff --git a/pint/registry.py b/pint/registry.py index 147ae3c9c..d7e97920e 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -149,6 +149,9 @@ class BaseRegistry(metaclass=RegistryMeta): :param preprocessors: list of callables which are iteratively ran on any input expression or unit string + :param locale: + locale identifier string, used in `format_babel` + string """ #: Map context prefix to function @@ -181,6 +184,7 @@ def __init__( on_redefinition="warn", auto_reduce_dimensions=False, preprocessors=None, + locale="en_US", ): self._register_parsers() self._init_dynamic_classes() @@ -195,6 +199,9 @@ def __init__( #: Determines if dimensionality should be reduced on appropriate operations. self.auto_reduce_dimensions = auto_reduce_dimensions + #: Default locale identifier string, used when calling format_babel without explicit locale. + self.locale = locale + #: Map between name (string) and value (string) of defaults stored in the #: definitions file. self._defaults = {} diff --git a/pint/systems.py b/pint/systems.py index 35752687c..d3237631f 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -10,7 +10,7 @@ import re -from pint.compat import Loc +from pint.compat import babel_parse from .babel_names import _babel_systems from .definitions import Definition, UnitDefinition @@ -362,7 +362,7 @@ def format_babel(self, locale): """ if locale and self.name in _babel_systems: name = _babel_systems[self.name] - locale = Loc.parse(locale) + locale = babel_parse(locale) return locale.measurement_systems[name] return self.name From f4d1f0fcc8f4df6eebea4c3dfea0580dc06b6427 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:00:35 -0300 Subject: [PATCH 233/612] Renamed argument locale to fmt_locale in Registry --- pint/quantity.py | 2 +- pint/registry.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index e771ca564..00c244ca2 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -334,7 +334,7 @@ def format_babel(self, spec="", **kwspec): kwspec = dict(kwspec) if "length" in kwspec: kwspec["babel_length"] = kwspec.pop("length") - kwspec["locale"] = babel_parse(kwspec.get("locale", self._REGISTRY.locale)) + kwspec["locale"] = babel_parse(kwspec.get("locale", self._REGISTRY.fmt_locale)) kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) return "{} {}".format( format(obj.magnitude, remove_custom_flags(spec)), diff --git a/pint/registry.py b/pint/registry.py index d7e97920e..6dcd1ef9a 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -149,7 +149,7 @@ class BaseRegistry(metaclass=RegistryMeta): :param preprocessors: list of callables which are iteratively ran on any input expression or unit string - :param locale: + :param fmt_locale: locale identifier string, used in `format_babel` string """ @@ -184,7 +184,7 @@ def __init__( on_redefinition="warn", auto_reduce_dimensions=False, preprocessors=None, - locale="en_US", + fmt_locale="en_US", ): self._register_parsers() self._init_dynamic_classes() @@ -200,7 +200,7 @@ def __init__( self.auto_reduce_dimensions = auto_reduce_dimensions #: Default locale identifier string, used when calling format_babel without explicit locale. - self.locale = locale + self.fmt_locale = fmt_locale #: Map between name (string) and value (string) of defaults stored in the #: definitions file. From acb6b52d894457c932f414e5353af360fe866eb7 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:00:48 -0300 Subject: [PATCH 234/612] Added tests --- pint/testsuite/test_babel.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 67bc22ed4..bb4d61661 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -6,7 +6,7 @@ class TestBabel(BaseTestCase): @helpers.requires_babel() - def test_babel(self): + def test_format(self): ureg = UnitRegistry() dirname = os.path.dirname(__file__) ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) @@ -27,3 +27,33 @@ def test_babel(self): ) mks = ureg.get_system("mks") self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") + + @helpers.requires_babel() + def test_registry_locale(self): + ureg = UnitRegistry(fmt_locale="fr_FR") + dirname = os.path.dirname(__file__) + ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) + + distance = 24.0 * ureg.meter + self.assertEqual( + distance.format_babel(length="long"), "24.0 mètres" + ) + time = 8.0 * ureg.second + self.assertEqual( + time.format_babel(length="long"), "8.0 secondes" + ) + self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") + acceleration = distance / time ** 2 + self.assertEqual( + acceleration.format_babel(length="long"), + "0.375 mètre par seconde²", + ) + mks = ureg.get_system("mks") + self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") + + def test_nobabel(self): + ureg = UnitRegistry() + distance = 24.0 * ureg.meter + self.assertRaises(Exception, + distance.format_babel, locale="fr_FR", length="long" + ) From b0207b144ad69d7d7d9225b254cb593d91e4f0c5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:01:04 -0300 Subject: [PATCH 235/612] Added tests for fmt_locale --- docs/tutorial.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b035b1076..158d2ad8b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -387,6 +387,10 @@ Finally, if Babel_ is installed you can translate unit names to any language >>> accel.format_babel(locale='fr_FR') '1.3 mètre par seconde²' +You can also specify the format locale at u + + >>> ureg = UnitRegistry(fmt_locale='fr_FR') + Using Pint in your projects --------------------------- From 0259d7f773312d2b5ea45747a1381afb8763e0a1 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:01:18 -0300 Subject: [PATCH 236/612] Add locale to CHANGES --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index b74f5bfd1..19f7d7ffb 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Added `fmt_locale` argument to registry. + (Issue #904) +- Better error message when Babel is not installed. + (Issue #899) - It is now possible to redefine units within a context, and use pint for currency conversions. Read From 8769ef7664fb592b6e6e39ffedfce8e4a96dba4c Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:02:25 -0300 Subject: [PATCH 237/612] linters --- pint/testsuite/test_babel.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index bb4d61661..dde9c4315 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -35,18 +35,13 @@ def test_registry_locale(self): ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) distance = 24.0 * ureg.meter - self.assertEqual( - distance.format_babel(length="long"), "24.0 mètres" - ) + self.assertEqual(distance.format_babel(length="long"), "24.0 mètres") time = 8.0 * ureg.second - self.assertEqual( - time.format_babel(length="long"), "8.0 secondes" - ) + self.assertEqual(time.format_babel(length="long"), "8.0 secondes") self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") acceleration = distance / time ** 2 self.assertEqual( - acceleration.format_babel(length="long"), - "0.375 mètre par seconde²", + acceleration.format_babel(length="long"), "0.375 mètre par seconde²", ) mks = ureg.get_system("mks") self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") @@ -54,6 +49,6 @@ def test_registry_locale(self): def test_nobabel(self): ureg = UnitRegistry() distance = 24.0 * ureg.meter - self.assertRaises(Exception, - distance.format_babel, locale="fr_FR", length="long" + self.assertRaises( + Exception, distance.format_babel, locale="fr_FR", length="long" ) From 8bc58aaaf05b8920b2e1a37c9575390735a7c7f0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 03:42:22 -0300 Subject: [PATCH 238/612] Expose fmt_locale in UnitRegistry --- pint/registry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 6dcd1ef9a..f38a9de32 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -151,7 +151,6 @@ class BaseRegistry(metaclass=RegistryMeta): string :param fmt_locale: locale identifier string, used in `format_babel` - string """ #: Map context prefix to function @@ -1752,6 +1751,8 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. :param preprocessors: list of callables which are iteratively ran on any input expression or unit string + :param fmt_locale: + locale identifier string, used in `format_babel` """ def __init__( @@ -1764,6 +1765,7 @@ def __init__( system=None, auto_reduce_dimensions=False, preprocessors=None, + fmt_locale='en_US' ): super().__init__( @@ -1775,6 +1777,7 @@ def __init__( system=system, auto_reduce_dimensions=auto_reduce_dimensions, preprocessors=preprocessors, + fmt_locale=fmt_locale ) def pi_theorem(self, quantities): From cb08e2678f6bb1e2cf04122b43a0718b53ec4eb2 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 18:55:25 -0300 Subject: [PATCH 239/612] Add set_fmt_locale, system detection and better user facing exception --- pint/quantity.py | 7 ++++++- pint/registry.py | 28 ++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 00c244ca2..02f8ec589 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -334,7 +334,12 @@ def format_babel(self, spec="", **kwspec): kwspec = dict(kwspec) if "length" in kwspec: kwspec["babel_length"] = kwspec.pop("length") - kwspec["locale"] = babel_parse(kwspec.get("locale", self._REGISTRY.fmt_locale)) + + loc = kwspec.get("locale", self._REGISTRY.fmt_locale) + if loc is None: + raise ValueError("Provide a `locale` value to localize translation.") + + kwspec["locale"] = babel_parse(loc) kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) return "{} {}".format( format(obj.magnitude, remove_custom_flags(spec)), diff --git a/pint/registry.py b/pint/registry.py index f38a9de32..2ac55afce 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -34,6 +34,7 @@ import copy import functools import itertools +import locale import math import os import re @@ -47,7 +48,7 @@ import pkg_resources from . import registry_helpers, systems -from .compat import tokenizer +from .compat import babel_parse, tokenizer from .context import Context, ContextChain from .converters import ScaleConverter from .definitions import ( @@ -183,7 +184,7 @@ def __init__( on_redefinition="warn", auto_reduce_dimensions=False, preprocessors=None, - fmt_locale="en_US", + fmt_locale=None, ): self._register_parsers() self._init_dynamic_classes() @@ -199,7 +200,7 @@ def __init__( self.auto_reduce_dimensions = auto_reduce_dimensions #: Default locale identifier string, used when calling format_babel without explicit locale. - self.fmt_locale = fmt_locale + self.fmt_locale = self.set_fmt_locale(fmt_locale) #: Map between name (string) and value (string) of defaults stored in the #: definitions file. @@ -290,6 +291,21 @@ def __getitem__(self, item): def __dir__(self): return list(self._units.keys()) + self._dir + def set_fmt_locale(self, loc): + """Change the locale used by default by `format_babel`. + + :param loc: + None` (do not translate), 'sys' (detect the system locale) or a locale id string. + """ + if isinstance(loc, str): + if loc == "sys": + loc = locale.getdefaultlocale()[0] + + # We call babel parse to fail here and not in the formatting operation + babel_parse(loc) + + self.fmt_locale = loc + @property def default_format(self): """Default formatting string for quantities. @@ -1752,7 +1768,7 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): :param preprocessors: list of callables which are iteratively ran on any input expression or unit string :param fmt_locale: - locale identifier string, used in `format_babel` + locale identifier string, used in `format_babel`. Default to None """ def __init__( @@ -1765,7 +1781,7 @@ def __init__( system=None, auto_reduce_dimensions=False, preprocessors=None, - fmt_locale='en_US' + fmt_locale=None, ): super().__init__( @@ -1777,7 +1793,7 @@ def __init__( system=system, auto_reduce_dimensions=auto_reduce_dimensions, preprocessors=preprocessors, - fmt_locale=fmt_locale + fmt_locale=fmt_locale, ) def pi_theorem(self, quantities): From 4c6b11a7385b18e588f04407c782521acd4c349a Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 23:00:23 -0300 Subject: [PATCH 240/612] Remove eval from Definitions --- pint/definitions.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 4188124b4..8ba55c267 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -13,6 +13,21 @@ from .util import ParserHelper, UnitsContainer, _is_dim +class _NotNumeric(Exception): + + def __init__(self, value): + self.value = value + + +def numeric_parse(s): + ph = ParserHelper.from_string(s) + + if len(ph): + raise _NotNumeric(s) + + return ph.scale + + class Definition: """Base class for definitions. @@ -94,7 +109,11 @@ class PrefixDefinition(Definition): def __init__(self, name, symbol, aliases, converter): if isinstance(converter, str): - converter = ScaleConverter(eval(converter)) + try: + converter = ScaleConverter(numeric_parse(converter)) + except _NotNumeric as ex: + raise ValueError(f'Prefix definition must contain only numbers, not {ex.value}') + aliases = tuple(alias.strip("-") for alias in aliases) if symbol: symbol = symbol.strip("-") @@ -114,10 +133,16 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal if isinstance(converter, str): if ";" in converter: [converter, modifiers] = converter.split(";", 2) - modifiers = dict( - (key.strip(), eval(value)) - for key, value in (part.split(":") for part in modifiers.split(";")) - ) + + try: + modifiers = dict( + (key.strip(), numeric_parse(value)) + for key, value in (part.split(":") for part in modifiers.split(";")) + ) + except _NotNumeric as ex: + raise ValueError(f'Prefix definition must contain only numbers, not {ex.value}') + + else: modifiers = {} From 7159c7f91dadf1f0f6e08dc52a5dd54364ae30fe Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 21 Dec 2019 23:02:19 -0300 Subject: [PATCH 241/612] linters --- pint/definitions.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 8ba55c267..1190d0d11 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -14,7 +14,6 @@ class _NotNumeric(Exception): - def __init__(self, value): self.value = value @@ -112,7 +111,9 @@ def __init__(self, name, symbol, aliases, converter): try: converter = ScaleConverter(numeric_parse(converter)) except _NotNumeric as ex: - raise ValueError(f'Prefix definition must contain only numbers, not {ex.value}') + raise ValueError( + f"Prefix definition must contain only numbers, not {ex.value}" + ) aliases = tuple(alias.strip("-") for alias in aliases) if symbol: @@ -137,11 +138,14 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal try: modifiers = dict( (key.strip(), numeric_parse(value)) - for key, value in (part.split(":") for part in modifiers.split(";")) + for key, value in ( + part.split(":") for part in modifiers.split(";") + ) ) except _NotNumeric as ex: - raise ValueError(f'Prefix definition must contain only numbers, not {ex.value}') - + raise ValueError( + f"Prefix definition must contain only numbers, not {ex.value}" + ) else: modifiers = {} From 298be3b094f020ed18b5f63607563264effe7817 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 22 Dec 2019 01:08:53 -0300 Subject: [PATCH 242/612] Improved error message in offending definitions --- pint/definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 1190d0d11..c0481df17 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -112,7 +112,7 @@ def __init__(self, name, symbol, aliases, converter): converter = ScaleConverter(numeric_parse(converter)) except _NotNumeric as ex: raise ValueError( - f"Prefix definition must contain only numbers, not {ex.value}" + f"Prefix definition ('{name}') must contain only numbers, not {ex.value}" ) aliases = tuple(alias.strip("-") for alias in aliases) @@ -144,7 +144,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal ) except _NotNumeric as ex: raise ValueError( - f"Prefix definition must contain only numbers, not {ex.value}" + f"Unit definition ('{name}') must contain only numbers in modifier, not {ex.value}" ) else: From 049449baf816e26c911ca929b4a83234d6d60863 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 22 Dec 2019 01:09:12 -0300 Subject: [PATCH 243/612] Added tests for offending definitions --- pint/testsuite/test_definitions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index e80622f93..a5e008666 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -19,6 +19,9 @@ def test_invalid(self): Definition.from_string("[x] = [time] * meter") def test_prefix_definition(self): + + self.assertRaises(ValueError, Definition.from_string, "m- = 1e-3 k") + for definition in ("m- = 1e-3", "m- = 10**-3", "m- = 0.001"): x = Definition.from_string(definition) self.assertIsInstance(x, PrefixDefinition) @@ -85,6 +88,12 @@ def test_unit_definition(self): self.assertEqual(x.converter.scale, 6.28) self.assertEqual(x.reference, UnitsContainer(radian=1)) + self.assertRaises( + ValueError, + Definition.from_string, + "degF = 9 / 5 * kelvin; offset: 255.372222 bla", + ) + def test_dimension_definition(self): x = DimensionDefinition("[time]", "", (), converter="") self.assertTrue(x.is_base) From 2d019416a319c70465ebd175565f77382432fc0e Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 22 Dec 2019 01:10:35 -0300 Subject: [PATCH 244/612] Updated CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 19f7d7ffb..f1bc2474f 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Removed eval usage when creating UnitDefinition and PrefixDefinition from string. + (Issue #942) - Added `fmt_locale` argument to registry. (Issue #904) - Better error message when Babel is not installed. From 19ac757f61bd8923191f3ba4630d7400dd073bac Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Thu, 10 Jan 2019 14:12:28 +0300 Subject: [PATCH 245/612] Moved the metadata into setup.cfg --- setup.cfg | 42 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 52 +++------------------------------------------------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/setup.cfg b/setup.cfg index b2de1ede9..a6bdcf9b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,43 @@ +[metadata] +name = Pint +author = Hernan E. Grecco +author_email = hernan.grecco@gmail.com +license = BSD +description = Physical quantities module +long_description = file: README, AUTHORS, CHANGES +keywords = physical, quantities, unit, conversion, science +url = https://github.com/hgrecco/pint +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + Intended Audience :: Science/Research + License :: OSI Approved :: BSD License + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX + Programming Language :: Python + Topic :: Scientific/Engineering + Topic :: Software Development :: Libraries + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +packages = pint +zip_safe = True +include_package_data = True +python_requires = >=3.6 +install_requires = setuptools +setup_requires = setuptools; setuptools_scm +test_suite = pint.testsuite.testsuite + +[options.extras_require] +numpy = numpy >= 1.14 +uncertainties = uncertainties >= 3.0 + +[options.package_data] +pint = default_en.txt; constants_en.txt + [check-manifest] ignore = .travis.yml @@ -5,6 +45,8 @@ ignore = [bdist_wheel] universal = 1 +[build-system] +requires = ["setuptools", "setuptools_scm", "wheel"] [flake8] ignore= diff --git a/setup.py b/setup.py index 03ac0e927..57e6ccddf 100644 --- a/setup.py +++ b/setup.py @@ -1,51 +1,5 @@ -#!/usr/bin/env python - +#!/usr/bin/env python3 from setuptools import setup - -def read(filename): - with open(filename) as fh: - return fh.read() - - -long_description = "\n\n".join([read("README"), read("AUTHORS"), read("CHANGES")]) -__doc__ = long_description - -setup( - name="Pint", - use_scm_version=True, - description="Physical quantities module", - long_description=long_description, - keywords="physical quantities unit conversion science", - author="Hernan E. Grecco", - author_email="hernan.grecco@gmail.com", - url="https://github.com/hgrecco/pint", - test_suite="pint.testsuite.testsuite", - zip_safe=True, - packages=["pint"], - package_data={"pint": ["default_en.txt", "constants_en.txt"]}, - include_package_data=True, - license="BSD", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - ], - python_requires=">=3.6", - install_requires=["setuptools"], - setup_requires=["setuptools", "setuptools_scm"], - extras_require={ - "numpy": ["numpy >= 1.14"], - "uncertainties": ["uncertainties >= 3.0"], - }, -) +if __name__ == "__main__": + setup(use_scm_version=True) From 555915f2b01f82e05c3a472c948819977c932b08 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 23 Dec 2019 23:20:57 +0100 Subject: [PATCH 246/612] expect other to be a type --- pint/compat.py | 2 +- pint/quantity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 24fa7699f..7264c35d1 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -148,7 +148,7 @@ def _to_magnitude(value, force_ndarray=False): def is_upcast_type(other): # Check if class name is in preset list - return other.__class__.__name__ in ("PintArray", "Series", "DataArray") + return other.__name__ in ("PintArray", "Series", "DataArray") def eq(first, second, check_all): diff --git a/pint/quantity.py b/pint/quantity.py index 02f8ec589..c46a02c93 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -95,7 +95,7 @@ def wrapped(self, *args, **kwargs): def check_implemented(f): def wrapped(self, *args, **kwargs): other = args[0] - if is_upcast_type(other): + if is_upcast_type(type(other)): return NotImplemented # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] # and expects Quantity * array[Quantity] should return NotImplemented From 1740e8b7c28879f7c31e24e31936e49d2ef1d3e3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 23 Dec 2019 23:26:35 +0100 Subject: [PATCH 247/612] black --- pint/testsuite/test_babel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index dde9c4315..2aac0cdd8 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -41,7 +41,7 @@ def test_registry_locale(self): self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") acceleration = distance / time ** 2 self.assertEqual( - acceleration.format_babel(length="long"), "0.375 mètre par seconde²", + acceleration.format_babel(length="long"), "0.375 mètre par seconde²" ) mks = ureg.get_system("mks") self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") From 6f1e41ed03a057a0236a36d399df7b5b57c16650 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 24 Dec 2019 00:06:09 +0100 Subject: [PATCH 248/612] add a docstring for is_upcast_type --- pint/compat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pint/compat.py b/pint/compat.py index 7264c35d1..25fafc240 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -147,6 +147,10 @@ def _to_magnitude(value, force_ndarray=False): def is_upcast_type(other): + """ check if the type object is a upcast type + + :param other: type + """ # Check if class name is in preset list return other.__name__ in ("PintArray", "Series", "DataArray") From 2df239db711bdd9578d17fc6b00e59d2c542df06 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 15:04:47 -0600 Subject: [PATCH 249/612] Modify test configuration to use pytest and allow matplotlib tests --- .travis.yml | 13 +++++++------ setup.cfg | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index eaacb73f4..4f8613f24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,9 +21,9 @@ env: - PKGS="python=3.8" - PKGS="python=3.6 uncertainties=3.0" - PKGS="python=3.7 uncertainties=3.0" - - PKGS="python=3.6 numpy=1.14" - - PKGS="python=3.7 numpy=1.14" - - PKGS="python=3.8 numpy=1.17" + - PKGS="python=3.6 numpy=1.14 matplotlib" + - PKGS="python=3.7 numpy=1.14 matplotlib" + - PKGS="python=3.8 numpy=1.17 matplotlib" - PKGS="python=3.6 numpy=1.14 uncertainties=3.0" - PKGS="python=3.7 numpy=1.14 uncertainties=3.0" - PKGS="python=3.6 numpy uncertainties" @@ -49,12 +49,14 @@ before_install: # But broke travis 2019-08 # - sudo rm -rf /dev/shm # - sudo ln -s /run/shm /dev/shm + - export TEST_OPTS="-rfsxEX -s --cov=pint" install: - - conda create -n travis $PKGS coveralls + - conda create -n travis $PKGS pytest pytest-cov coveralls - source activate travis - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi + - if [[ $PKGS =~ matplotlib ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list @@ -65,9 +67,8 @@ script: # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi # test notebooks too if pandas available # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*","*pandas*" setup.py test; fi + - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - if [[ $LINT == 0 ]]; then coverage combine; fi - if [[ $LINT == 0 ]]; then coverage report -m; fi after_success: diff --git a/setup.cfg b/setup.cfg index a6bdcf9b2..6c086432e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ test_suite = pint.testsuite.testsuite [options.extras_require] numpy = numpy >= 1.14 uncertainties = uncertainties >= 3.0 +test = pytest; pytest-mpl; pytest-cov [options.package_data] pint = default_en.txt; constants_en.txt From 0f2b073f396d57902744fd4f27b58b8da3f176ce Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 15:24:41 -0600 Subject: [PATCH 250/612] Add matplotlib tests using examples from docs --- pint/testsuite/baseline/test_basic_plot.png | Bin 0 -> 17483 bytes .../baseline/test_plot_with_set_units.png | Bin 0 -> 18145 bytes pint/testsuite/test_matplotlib.py | 43 ++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 pint/testsuite/baseline/test_basic_plot.png create mode 100644 pint/testsuite/baseline/test_plot_with_set_units.png create mode 100644 pint/testsuite/test_matplotlib.py diff --git a/pint/testsuite/baseline/test_basic_plot.png b/pint/testsuite/baseline/test_basic_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..63be609b98d89432e8ae644c6d0df01608daee3e GIT binary patch literal 17483 zcmeIac{o(>A3lCeDs7euNtRM6LM0@PQ7R#nHL|tH7P2$8X;mpoA^W~($-ZVvMPy$l z`^Y+$Obo{Uea=zQ=RN)Y`(4-fkMH}s-q&0l&vTyhJooE$?$>?4rpFhR<+g0xxe6W>^P)6XbCBHu#{oJuR=!0RD4jxbX=5yy5nFZCeOpx(feKmw<^k z1;0FQclNTKn$=A^$E!9*ki}KI+qbOjZkb-=buhBAHMO!76+R+-^boI!o!#vdA|fl- zg{^FiMfQ0`DnJk~B!A|Vx>LwVhlhIeWcSjNCp+(fN5>D>U)+wqza#P@W21IhZgX3s z^fO1&VIQG^jrRj}#zq3>bdpJdij|Jdvqe&|F8A)smqv9eswgcK*^?x7i5hobs;jdl zL@?-$%cc%b&Rm}xDHIpHlov+unHvoW&EAw!#$lsJ;L=AMxE-p@pa+ivaT|)F;ZGSN zuMGU@w3!YCKJeEWnBgC}WpVHac?SyqYg`#KFZ|=lhY%h3AReJ-fPeJaEdzi2zoGxX zZ_|Zz5*>9(Ok8{@+DE$n5er{qngON(02CrKwYIjZU%8SjHCCi{#c;~j|u{0)_H?b@{kON(<#HDr1EN9-I&uHBC*nZLs=dh=<^5%MFWN`KRZ*~vI# z{2FgDo`yB~OePcN!G6qshFHdvxU@0+hk>?|rVz^}5zHISxqSisPZ{ecmcr(8r!5Ah z!OiANDpNl{<%rJ8I#y~<+^}XnW3Me2?}wld+Z!?Gza)N9@KzQ_ujj@S91XiCM#B$P zs@C06)0xD?i}imF&ic7{EL76{a*d*{u*VhzeMFut`vS@Z(tyBQf~ouG+bJP_Z)%CW zp^}!^z%f1<@I<&0wYEWZ%SgR4&XW29$;1i;FH!WDGz6u1gVE!S5prFU34`Q!0#(5= zF~fXHD&tc042BmS9CAn-$Psrq)+njq*8iO3x=eMQdnX)lMdZiHD1rl5Q|R^U*;DV- z&pv$cI;<~%ff>)MpN>*gKlcg@<9+vT&8BdxE{B|KS;Z#;SxZhY!$Mxfc*gBbPzjfg zGoBGbf`u-5{-dOja8_lX|3-GB1Gef}BzboqIB;a}Zx1O3Pc;oaU`i5mcY38C7!dcZ z{BT1i3Izur4)=Xzu>Q$b&x&1>vtN2&V6UC?J$?69oblSt%MOizB7uf#bJ zYuaBWdqHL6aU>Q|rvrNw(eJ7vIiqGxT^4UVdnf#X5`#N~1Dl0U)3eE+tYk`_Z0^+Q z2tA>moT<8!GU|})+=+>z0-7ZDf_MBF#`C!1p3uYyA3XKL( zzBS)#y=1TBtR`nVL*Zn-(c>#I3yA=d_q z&C?pxO6D$XIKt9ebo=i2=u#z>4-!bGB4(Z_(8c~(5Mcd*Nt~>8T4O%#4(KRh@y>Gak3s18|z8ClsnD8p^P8@&t z#^n`rSmwqqQF}KPQ+~2^gdM*;Jm9USzVt56IFb{=CH4M9wX#r~PGW~#wOLEnnLZUR zAx^oG*DOE$ylF=j3OhKMPTbpeY@c;HNj^^jSyPd}k+#5z+X~5@QZYwl*9|t! z1QzU03MXmjty{jXdL>>_FxMm1qR&1k{D`ig@jk(0ulXF@xR9)zW*hY-F$7iUE#V$( zZ>#F($7YP(ie*^7mXSG>i_1rRsgJ+f)H)JOqMmm^HZVF$N#(T2 z9vKLN4DTChw`Qejinv}(`B@rGsVF-3>Dum}3f6ZeF0>FHehelajw9}{>|Rpsk> z)$`A*D2JRNWYIpe-!CNFY#d3lvTXS5Y#QV|!QAkYcnaBzlW|{v#&UP{*VZPuw}xS~ zqtm<&qc18>8PBi-wWoH zEiFB)+gQx7Y(>a=aoqNiqrTiO0}&3Ip_-kImvp2T&aG|9>)q&q05+lw26KFb?m$Q= z_WFCFApc$F*sVp#D;39mNmV~SUJ=t!{+eOIp_~4boM&(ty~r6H*hXSdmVpf^KSRKx z#Y1&Eu8cJE8_Z-?WHVBA{swdV6;JDr!tYp-e36_ntYVy%m8ZHI%Z#$o=Kazl?f5A zOCTPfI@>R95fTs=D8Y0Dy@Bp7oNO2l8+nUlUkjF%jb6#gd+C|sY{4s|YXg8>T8gzD zvt9x6DDO!xj5U~-E@CAo{{b@lIpk>FEotOrdL&=tRkZzM3Ddh8(%d~Rb8M(Y z!V(#0yc>@^tb0Q2f-iR)_M%jhvY^$_0ikgB=)1RcZPrd82@a#C*A>-%*G5cIYBGG2 zWgVsWa>Gkp2}1Mc%L}JYpeGA!jvd{9+GrF10qGO%l)Xgr<$<+zef73uT8X!pd>s_!no(bim<~^`5d~lk-HNC*q+S-L`f8aBP0h_NJ0Fk~ZFPj||u-*RRgT z@&0UDW-3+MLc#tC^NG<$J&*LL4Dc@FJ&z9s`CTy#$jnp><_?Jf6M)dO+XUl@xaKzx z>D>K{ahAxz7wLkOT=U93b*oEgzwu^5h4zPV|7x4&bw^Yx^6d(APISSf?QA-y{;%SuF@{Q9cw+nQk*lK@Z-F9Q`$g`z|lh= zDTgBDki*sQy=Cs{zg@#*>F0j5B%eH!k~}fBpDYMyn@-NHz;%qfsM%#1nVEi}p%x}A zU}x|D7ujpGqN4V&va-63zTLzwyhEo4!@w+(<-UN8jg1Wq1IKalBW=uYbPAYTm)>Ei zg~b*`o?W~HAKl^dmbzHheT;sRL)j+d=Tlz4h~x+8 zns69yi~5=;BO7yCC{wb53O+nHuv-n0G*RnysIq?8#hav6f_d}wzKKOSciQVm7aKAm zq_V;3*Vntt`&gh8xHRJZB?wOUFPlvjO}!(HoFD?S;0S=y zWN-j#lyu+m6KK8Jz^&KzWI6v%gG3%%Mq;0Ck4b;d3+|hS z@Er=SV1gaasHIy{bYA*J@hI)E#BL?}!4ZHn&-xVsdrsfe@D;su&>HYlR1q7D)7yUS zmWrIxiFK7cgu$tg?{zSaAyKo_V(3j|GdM!<8pp>*8tcC+j3!DZ8#Qe8!`Nrt+Vysm zD?hTX;9d4r6+-F5*Ng+iF~?7-ArU}D2qkeNNET6R;6BBieMNRmdI>Oo4Vf0iqydwG#Md4ic znXB2Ityhv;5@oaZ3!d$cED}PUypwhef|xJv6wVU#@Y3@G?^H8~4DQ#*@A5g&F(LQF ze{;;b1geR@NC^W%>d7wuASKMt;Y}tIbXrA7bt&t!0Rd#|l2-8Pv|di;uMxzRXwXXL zI3zMl)K@r(pHN5yb?`QTIYRo}{eVQOgV5Ww*N@w z&^tO9>8{N*Q#n2vMg^Xq>txd#aO#p$g;}Kc8w!MAdJUx_{LJ;qU-UYGy14n%mLV%F z8&2>H4uYWYI@*kcybneVc>@g~Ps{xUWKi0y>Y;X{(Se!F(Juj z%3f$4d;}4MA{vz<%5ZsJUAX|onqq_44*0Z;>%g94m-k6-^V=#>3+}3qC}4!Iqd8gKFLxHt zTR*c@+4W}9w(MY%hXFVsr`_;I>b`^vMWNKju%JvGaiL2yYI2pISqgvQJT)jfRXcyJ zgWkwUegArX6NT@F;0T-~;gNR1e<9hVXMy7hXJZ%T5G0L2s-u1d$#W$bT}*iBn_4(0 z-!_7S^Y$k*&d`d32ed3+stxzY2nY&QgGEati`KeP7P$^RTYc)R=kyA>hIo)Ywrc9L z8U7S1M`=xsF0j4zrnxg9sXUz-JQ4K(ye`-1|5l0s%ETb}@QE1}Dj^LZCFoiv-oEtk zsN7iqWjqsd$sC;h?~KgJ7W-^EemNsU?g&Tr=%Fon&@Get=X>3(<1{4KIy6b-n)VbO zt15DS1fqT(DMZvkMi$JGJhnF}JZC_qd2o2o1r-!A9^MDkw|xOuk})UWNKA7RMj9jG zH!k^NCpR>RK3NJfas#C8g)$=kj)BV| zDXA+hEiDh(=%D!rM%q}nnj;HC(|?zaM879bo~WyP4cy6*`YW~YkH@*8HJZgJj$ZRg`wK1Rc^qlPP>I5DPnI8pBha#ky4Zxo#i_FY-5J?7gih|mP&}bnC7gO61&{u?!RWR&_90aXI*z3KOGL)hT;T3O9ndARJ#{n7KosgOY(lx66iC0 zd=y{6ul=-4wCq;{Sq9H119JIVnz@|35#C>P`Kny5xa&UP9en#GRiuRq*e`*}`uyF@|> zNgE;}g>Nb>`q8K`A*wjMFs(=$K!(C)a1d0hB*iE;Sm($naH1$pVPgM5((HNg3c(x0 zJE0r-PasP_0NYI57-BIB19=qF8N+Y9mlSz$LwnOMC>0i=@s0;D*2>_WXp;WdR)~5CPK;KD+Wz9B$xWMh)XnF~2a1o1D7}4g71a1R6?m8PHvfZ_;A-H@)b$1- zrW(q3^d_q;T91n;iCS@AgQteeCm`q3suZF5jbKT*m}=o%%_T>KT^Fi(!ld(qtaBhe zF6^_YARzT(x+YqkY=xlE(GioesvYsON{4@I8_ksrU6 z5*e92V4K`iXeDH5Gjh#*`b8$TcXZ>~`|FbT8b_&qUk8K*hz{1(s9gWh`%n-}>#2dX z83|DgJS(p2cB7bJF8*(|4YWw)LwB37@29N!ZI@gapz;9_T4eLbA+t zs5C?oIwH7hHkt#3z&b zfoGAt-st8{Gdzy&iH}b}Y4`KJeSLkT4#zIBB1UN56DWfi!KB*EF)b1hn$;;6A0o;j z3Pe6CCRlo=cUy;b8JC%r72&!MdVg-)LaxuJj~`7M;*|P7)8Tk#zJJx4HQ<~cV)Cpp z0ptd@xt{at=RkPE&kLPO@FMLAdX9-vQKxG#OY;P2|Dd3xiV6kUN(k@qO7dOf61LA{ zuYB7Czd5jiHjAJj%J#*56w9Q9gj(PR^2smRjaLxg-?Q)k%CRjII4(3L$JmX~{DzvL zj_td4y-^Gh)| zN3tEd93kyCYqGSsfSst4$hK$`U+8A?IE-e2Tp#{}R6!B!EW91ZELx9QbQWoY^)^k7 zw(VQ!qjn@D9I$B3l7GpsB3{G{xyr886f1%~HE}&el|uCjd=1AKEKON0#ktK*ktn2w zCyyW3#mK+h&dqI!-3d|e56!amvAs(E(vm2_)*fa0M`&R4Q2%)yq*{tn!olw$0 zKt~i9mA29JDbkC4RBPzhdqG{HT?V`h;QC05ScX}AOWFVtZU3bqL7-In{=i@8O5B|_ zC5v1iMn-NK85?uz&_f-zwE3IQK>-2POPA^h+=aKh??#F(-h=W{B@3fgVz&Jk&dSLR z=Yk&`7iZUQV$*HFSb_agP*g08D_IY{{+?j2X?aJF9aQi$?>7Yb9{&cDB!U0Y9P?3_ z#h*Ttr1)@w6DMY+M9PL5;%mg#GeFcmtKX;RhMML??Pb+5phkhqvV2sT4-et_ZrQ~t z0)$I5AGRX}*4JoUOKX^a+N~ogs2%wpNJtRXn7*9#k&jQwh`|~truAIB4XFP*seXT~ z2nZ56htqvfU$4r_#y0fv@iq{@ORZ(I6jJoP^-Mc;)G?5_%zbA=eruQKQc=PJ%W0#X=6ynZWgJvB)F<^IHi zf>z3AhA?K%rx|05mup`cn(r?aH}0hO_&hWKC*pv6%uF3KNg zn;oi+rsgsflDwsU)W(Oqn}J%4lIJGJDK?B*^ckwoh0Q_qVQc!IbkLynJbg^CW~y#t zzU_d($& z*tj?aUw;vU)V|69gDFCuuCR(S3YXjrOVF0+gwWLzwEGaa9U2T;m><9l_|ik}kp=J0 z=CAEDvvC-Dwl>#9$L0;-_BcoeXV;LVk$gb~=*F7Mm7m(_3-`rEe#>|eY(L?k`c;)* z0C&7WyY0&lr|*I?3PLO>)S3S}i}D8{JX||4Kr{)n3=XgmyifkW3O^YL5Iwv~sH20K zsdemBV@Okm>Env<6FTBBAJ_q^9UPAh!Zd`9fgFuKekyk#BaB!`9xw?i>+0~{eDUWW z=>uAX1q{qTL_CP72bN4;1B-ZUt@cGf1yh1wCcViz4#O=EX0ER7u&qc5z4wH1EV;f& zUZFs`9%LTq9SfkB(n*zl&0bPIgV}m=_Hs6F+|m3RFRtzev=$<#z!RT(W?&D8qBOdB zHC-ldgD)Og6@b4Sauh^-0_Z;xR=sE{A5Z~I!+D5*ln6!GVBGR}D1LP2rPyo%EaENKn@*8H zjMnsUx@`3Wo%%J+LN`3>wl0D3PAP*8ux>&zyVR>~8m$ zvq-mGZXSN~4`&e~tANAZp-zh~Ls;0ze)x+n|79!!SrG1h;BH=p;=hbVhzQq(PPKvl z4(Ll1IW9|+rl2?5tBUP+04V@M!Ic9y<-B$5ItoYRvUp*F1uX*HK>wBzft6_Bxck~& zAAuIe@?Ww7OR-x6f?k6fRYv5*vZ1n2^X26bMs;@S=Duw_fbrjiJLcy7K*5)}*yGWg zVBVbR=;OC{vU`ymge5#LQ|6lzT^su`a}V{exQl*5Gk5C0uLsIGgwrB!Jp>J&vIrGM ztIg&sjI4Bd96!$08>Ev;RyLn$ zRsb1h#Z!Txv@LKT=M|U3$$TO)B=zDypgs& zh=L;GK~SUgX~}R9`3gM)4)-e=!+jnro(fWI;&6@shA~`i(#6xP`XHv=G7$YM&~7yJ z0`#r?k>N~0;SAA%EW*sZirD3Re2+E#as2G$m^qdWTnAn$ahhhA2MU7CQhb^u4R;(A z%^<~4_x&>&qqxD)v42>D5S|mjcwPZieUfYYk}Pb@IzrTqVm}3f+yyN%f{|7uG`jOp z5VQpJZQ?|Mu4QrF#3TJSdv!4ECJ-=+)7X&Y5gQIW_h$ACw5ee$P2#JsNeh0YPa-)S7r3UEw) zN;?4Twt=nShl{wLwq0;5*ouM;2o^BjiA^*~P9lgqgHOrFp|n+As}lDMC;Yu)~N+e|w5)UlrAE-FOY;E(l)l29 zK$TFJe>3Y@O<4p! zQhqV)OwY~^|KpcMs($tC!Pfz&N0rkw$bqU30Ljd|jOEIfmRKbw56|7LBWFcBJ}c`m*l)3a=RtdRxhAH2iT0 z%#5N&b9wHh3^w0;;yt*USi^jts0DKQ0=A50;pX(Eju*R<4Qk|<`}M$35AgK@_wWB{ z^8lSr;FiP8yf_|OGBkuYV&TQX1_Z4pYA`KK=&1)*WN4NBK8@Spy7dM^Scq5P;j~&$ z!Pr#LPD_0a+;OaHu&l(H)qV=d$WI|U*N?zcESXQ&u^LTj>Hu9HC*Up**9~hRsOv)^ zJAVB!phGXR6$+?`4Yw4QnVq_M5@wS>tiu_2VQ7f%4D$w7^bHgXalIOtrdXc`83BWL4`afH?f+3=vgY z&LCh(pi$H5j<;|?m`jPmgE^I)D(}*e&Qp;4 zoVZZ*dFT9%OdHn8E(CU&GG+9$>a^-iM9lQCt4(@cecCeW&%S+2{u$_j>3RbEwV+WUMzj!&}a2F zd0ahHvr4&n%{IB)@EcP`f`wN>XUS62<(|k)nVdrr8F&Z7Q;I4FD&>e#|>x5IV;x+ha$4D+f1l_~6w z+;!%K-b2-I-|Dy+B<(3Z4eL}8gA=%3-O`K@o;$6!*rC+D3zYSUT?ye8nR&^AtMr#Y z0w{RDc!Haz|MNXTW@2Ebo?LlP{MAGSw(%&qoJE_sLtS^FJ2e8Ja%FAXwR2~a3T(en z>{*aqB!U{AED54(VZ79=I6GH!CAy+FgLyYO5!Qv*gRdyeTp39M?Q9yEFDLOIS&(xl z0yay13S`J^(8B&+9JWr>vGcr0TCG*0XDAB$m(&PeBQiLAayIR55^w~rKxIi=)kO8D zwHs7jxy3F%Z!-?p9-n8-?asLy7>SEQ?MzlzPdEChNq`jJ2onFG-|0wFjMst*q*Jb9jg>^r}3tr+4lq2*Y;Rkl0Rkm%Bu%D8nO_rE&iri;KOuP zpYx)WarIYc6-NFLgYnQY$n`Ypz9uPPGTC;rw8rSX#n_<`aRJXpihv8KYF+L?ks2*I z(fbIPf<3n3u4oUB_K5k3K)F~KLbVY9g`o%wZ`RJ_E_aE5s+5{d<~hA{+dKy;_OTAfD_oCj|5F4kG z`LDcSS zp=#lAqf$rb1fy*#x)&-yaVtYd&+s5gT_A9}ku%YycV?F)Ic*%DA1!LK6b3Ilf){oF z!!awXFOKZejq2{d>NRh0=a~wCQ7@CUWd<5`OS}}FXs2X zxt?J#or>J~4C=nywFQ5R{A-<}&f_JV6QK>><)6ZNhOSi6LCm}(ZYm{Hn!>?X!u>=2 zz82rM^TuSwjez9OE#;XQ_1tMA)5SegQ>jvMHGD{K3P`g$N9aCqJEs{LTh32bv`-+OG^ch3dDJ0^YxM}DC$;Me~1TXMd0-6jrn*8bcu^q%bEA_7_CBYoF!;t_E^=wSs(h8 zem6#XV|J^;r~1VHZE}oAymLK;NW5&A67jD2PvebKsnvHm`1>ExE#H{&JTA*tO2?t! zv&8ubW@c27V<~Vk-+BBa?9Ly`;;YZLjfd}y7e>W|Iyrav>mH4lHWwEkn6j1Y-&DHZAAV1F%Hgus zY~oyeZG~xkzGiN!oX-G#&~s`Sfi_hJ6|==<6|^rL@5i}%d?!3S zODtfiNV-zX>MKTxdZ8M#$PDL+h^B447PZ^bZk4l6gvkdehuSP*xWjuGTT0-VBEMjG zE*_(r*y~-UUsdI$Up^QqmOnYdk~pfT2L~jzoc=uLTjybyndWVoaeq53`&H|z_l3M7v$`5C2&_rus0E#uiO-TK&hqTdgF2W2t8@HvD3B5&H5 z_os<1JfV)6-m`eO3|WMBFB$mm2$NJzy0@HTS33PQl}JK5E_ z@jaZtamA-`S*Nu+mu^Xm^_G7v>Ak<1r!js6^d_eh7@lj!$1IF{nF zZ*o|H`;&CGX+vBife?Vchbk)g*_2h(=F3{Cb;Ga5pMCo+*T zSQibiVIEUeRi&M6rt#T_Cmeh$J7XYA9!T%dfv011fMRXrlL18P1JH1w2;|FJ2S8B? zLAyP6%z%H9;065e0{r7o;9pZ^?CtFnlaeYPZsDv5L&3@5*K9h){r&wQon;t9>Fi*3 z>BeBIcY&ET(o$}-6nW5GAqx5;ZUL{BP?Kxf5u}}Y)2RIUZc=l~6~>cf{MXk<_&6mj z+egRLH8f&VQ-8R5c#w(j^NDviIp~F!I2LrJ!R6)#5IE0eut`?Y1>W&2O~jeGA9lNr9AM9Oit}|B$vXxNBeUOBH2V zS=rRW9|ec57gDshpXBZs%Q9`SnI**!yqo)>2+ZvQhtUEF9yVwnP)wsmLO zN9z{6IYjq6b~+E}yX=Cp7#ZKAh_Xtr4`0o#l;d%;%K^J#Kiq6Ys*jb=XxptWf7E%r zmon5OsOW#FrDrbmm}x!tk}#AK0MQX;n;IKO9A-S263r|vm9JmFUSLP^bzhhrbqEF< zw0Xy2-e=FAxww{q@4mP9bghk;+{z1$ybTHp8u|8?4=BrBqq7RJzS-u4xSSjZf_+m{ z6A$&&IEMY8hK7cV;~^*o$UBBU&%ko!kAnW2j?{3BhdUVVuyAswp(S_sUx*Qg)8F0P@*168Y*$CGyNzD&&%zi^e3YQ#BE%J^e*^wdcw&NeN->+G)@^O0kJ>R$Kr+)g@yM0=K? zQ%}!2_`WmI3O@#0#-_0H5eoDV%B>WX@U6y(njZbgdVzqxO&y@5f^UR(^z7(Iv3nT$ zF!3}MHTp5=kRkS zRU>rmA7~d_nd2WS9hR(kpP`=V*s37(GzRXCU~t-HLbuoTYha4$;!(=i{*I$BNFFcJaSO~uBH89+v?-pK{lNi3~)(9jb4g+kuKBkgGR%$5;{`O&-yBj(k?k#o8yD~*(G5zfvvr*lr$rw zGxt5B+eyDkxRPI7mgmR?g{B!UvU#(cFMQdaY%CmBlByP@-=|X&G|FAxYl08&C!0O} zl*YFotrut<+A7SS@9z;kzo|<|2=~Eu7dtFYeJxCY7gyww$UNq3d1bvAKOgDkx1B~1 zSD(8vep!c(xpQ^oX_uZ#{7}wF>NeA&h=#Y1Gv8h!z;yPwYvIXm{R|f0^Tmp6cf}{C zC$=Rzooy|k+=8HnK)98ap%KJg*FF>FDR;0%>g?O&oMK1c&t(zLlc%%aWMEZ~tW7^M zD1a9*j?1}hVTxCgJS+)I6FYlsXDBK3UaCxxc%W@NFfv_$&cP-uLw65ZV*VS|$&TCg(4@4iTYwQHK% zN_&=CO}pntO?3pa9J2IFy$ISecb~@GWg{PZifLJnOW?!fWH#q8v$|o{vh@|4?fDM2 zbhhMu*YJ7dPZLu3-fOtpJvuYNm~$xF=!()SSu*%enYTOf;ugM`tbTnK&DdKWre7f_ z%M*F;NzC0?De}`z$4~NVMjlc9xm-W@df$UGt(Krdr|Oy-IYH`1;d$d7fElb)9sw+-Hv8uegcq7;-hV zwK9G8e;=-;T_$xKvXF+K^lhivc?L(5`T+S8LT6o%=SA}oMV8!*b|xX(mSoTF$e2IQ zUL>Dt>_u;Q-->;?{*sx0@a}}LTb-whf*zd0(a;jelQqvdR%ojZnU0KEzbRYj$%(XA zrrW&rvqIDPO=LYK3RLxJXMLE>KdQCVX??-bc|o`}q`8Ld<_>6%*;~%P`-%5-x_!yX zGCd+!UM17SFvT=}7g^?~GKMeTJ#KuHBXcT^^+i(Z#JSYWAI=lwUvf957ueT|<6f}K zG`#W=SfBVJH}UD!+)@5BWRbFJs*km>X;`&XcTTh%^VA-@drQ{uA+;cmhEN-;yk*a+A1aEc*|alx%`oL4ulo4P)~@Ve(?6YdYH-QNxI>s62CtQd^(`^5s# zu_Y6^GILtLPTDr#7-H3X78RG8^CR&-y}sX;!)v*lFf2~l`jct~zpv`nAa(QkKaT3* zPQ9EvdVnm_IAyRdmyRE81#DIt3muQO#iy;DY(BTmaUURCU))O;j@*x%Xzb@cq--j4 zx_JWUsKs|za2s5hK3V3HceG0qFNU0n%E)pWv8`sx^53oVEQX4Yti*9rFM7h)W_}7b ztQ|GDBl^}%u?{N=+$jW;$ZaAV@k2st(mdb?NhvYkvPK*+}bQb)(EOJ&*ZSkP=s6IW?j-a$4R%| zy*G#YSj}|N<|MJDn_npu3?5}q5!sD<1avQmyn7P!2S$b^sz;+D5i!el1#8m{ih&co z+I)eNx!-pkN-;e|An%;whRm(f7UrjQ{RSydHII&d-xYlm>|lG0G%^z^!a}tB-0*;I zk>B>#gyl1I zDaU0&TE2Tu`)P?mHB+B*kZzxj;Y+C$Q;K!+sTLoQ%H{Y!K2H9sUWEUeB2`vstJ4}n z*h4;%cq81^(kNxalCXj7wvUl63#X11-UrAM=0G}HCG2?RXm3^VHg4`~vy-ej-yZT@QKB3T zL!v(I(XHHU2)P*9>|+)KWa&D`}ZqL$91pXmfMWu9GL{n~M8 zv?o7(L`(9{5p<)0E@eRx(%UL z)Z^qL(Vdn9n|s5YrGx~w<%4y(QjvEj6>EDg&3v|CAM;JsC6-$L3d^h!S%M517g(>V z4t4lv3Xa(tj5q(peVuDrJo@I+J-9Phvf($scV^4E3Z9#3fu+^R2tQW*ING?R`$`jK z09jcXKj#0aM6O$#pZ24V`=yiVo(`=cSR&bWFJ0C8+M-%N`|X-$8dq@FNRZ}&dI7>L z>??VEOR@3#t>hHbjH%0aW7s;CI%#eAo&_G!BwR%2;y+NIA3E(BZ^GN4`tB1oj{-|F zYxc9Ju(N$go8l;#Gc($2{q2hmo5rf#b8HUCd$64jd4eesWPvpY>o~SH1T6YGS>iZ*^3M<%@Bv*S^>hWduB#2Lcl&dy+()I4*6IfgTEl%X$3!zrmidA-_ zL{fRvNP;=Tg4*RhVlOw^2Pa?G#U$?H@~TW!9IfsfeLu z6_&{O8}K@J1azuy0A2*)-s{|xaGkPD3#_UcM^u;PGv{U&j|R#kz?+gsD^r_+bDHSV z!{gr^cM9}x%P&+OWn_RTjaY*8g#P(|MLaJuHc*(af*c)hre~EY+O7#|Ma-ES;dj&1 zlP6z=pgK2~w>c|lR^Yj(Lxb$Ww8igV05&M@AWfk7N}T3Yh>pm%-R|eK?$*oi=IF9v zJyd&u0&& z0>7XBIeENfyF4fV>Du(31Qncz7GJNMGsEX`L9krHC8XiibA}V*t6qi=6JFgBSdaO_ z*cN|xL>9Y<#w1iBMmx(TXfe^%TUKGfgmYK$miFBWR!x*aGMi%R-pQ(IQKl;DS5NZI zTpqo5h=uH%rjC<`-NKDWDoN7#8?rG2I)7Noj$S5N>;bqVgs@A3ds0F%TB5&0(NW9X z<2!6}Pk4o-L5wzl_4ye@gYlRS?hoT^c$_|v&)5K&LJl*%Eq*t_fb3vmjkLo1)=W~t zbB5rH9<+#->{Lz-S5Ro8wA)TTwyIp68~&cS7okp#sYl7` za|mA3%NmnQY*@2s+)D&;+az)1!LibwaQ$VgXWu;{b6-Z^;2&C|uwY`bfvvYnBFW?y zR~O1bgtz2n`k;k(IOhR_v*x2{cXzCc443+21r1yIS1Xwz(uNrA7u;cu^vJR*6cwU% zz~-~S_Ad-A1hmS34cW_d5N?dHWn=ODF+1@1iq>5&SI9A%j8ixye_S;NNXcs~x$FBf zeHjKh12T)KqL9K@BK z)qVtFolC3@bC*<5kb>GZyGJ8hm>4;BWDQeyRZAav$_XEfwLOq@dKJ)7=h z)>Tb^wC_20W!&g6KseD5sfwB78{>AAiD%BzjZ8R{nJpc|fvbS1m_R`uSFTF-EmkF#rkOur2<2(+4%8UYsS7~8o5^v#AzrSf_a@H`*d2H$&Ydh zb#|N%3(5-RX}%M<+-E*Ndb&4Z=sFlb$+#_(YJ8XlXt$}n)vI;XNId}F!*v(3JpTJA zI$OP7(5F?P%5bc>!@qN0bM`(xxDVHZ=mSxCg&NL6y}~*@>?ThpTptGO=0XcVm1=poCTxeZAC!@-u~}3NHVKRy^3snV4g6 zuFAo29(C<9)#V^{WYvrjQP*KE8`6J*Dln95l@Oq5L4fx4a1pn4XxUq7aG5o}NJ)sH zp&D(YFYkWV4s($dqMJ72rhWauq;X^7WPzS=nsNuctX7Ugam=$mw?D$S!h*lB1OdYP3R)?&GNJ-DCSApL|iXQBG&A z%8Hl}EBF85{0xng^XY%as};0yNh~wNY2@iXCIOdm8@;~Vz?jOE>V(=mXk+h?8dA0u z-+=^WqV3^;{py#|sxrgNTTy~S+5Cb$9AK=4xAlA+b1gV^asL?n~j&8*2URg;Mm*^@YvXz9`+E@}r#$L+1b0j_J z1({PInb#Q9kO)qwB15?8GgYQeZLS!6t3 z#@^;($Bl(atBt!ADxQ}kz+x9&n8%1X^7PlH&*;pS+|O;_md~}?Xvo-ctYhvdCQO0$ z9h8~wFm^ZW8EGNcW%!kT_S^u(X8g3LaWt4Bg1Q>taS`1dNTi?5v$TFS+5?fLB01u% zaYsP9n3nw6o%B9I7vedyFO^5yB;rd!CR3T{!5&wn6{2Hw%amumPPW-XY)1F5*lfd} zFFtBm4&o5#O;0$o4{xaJ9mp0{^Oy@0N3nLzR>;!loXSaU`GF|T4NnbJDbZ+vLlf{Q zI-0Ui4l6=Ip~km~*1!v*!_yF&ky#Z?kp`WN!C^u?0X7aFTbiZ4!=K^}845-WKsW7r zsHD5apt>t3Lc?0Qf6?sR1ak^Go1~~YNM8I5f-4aT9fR*|Ew~})_CkO6lfv)8KVbaS z2p0He8j5cy#6%o}btG(=5**xLKaHWnXvrd@0U8WH7?Qm-%h1iAV3}oNAS9Wc@Ct2x z3yP+%e~L*Ai~MF+r{${F$~Tej-Z-AIabK}WY&*2WN(|_D-z&F1Vs9w7e6q#G?=)xv z<34mM3dm3p3szZRlFF-SmCcYdzL8B*NG6hZ{v6>ISU~X|WBh2H_|o|EhvfTWKSe%x zbHbm8ar0|iiXLol9axr0Qz|zDsnn2TX|cG9GQ}Xi&vDXE-T7^<`oxsw2SwhSF+U(l z{PL6zBU}nEDNzHSYFt7iS*!EdNTk#@2zCkNYDnPgd~TFA;RW} zQNUriEi)<^E{i#$53^_CG{NTJM_D?9KKXbBAEd)%NMZa$Co0d+aYWv@-DzpeHpJ;x ztRPyvlO4@w=PgMV(qJ&_c=r?Hmr`T^S0GG>jFMwEzc7XPT%~C;MHbw3 z2s=d57|{udw6S?~G?% ztJJ^03Q$^;iF7>b-mBp4$>*ZFo~NqO8Fa|I#Pmu#mp6drPT`UHk>;d+MOd|WNPW0k zf#(-!kW5XHz@ri}v?0r`Ql12c&}gZM%&81dq5PyKmHYvp?5z*Ezy_jy>q+l2mpP0t zlQ|Vi!qQ93W3l=18;xxFiwZ7K04f;PN1Iy-KpD8>^cJ3mzRYcRx-GxkE}cYN zFzn78?e4^5RYx^*;MF7A%^FFY90v)mXmfn%*((h7`TrtSdIjay%dGEf&}MW^2-e@Nj6pc=sdv5SuiotF*4^Jb(pW|#a8S}TY|pf-a` zlkhB4&-E%+H+Ay+7&W#vL&5P0x)c<0OV)H5XeUEH>RDZIF@*O=d>c&}$NR=g92R#P zPDUWieg3`)g$$r3p*B$0ONeaZ~eXO2yJl5z+( zCRu|thQi1ckt&MTeco4q9Utv@yg{m9Vu*63`s0`uB2{Sp0(X2&aF@w2I1i)>1_Ae@ zR=btOTb_$we7lM)BRILZyl0*?n6!QklCUv5h!MTj+uBAxBr^$2`kvQ27Z)8=A248GS%r1+rS%PwJ$0yaSBh* zNKa2sYnRwWXJll2b1wIu@vR>`gWH8s#(mT(Ix|{*7rF)0+#24wFPJ()5Rbcc%<5#XC%|Q2eO$i2S|9C z70=NGSuDN5IZc-88JEmq|x>ho>uU8SD6!p*c6h)Yo&(OyzYbY z<6PM-E9Jf;YqRCM*f64z>(->06vgNaRvdn~$Ds-LCC|7%oI^swKt6(3;Q0=0MN>Un zzRByk?D~px1n)?zc2m&UFCfMgH^C*fF7$inIy9aeN(zd0pD0~gTb{&KP-81PFXPYX ztwspgXK}TuYR-RT5<0dA9$1SWC?_w!@8ChVmUpjeYn{Hm-$lnqiLDpec|K+}$d|HC zcvZN#wrjMN7wQ(kJfN!;7uO%_$Zj3}n5g7x)m!Fd(LMjCt%rbd-Ck*#^|=r$IXOQl z88$9f(_&R*aIdbG@{jr*+&O3nrsw`WeScZthSPIuSI1A34)J>|+C_V>{apXK$Y9_R zKNJgP&VH}ugX;$hOq)95r0&4lo3JXg+87l;ND*MR0Xoz|TdmOX#1C zevfoml`OJkMNvWA<6Z2jh7&!da!N{_IeO)4T3SCt-dPDg+rEAK4xUX|zunZeu#`WW zpmeTff{*aN9oTIG;0BT_oy5xX-Fxq)iDF7_0@&3c1;5T+-A%}F1tQELcA+MNYl2n6gMz8<0NZw z@B_mwjJ=tjomA+(?xp8RY6m`spv1wQeif>NwC-}G_k)#=@6vuTq}UE(jWrH#YZk|f zU8Uo)pC6eyXE0*tR)p%CF43)Fix33A6=f&!TXt|m&0B$y-;CbnB^ANoDi8^(^+KGh z<-Lrpx!Ex+ol44vF9D9R9Y-WbM>=$D+rQNM3fz28Uu512kdY_h)?-q@Au8J4EP_q# zglZ_6W}u50`)s*1vQ*GjZ}L@-4scw>7Cvn8Ik-P>Gp6`9pSxi_{;CppgX;4BUD}Z% za=31OgabrrP_Cr`f4(b9kh?oppz3H}obhM(uro=OCIRD#Q;$$;gh2`B+~#tZNsaDe z>wV13S-s^h>!T9=@fWtzWl>_2$W;PZqw-HC$0jFf6;Xtc`hGAeTGXWjG(Ypug?nkL zsfyM3ho1-S-M}Y2F$SVy3GW+YP^$I7I`Loi)cru`f=&=Y4>dp|)@M6CjvT`-9~%^i zT+=TW@AkL~afB9~ZVNcn#ok^DtU3T5Bh>FP>%F!Vu48jrhzhHC-8Jqjz)_p79g*za zV;|l)*PY6OTF7;2KY&QtNVvEZ|2P)4P#&uovg=_|!It1EE4E@2C-If%X3^ny$kbG9p{E>Yi&A2@aAtnUF;Q^>GWo@hh&5n78ic` ztJtb7NYnnegWbpw*Ak)FdN$&_`5K)x^ zwrXug`t~Jf>vXL>!EG5@C>$bS(gY~xbpOkJjoy~WkH$EZ)!9Ie3Bw#fC{wDLGxK9F zH=py{VV&^Pg-+A{-F}FVF-e$Noeo(slm$j&`zN4y+%^33*6Kc%T~;6yEEvt@+Dy&~ z)WG%NJk=Q52GE5Pz9fUZ%fCVA5 z#|}YIKWYY-rF@ZEX%fRGUz?u9rVN?pLpdA1w4qK6+#y*_x+}oALaWV1m`evfeKPBF zx~ascELQ8oMIHm6H!2nod2)g{J^p+=!-#v%E)#1d5~m~M+;Sm;q+%9Awp8I`a3710 zCSAhK5^B@yv?JE$%U_nA%qxO84QoRQo;WI5L;(3cf8r-&&*@7JDqGU59bbSvu{I_c z*Pp1#IB)TUS9lJRphePy0%rexX`vK4yRy6fY? zbMB<6SGoeWj9Hr(9ySx^5sQb#Nui);GpD64-oh=0d2G7?nkz^&WdH&CJ zRbn3pJs1XKEorLF##T6!K1kZH{iGt545Lg|E z*=~MAr~*0%QNK;x<7|$T9k1^`Rk1`Y|B{})Et+}i9$z(uOzoTXzW5wgQ>oLAs8kO#Q}n< znzk}@hYbGD?hp)%g^q$6j%(Fp_aPz?(O1tDvSCb;UoI9vTZ;t~tKqXN=c$}FzOBD@ z5|vu(UReGrF+n5)phGlehEihsY{49<_(re0jiy3rny=3tl)z!cR*Y{`~7 z+GUjT13%pXp*2UTQ2^4j#bcCIl9GtXqvH}B>9=$l2^P;Ly?Sv}T80WGQiOh(OqJzx z!B|egC4+eNX-ri{t_@?Clpqr@*_hcCDry2@qc!AnT=B=htE{^rT)2%<&|j^~7~}uC zN~`^?ClT^3{7juVuz2b87^d{4fR{kXGgL~4)R09?BU$tKj%M{uUy)(569E%eP~rvk zE!^@;6O!4U^uaw%8bjo&1IEq{hP%>*5bMP$88%z7IB2ctnB*HvZ$2)CH|A{Ep=6b(`mxP81_OA`pQ0nG%JBxZ0yip+Mq8C-LFw$dEIV)`_)1USh~WEI!@z zE`$Hv5qV1&=8o4iWnAMvHWs@PMGp6IAEO`&Reb`y`fi#j`$%ELWk>5&P9E|Agmo!c z+fBg9rgG)u{>q<&&e8p675M^{RIF|y))gZjg^L!q-som4x@DWr%G9b;68MAM%?1Xr zhzg3asKUvr{UmX}!i7R&%q}^L{vMpo3ipuBa{1l)<3N|2*g_kek0kKE&;mM;ANQ%m+q4YObJ1tDJxm1_E0RWIcYlH?sR7l|E!Sb-d(R+ zw?H3(XGJh(r-A}4D5Cp5)U^DY$AhpDj9}#tW38okLZYM}zR%i#9fSradM69i(FWFk z0-BQf$&7%^`Zpt}=F7>-o-U-mJ#g7AkoDT5Y$ZXKw(*UE1Sg~d=qe6;u@nAgC+szo z*V3YL!E$9Q^aPkR#bis}+43~zHa6QkUyHh-pmf$(=JuTTM=xR|umF+I41}|1&xS=s zS(!cdW{2=`$X*9OUSJ(LYhDE`Hevw(3eixoE{Ty_;h@TDBlF<6ezTrAgQMyOZ41;R zj1!;slrMTl2&%zrg5h34Ro0xYtZL7qf3LDatsf#VdddIRR08wi zz9?V+V^awxiLh8g^Y5mTyw&c}pw2l}wQJ9jY}h4tu|uq$zy3m{^{l#U()lL6R;4|S z7UZT9&~j!nIKCphi%fl(_iuOy8((S2%h1MEoYaK;8pz(Q4F{g3{Yp=Ir4oW&Od`4&iE?ZCY)l47eahxm0c$q_srL&tu|soW1}h#@5=yc?9i#syts!&ixcvsXFwPo+aQdv zW;1H~8hW^{F=$kS$d}ftUl&po&MG%@ih`lOs~y6w6>_RW#$(TXZuENV3n{Sn0xLw0 z&Mr(`%S}6Q!$61o5MyCf=i|D_q7EtLwB1yP34}Bphl!5-XNbe>)+RVAO)*kY zH%mU$Y#+^ooKm(!Ur$I_>a2IG%&tQ#yEj2BVp}0X$tVe>VHK9ABDxxhf49NGWH~Bo zyDi|Iy8C~$Ik4HNhSIq+4 z05o0+)An0SWXT@X%E)mT$$qwV>{VYj8C9mXQBhx|gBYz-ygTJw>~~AM)B~t5rRx&8 z(u5JuQqvRnvIuLSq6O-uE2Js2gjWvsp-xbEItleXW%y94--Fh++v_v6jYm+vANk3; z?!}ALrJ_vrh7@@9`O3*Pe_V zT;$_9w)$m#wPCw?YAMH*vb&CaS@wf@ak$r=PxLoo!g|XY8SpZQ8`XUVi{$~D(aS; z;AI$o%GWe^oyIe!#67PxQJdB0FN}UsFZHwTP|WQUxVpM061tn&81SEtTL+&ncUE@g zHn@&q2KzMhtjVu*07DOO8B#xkI_cnLgH#FqUQOr;-_S)!6{Zq-mV;0S*j*XUxmPdN z7$t-unl}ae#29x|_z<9h9x`Flw|p@tKkE!*c-x^8z3l+QP)YJ3p#J&4>KmsD@0bMq z)t3Ue<&R0#*#9bQ?EAgY3v*dv?pJ4>rEbk`(m)0ev<>sTi>_@GKmH0iMgl>Sz#jK+ zEqLPzBhZ3p_Xt(xVO)y9em(26MwaYs{x@eG{7^@MqkfYHin%c=fO)Un#=#D*SgZEf z>QDcPmy(i)C5fH$tU$o+NIm1i1_fmNaNq2&0vCo|IV4Fe5wGjc-Mar(r3VqqYySY1u=Xyjx`DLngkNcJMhgZh=vRmrE zhVb*AgVuR;7x1ox_y?e1^~&Xyaz7>609wWXS`KWWWetS-M2G(kS_*HbqV}j;(mNJ8 z{9Z-NDSB-Q+ERMRZ7JXaw!-2s|Lz;!)GgE*6?)sxmQ1f>*ysc7cBkmyaG4y_uK-2E z9CGJ8Y|f66n%=eOAKOwO5de3etM(gS{#%t7Cf|d}(ZAv4zg2mmO9-Q+(jTnt@xxrPNtXSl@XQboV{-*9~Bot+*cGmy`M4F!S>6KY(p|rQ_Rq5c4pk2N!k_t+nup(GnKFQ*k|z#JHXq|JDjd~Z_78Jg3HA~@ ztXmy(`utkoghmi}dxGL6LXABG=V(}2=2ZjD-j>{0miW(K4}TkY#iI%G!hyrDKX5Pp zM~G&j$dEj4FEQIQT~ONhrxp%ULW~u3EIiHD;^Q-K;Y82WoDox_BXjd}UuNd*3h-bY znAmkH>MJfwa@pZG_hl?~n^Z3A1_a+=&Br56dGZH#+k8iDv#&R2yY0@H_UCpImVC%?F|2Q)|XhFIL^G_Y5@Yy;s>))=uBpV+& zSvXP~nit8;VnMnLgU_j9Fr}!WIlO1K%VVx$O`P-Uq_~i_;hH}Ab+e>?g!2%r2k7A+lQH;=0$>|j536|m09M@*f{3PR(#R{AXrbh*X ztsbcgV{~D|C9$5_A3h%HI^_^b>F$1wESNHSN~-9D!qISa?xY_nUQp(l^Wl->CIw=EC}lb z*;UJZNoLXxR!=K?yT4M%G$19Ax`cddju)t?t5kKK#e1(e`7cG(4$UoISgQl++?Qm_ ze20`yNSZ*5$*8K#2GOYx3Gq=ItHh`x{TK$E3718j(?xfv$@gK z)X;Ttxpz47)SPZ+o#?f6=p zJmK{&Q7Kc09L@aR`-TUW#!U?@SL1in(k!$-WO!?S{i^MTl)UIElV~z5tvG; za{s-|{O(+Bu`tr;=qPX(*{q53aE!7gMcxj5H6!yxKYzdWwvrE_LqA4EPc!}NU`dHk zu9sWt@3em5&3n)AfCk;|^~X=2rk6SlY2$!mz^Y^}UAp)E&!o3oa=sY5xizA+DQSS*e+ur=zz6h{Wre??%Fb8-%ZvFG^f+AJ{2W`h%Q{AIL5E zY6U(AzOHP%O8ret6a+>1hpf0!yX@fL-3>7~o%s0p znYlSNsQZ6!ip}jVu`P*p`2Od?W5+AbHc<^z4yjxWF1RtLgFE4LhU0n;4 zZAJXr>T-Qklai9y^dF7UqMcuKLG@x{a&B+hT325$4{bjmU*6d&>L!m7wkAikLp1fL zfNh`H=W*w3of5?h_jV{-TW9}pF3-r!JSKba;K4`<@2cS7;6l%3=hE_WOX%Z>JY(#) zlTAG9_3MkWD$o}(@bxPyQWqBT)3S

&!LKR8)Kz`Rv&soY!Hf5gL0G4;?zxRpD0P z?xL6Q%0ma0`h71H(m90Q zsGpzc&72)-;yQddVRf<4yw&l?r_*}n&I)U5Yalw+^XGk_pD1P1)}4j15z*1vS_LNQ zadDDR_|M4BR(ExM5#zOFKK{8Nb+uw;#RJ}vP>2#%c5`!+aFhmF-`9DL)qDtG5>jOI zUdijo(aUU-Sl8v@;TaEtmY>X&luI=GxtpPW-&Nh!)y3~LsunGL>wJk#ugZlBcOJ5c zroiKR&x#-A5E9Y^tzj9s5DYytZOzSbAl04kQk7!+zAQgJA|n@E_47NEtcsM>?a0VT ze$&RNokMTlyfG)%++L{Ofe}W)80^kKb3upCW|~6lZb5}7r=)J*zK!k~R7g!#RSuqrI(d_-!DQg|&4%fnrnDY$@o*K33KVy9)31weg^J11Tvfma2uh zxzM8mf`ZP%65QN>eu;Du-GU9{7g^BbUcP+URcIa}Xx)_rjY%9QPRMX+WL=e(zN_y^ zQkQKB=h4~A#N^)T&Bn->(b1s+S3go0#?}8*7gjFwS}m)cw(QI)YOP_wYei1C<_1ziT|lg7I>;oyv-&Oh|3#l*zEjgMzP z*mHtYR8&V@U45a#>&A@)dk2T^J74*ne|$Q`#un>-sj>3)+qcL}o<4ob@^ENqsH8t- zGdA3XehER;Wj(#<-Me?st;{xYiHK-dRaJ@H8u();i%8PJvnFXuvEp6%#{T=cuf;`1 zGD3F>*U_UFVVlt1qhf5F+~>XSDcb^4+{eb2JN@;Y_;ah*%8&v{`|mdgFf8- zTNC>QVo&TkqZf!1LLu{CxO_+we!`w?{MM4|jFkV64}PzvxF!fM)$ts@g3c`~t#~o# I{B@uI1w=5ME&u=k literal 0 HcmV?d00001 diff --git a/pint/testsuite/test_matplotlib.py b/pint/testsuite/test_matplotlib.py new file mode 100644 index 000000000..3c590ae27 --- /dev/null +++ b/pint/testsuite/test_matplotlib.py @@ -0,0 +1,43 @@ +import pytest + +from pint import UnitRegistry + +# Conditionally import matplotlib and NumPy +plt = pytest.importorskip("matplotlib.pyplot", reason="matplotlib is not available") +np = pytest.importorskip("numpy", reason="NumPy is not available") + +# Set up unit registry for matplotlib +ureg = UnitRegistry() +ureg.setup_matplotlib(True) + +# Set up matplotlib +plt.switch_backend("agg") + + +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) +def test_basic_plot(): + y = np.linspace(0, 30) * ureg.miles + x = np.linspace(0, 5) * ureg.hours + + fig, ax = plt.subplots() + ax.plot(x, y, "tab:blue") + ax.axhline(26400 * ureg.feet, color="tab:red") + ax.axvline(120 * ureg.minutes, color="tab:green") + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True) +def test_plot_with_set_units(): + y = np.linspace(0, 30) * ureg.miles + x = np.linspace(0, 5) * ureg.hours + + fig, ax = plt.subplots() + ax.yaxis.set_units(ureg.inches) + ax.xaxis.set_units(ureg.seconds) + + ax.plot(x, y, "tab:blue") + ax.axhline(26400 * ureg.feet, color="tab:red") + ax.axvline(120 * ureg.minutes, color="tab:green") + + return fig From f666135d1650768485e2900f21460ebdf0c068bb Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 15:29:58 -0600 Subject: [PATCH 251/612] Update changelog for test suite updates --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index f1bc2474f..3f028ea7f 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Switched test configuration to pytest and added tests of Pint's matplotlib support. + (Issue #954) - Removed eval usage when creating UnitDefinition and PrefixDefinition from string. (Issue #942) - Added `fmt_locale` argument to registry. From c2d4458119a2079c0f4aef22fda6b16020e41e53 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 13:47:05 -0600 Subject: [PATCH 252/612] Deprecate array protocol fallback --- pint/compat.py | 8 ++++- pint/quantity.py | 52 +++++++++++++++++++++++---------- pint/testsuite/test_numpy.py | 23 ++++++++++++++- pint/testsuite/test_quantity.py | 4 +++ 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 24fa7699f..0f68a1dfb 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -7,6 +7,7 @@ :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 @@ -94,6 +95,8 @@ def __array_function__(self, *args, **kwargs): NP_NO_VALUE = np._NoValue + ARRAY_FALLBACK = bool(int(os.environ.get("PINT_ARRAY_PROTOCOL_FALLBACK", 1))) + except ImportError: np = None @@ -107,9 +110,12 @@ class ndarray: 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): - if isinstance(value, (dict, bool)) or value is None: + if force_ndarray: + raise ValueError("Cannot force to ndarray when NumPy is not present.") + elif isinstance(value, (dict, bool)) or value is None: raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value)) elif isinstance(value, str) and value == "": raise ValueError("Quantity magnitude cannot be an empty string.") diff --git a/pint/quantity.py b/pint/quantity.py index 02f8ec589..9f9c6defb 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -22,6 +22,7 @@ from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 from .compat import ( + ARRAY_FALLBACK, NUMPY_VER, BehaviorChangeWarning, _to_magnitude, @@ -1454,6 +1455,14 @@ def _numpy_method_wrap(self, func, *args, **kwargs): else: return value + def __array__(self): + warnings.warn( + "The unit of the quantity is stripped when downcasting to ndarray.", + UnitStrippedWarning, + stacklevel=2, + ) + return _to_magnitude(self._magnitude, force_ndarray=True) + def clip(self, first=None, second=None, out=None, **kwargs): minimum = kwargs.get("min", first) maximum = kwargs.get("max", second) @@ -1548,23 +1557,36 @@ def __len__(self): return len(self._magnitude) def __getattr__(self, item): - # Attributes starting with `__array_` are common attributes of NumPy ndarray. - # They are requested by numpy functions. if item.startswith("__array_"): - warnings.warn( - "The unit of the quantity is stripped when getting {} " - "attribute".format(item), - UnitStrippedWarning, - stacklevel=2, - ) - if isinstance(self._magnitude, ndarray): - return getattr(self._magnitude, item) + # 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 isinstance(self._magnitude, ndarray): + return getattr(self._magnitude, item) + else: + # If an `__array_` attributes is requested but the magnitude is not an ndarray, + # we convert the magnitude to a numpy ndarray. + # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays + magnitude_as_array = _to_magnitude( + self._magnitude, force_ndarray=True + ) + return getattr(magnitude_as_array, item) else: - # If an `__array_` attributes is requested but the magnitude is not an ndarray, - # we convert the magnitude to a numpy ndarray. - # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays - magnitude_as_array = _to_magnitude(self._magnitude, force_ndarray=True) - return getattr(magnitude_as_array, item) + # TODO (next minor version): ARRAY_FALLBACK is removed and this becomes the standard behavior + raise AttributeError(f"Array protocol attribute {item} not available.") elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods: # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars magnitude_as_array = _to_magnitude(self._magnitude, True) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index b1871f942..c3a17c092 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,8 +1,9 @@ import copy import operator as op import unittest +from unittest.mock import patch -from pint import DimensionalityError, OffsetUnitCalculusError +from pint import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.test_umath import TestUFuncs @@ -1004,6 +1005,26 @@ def test_insert(self): 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) + + 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) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 9f02eaccb..eecb39e0f 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -532,6 +532,10 @@ def test_array_function_warning_on_creation(self): warnings.filterwarnings("error") self.Q_([]) + @helpers.requires_not_numpy() + def test_no_ndarray_coercion_without_numpy(self): + self.assertRaises(ValueError, self.Q_(1, "m").__array__) + class TestQuantityToCompact(QuantityTestCase): def assertQuantityAlmostIdentical(self, q1, q2): From 138346484c62304d3d46ab5c4b7c001e21a8b1cd Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 14:14:17 -0600 Subject: [PATCH 253/612] Add documentation for array interface protocol deprecation --- CHANGES | 7 ++++++- docs/numpy.rst | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3f028ea7f..617d9a432 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,12 @@ Pint Changelog ----------------- - Switched test configuration to pytest and added tests of Pint's matplotlib support. - (Issue #954) + (Issue #954, Thanks Jon Thielen) +- Deprecate array protocol fallback except where explicitly defined (`__array__`, + `__array_priority__`, `__array_function__`, `__array_ufunc__`). The fallback will + remain until the next minor version, or if the environment variable + `PINT_ARRAY_PROTOCOL_FALLBACK` is set to 0. + (Issue #953, Thanks Jon Thielen) - Removed eval usage when creating UnitDefinition and PrefixDefinition from string. (Issue #942) - Added `fmt_locale` argument to registry. diff --git a/docs/numpy.rst b/docs/numpy.rst index c4965eebe..01098c042 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -159,6 +159,15 @@ memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see :ref:`wrapping`). +Array interface protocol attributes (such as `__array_struct__` and +`__array_interface__`) are available on Pint Quantities by deferring to the +corresponding `__array_*` attribute on the magnitude as casted to an ndarray. This +has been found to be potentially incorrect and to cause unexpected behavior, and has +therefore been deprecated. As of the next minor version of Pint (or when the +`PINT_ARRAY_PROTOCOL_FALLBACK` environment variable is set to 0 prior to importing +Pint), attempting to access these attributes will instead raise an AttributeError. + + From ca3d9c790a8825802efd7b66558c82394e161334 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 21:06:29 -0600 Subject: [PATCH 254/612] Add parameterized test for type immutability --- CHANGES | 2 + pint/testsuite/test_issues.py | 83 +++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/CHANGES b/CHANGES index 3f028ea7f..7f476389b 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Added tests for immutability of the magnitude's type under common operations + (Issue #956, Thanks Jon Thielen) - Switched test configuration to pytest and added tests of Pint's matplotlib support. (Issue #954) - Removed eval usage when creating UnitDefinition and PrefixDefinition from string. diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 68d783b90..bc4a2f7fd 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -3,12 +3,16 @@ import pprint import unittest +import pytest + from pint import Context, DimensionalityError, UnitRegistry from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.unit import UnitsContainer from pint.util import ParserHelper +ureg = UnitRegistry() + class TestIssues(QuantityTestCase): @@ -23,7 +27,6 @@ def test_issue25(self): self.assertEqual(x, ParserHelper(10, {"%": 1})) x = ParserHelper.from_string("10 ‰") self.assertEqual(x, ParserHelper(10, {"‰": 1})) - ureg = UnitRegistry() ureg.define("percent = [fraction]; offset: 0 = %") ureg.define("permille = percent / 10 = ‰") x = ureg.parse_expression("10 %") @@ -33,7 +36,6 @@ def test_issue25(self): self.assertEqual(x.to("‰"), ureg.Quantity(1, {"‰": 1})) def test_issue29(self): - ureg = UnitRegistry() t = 4 * ureg("mW") self.assertEqual(t.magnitude, 4) self.assertEqual(t._units, UnitsContainer(milliwatt=1)) @@ -43,7 +45,6 @@ def test_issue29(self): @helpers.requires_numpy() def test_issue37(self): x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) - ureg = UnitRegistry() q = ureg.meter * x self.assertIsInstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) @@ -67,7 +68,6 @@ def test_issue37(self): @helpers.requires_numpy() def test_issue39(self): x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) - ureg = UnitRegistry() q = ureg.meter * x self.assertIsInstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) @@ -89,7 +89,6 @@ def test_issue39(self): @helpers.requires_numpy() def test_issue44(self): - ureg = UnitRegistry() x = 4.0 * ureg.dimensionless np.sqrt(x) self.assertQuantityAlmostEqual( @@ -102,13 +101,11 @@ def test_issue44(self): def test_issue45(self): import math - ureg = UnitRegistry() self.assertAlmostEqual(math.sqrt(4 * ureg.m / ureg.cm), math.sqrt(4 * 100)) self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.0) @helpers.requires_numpy() def test_issue45b(self): - ureg = UnitRegistry() self.assertAlmostEqual( np.sin([np.pi / 2] * ureg.m / ureg.m), np.sin([np.pi / 2] * ureg.dimensionless), @@ -119,7 +116,6 @@ def test_issue45b(self): ) def test_issue50(self): - ureg = UnitRegistry() Q_ = ureg.Quantity self.assertEqual(Q_(100), 100 * ureg.dimensionless) self.assertEqual(Q_("100"), 100 * ureg.dimensionless) @@ -146,18 +142,15 @@ def test_issue52(self): self.assertRaises(ValueError, fun, q1, q2) def test_issue54(self): - ureg = UnitRegistry() self.assertEqual((1 * ureg.km / ureg.m + 1).magnitude, 1001) def test_issue54_related(self): - ureg = UnitRegistry() self.assertEqual(ureg.km / ureg.m, 1000) self.assertEqual(1000, ureg.km / ureg.m) self.assertLess(900, ureg.km / ureg.m) self.assertGreater(1100, ureg.km / ureg.m) def test_issue61(self): - ureg = UnitRegistry() Q_ = ureg.Quantity for value in ({}, {"a": 3}, None): self.assertRaises(TypeError, Q_, value) @@ -167,19 +160,16 @@ def test_issue61(self): @helpers.requires_not_numpy() def test_issue61_notNP(self): - ureg = UnitRegistry() Q_ = ureg.Quantity for value in ([1, 2, 3], (1, 2, 3)): self.assertRaises(TypeError, Q_, value) self.assertRaises(TypeError, Q_, value, "meter") def test_issue62(self): - ureg = UnitRegistry() m = ureg("m**0.5") self.assertEqual(str(m.units), "meter ** 0.5") def test_issue66(self): - ureg = UnitRegistry() self.assertEqual( ureg.get_dimensionality(UnitsContainer({"[temperature]": 1})), UnitsContainer({"[temperature]": 1}), @@ -192,7 +182,6 @@ def test_issue66(self): ) def test_issue66b(self): - ureg = UnitRegistry() self.assertEqual( ureg.get_base_units(ureg.kelvin), (1.0, ureg.Unit(UnitsContainer({"kelvin": 1}))), @@ -203,13 +192,11 @@ def test_issue66b(self): ) def test_issue69(self): - ureg = UnitRegistry() q = ureg("m").to(ureg("in")) self.assertEqual(q, ureg("m").to("in")) @helpers.requires_numpy() def test_issue74(self): - ureg = UnitRegistry() v1 = np.asarray([1.0, 2.0, 3.0]) v2 = np.asarray([3.0, 2.0, 1.0]) q1 = v1 * ureg.ms @@ -232,7 +219,6 @@ def test_issue74(self): @helpers.requires_numpy() def test_issue75(self): - ureg = UnitRegistry() v1 = np.asarray([1.0, 2.0, 3.0]) v2 = np.asarray([3.0, 2.0, 1.0]) q1 = v1 * ureg.ms @@ -249,14 +235,12 @@ def test_issue75(self): @helpers.requires_uncertainties() def test_issue77(self): - ureg = UnitRegistry() acc = (5.0 * ureg("m/s/s")).plus_minus(0.25) tim = (37.0 * ureg("s")).plus_minus(0.16) dis = acc * tim ** 2 / 2 self.assertEqual(dis.value, acc.value * tim.value ** 2 / 2) def test_issue85(self): - ureg = UnitRegistry() T = 4.0 * ureg.kelvin m = 1.0 * ureg.amu @@ -329,7 +313,6 @@ def test_issue86c(self): self.assertQuantityAlmostEqual(ureg.k * 2 * T, ureg.k * (2 * T)) def test_issue93(self): - ureg = UnitRegistry() x = 5 * ureg.meter self.assertIsInstance(x.magnitude, int) y = 0.1 * ureg.meter @@ -344,7 +327,6 @@ def test_issue93(self): @helpers.requires_numpy_previous_than("1.10") def test_issue94(self): - ureg = UnitRegistry() v1 = np.array([5, 5]) * ureg.meter v2 = 0.1 * ureg.meter v3 = np.array([5, 5]) * ureg.meter @@ -354,7 +336,6 @@ def test_issue94(self): np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) def test_issue104(self): - ureg = UnitRegistry() x = [ureg("1 meter"), ureg("1 meter"), ureg("1 meter")] y = [ureg("1 meter")] * 3 @@ -374,7 +355,6 @@ def summer(values): self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, "meter")) def test_issue105(self): - ureg = UnitRegistry() func = ureg.parse_unit_name val = list(func("meter")) @@ -388,7 +368,6 @@ def test_issue105(self): self.assertEqual(val, func("METER", False)) def test_issue121(self): - ureg = UnitRegistry() z, v = 0, 2.0 self.assertEqual(z + v * ureg.meter, v * ureg.meter) self.assertEqual(z - v * ureg.meter, -v * ureg.meter) @@ -400,7 +379,6 @@ def test_issue121(self): @helpers.requires_numpy18() def test_issue121b(self): sh = (2, 1) - ureg = UnitRegistry() z, v = 0, 2.0 self.assertEqual(z + v * ureg.meter, v * ureg.meter) @@ -441,15 +419,12 @@ def test_issue170(self): self.assertIsInstance(iq, int) def test_angstrom_creation(self): - ureg = UnitRegistry() ureg.Quantity(2, "Å") def test_alternative_angstrom_definition(self): - ureg = UnitRegistry() ureg.Quantity(2, "\u212B") def test_micro_creation(self): - ureg = UnitRegistry() ureg.Quantity(2, "µm") @helpers.requires_numpy() @@ -505,8 +480,6 @@ def test_issue354_356_370(self): self.assertEqual("{:~}".format(1 * self.ureg("MiB")), "1 MiB") def test_issue468(self): - ureg = UnitRegistry() - @ureg.wraps(("kg"), "meter") def f(x): return x @@ -548,7 +521,6 @@ def f(x): self.assertRaises(DimensionalityError, f, ureg.Quantity(1, "m")) def test_issue625a(self): - ureg = UnitRegistry() Q_ = ureg.Quantity from math import sqrt @@ -574,7 +546,6 @@ def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): self.assertAlmostEqual(t2, Q_(3.508232077228117, "s")) def test_issue625b(self): - ureg = UnitRegistry() Q_ = ureg.Quantity @ureg.wraps("=A*B", ("=A", "=B")) @@ -601,7 +572,6 @@ def get_product(a=2 * u.m, b=3 * u.m, c=5 * u.m): self.assertEqual(get_product(c=1 * u.dimensionless), 6 * u.m ** 2) def test_issue655a(self): - ureg = UnitRegistry() distance = 1 * ureg.m time = 1 * ureg.s velocity = distance / time @@ -611,7 +581,6 @@ def test_issue655a(self): self.assertEqual(velocity.check("1 / [time] * [length]"), True) def test_issue655b(self): - ureg = UnitRegistry() Q_ = ureg.Quantity @ureg.check("[length]", "[length]/[time]^2") @@ -629,7 +598,6 @@ def pendulum_period(length, G=Q_(1, "standard_gravity")): self.assertAlmostEqual(t, Q_("4.928936075204336 second")) def test_issue783(self): - ureg = UnitRegistry() assert not ureg("g") == [] def test_issue856(self): @@ -682,13 +650,11 @@ def test_issue912(self): handles TypeError, but not generic Exceptions. This test will fail if pint.DimensionalityError stops being a subclass of TypeError. """ - ureg = UnitRegistry() meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) pprint.pformat(meter_units | hertz_units) def test_issue932(self): - ureg = UnitRegistry() q = ureg.Quantity("1 kg") with self.assertRaises(DimensionalityError): q.to("joule") @@ -697,3 +663,44 @@ def test_issue932(self): ureg.disable_contexts() with self.assertRaises(DimensionalityError): q.to("joule") + + +try: + + @pytest.mark.skipif(np is None, reason="NumPy is not available") + @pytest.mark.parametrize( + "callable", + [ + lambda x: np.sin(x / x.units), # Issue 399 + lambda x: np.cos(x / x.units), # Issue 399 + np.isfinite, # Issue 481 + np.shape, # Issue 509 + np.size, # Issue 509 + np.sqrt, # Issue 622 + lambda x: x.mean(), # Issue 678 + lambda x: x.copy(), # Issue 678 + np.array, + lambda x: x.conjugate, + ], + ) + @pytest.mark.parametrize( + "q", + [ + pytest.param(ureg.Quantity(1, "m"), id="python scalar int"), + pytest.param(ureg.Quantity([1, 2, 3, 4], "m"), id="array int"), + pytest.param(ureg.Quantity([1], "m")[0], id="numpy scalar int"), + pytest.param(ureg.Quantity(1.0, "m"), id="python scalar float"), + pytest.param(ureg.Quantity([1.0, 2.0, 3.0, 4.0], "m"), id="array float"), + pytest.param(ureg.Quantity([1.0], "m")[0], id="numpy scalar float"), + ], + ) + def test_issue925(callable, q): + # Test for immutability of type + type_before = type(q._magnitude) + callable(q) + assert isinstance(q._magnitude, type_before) + + +except AttributeError: + # Calling attributes on np will fail if NumPy is not available + pass From 649672baa04777a614e3ff91859545ea31da968f Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 19:59:04 -0600 Subject: [PATCH 255/612] Add np.pad and np.resize implementations --- docs/numpy.rst | 2 +- pint/numpy_func.py | 26 ++++++++++ pint/testsuite/test_numpy.py | 96 ++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/docs/numpy.rst b/docs/numpy.rst index 01098c042..f849af23b 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -112,7 +112,7 @@ The following ufuncs_ can be applied to a Quantity object: And the following NumPy functions: -- alen, amax, amin, append, argmax, argmin, argsort, around, atleast_1d, atleast_2d, atleast_3d, average, block, broadcast_to, clip, column_stack, compress, concatenate, copy, copyto, count_nonzero, cross, cumprod, cumproduct, cumsum, diagonal, diff, dot, dstack, ediff1d, einsum, empty_like, expand_dims, fix, flip, full_like, gradient, hstack, insert, interp, isclose, iscomplex, isin, isreal, linspace, mean, median, meshgrid, moveaxis, nan_to_num, nanargmax, nanargmin, nancumprod, nancumsum, nanmax, nanmean, nanmedian, nanmin, nanpercentile, nanstd, nanvar, ndim, nonzero, ones_like, percentile, ptp, ravel, result_type, rollaxis, rot90, round\_, searchsorted, shape, size, sort, squeeze, stack, std, sum, swapaxes, tile, transpose, trapz, trim_zeros, unwrap, var, vstack, where, zeros_like +- alen, amax, amin, append, argmax, argmin, argsort, around, atleast_1d, atleast_2d, atleast_3d, average, block, broadcast_to, clip, column_stack, compress, concatenate, copy, copyto, count_nonzero, cross, cumprod, cumproduct, cumsum, diagonal, diff, dot, dstack, ediff1d, einsum, empty_like, expand_dims, fix, flip, full_like, gradient, hstack, insert, interp, isclose, iscomplex, isin, isreal, linspace, mean, median, meshgrid, moveaxis, nan_to_num, nanargmax, nanargmin, nancumprod, nancumsum, nanmax, nanmean, nanmedian, nanmin, nanpercentile, nanstd, nanvar, ndim, nonzero, ones_like, pad, percentile, ptp, ravel, resize, result_type, rollaxis, rot90, round\_, searchsorted, shape, size, sort, squeeze, stack, std, sum, swapaxes, tile, transpose, trapz, trim_zeros, unwrap, var, vstack, where, zeros_like And the following `NumPy ndarray methods`_: diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 811d32909..379d4c535 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -597,6 +597,31 @@ def _isin(element, test_elements, assume_unique=False, invert=False): return np.isin(element.m, test_elements, assume_unique=assume_unique, invert=invert) +@implements("pad", "function") +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 + + # 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) + + return units._REGISTRY.Quantity( + np.pad(array._magnitude, pad_width, mode=mode, **kwargs), units + ) + + # Implement simple matching-unit or stripped-unit functions based on signature @@ -675,6 +700,7 @@ def implementation(*args, **kwargs): ("tile", "A", True), ("rot90", "m", True), ("insert", ["arr", "values"], True), + ("resize", "a", True), ]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index c3a17c092..5adf0e43d 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1025,6 +1025,102 @@ def test_array_protocol_unavailable(self): for attr in ("__array_struct__", "__array_interface__"): self.assertRaises(AttributeError, getattr, self.q, attr) + @helpers.requires_array_function_protocol() + def test_resize(self): + self.assertQuantityEqual( + np.resize(self.q, (2, 4)), [[1, 2, 3, 4], [1, 2, 3, 4]] * self.ureg.m + ) + + @helpers.requires_array_function_protocol() + def test_pad(self): + # Tests reproduced with modification from NumPy documentation + a = [1, 2, 3, 4, 5] * 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, + ) + 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", end_values=(5, -4) * self.ureg.m), + [5, 3, 1, 2, 3, 4, 5, 2, -1, -4] * self.ureg.m, + ) + self.assertQuantityEqual( + np.pad(a, (2,), "maximum"), [5, 5, 1, 2, 3, 4, 5, 5, 5] * self.ureg.m + ) + self.assertQuantityEqual( + np.pad(a, (2,), "mean"), [3, 3, 1, 2, 3, 4, 5, 3, 3] * self.ureg.m + ) + self.assertQuantityEqual( + np.pad(a, (2,), "median"), [3, 3, 1, 2, 3, 4, 5, 3, 3] * self.ureg.m + ) + self.assertQuantityEqual( + np.pad(self.q, ((3, 2), (2, 3)), "minimum"), + [ + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [3, 3, 3, 4, 3, 3, 3], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + ] + * self.ureg.m, + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "reflect"), [3, 2, 1, 2, 3, 4, 5, 4, 3, 2] * self.ureg.m + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "reflect", reflect_type="odd"), + [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8] * self.ureg.m, + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "symmetric"), [2, 1, 1, 2, 3, 4, 5, 5, 4, 3] * self.ureg.m + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "symmetric", reflect_type="odd"), + [0, 1, 1, 2, 3, 4, 5, 5, 6, 7] * self.ureg.m, + ) + self.assertQuantityEqual( + np.pad(a, (2, 3), "wrap"), [4, 5, 1, 2, 3, 4, 5, 1, 2, 3] * self.ureg.m + ) + + def pad_with(vector, pad_width, iaxis, kwargs): + pad_value = kwargs.get("padder", 10) + vector[: pad_width[0]] = pad_value + vector[-pad_width[1] :] = pad_value + + b = self.Q_(np.arange(6).reshape((2, 3)), "degC") + self.assertQuantityEqual( + np.pad(b, 2, pad_with), + self.Q_( + [ + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 0, 1, 2, 10, 10], + [10, 10, 3, 4, 5, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + ], + "degC", + ), + ) + self.assertQuantityEqual( + np.pad(b, 2, pad_with, padder=100), + self.Q_( + [ + [100, 100, 100, 100, 100, 100, 100], + [100, 100, 100, 100, 100, 100, 100], + [100, 100, 0, 1, 2, 100, 100], + [100, 100, 3, 4, 5, 100, 100], + [100, 100, 100, 100, 100, 100, 100], + [100, 100, 100, 100, 100, 100, 100], + ], + "degC", + ), + ) # Note: Does not support Quantity pad_with vectorized callable use + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): From 758a23a559338b8bedcc3dcf7f7567d378fe6c9b Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 00:07:59 -0300 Subject: [PATCH 256/612] Run pyment -w --convert -o numpydoc . --- bench/bench.py | 37 +- pint/__init__.py | 37 +- pint/compat.py | 24 +- pint/context.py | 156 +++- pint/converters.py | 9 +- pint/definitions.py | 48 +- pint/errors.py | 15 +- pint/formatting.py | 78 +- pint/matplotlib.py | 77 +- pint/measurement.py | 14 +- pint/numpy_func.py | 160 +++- pint/pint_eval.py | 87 +- pint/quantity.py | 309 +++++-- pint/registry.py | 857 +++++++++++++++----- pint/registry_helpers.py | 95 ++- pint/systems.py | 132 ++- pint/testsuite/__init__.py | 24 +- pint/testsuite/parameterized.py | 14 +- pint/testsuite/test_application_registry.py | 7 + pint/testsuite/test_issues.py | 37 +- pint/testsuite/test_numpy.py | 14 +- pint/testsuite/test_quantity.py | 7 + pint/testsuite/test_umath.py | 197 +++-- pint/unit.py | 54 +- pint/util.py | 248 +++++- 25 files changed, 2146 insertions(+), 591 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index e7c3a97e0..9f48c5c22 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -10,8 +10,22 @@ def time_stmt(stmt="pass", setup="pass", number=0, repeat=3): """Timer function with the same behaviour as running `python -m timeit ` in the command line. - :return: elapsed time in seconds or NaN if the command failed. - :rtype: float + Parameters + ---------- + stmt : + (Default value = "pass") + setup : + (Default value = "pass") + number : + (Default value = 0) + repeat : + (Default value = 3) + + Returns + ------- + float + elapsed time in seconds or NaN if the command failed. + """ t = Timer(stmt, setup) @@ -75,8 +89,25 @@ def time_task(name, stmt="pass", setup="pass", number=0, repeat=3, stmts="", bas def time_file(filename, name="", setup="", number=0, repeat=3): """Open a yaml benchmark file an time each statement, - + yields a tuple with filename, task name, time in seconds. + + Parameters + ---------- + filename : + + name : + (Default value = "") + setup : + (Default value = "") + number : + (Default value = 0) + repeat : + (Default value = 3) + + Returns + ------- + """ with open(filename, "r") as fp: tasks = yaml.load(fp) diff --git a/pint/__init__.py b/pint/__init__.py index 682e4e717..ab3e99924 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -61,8 +61,16 @@ def _unpickle(cls, *args): """Rebuild object upon unpickling. All units must exist in the application registry. - :param cls: + Parameters + ---------- + cls : Quantity, Magnitude, or Unit + *args : + + + Returns + ------- + """ from .unit import UnitsContainer @@ -81,7 +89,14 @@ def set_application_registry(registry): """Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly. - :param registry: a UnitRegistry instance. + Parameters + ---------- + registry : + a UnitRegistry instance. + + Returns + ------- + """ if not isinstance(registry, (LazyRegistry, UnitRegistry)): raise TypeError("Expected UnitRegistry; got %s" % type(registry)) @@ -95,15 +110,29 @@ def get_application_registry(): invoked, return a registry built using :file:`defaults_en.txt` embedded in the pint package. - :param registry: a UnitRegistry instance. + Parameters + ---------- + registry : + a UnitRegistry instance. + + Returns + ------- + """ return _APP_REGISTRY def test(): """Run all tests. - + :return: a :class:`unittest.TestResult` object + + Parameters + ---------- + + Returns + ------- + """ from .testsuite import run diff --git a/pint/compat.py b/pint/compat.py index 5782cace7..213a4f0bc 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -153,9 +153,16 @@ def _to_magnitude(value, force_ndarray=False): def is_upcast_type(other): - """ check if the type object is a upcast type + """check if the type object is a upcast type + + Parameters + ---------- + other : + type + + Returns + ------- - :param other: type """ # Check if class name is in preset list return other.__name__ in ("PintArray", "Series", "DataArray") @@ -163,6 +170,19 @@ def is_upcast_type(other): def eq(first, second, check_all): """Comparison of scalars and arrays + + Parameters + ---------- + first : + + second : + + check_all : + + + Returns + ------- + """ out = first == second if check_all and isinstance(out, ndarray): diff --git a/pint/context.py b/pint/context.py index dce298d1d..0a0b73b32 100644 --- a/pint/context.py +++ b/pint/context.py @@ -36,8 +36,26 @@ class Context: """A specialized container that defines transformation functions from one dimension to another. Each Dimension are specified using a UnitsContainer. Simple transformation are given with a function taking a single parameter. + + + Conversion functions may take optional keyword arguments and the context + can have default values for these arguments. + + + Additionally, a context may host redefinitions: + + + A redefinition must be performed among units that already exist in the registry. It + cannot change the dimensionality of a unit. The symbol and aliases are automatically + inherited from the registry. + + Parameters + ---------- - >>> from pint.util import UnitsContainer + Returns + ------- + + >>> from pint.util import UnitsContainer >>> timedim = UnitsContainer({'[time]': 1}) >>> spacedim = UnitsContainer({'[length]': 1}) >>> def f(time): @@ -47,10 +65,7 @@ class Context: >>> c.add_transformation(timedim, spacedim, f) >>> c.transform(timedim, spacedim, 2) 6 - - Conversion functions may take optional keyword arguments and the context - can have default values for these arguments. - + >>> def f(time, n): ... 'Time to length converter, n is the index of refraction of the material' ... return 3. * time / n @@ -58,14 +73,8 @@ class Context: >>> c.add_transformation(timedim, spacedim, f) >>> c.transform(timedim, spacedim, 2) 2 - - Additionally, a context may host redefinitions: - + >>> c.redefine("pound = 0.5 kg") - - A redefinition must be performed among units that already exist in the registry. It - cannot change the dimensionality of a unit. The symbol and aliases are automatically - inherited from the registry. """ def __init__(self, name=None, aliases=(), defaults=None): @@ -94,8 +103,19 @@ def from_context(cls, context, **defaults): """Creates a new context that shares the funcs dictionary with the original context. The default values are copied from the original context and updated with the new defaults. - + If defaults is empty, return the same context. + + Parameters + ---------- + context : + + **defaults : + + + Returns + ------- + """ if defaults: newdef = dict(context.defaults, **defaults) @@ -192,6 +212,19 @@ def to_num(val): def add_transformation(self, src, dst, func): """Add a transformation function to the context. + + Parameters + ---------- + src : + + dst : + + func : + + + Returns + ------- + """ _key = self.__keytransform__(src, dst) self.funcs[_key] = func @@ -199,6 +232,17 @@ def add_transformation(self, src, dst, func): def remove_transformation(self, src, dst): """Add a transformation function to the context. + + Parameters + ---------- + src : + + dst : + + + Returns + ------- + """ _key = self.__keytransform__(src, dst) del self.funcs[_key] @@ -210,6 +254,21 @@ def __keytransform__(src, dst): def transform(self, src, dst, registry, value): """Transform a value. + + Parameters + ---------- + src : + + dst : + + registry : + + value : + + + Returns + ------- + """ _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) @@ -217,8 +276,16 @@ def transform(self, src, dst, registry, value): def redefine(self, definition: str) -> None: """Override the definition of a unit in the registry. - :param definition: - `` = ``, e.g. ``pound = 0.5 kg`` + Parameters + ---------- + definition : + unit> = ``, e.g. ``pound = 0.5 kg`` + definition: str : + + + Returns + ------- + """ for line in definition.splitlines(): d = Definition.from_string(line) @@ -238,6 +305,13 @@ def hashable(self): """Generate a unique hashable and comparable representation of self, which can be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. + + Parameters + ---------- + + Returns + ------- + """ return ( self.name, @@ -251,6 +325,13 @@ def hashable(self): class ContextChain(ChainMap): """A specialized ChainMap for contexts that simplifies finding rules to transform from one dimension to another. + + Parameters + ---------- + + Returns + ------- + """ def __init__(self): @@ -262,9 +343,18 @@ def __init__(self): def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. (A rule in last context will take precedence) - + To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. + + Parameters + ---------- + *contexts : + + + Returns + ------- + """ self.contexts = list(reversed(contexts)) + self.contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps @@ -272,6 +362,15 @@ def insert_contexts(self, *contexts): def remove_contexts(self, n: int = None): """Remove the last n inserted contexts from the chain. + + Parameters + ---------- + n: int : + (Default value = None) + + Returns + ------- + """ del self.contexts[:n] del self.maps[:n] @@ -285,8 +384,7 @@ def defaults(self): @property def graph(self): - """The graph relating - """ + """The graph relating""" if self._graph is None: self._graph = defaultdict(set) for fr_, to_ in self: @@ -297,7 +395,20 @@ def transform(self, src, dst, registry, value): """Transform the value, finding the rule in the chained context. (A rule in last context will take precedence) - :raises: KeyError if the rule is not found. + Parameters + ---------- + src : + + dst : + + registry : + + value : + + + Returns + ------- + """ return self[(src, dst)].transform(src, dst, registry, value) @@ -305,5 +416,12 @@ def hashable(self): """Generate a unique hashable and comparable representation of self, which can be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. + + Parameters + ---------- + + Returns + ------- + """ return tuple(ctx.hashable() for ctx in self.contexts) diff --git a/pint/converters.py b/pint/converters.py index ff1624ce3..bcb659e89 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -10,8 +10,7 @@ class Converter: - """Base class for value converters. - """ + """Base class for value converters.""" is_multiplicative = True @@ -23,8 +22,7 @@ def from_reference(self, value, inplace=False): class ScaleConverter(Converter): - """A linear transformation - """ + """A linear transformation""" is_multiplicative = True @@ -49,8 +47,7 @@ def from_reference(self, value, inplace=False): class OffsetConverter(Converter): - """An affine transformation - """ + """An affine transformation""" def __init__(self, scale, offset): self.scale = scale diff --git a/pint/definitions.py b/pint/definitions.py index c0481df17..930488d6c 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -30,10 +30,20 @@ def numeric_parse(s): class Definition: """Base class for definitions. - :param name: name. - :param symbol: a short name or symbol for the definition - :param aliases: iterable of other names. - :param converter: an instance of Converter. + Parameters + ---------- + name : + name. + symbol : + a short name or symbol for the definition + aliases : + iterable of other names. + converter : + an instance of Converter. + + Returns + ------- + """ def __init__(self, name, symbol, aliases, converter): @@ -49,6 +59,15 @@ def is_multiplicative(self): @classmethod def from_string(cls, definition): """Parse a definition + + Parameters + ---------- + definition : + + + Returns + ------- + """ name, definition = definition.split("=", 1) name = name.strip() @@ -103,8 +122,7 @@ def __str__(self): class PrefixDefinition(Definition): - """Definition of a prefix. - """ + """Definition of a prefix.""" def __init__(self, name, symbol, aliases, converter): if isinstance(converter, str): @@ -124,8 +142,16 @@ def __init__(self, name, symbol, aliases, converter): class UnitDefinition(Definition): """Definition of a unit. - :param reference: Units container with reference units. - :param is_base: indicates if it is a base unit. + Parameters + ---------- + reference : + Units container with reference units. + is_base : + indicates if it is a base unit. + + Returns + ------- + """ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): @@ -171,8 +197,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal class DimensionDefinition(Definition): - """Definition of a dimension. - """ + """Definition of a dimension.""" def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference @@ -195,8 +220,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal class AliasDefinition(Definition): - """Additional alias(es) for an already existing unit - """ + """Additional alias(es) for an already existing unit""" def __init__(self, name, aliases): super().__init__(name=name, symbol=None, aliases=aliases, converter=None) diff --git a/pint/errors.py b/pint/errors.py index c0d9ab7ae..f2be370ef 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -21,8 +21,7 @@ def _file_prefix(filename=None, lineno=None): class DefinitionSyntaxError(SyntaxError): - """Raised when a textual definition has a syntax error. - """ + """ """ def __init__(self, msg, *, filename=None, lineno=None): super().__init__(msg) @@ -44,8 +43,7 @@ def __reduce__(self): class RedefinitionError(ValueError): - """Raised when a unit or prefix is redefined. - """ + """ """ def __init__(self, name, definition_type, *, filename=None, lineno=None): super().__init__(name, definition_type) @@ -61,8 +59,7 @@ def __reduce__(self): class UndefinedUnitError(AttributeError): - """Raised when the units are not defined in the unit registry. - """ + """ """ def __init__(self, *unit_names): if len(unit_names) == 1 and not isinstance(unit_names[0], str): @@ -80,8 +77,7 @@ class PintTypeError(TypeError): class DimensionalityError(PintTypeError): - """Raised when trying to convert between incompatible units. - """ + """ """ def __init__(self, units1, units2, dim1="", dim2="", *, extra_msg=""): super().__init__() @@ -109,8 +105,7 @@ def __reduce__(self): class OffsetUnitCalculusError(PintTypeError): - """Raised on ambiguous operations with offset units. - """ + """ """ def __str__(self): return ( diff --git a/pint/formatting.py b/pint/formatting.py index 002925df2..f4320e42f 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -18,10 +18,21 @@ def _join(fmt, iterable): """Join an iterable with the format specified in fmt. - + The format can be specified in two ways: - PEP3101 format with two replacement fields (eg. '{} * {}') - The concatenating string (eg. ' * ') + + Parameters + ---------- + fmt : + + iterable : + + + Returns + ------- + """ if not iterable: return "" @@ -40,6 +51,15 @@ def _join(fmt, iterable): def _pretty_fmt_exponent(num): """Format an number into a pretty printed exponent. + + Parameters + ---------- + num : + + + Returns + ------- + """ # TODO: Will not work for decimals ret = f"{num:n}".replace("-", "⁻") @@ -110,19 +130,37 @@ def formatter( ): """Format a list of (name, exponent) pairs. - :param items: a list of (name, exponent) pairs. - :param as_ratio: True to display as ratio, False as negative powers. - :param single_denominator: all with terms with negative exponents are - collected together. - :param product_fmt: the format used for multiplication. - :param division_fmt: the format used for division. - :param power_fmt: the format used for exponentiation. - :param parentheses_fmt: the format used for parenthesis. - :param locale: the locale object as defined in babel. - :param babel_length: the length of the translated unit, as defined in babel cldr. - :param babel_plural_form: the plural form, calculated as defined in babel. - - :return: the formula as a string. + Parameters + ---------- + items : + a list of (name, exponent) pairs. + as_ratio : + True to display as ratio, False as negative powers. (Default value = True) + single_denominator : + all with terms with negative exponents are + collected together. (Default value = False) + product_fmt : + the format used for multiplication. (Default value = " * ") + division_fmt : + the format used for division. (Default value = " / ") + power_fmt : + the format used for exponentiation. (Default value = "{} ** {}") + parentheses_fmt : + the format used for parenthesis. (Default value = "({0})") + locale : + the locale object as defined in babel. (Default value = None) + babel_length : + the length of the translated unit, as defined in babel cldr. (Default value = "long") + babel_plural_form : + the plural form, calculated as defined in babel. (Default value = "one") + exp_call : + (Default value = lambda x: f"{x:n}") + + Returns + ------- + type + the formula as a string. + """ if not items: @@ -240,7 +278,17 @@ def format_unit(unit, spec, **kwspec): def siunitx_format_unit(units): - """Returns LaTeX code for the unit that can be put into an siunitx command.""" + """Returns LaTeX code for the unit that can be put into an siunitx command. + + Parameters + ---------- + units : + + + Returns + ------- + + """ # NOTE: unit registry is required to identify unit prefixes. registry = units._REGISTRY diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 3b7b0af90..68a74fcf1 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -30,14 +30,42 @@ def __init__(self, registry): self._reg = registry def convert(self, value, unit, axis): - """Convert :`Quantity` instances for matplotlib to use.""" + """Convert :`Quantity` instances for matplotlib to use. + + Parameters + ---------- + value : + + unit : + + axis : + + + Returns + ------- + + """ if iterable(value): return [self._convert_value(v, unit, axis) for v in value] else: return self._convert_value(value, unit, axis) def _convert_value(self, value, unit, axis): - """Handle converting using attached unit or falling back to axis units.""" + """Handle converting using attached unit or falling back to axis units. + + Parameters + ---------- + value : + + unit : + + axis : + + + Returns + ------- + + """ if hasattr(value, "units"): return value.to(unit).magnitude else: @@ -45,12 +73,38 @@ def _convert_value(self, value, unit, axis): @staticmethod def axisinfo(unit, axis): - """Return axis information for this particular unit.""" + """ + + Parameters + ---------- + unit : + + axis : + + + Returns + ------- + type + + + """ return PintAxisInfo(unit) @staticmethod def default_units(x, axis): - """Get the default unit to use for the given combination of unit and axis.""" + """Get the default unit to use for the given combination of unit and axis. + + Parameters + ---------- + x : + + axis : + + + Returns + ------- + + """ if iterable(x) and sized(x): return getattr(x[0], "units", None) return getattr(x, "units", None) @@ -58,10 +112,17 @@ def default_units(x, axis): def setup_matplotlib_handlers(registry, enable): """Set up matplotlib's unit support to handle units from a registry. - :param registry: the registry that will be used - :type registry: UnitRegistry - :param enable: whether support should be enabled or disabled - :type enable: bool + + Parameters + ---------- + registry : UnitRegistry + the registry that will be used + enable : bool + whether support should be enabled or disabled + + Returns + ------- + """ if matplotlib.__version__ < "2.0": raise RuntimeError("Matplotlib >= 2.0 required to work with pint.") diff --git a/pint/measurement.py b/pint/measurement.py index 72db28014..c6d99f6f1 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -17,10 +17,16 @@ class Measurement(Quantity): """Implements a class to describe a quantity with uncertainty. - :param value: The expected value of the measurement - :type value: pint.Quantity or any numeric type - :param error: The error or uncertainty of the measurement - :type error: pint.Quantity or any numeric type + Parameters + ---------- + value : pint.Quantity or any numeric type + The expected value of the measurement + error : pint.Quantity or any numeric type + The error or uncertainty of the measurement + + Returns + ------- + """ def __new__(cls, value, error, units=MISSING): diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 379d4c535..8c50bea43 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -23,14 +23,33 @@ def _is_quantity(arg): """Test for _units and _magnitude attrs. - + This is done in place of isinstance(Quantity, arg), which would cause a circular import. + + Parameters + ---------- + arg : + + + Returns + ------- + """ return hasattr(arg, "_units") and hasattr(arg, "_magnitude") def _is_quantity_sequence(arg): - """Test for sequences of quantities.""" + """Test for sequences of quantities. + + Parameters + ---------- + arg : + + + Returns + ------- + + """ return ( iterable(arg) and sized(arg) @@ -40,7 +59,19 @@ def _is_quantity_sequence(arg): def _get_first_input_units(args, kwargs={}): - """Obtain the first valid unit from a collection of args and kwargs.""" + """Obtain the first valid unit from a collection of args and kwargs. + + Parameters + ---------- + args : + + kwargs : + (Default value = {}) + + Returns + ------- + + """ for arg in chain(args, kwargs.values()): if _is_quantity(arg): return arg.units @@ -50,9 +81,20 @@ def _get_first_input_units(args, kwargs={}): def convert_arg(arg, pre_calc_units): """Convert quantities and sequences of quantities to pre_calc_units and strip units. - + Helper function for convert_to_consistent_units. pre_calc_units must be given as a pint Unit or None. + + Parameters + ---------- + arg : + + pre_calc_units : + + + Returns + ------- + """ if pre_calc_units is not None: if _is_quantity(arg): @@ -74,11 +116,24 @@ def convert_arg(arg, pre_calc_units): def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): """Prepare args and kwargs for wrapping by unit conversion and stripping. - + If pre_calc_units is not None, takes the args and kwargs for a NumPy function and converts any Quantity or Sequence of Quantities into the units of the first Quantiy/Sequence of Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless Quantities. If pre_calc_units is None, units are simply stripped. + + Parameters + ---------- + *args : + + pre_calc_units : + (Default value = None) + **kwargs : + + + Returns + ------- + """ return ( tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), @@ -91,9 +146,18 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): def unwrap_and_wrap_consistent_units(*args): """Strip units from args while providing a rewrapping function. - + Returns the given args as parsed by convert_to_consistent_units assuming units of first arg with units, along with a wrapper to restore that unit to the output. + + Parameters + ---------- + *args : + + + Returns + ------- + """ first_input_units = _get_first_input_units(args) args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) @@ -105,9 +169,9 @@ def unwrap_and_wrap_consistent_units(*args): def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): """Determine resulting unit from given operation. - + Options for `unit_op`: - + - "sum": `first_input_units`, unless non-multiplicative, which raises OffsetUnitCalculusError - "mul": product of all units in `all_args` @@ -121,6 +185,21 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): - "sqrt": square root of `first_input_units` - "reciprocal": reciprocal of `first_input_units` - "size": `first_input_units` raised to the power of `size` + + Parameters + ---------- + unit_op : + + first_input_units : + + all_args : + (Default value = []) + size : + (Default value = None) + + Returns + ------- + """ if unit_op == "sum": result_unit = (1 * first_input_units + 1 * first_input_units).units @@ -169,6 +248,17 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): def implements(numpy_func_string, func_type): """Register an __array_function__/__array_ufunc__ implementation for Quantity objects. + + Parameters + ---------- + numpy_func_string : + + func_type : + + + Returns + ------- + """ def decorator(func): @@ -188,25 +278,19 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): Parameters ---------- - func_type : str - "function" for NumPy functions, "ufunc" for NumPy ufuncs - func_str : str - String representing the name of the NumPy function/ufunc to add - input_units : pint.Unit or str or None - Parameter to control how the function downcasts to magnitudes of arguments. If - `pint.Unit`, converts all args and kwargs to this unit before downcasting to - magnitude. If "all_consistent", converts all args and kwargs to the unit of the - first Quantity in args and kwargs before downcasting to magnitude. If some - other string, the string is parsed as a unit, and all args and kwargs are - converted to that unit. If None, units are stripped without conversion. - output_unit : pint.Unit or str or None - Parameter to control the unit of the output. If `pint.Unit`, output is wrapped - with that unit. If "match_input", output is wrapped with the unit of the first - Quantity in args and kwargs. If a string representing a unit operation defined - in `get_op_output_unit`, output is wrapped by the unit determined by - `get_op_output_unit`. If some other string, the string is parsed as a unit, - which becomes the unit of the output. If None, the bare magnitude is returned. - + func_type : + + func_str : + + input_units : + (Default value = None) + output_unit : + (Default value = None) + + Returns + ------- + + """ # If NumPy is not available, do not attempt implement that which does not exist if np is None: @@ -797,7 +881,27 @@ def implementation(a, *args, **kwargs): def numpy_wrap(func_type, func, args, kwargs, types): - """Return the result from a NumPy function/ufunc as wrapped by Pint.""" + """ + + Parameters + ---------- + func_type : + + func : + + args : + + kwargs : + + types : + + + Returns + ------- + type + + + """ if func_type == "function": handled = HANDLED_FUNCTIONS elif func_type == "ufunc": diff --git a/pint/pint_eval.py b/pint/pint_eval.py index afa3877f9..4916da9c2 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -39,12 +39,18 @@ class EvalTreeNode: def __init__(self, left, operator=None, right=None): - """ - left + operator + right --> binary op + """left + operator + right --> binary op left + operator --> unary op left + right --> implicit op left --> single value - """ + + Parameters + ---------- + + Returns + ------- + + """ self.left = left self.operator = operator self.right = right @@ -65,9 +71,21 @@ def to_string(self): def evaluate( self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR_MAP ): - """ - define_op is a callable that translates tokens into objects + """define_op is a callable that translates tokens into objects bin_op and un_op provide functions for performing binary and unary operations + + Parameters + ---------- + define_op : + + bin_op : + (Default value = _BINARY_OPERATOR_MAP) + un_op : + (Default value = _UNARY_OPERATOR_MAP) + + Returns + ------- + """ if self.right: @@ -90,22 +108,49 @@ def evaluate( def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op=None): """ - Params: - Index, depth, and prev_op used recursively, so don't touch. - Tokens is an iterable of tokens from an expression to be evaluated. - - Transform the tokens from an expression into a recursive parse tree, following order - of operations. Operations can include binary ops (3 + 4), implicit ops (3 kg), or - unary ops (-1). - - General Strategy: - 1) Get left side of operator - 2) If no tokens left, return final result - 3) Get operator - 4) Use recursion to create tree starting at token on right side of operator (start at step #1) - 4.1) If recursive call encounters an operator with lower or equal priority to step #2, exit recursion - 5) Combine left side, operator, and right side into a new left side - 6) Go back to step #2 + + Parameters + ---------- + Index : + depth + Tokens : + is an iterable of tokens from an expression to be evaluated + Transform : + the tokens from an expression into a recursive parse tree + of : + operations + unary : + ops + General : + Strategy + 1 : + Get left side of operator + 2 : + If no tokens left + 3 : + Get operator + 4 : + Use recursion to create tree starting at token on right side of operator + 4 : + + 5 : + Combine left side + 6 : + Go back to step + tokens : + + op_priority : + (Default value = _OP_PRIORITY) + index : + (Default value = 0) + depth : + (Default value = 0) + prev_op : + (Default value = None) + + Returns + ------- + """ if depth == 0 and prev_op is None: diff --git a/pint/quantity.py b/pint/quantity.py index 63a939708..9c2d4fcc6 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -110,9 +110,19 @@ def wrapped(self, *args, **kwargs): @contextlib.contextmanager def printoptions(*args, **kwargs): - """ - Numpy printoptions context manager released with version 1.15.0 + """Numpy printoptions context manager released with version 1.15.0 https://docs.scipy.org/doc/numpy/reference/generated/numpy.printoptions.html + + Parameters + ---------- + *args : + + **kwargs : + + + Returns + ------- + """ opts = np.get_printoptions() @@ -127,10 +137,16 @@ class Quantity(PrettyIPython, SharedRegistryObject): """Implements a class to describe a physical quantity: the product of a numerical value and a unit of measurement. - :param value: value of the physical quantity to be created - :type value: str, pint.Quantity or any numeric type - :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or pint.Quantity + Parameters + ---------- + value : str, pint.Quantity or any numeric type + value of the physical quantity to be created + units : UnitsContainer, str or pint.Quantity + units of the physical quantity to be created + + Returns + ------- + """ #: Default formatting string. @@ -349,50 +365,46 @@ def format_babel(self, spec="", **kwspec): @property def magnitude(self): - """Quantity's magnitude. Long form for `m` - """ + """Quantity's magnitude. Long form for `m`""" return self._magnitude @property def m(self): - """Quantity's magnitude. Short form for `magnitude` - """ + """Quantity's magnitude. Short form for `magnitude`""" return self._magnitude def m_as(self, units): """Quantity's magnitude expressed in particular units. - :param units: destination units - :type units: pint.Quantity, str or dict + Parameters + ---------- + units : pint.Quantity, str or dict + destination units + + Returns + ------- + """ return self.to(units).magnitude @property def units(self): - """Quantity's units. Long form for `u` - - :rtype: UnitsContainer - """ + """Quantity's units. Long form for `u`""" return self._REGISTRY.Unit(self._units) @property def u(self): - """Quantity's units. Short form for `units` - - :rtype: UnitsContainer - """ + """Quantity's units. Short form for `units`""" return self._REGISTRY.Unit(self._units) @property def unitless(self): - """Return true if the quantity does not have units. - """ + """ """ return not bool(self.to_root_units()._units) @property def dimensionless(self): - """Return true if the quantity is dimensionless. - """ + """ """ tmp = self.to_root_units() return not bool(tmp.dimensionality) @@ -401,15 +413,25 @@ def dimensionless(self): @property def dimensionality(self): - """Quantity's dimensionality (e.g. {length: 1, time: -1}) - """ + """Quantity's dimensionality (e.g. {length: 1, time: -1})""" if self._dimensionality is None: self._dimensionality = self._REGISTRY._get_dimensionality(self._units) return self._dimensionality def check(self, dimension): - """Return true if the quantity's dimension matches passed dimension. + """ + + Parameters + ---------- + dimension : + + + Returns + ------- + type + + """ return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) @@ -418,14 +440,20 @@ def from_list(cls, quant_list, units=None): """Transforms a list of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. Same as from_sequence. - + If units is not specified and list is empty, the unit cannot be determined and a ValueError is raised. - :param quant_list: list of pint.Quantity - :type quant_list: list of pint.Quantity - :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or pint.Quantity + Parameters + ---------- + quant_list : list: list of pint.Quantity + list of pint.Quantity + units : UnitsContainer, str or pint.Quantity + units of the physical quantity to be created (Default value = None) + + Returns + ------- + """ return cls.from_sequence(quant_list, units=units) @@ -433,14 +461,20 @@ def from_list(cls, quant_list, units=None): def from_sequence(cls, seq, units=None): """Transforms a sequence of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. - + If units is not specified and sequence is empty, the unit cannot be determined and a ValueError is raised. - :param seq: sequence of pint.Quantity - :type seq: sequence of pint.Quantity - :param units: units of the physical quantity to be created - :type units: UnitsContainer, str or pint.Quantity + Parameters + ---------- + seq : sequence of pint.Quantity + sequence of pint.Quantity + units : UnitsContainer, str or pint.Quantity + units of the physical quantity to be created (Default value = None) + + Returns + ------- + """ len_seq = len(seq) @@ -494,8 +528,18 @@ def _convert_magnitude(self, other, *contexts, **ctx_kwargs): def ito(self, other=None, *contexts, **ctx_kwargs): """Inplace rescale to different units. - :param other: destination units. - :type other: pint.Quantity, str or dict + Parameters + ---------- + other : pint.Quantity, str or dict + destination units. (Default value = None) + *contexts : + + **ctx_kwargs : + + + Returns + ------- + """ other = to_units_container(other, self._REGISTRY) @@ -507,8 +551,18 @@ def ito(self, other=None, *contexts, **ctx_kwargs): def to(self, other=None, *contexts, **ctx_kwargs): """Return Quantity rescaled to different units. - :param other: destination units. - :type other: pint.Quantity, str or dict + Parameters + ---------- + other : pint.Quantity, str or dict + destination units. (Default value = None) + *contexts : + + **ctx_kwargs : + + + Returns + ------- + """ other = to_units_container(other, self._REGISTRY) @@ -517,8 +571,7 @@ def to(self, other=None, *contexts, **ctx_kwargs): return self.__class__(magnitude, other) def ito_root_units(self): - """Return Quantity rescaled to base units - """ + """ """ _, other = self._REGISTRY._get_root_units(self._units) @@ -528,8 +581,7 @@ def ito_root_units(self): return None def to_root_units(self): - """Return Quantity rescaled to base units - """ + """ """ _, other = self._REGISTRY._get_root_units(self._units) magnitude = self._convert_magnitude_not_inplace(other) @@ -537,8 +589,7 @@ def to_root_units(self): return self.__class__(magnitude, other) def ito_base_units(self): - """Return Quantity rescaled to base units - """ + """ """ _, other = self._REGISTRY._get_base_units(self._units) @@ -548,8 +599,7 @@ def ito_base_units(self): return None def to_base_units(self): - """Return Quantity rescaled to base units - """ + """ """ _, other = self._REGISTRY._get_base_units(self._units) magnitude = self._convert_magnitude_not_inplace(other) @@ -557,9 +607,17 @@ def to_base_units(self): return self.__class__(magnitude, other) def ito_reduced_units(self): - """Return Quantity scaled in place to reduced units, i.e. one unit per - dimension. This will not reduce compound units (intentionally), nor - can it make use of contexts at this time. + """ + + Parameters + ---------- + + Returns + ------- + type + dimension. This will not reduce compound units (intentionally), nor + can it make use of contexts at this time. + """ # shortcuts in case we're dimensionless or only a single unit if self.dimensionless: @@ -581,9 +639,17 @@ def ito_reduced_units(self): return self.ito(newunits) def to_reduced_units(self): - """Return Quantity scaled in place to reduced units, i.e. one unit per - dimension. This will not reduce compound units (intentionally), nor - can it make use of contexts at this time. + """ + + Parameters + ---------- + + Returns + ------- + type + dimension. This will not reduce compound units (intentionally), nor + can it make use of contexts at this time. + """ # can we make this more efficient? newq = copy.copy(self) @@ -591,9 +657,17 @@ def to_reduced_units(self): return newq def to_compact(self, unit=None): - """Return Quantity rescaled to compact, human-readable units. + """ - To get output in terms of a different unit, use the unit parameter. + Parameters + ---------- + unit : + (Default value = None) + + Returns + ------- + type + To get output in terms of a different unit, use the unit parameter. >>> import pint >>> ureg = pint.UnitRegistry() @@ -680,10 +754,16 @@ def __complex__(self): def _iadd_sub(self, other, op): """Perform addition or subtraction operation in-place and return the result. - :param other: object to be added to / subtracted from self - :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` - :param op: operator function (e.g. operator.add, operator.isub) - :type op: function + Parameters + ---------- + other : pint.Quantity or any type accepted by :func:`_to_magnitude` + object to be added to / subtracted from self + op : function + operator function (e.g. operator.add, operator.isub) + + Returns + ------- + """ if not self._check(other): # other not from same Registry or not a Quantity @@ -788,10 +868,16 @@ def _iadd_sub(self, other, op): def _add_sub(self, other, op): """Perform addition or subtraction operation and return the result. - :param other: object to be added to / subtracted from self - :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` - :param op: operator function (e.g. operator.add, operator.isub) - :type op: function + Parameters + ---------- + other : pint.Quantity or any type accepted by :func:`_to_magnitude` + object to be added to / subtracted from self + op : function + operator function (e.g. operator.add, operator.isub) + + Returns + ------- + """ if not self._check(other): # other not from same Registry or not a Quantity @@ -927,14 +1013,20 @@ def _imul_div(self, other, magnitude_op, units_op=None): """Perform multiplication or division operation in-place and return the result. - :param other: object to be multiplied/divided with self - :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` - :param magnitude_op: operator function to perform on the magnitudes + Parameters + ---------- + other : pint.Quantity or any type accepted by :func:`_to_magnitude` + object to be multiplied/divided with self + magnitude_op : function + operator function to perform on the magnitudes (e.g. operator.mul) - :type magnitude_op: function - :param units_op: operator function to perform on the units; if None, - *magnitude_op* is used - :type units_op: function or None + units_op : function or None + operator function to perform on the units; if None, + *magnitude_op* is used (Default value = None) + + Returns + ------- + """ if units_op is None: units_op = magnitude_op @@ -989,14 +1081,20 @@ def _imul_div(self, other, magnitude_op, units_op=None): def _mul_div(self, other, magnitude_op, units_op=None): """Perform multiplication or division operation and return the result. - :param other: object to be multiplied/divided with self - :type other: pint.Quantity or any type accepted by :func:`_to_magnitude` - :param magnitude_op: operator function to perform on the magnitudes + Parameters + ---------- + other : pint.Quantity or any type accepted by :func:`_to_magnitude` + object to be multiplied/divided with self + magnitude_op : function + operator function to perform on the magnitudes (e.g. operator.mul) - :type magnitude_op: function - :param units_op: operator function to perform on the units; if None, - *magnitude_op* is used - :type units_op: function or None + units_op : function or None + operator function to perform on the units; if None, + *magnitude_op* is used (Default value = None) + + Returns + ------- + """ if units_op is None: units_op = magnitude_op @@ -1422,6 +1520,19 @@ def __array_function__(self, func, types, args, kwargs): def _numpy_method_wrap(self, func, *args, **kwargs): """Convenience method to wrap on the fly NumPy ndarray methods taking care of the units. + + Parameters + ---------- + func : + + *args : + + **kwargs : + + + Returns + ------- + """ # Set input units if needed if func.__name__ in set_units_ufuncs: @@ -1542,8 +1653,17 @@ def searchsorted(self, v, side="left", sorter=None): def dot(self, b): """Dot product of two arrays. - + Wraps np.dot(). + + Parameters + ---------- + b : + + + Returns + ------- + """ return np.dot(self, b) @@ -1676,13 +1796,11 @@ def plus_minus(self, error, relative=False): # methods/properties that help for math operations with offset units @property def _is_multiplicative(self): - """Check if the Quantity object has only multiplicative units. - """ + """Check if the Quantity object has only multiplicative units.""" return not self._get_non_multiplicative_units() def _get_non_multiplicative_units(self): - """Return a list of the of non-multiplicative units of the Quantity object - """ + """ """ offset_units = [ unit for unit in self._units.keys() @@ -1691,13 +1809,21 @@ def _get_non_multiplicative_units(self): return offset_units def _get_delta_units(self): - """Return list of delta units ot the Quantity object - """ + """ """ delta_units = [u for u in self._units.keys() if u.startswith("delta_")] return delta_units def _has_compatible_delta(self, unit): """"Check if Quantity object has a delta_unit that is compatible with unit + + Parameters + ---------- + unit : + + + Returns + ------- + """ deltas = self._get_delta_units() if "delta_" + unit in deltas: @@ -1711,9 +1837,18 @@ def _has_compatible_delta(self, unit): def _ok_for_muldiv(self, no_offset_units=None): """Checks if Quantity object can be multiplied or divided - + :q: pint.Quantity object that is checked :no_offset_units: number of offset units in q + + Parameters + ---------- + no_offset_units : + (Default value = None) + + Returns + ------- + """ is_ok = True if no_offset_units is None: diff --git a/pint/registry.py b/pint/registry.py index 2ac55afce..591538091 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -86,6 +86,13 @@ class RegistryMeta(type): """This is just to call after_init at the right time instead of asking the developer to do it when subclassing. + + Parameters + ---------- + + Returns + ------- + """ def __call__(self, *args, **kwargs): @@ -95,8 +102,7 @@ def __call__(self, *args, **kwargs): class RegistryCache: - """Cache to speed up unit registries - """ + """Cache to speed up unit registries""" def __init__(self): #: Maps dimensionality (UnitsContainer) to Units (str) @@ -112,6 +118,13 @@ def __init__(self): class ContextCacheOverlay: """Layer on top of the base UnitRegistry cache, specific to a combination of active contexts which contain unit redefinitions. + + Parameters + ---------- + + Returns + ------- + """ def __init__(self, registry_cache: RegistryCache): @@ -123,9 +136,9 @@ def __init__(self, registry_cache: RegistryCache): class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. - + Capabilities: - + - Register units, prefixes, and dimensions, and their relations. - Convert between units. - Find dimensionality of a unit. @@ -134,24 +147,26 @@ class BaseRegistry(metaclass=RegistryMeta): - Parse a definition file. - Allow extending the definition file parser by registering @ directives. - :param filename: + Parameters + ---------- + filename : str or None path of the units definition file to load or line iterable object. Empty to load the default definition file. None to leave the UnitRegistry empty. - :type filename: - str or None - :param force_ndarray: + force_ndarray : convert any input, scalar or not to a numpy.ndarray. - :param on_redefinition: + on_redefinition : str action to take in case a unit is redefined: 'warn', 'raise', 'ignore' - :type on_redefinition: - str - :param auto_reduce_dimensions: + auto_reduce_dimensions : If True, reduce dimensionality on appropriate operations. - :param preprocessors: + preprocessors : list of callables which are iteratively ran on any input expression or unit string - :param fmt_locale: + fmt_locale : locale identifier string, used in `format_babel` + + Returns + ------- + """ #: Map context prefix to function @@ -231,8 +246,7 @@ def __init__( self._initialized = False def _init_dynamic_classes(self): - """Generate subclasses on the fly and attach them to self - """ + """Generate subclasses on the fly and attach them to self""" from .unit import build_unit_class self.Unit = build_unit_class(self) @@ -246,8 +260,7 @@ def _init_dynamic_classes(self): self.Measurement = build_measurement_class(self) def _after_init(self): - """This should be called after all __init__ - """ + """This should be called after all __init__""" self.define(UnitDefinition("pi", "π", (), ScaleConverter(math.pi))) if self._filename == "": @@ -264,7 +277,14 @@ def _register_parsers(self): def _parse_defaults(self, ifile): """Loader for a @default section. - :type ifile: SourceITerator + Parameters + ---------- + ifile : + + + Returns + ------- + """ next(ifile) for lineno, part in ifile.block_iter(): @@ -294,8 +314,14 @@ def __dir__(self): def set_fmt_locale(self, loc): """Change the locale used by default by `format_babel`. - :param loc: + Parameters + ---------- + loc : None` (do not translate), 'sys' (detect the system locale) or a locale id string. + + Returns + ------- + """ if isinstance(loc, str): if loc == "sys": @@ -308,8 +334,7 @@ def set_fmt_locale(self, loc): @property def default_format(self): - """Default formatting string for quantities. - """ + """Default formatting string for quantities.""" return self.Quantity.default_format @default_format.setter @@ -320,8 +345,14 @@ def default_format(self, value): def define(self, definition): """Add unit to the registry. - :param definition: a dimension, unit or prefix definition. - :type definition: str or Definition + Parameters + ---------- + definition : str or Definition + a dimension, unit or prefix definition. + + Returns + ------- + """ if isinstance(definition, str): @@ -332,14 +363,20 @@ def define(self, definition): def _define(self, definition): """Add unit to the registry. - + This method defines only multiplicative units, converting any other type to `delta_` units. - :param definition: a dimension, unit or prefix definition. - :type definition: Definition - :return: Definition instance, case sensitive unit dict, case insensitive unit dict. - :rtype: Definition, dict, dict + Parameters + ---------- + definition : Definition + a dimension, unit or prefix definition. + + Returns + ------- + Definition, dict, dict + Definition instance, case sensitive unit dict, case insensitive unit dict. + """ if isinstance(definition, DimensionDefinition): @@ -413,6 +450,19 @@ def _define(self, definition): def _define_adder(self, definition, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. It stores the definition under its name, symbol and aliases. + + Parameters + ---------- + definition : + + unit_dict : + + casei_unit_dict : + + + Returns + ------- + """ self._define_single_adder( definition.name, definition, unit_dict, casei_unit_dict @@ -431,8 +481,23 @@ def _define_adder(self, definition, unit_dict, casei_unit_dict): def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. - + It warns or raise error on redefinition. + + Parameters + ---------- + key : + + value : + + unit_dict : + + casei_unit_dict : + + + Returns + ------- + """ if key in unit_dict: if self._on_redefinition == "raise": @@ -454,9 +519,16 @@ def _define_alias(self, definition, unit_dict, casei_unit_dict): def _register_parser(self, prefix, parserfunc): """Register a loader for a given @ directive.. - :param prefix: string identifying the section (e.g. @context) - :param parserfunc: A function that is able to parse a Definition section. - :type parserfunc: SourceIterator -> None + Parameters + ---------- + prefix : + string identifying the section (e.g. @context) + parserfunc : SourceIterator -> None + A function that is able to parse a Definition section. + + Returns + ------- + """ if self._parsers is None: self._parsers = {} @@ -469,9 +541,17 @@ def _register_parser(self, prefix, parserfunc): def load_definitions(self, file, is_resource=False): """Add units and prefixes defined in a definition text file. - :param file: can be a filename or a line iterable. - :param is_resource: used to indicate that the file is a resource file - and therefore should be loaded from the package. + Parameters + ---------- + file : + can be a filename or a line iterable. + is_resource : + used to indicate that the file is a resource file + and therefore should be loaded from the package. (Default value = False) + + Returns + ------- + """ # Permit both filenames and line-iterables if isinstance(file, str): @@ -535,8 +615,7 @@ def load_definitions(self, file, is_resource=False): logger.error("In line {}, cannot add '{}' {}".format(no, line, ex)) def _build_cache(self): - """Build a cache of dimensionality and base units. - """ + """Build a cache of dimensionality and base units.""" self._cache = RegistryCache() deps = { @@ -573,7 +652,20 @@ def _build_cache(self): logger.warning(f"Could not resolve {unit_name}: {exc!r}") def get_name(self, name_or_alias, case_sensitive=True): - """Return the canonical name of a unit. + """ + + Parameters + ---------- + name_or_alias : + + case_sensitive : + (Default value = True) + + Returns + ------- + type + + """ if name_or_alias == "dimensionless": @@ -608,7 +700,18 @@ def get_name(self, name_or_alias, case_sensitive=True): return unit_name def get_symbol(self, name_or_alias): - """Return the preferred alias for a unit + """ + + Parameters + ---------- + name_or_alias : + + + Returns + ------- + type + + """ candidates = self.parse_unit_name(name_or_alias) if not candidates: @@ -631,18 +734,34 @@ def get_dimensionality(self, input_units): """Convert unit or dict of units or dimensions to a dict of base dimensions dimensions - :param input_units: - :return: dimensionality + Parameters + ---------- + input_units : + return: dimensionality + + Returns + ------- + type + dimensionality + """ input_units = to_units_container(input_units) return self._get_dimensionality(input_units) def _get_dimensionality(self, input_units): - """ Convert a UnitsContainer to base dimensions. + """Convert a UnitsContainer to base dimensions. + + Parameters + ---------- + input_units : + return: dimensionality + + Returns + ------- + type + dimensionality - :param input_units: - :return: dimensionality """ if not input_units: return UnitsContainer() @@ -681,12 +800,20 @@ def _get_dimensionality_recurse(self, ref, exp, accumulator): self._get_dimensionality_recurse(reg.reference, exp2, accumulator) def _get_dimensionality_ratio(self, unit1, unit2): - """ Get the exponential ratio between two units, i.e. solve unit2 = unit1**x for x. - :param unit1: first unit - :type unit1: UnitsContainer compatible (str, Unit, UnitsContainer, dict) - :param unit2: second unit - :type unit2: UnitsContainer compatible (str, Unit, UnitsContainer, dict) - :returns: exponential proportionality or None if the units cannot be converted + """Get the exponential ratio between two units, i.e. solve unit2 = unit1**x for x. + + Parameters + ---------- + unit1 : UnitsContainer compatible (str, Unit, UnitsContainer, dict) + first unit + unit2 : UnitsContainer compatible (str, Unit, UnitsContainer, dict) + second unit + + Returns + ------- + type + exponential proportionality or None if the units cannot be converted + """ # shortcut in case of equal units if unit1 == unit2: @@ -704,16 +831,24 @@ def _get_dimensionality_ratio(self, unit1, unit2): def get_root_units(self, input_units, check_nonmult=True): """Convert unit or dict of units to the root units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. - :param input_units: units - :type input_units: UnitsContainer or str - :param check_nonmult: if True, None will be returned as the - multiplicative factor if a non-multiplicative - units is found in the final Units. - :return: multiplicative factor, base units + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + + Returns + ------- + type + multiplicative factor, base units + """ input_units = to_units_container(input_units) @@ -723,16 +858,24 @@ def get_root_units(self, input_units, check_nonmult=True): def _get_root_units(self, input_units, check_nonmult=True): """Convert unit or dict of units to the root units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. - :param input_units: units - :type input_units: UnitsContainer or dict - :param check_nonmult: if True, None will be returned as the - multiplicative factor if a non-multiplicative - units is found in the final Units. - :return: multiplicative factor, base units + Parameters + ---------- + input_units : UnitsContainer or dict + units + check_nonmult : + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + + Returns + ------- + type + multiplicative factor, base units + """ if not input_units: return 1.0, UnitsContainer() @@ -759,16 +902,26 @@ def _get_root_units(self, input_units, check_nonmult=True): def get_base_units(self, input_units, check_nonmult=True, system=None): """Convert unit or dict of units to the base units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. - :param input_units: units - :type input_units: UnitsContainer or str - :param check_nonmult: if True, None will be returned as the - multiplicative factor if a non-multiplicative - units is found in the final Units. - :return: multiplicative factor, base units + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + system : + (Default value = None) + + Returns + ------- + type + multiplicative factor, base units + """ return self.get_root_units(input_units, check_nonmult) @@ -787,6 +940,17 @@ def _get_root_units_recurse(self, ref, exp, accumulators): def get_compatible_units(self, input_units, group_or_system=None): """ + + Parameters + ---------- + input_units : + + group_or_system : + (Default value = None) + + Returns + ------- + """ input_units = to_units_container(input_units) @@ -796,6 +960,17 @@ def get_compatible_units(self, input_units, group_or_system=None): def _get_compatible_units(self, input_units, group_or_system): """ + + Parameters + ---------- + input_units : + + group_or_system : + + + Returns + ------- + """ if not input_units: return frozenset() @@ -806,13 +981,22 @@ def _get_compatible_units(self, input_units, group_or_system): def convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. - :param value: value - :param src: source units. - :type src: pint.Quantity or str - :param dst: destination units. - :type dst: pint.Quantity or str + Parameters + ---------- + value : + value + src : pint.Quantity or str + source units. + dst : pint.Quantity or str + destination units. + inplace : + (Default value = False) + + Returns + ------- + type + converted value - :return: converted value """ src = to_units_container(src, self) @@ -826,13 +1010,24 @@ def convert(self, value, src, dst, inplace=False): def _convert(self, value, src, dst, inplace=False, check_dimensionality=True): """Convert value from some source to destination units. - :param value: value - :param src: source units. - :type src: UnitsContainer - :param dst: destination units. - :type dst: UnitsContainer + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + check_dimensionality : + (Default value = True) + + Returns + ------- + type + converted value - :return: converted value """ if check_dimensionality: @@ -869,10 +1064,18 @@ def parse_unit_name(self, unit_name, case_sensitive=True): In case of equivalent combinations (e.g. ('kilo', 'gram', '') and ('', 'kilogram', ''), prefer those with prefix. - :returns: + Parameters + ---------- + unit_name : + + case_sensitive : + (Default value = True) + + Returns + ------- + str, str, str), ...) all non-equivalent combinations of (prefix, unit name, suffix) - :rtype: - ((str, str, str), ...) + """ return self._dedup_candidates( self._parse_unit_name(unit_name, case_sensitive=case_sensitive) @@ -880,6 +1083,17 @@ def parse_unit_name(self, unit_name, case_sensitive=True): def _parse_unit_name(self, unit_name, case_sensitive=True): """Helper of parse_unit_name + + Parameters + ---------- + unit_name : + + case_sensitive : + (Default value = True) + + Returns + ------- + """ stw = unit_name.startswith edw = unit_name.endswith @@ -908,11 +1122,20 @@ def _parse_unit_name(self, unit_name, case_sensitive=True): @staticmethod def _dedup_candidates(candidates): """Helper of parse_unit_name. - + Given an iterable of unit triplets (prefix, name, suffix), remove those with different names but equal value, preferring those with a prefix. - + e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') + + Parameters + ---------- + candidates : + + + Returns + ------- + """ candidates = dict.fromkeys(candidates) # ordered set for cp, cu, cs in list(candidates): @@ -927,15 +1150,20 @@ def _dedup_candidates(candidates): def parse_units(self, input_string, as_delta=None): """Parse a units expression and returns a UnitContainer with the canonical names. - + The expression can only contain products, ratios and powers of units. - :param as_delta: if the expression has multiple units, the parser will - interpret non multiplicative units as their `delta_` counterparts. + Parameters + ---------- + as_delta : + if the expression has multiple units, the parser will + interpret non multiplicative units as their `delta_` counterparts. (Default value = None) + input_string : + + + Returns + ------- - :raises: - :class:`pint.UndefinedUnitError` if a unit is not in the registry - :class:`ValueError` if the expression is invalid. """ for p in self.preprocessors: input_string = p(input_string) @@ -944,6 +1172,17 @@ def parse_units(self, input_string, as_delta=None): def _parse_units(self, input_string, as_delta=True): """ + + Parameters + ---------- + input_string : + + as_delta : + (Default value = True) + + Returns + ------- + """ cache = self._cache.parse_unit if as_delta: @@ -1006,9 +1245,24 @@ def parse_expression( self, input_string, case_sensitive=True, use_decimal=False, **values ): """Parse a mathematical expression including units and return a quantity object. - + Numerical constants can be specified as keyword arguments and will take precedence over the names defined in the registry. + + Parameters + ---------- + input_string : + + case_sensitive : + (Default value = True) + use_decimal : + (Default value = False) + **values : + + + Returns + ------- + """ if not input_string: @@ -1030,15 +1284,23 @@ def parse_expression( class NonMultiplicativeRegistry(BaseRegistry): """Handle of non multiplicative units (e.g. Temperature). - + Capabilities: - Register non-multiplicative units and their relations. - Convert between non-multiplicative units. - :param default_as_delta: If True, non-multiplicative units are interpreted as - their *delta* counterparts in multiplications. - :param autoconvert_offset_to_baseunit: If True, non-multiplicative units are - converted to base units in multiplications. + Parameters + ---------- + default_as_delta : + If True, non-multiplicative units are interpreted as + their *delta* counterparts in multiplications. + autoconvert_offset_to_baseunit : + If True, non-multiplicative units are + converted to base units in multiplications. + + Returns + ------- + """ def __init__( @@ -1056,6 +1318,17 @@ def __init__( def _parse_units(self, input_string, as_delta=None): """ + + Parameters + ---------- + input_string : + + as_delta : + (Default value = None) + + Returns + ------- + """ if as_delta is None: as_delta = self.default_as_delta @@ -1064,14 +1337,20 @@ def _parse_units(self, input_string, as_delta=None): def _define(self, definition): """Add unit to the registry. - + In addition to what is done by the BaseRegistry, registers also non-multiplicative units. - :param definition: a dimension, unit or prefix definition. - :type definition: str | Definition - :return: Definition instance, case sensitive unit dict, case insensitive unit dict. - :rtype: Definition, dict, dict + Parameters + ---------- + definition : str | Definition + a dimension, unit or prefix definition. + + Returns + ------- + Definition, dict, dict + Definition instance, case sensitive unit dict, case insensitive unit dict. + """ definition, d, di = super()._define(definition) @@ -1127,17 +1406,26 @@ def _validate_and_extract(self, units): def _convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. - + In addition to what is done by the BaseRegistry, converts between non-multiplicative units. - :param value: value - :param src: source units. - :type src: UnitsContainer - :param dst: destination units. - :type dst: UnitsContainer + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + + Returns + ------- + type + converted value - :return: converted value """ # Conversion needs to consider if non-multiplicative (AKA offset @@ -1190,16 +1478,22 @@ def _convert(self, value, src, dst, inplace=False): class ContextRegistry(BaseRegistry): """Handle of Contexts. - + Conversion between units with different dimenstions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) - + Capabilities: - Register contexts. - Enable and disable contexts. - Parse @context directive. + Parameters + ---------- + + Returns + ------- + """ def __init__(self, **kwargs): @@ -1231,10 +1525,19 @@ def _parse_context(self, ifile): def add_context(self, context: Context) -> None: """Add a context object to the registry. - + The context will be accessible by its name and aliases. - + Notice that this method will NOT enable the context. Use `enable_contexts`. + + Parameters + ---------- + context: Context : + + + Returns + ------- + """ if not context.name: raise ValueError("Can't add unnamed context to registry") @@ -1253,8 +1556,17 @@ def add_context(self, context: Context) -> None: def remove_context(self, name_or_alias: str) -> Context: """Remove a context from the registry and return it. - + Notice that this methods will not disable the context. Use `disable_contexts`. + + Parameters + ---------- + name_or_alias: str : + + + Returns + ------- + """ context = self._contexts[name_or_alias] @@ -1273,6 +1585,13 @@ def _switch_context_cache_and_units(self) -> None: and self._units specific to the combination of active contexts. The next time this method is invoked with the same combination of contexts, reuse the same variant self._cache and self._units as in the previous time. + + Parameters + ---------- + + Returns + ------- + """ del self._units.maps[:-1] units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) @@ -1307,6 +1626,15 @@ def _switch_context_cache_and_units(self) -> None: def _redefine(self, definition: UnitDefinition) -> None: """Redefine a unit from a context + + Parameters + ---------- + definition: UnitDefinition : + + + Returns + ------- + """ # Find original definition in the UnitRegistry candidates = self.parse_unit_name(definition.name) @@ -1351,8 +1679,20 @@ def _redefine(self, definition: UnitDefinition) -> None: def enable_contexts(self, *names_or_contexts, **kwargs) -> None: """Enable contexts provided by name or by object. - :param names_or_contexts: sequence of the contexts or contexts names/alias - :param kwargs: keyword arguments for the context + Parameters + ---------- + names_or_contexts : + sequence of the contexts or contexts names/alias + kwargs : + keyword arguments for the context + *names_or_contexts : + + **kwargs : + + + Returns + ------- + """ # If present, copy the defaults from the containing contexts @@ -1387,6 +1727,15 @@ def enable_contexts(self, *names_or_contexts, **kwargs) -> None: def disable_contexts(self, n: int = None) -> None: """Disable the last n enabled contexts. + + Parameters + ---------- + n: int : + (Default value = None) + + Returns + ------- + """ self._active_ctx.remove_contexts(n) self._switch_context_cache_and_units() @@ -1396,37 +1745,51 @@ def context(self, *names, **kwargs): """Used as a context manager, this function enables to activate a context which is removed after usage. - :param names: name of the context. - :param kwargs: keyword arguments for the contexts. - - Context are called by their name:: - - >>> with ureg.context('one'): + Parameters + ---------- + names : + name of the context. + kwargs : + keyword arguments for the contexts. + + Context are called by their name:: + + + If the context has an argument, you can specify its value as a keyword + argument:: + + + Multiple contexts can be entered in single call: + + + or nested allowing you to give different values to the same keyword argument:: + + + A nested context inherits the defaults from the containing context:: + *names : + + **kwargs : + + + Returns + ------- + + >>> with ureg.context('one'): ... pass - - If the context has an argument, you can specify its value as a keyword - argument:: - + >>> with ureg.context('one', n=1): ... pass - - Multiple contexts can be entered in single call: - + >>> with ureg.context('one', 'two', n=1): ... pass - - or nested allowing you to give different values to the same keyword argument:: - + >>> with ureg.context('one', n=1): ... with ureg.context('two', n=2): ... pass - - A nested context inherits the defaults from the containing context:: - + >>> with ureg.context('one', n=1): ... with ureg.context('two'): # Here n takes the value of the upper context ... pass - """ # Enable the contexts. @@ -1443,18 +1806,29 @@ def context(self, *names, **kwargs): def with_context(self, name, **kw): """Decorator to wrap a function call in a Pint context. - + Use it to ensure that a certain context is active when calling a function:: - >>> @ureg.with_context('sp') + Parameters + ---------- + names : + name of the context. + kwargs : + keyword arguments for the contexts. + name : + + **kw : + + + Returns + ------- + type + the wrapped function. + + >>> @ureg.with_context('sp') ... def my_cool_fun(wavelenght): ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz')) - - - :param names: name of the context. - :param kwargs: keyword arguments for the contexts. - :return: the wrapped function. """ def decorator(func): @@ -1476,18 +1850,27 @@ def wrapper(*values, **kwargs): def _convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. - + In addition to what is done by the BaseRegistry, converts between units with different dimensions by following transformation rules defined in the context. - :param value: value - :param src: source units. - :type src: UnitsContainer - :param dst: destination units. - :type dst: UnitsContainer + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + + Returns + ------- + type + converted value - :return: converted value """ # If there is an active context, we look for a path connecting source and @@ -1510,6 +1893,17 @@ def _convert(self, value, src, dst, inplace=False): def _get_compatible_units(self, input_units, group_or_system): """ + + Parameters + ---------- + input_units : + + group_or_system : + + + Returns + ------- + """ src_dim = self._get_dimensionality(input_units) @@ -1528,17 +1922,23 @@ def _get_compatible_units(self, input_units, group_or_system): class SystemRegistry(BaseRegistry): """Handle of Systems and Groups. - + Conversion between units with different dimenstions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) - + Capabilities: - Register systems and groups. - List systems - Get or get the default system. - Parse @system and @group directive. + Parameters + ---------- + + Returns + ------- + """ def __init__(self, system=None, **kwargs): @@ -1564,10 +1964,17 @@ def _init_dynamic_classes(self): def _after_init(self): """After init function - + Create default group. Add all orphan units to it. Set default system. + + Parameters + ---------- + + Returns + ------- + """ super()._after_init() @@ -1604,9 +2011,18 @@ def _parse_system(self, ifile): def get_group(self, name, create_if_needed=True): """Return a Group. - :param name: Name of the group to be - :param create_if_needed: Create a group if not Found. If False, raise an Exception. - :return: Group + Parameters + ---------- + name : + Name of the group to be + create_if_needed : + Create a group if not Found. If False, raise an Exception. (Default value = True) + + Returns + ------- + type + Group + """ if name in self._groups: return self._groups[name] @@ -1637,9 +2053,18 @@ def default_system(self, name): def get_system(self, name, create_if_needed=True): """Return a Group. - :param name: Name of the group to be - :param create_if_needed: Create a group if not Found. If False, raise an Exception. - :return: System + Parameters + ---------- + name : + Name of the group to be + create_if_needed : + Create a group if not Found. If False, raise an Exception. (Default value = True) + + Returns + ------- + type + System + """ if name in self._systems: return self._systems[name] @@ -1664,19 +2089,29 @@ def _define(self, definition): def get_base_units(self, input_units, check_nonmult=True, system=None): """Convert unit or dict of units to the base units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. - + Unlike BaseRegistry, in this registry root_units might be different from base_units - :param input_units: units - :type input_units: UnitsContainer or str - :param check_nonmult: if True, None will be returned as the - multiplicative factor if a non-multiplicative - units is found in the final Units. - :return: multiplicative factor, base units + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + system : + (Default value = None) + + Returns + ------- + type + multiplicative factor, base units + """ input_units = to_units_container(input_units) @@ -1728,6 +2163,17 @@ def _get_base_units(self, input_units, check_nonmult=True, system=None): def _get_compatible_units(self, input_units, group_or_system): """ + + Parameters + ---------- + input_units : + + group_or_system : + + + Returns + ------- + """ if group_or_system is None: @@ -1752,23 +2198,35 @@ def _get_compatible_units(self, input_units, group_or_system): class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): """The unit registry stores the definitions and relationships between units. - :param filename: path of the units definition file to load or line-iterable object. - Empty to load the default definition file. - None to leave the UnitRegistry empty. - :param force_ndarray: convert any input, scalar or not to a numpy.ndarray. - :param default_as_delta: In the context of a multiplication of units, interpret - non-multiplicative units as their *delta* counterparts. - :param autoconvert_offset_to_baseunit: If True converts offset units in quantites are - converted to their base units in multiplicative - context. If False no conversion happens. - :param on_redefinition: action to take in case a unit is redefined. - 'warn', 'raise', 'ignore' - :type on_redefinition: str - :param auto_reduce_dimensions: If True, reduce dimensionality on appropriate operations. - :param preprocessors: list of callables which are iteratively ran on any input expression - or unit string - :param fmt_locale: + Parameters + ---------- + filename : + path of the units definition file to load or line-iterable object. + Empty to load the default definition file. + None to leave the UnitRegistry empty. + force_ndarray : + convert any input, scalar or not to a numpy.ndarray. + default_as_delta : + In the context of a multiplication of units, interpret + non-multiplicative units as their *delta* counterparts. + autoconvert_offset_to_baseunit : + If True converts offset units in quantites are + converted to their base units in multiplicative + context. If False no conversion happens. + on_redefinition : str + action to take in case a unit is redefined. + 'warn', 'raise', 'ignore' + auto_reduce_dimensions : + If True, reduce dimensionality on appropriate operations. + preprocessors : + list of callables which are iteratively ran on any input expression + or unit string + fmt_locale : locale identifier string, used in `format_babel`. Default to None + + Returns + ------- + """ def __init__( @@ -1798,16 +2256,31 @@ def __init__( def pi_theorem(self, quantities): """Builds dimensionless quantities using the Buckingham π theorem - :param quantities: mapping between variable name and units - :type quantities: dict - :return: a list of dimensionless quantities expressed as dicts + + Parameters + ---------- + quantities : dict + mapping between variable name and units + + Returns + ------- + type + a list of dimensionless quantities expressed as dicts + """ return pi_theorem(quantities, self) def setup_matplotlib(self, enable=True): """Set up handlers for matplotlib's unit support. - :param enable: whether support should be enabled or disabled - :type enable: bool + + Parameters + ---------- + enable : bool + whether support should be enabled or disabled (Default value = True) + + Returns + ------- + """ # Delays importing matplotlib until it's actually requested from .matplotlib import setup_matplotlib_handlers diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 07877ba4a..14e68f9ff 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -19,8 +19,16 @@ def _replace_units(original_units, values_by_name): """Convert a unit compatible type to a UnitsContainer. - :param original_units: a UnitsContainer instance. - :param values_by_name: a map between original names and the new values. + Parameters + ---------- + original_units : + a UnitsContainer instance. + values_by_name : + a map between original names and the new values. + + Returns + ------- + """ q = 1 for arg_name, exponent in original_units.items(): @@ -34,7 +42,18 @@ def _to_units_container(a, registry=None): checking if it is string field prefixed with an equal (which is considered a reference) - Return a tuple with the unit container and a boolean indicating if it was a reference. + Parameters + ---------- + a : + + registry : + (Default value = None) + + Returns + ------- + type + + """ if isinstance(a, str) and "=" in a: return to_units_container(a.split("=", 1)[1]), True @@ -135,9 +154,22 @@ def _converter(ureg, values, strict): def _apply_defaults(func, args, kwargs): """Apply default keyword arguments. - + Named keywords may have been left blank. This function applies the default values so that every argument is defined. + + Parameters + ---------- + func : + + args : + + kwargs : + + + Returns + ------- + """ sig = signature(func) @@ -151,25 +183,34 @@ def _apply_defaults(func, args, kwargs): def wraps(ureg, ret, args, strict=True): """Wraps a function to become pint-aware. - + Use it when a function requires a numerical value but in some specific units. The wrapper function will take a pint quantity, convert to the units specified in `args` and then call the wrapped function with the resulting magnitude. - + The value returned by the wrapped function will be converted to the units specified in `ret`. - + Use None to skip argument conversion. Set strict to False, to accept also numerical values. - :param ureg: a UnitRegistry instance. - :param ret: output units. - :param args: iterable of input units. - :param strict: boolean to indicate that only quantities are accepted. - :return: the wrapped function. - :raises: - :class:`ValueError` if strict and one of the arguments is not a Quantity. + Parameters + ---------- + ureg : + a UnitRegistry instance. + ret : + output units. + args : + iterable of input units. + strict : + boolean to indicate that only quantities are accepted. (Default value = True) + + Returns + ------- + type + the wrapped function. + """ if not isinstance(args, (list, tuple)): @@ -228,17 +269,31 @@ def wrapper(*values, **kw): def check(ureg, *args): """Decorator to for quantity type checking for function inputs. - + Use it to ensure that the decorated function input parameters match the expected type of pint quantity. - + Use None to skip argument checking. - :param ureg: a UnitRegistry instance. - :param args: iterable of input units. - :return: the wrapped function. - :raises pint.DimensionalityError: + Parameters + ---------- + ureg : + a UnitRegistry instance. + args : + iterable of input units. + *args : + + + Returns + ------- + type + the wrapped function. + + Raises + ------ + pint.DimensionalityError if the parameters don't match dimensions + """ dimensions = [ ureg.get_dimensionality(dim) if dim is not None else None for dim in args diff --git a/pint/systems.py b/pint/systems.py index d3237631f..64d90114d 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -26,21 +26,28 @@ class Group(SharedRegistryObject): """A group is a set of units. - + Units can be added directly or by including other groups. - + Members are computed dynamically, that is if a unit is added to a group X all groups that include X are affected. - + The group belongs to one Registry. - + It can be specified in the definition file as:: - + @group [using , ..., ] ... @end + + Parameters + ---------- + + Returns + ------- + """ #: Regex to match the header parts of a definition. @@ -86,10 +93,15 @@ def __init__(self, name): @property def members(self): """Names of the units that are members of the group. - + Calculated to include to all units in all included _used_groups. - :rtype: frozenset[str] + Parameters + ---------- + + Returns + ------- + """ if self._computed_members is None: self._computed_members = set(self._unit_names) @@ -102,8 +114,7 @@ def members(self): return self._computed_members def invalidate_members(self): - """Invalidate computed members in this Group and all parent nodes. - """ + """Invalidate computed members in this Group and all parent nodes.""" self._computed_members = None d = self._REGISTRY._groups for name in self._used_by: @@ -127,7 +138,14 @@ def is_used_group(self, group_name): def add_units(self, *unit_names): """Add units to group. - :type unit_names: str + Parameters + ---------- + *unit_names : + + + Returns + ------- + """ for unit_name in unit_names: self._unit_names.add(unit_name) @@ -141,7 +159,14 @@ def non_inherited_unit_names(self): def remove_units(self, *unit_names): """Remove units from group. - :type unit_names: str + Parameters + ---------- + *unit_names : + + + Returns + ------- + """ for unit_name in unit_names: self._unit_names.remove(unit_name) @@ -151,7 +176,14 @@ def remove_units(self, *unit_names): def add_groups(self, *group_names): """Add groups to group. - :type group_names: str + Parameters + ---------- + *group_names : + + + Returns + ------- + """ d = self._REGISTRY._groups for group_name in group_names: @@ -172,7 +204,14 @@ def add_groups(self, *group_names): def remove_groups(self, *group_names): """Remove groups from group. - :type group_names: str + Parameters + ---------- + *group_names : + + + Returns + ------- + """ d = self._REGISTRY._groups for group_name in group_names: @@ -187,10 +226,16 @@ def remove_groups(self, *group_names): def from_lines(cls, lines, define_func): """Return a Group object parsing an iterable of lines. - :param lines: iterable - :type lines: list[str] - :param define_func: Function to define a unit in the registry. - :type define_func: str -> None + Parameters + ---------- + lines : list[str] + iterable + define_func : str -> None + Function to define a unit in the registry. + + Returns + ------- + """ lines = SourceIterator(lines) lineno, header = next(lines) @@ -246,29 +291,36 @@ def __getattr__(self, item): class System(SharedRegistryObject): """A system is a Group plus a set of base units. - + Members are computed dynamically, that is if a unit is added to a group X all groups that include X are affected. - + The System belongs to one Registry. - + It can be specified in the definition file as:: - + @system [using , ..., ] ... @end - + The syntax for the rule is: - + new_unit_name : old_unit_name - + where: - old_unit_name: a root unit part which is going to be removed from the system. - new_unit_name: a non root unit which is going to replace the old_unit. - + If the new_unit_name and the old_unit_name, the later and the colon can be ommited. + + Parameters + ---------- + + Returns + ------- + """ #: Regex to match the header parts of a context. @@ -333,14 +385,20 @@ def members(self): return self._computed_members def invalidate_members(self): - """Invalidate computed members in this Group and all parent nodes. - """ + """Invalidate computed members in this Group and all parent nodes.""" self._computed_members = None def add_groups(self, *group_names): """Add groups to group. - :type group_names: str + Parameters + ---------- + *group_names : + + + Returns + ------- + """ self._used_groups |= set(group_names) @@ -349,7 +407,14 @@ def add_groups(self, *group_names): def remove_groups(self, *group_names): """Remove groups from group. - :type group_names: str + Parameters + ---------- + *group_names : + + + Returns + ------- + """ self._used_groups -= set(group_names) @@ -358,7 +423,14 @@ def remove_groups(self, *group_names): def format_babel(self, locale): """translate the name of the system - :type locale: Locale + Parameters + ---------- + locale : + + + Returns + ------- + """ if locale and self.name in _babel_systems: name = _babel_systems[self.name] diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index a1e51d92e..bd384d612 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -117,8 +117,7 @@ def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None) def testsuite(): - """A testsuite that has all the pint tests. - """ + """A testsuite that has all the pint tests.""" suite = unittest.TestLoader().discover(os.path.dirname(__file__)) from pint.compat import HAS_NUMPY, HAS_UNCERTAINTIES @@ -135,8 +134,7 @@ def testsuite(): def main(): - """Runs the testsuite as command line application. - """ + """Runs the testsuite as command line application.""" try: unittest.main() except Exception as e: @@ -145,8 +143,15 @@ def main(): def run(): """Run all tests. - + :return: a :class:`unittest.TestResult` object + + Parameters + ---------- + + Returns + ------- + """ test_runner = unittest.TextTestRunner() return test_runner.run(testsuite()) @@ -165,7 +170,14 @@ def run(): def add_docs(suite): """Add docs to suite - :type suite: unittest.TestSuite + Parameters + ---------- + suite : + + + Returns + ------- + """ docpath = os.path.join(os.path.dirname(__file__), "..", "..", "docs") docpath = os.path.abspath(docpath) diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index ca0a72442..ee4917d2a 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -109,12 +109,24 @@ def parameterize( cls, param_names, data, func_name_format="{func_name}_{case_num:05d}" ): """Decorator for parameterizing a test method - example: - + @ParameterizedTestCase.parameterize( ("isbn", "expected_title"), [ ("0262033844", "Introduction to Algorithms"), ("0321558146", "Campbell Essential Biology")]) + Parameters + ---------- + param_names : + + data : + + func_name_format : + (Default value = "{func_name}_{case_num:05d}") + + Returns + ------- + """ def decorator(func): diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index 923304c68..6e2055c09 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -145,6 +145,13 @@ def test_measurement_3args(self): class TestSwapApplicationRegistry(BaseTestCase): """Test that the constructors of Quantity, Unit, and Measurement capture the registry that is set as the application registry at creation time + + Parameters + ---------- + + Returns + ------- + """ def setUp(self): diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index bc4a2f7fd..04aa31dbb 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -527,12 +527,25 @@ def test_issue625a(self): @ureg.wraps(ureg.second, (ureg.meters, ureg.meters / ureg.second ** 2)) def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): """Calculate time to fall from a height h with a default gravity. - + By default, the gravity is assumed to be earth gravity, but it can be modified. - + d = .5 * g * t**2 t = sqrt(2 * d / g) + + Parameters + ---------- + height : + + gravity : + (Default value = Q_(9.8) + "m/s^2") : + + + Returns + ------- + """ return sqrt(2 * height / gravity) @@ -551,6 +564,19 @@ def test_issue625b(self): @ureg.wraps("=A*B", ("=A", "=B")) def get_displacement(time, rate=Q_(1, "m/s")): """Calculates displacement from a duration and default rate. + + Parameters + ---------- + time : + + rate : + (Default value = Q_(1) + "m/s") : + + + Returns + ------- + """ return time * rate @@ -649,6 +675,13 @@ def test_issue912(self): """pprint.pformat() invokes sorted() on large sets and frozensets and graciously handles TypeError, but not generic Exceptions. This test will fail if pint.DimensionalityError stops being a subclass of TypeError. + + Parameters + ---------- + + Returns + ------- + """ meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 5adf0e43d..72fa70e85 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -802,8 +802,7 @@ def test_iterable(self): self.assertFalse(np.iterable(1 * self.ureg.m)) def test_reversible_op(self): - """ - """ + """ """ x = self.q.magnitude u = self.Q_(np.ones(x.shape)) self.assertQuantityEqual(x / self.q, u * x / self.q) @@ -1125,15 +1124,22 @@ def pad_with(vector, pad_width, iaxis, kwargs): @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): """Universal functions (ufuncs) > Bittwiddling functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#bittwiddlingfunctions - + bitwise_and(x1, x2[, out]) Compute the bitwise AND of two arrays elementwise. bitwise_or(x1, x2[, out]) Compute the bitwise OR of two arrays elementwise. bitwise_xor(x1, x2[, out]) Compute the bitwise XOR of two arrays elementwise. invert(x[, out]) Compute bitwise inversion, or bitwise NOT, elementwise. left_shift(x1, x2[, out]) Shift the bits of an integer to the left. right_shift(x1, x2[, out]) Shift the bits of an integer to the right. + + Parameters + ---------- + + Returns + ------- + """ @property diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index eecb39e0f..d82096422 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1502,6 +1502,13 @@ class TestCompareZero(QuantityTestCase): """This test case checks the special treatment that the zero value receives in the comparisons: pint>=0.9 supports comparisons against zero even for non-dimensionless quantities + + Parameters + ---------- + + Returns + ------- + """ def test_equal_zero(self): diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index a1accc3ac..60559ed93 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -42,18 +42,30 @@ def _test1( ): """Test function that takes a single argument and returns Quantity. - :param func: function callable. - :param ok_with: iterables of values that work fine. - :param raise_with: iterables of values that raise exceptions. - :param output_units: units to be used when building results. - 'same': ok_with[n].units (default). - is float: ok_with[n].units ** output_units. - None: no output units, the result should be an ndarray. - Other value will be parsed as unit. - :param results: iterable of results. - If None, the result will be obtained by applying - func to each ok_with value - :param rtol: relative tolerance. + Parameters + ---------- + func : + function callable. + ok_with : + iterables of values that work fine. + raise_with : + iterables of values that raise exceptions. (Default value = ()) + output_units : + units to be used when building results. + 'same': ok_with[n].units (default). + is float: ok_with[n].units ** output_units. + None: no output units, the result should be an ndarray. + Other value will be parsed as unit. + results : + iterable of results. + If None, the result will be obtained by applying + func to each ok_with value (Default value = None) + rtol : + relative tolerance. (Default value = 1e-6) + + Returns + ------- + """ if results is None: results = [None] * len(ok_with) @@ -83,12 +95,22 @@ def _test1( def _testn(self, func, ok_with, raise_with=(), results=None): """Test function that takes a single argument and returns and ndarray (not a Quantity) - :param func: function callable. - :param ok_with: iterables of values that work fine. - :param raise_with: iterables of values that raise exceptions. - :param results: iterable of results. - If None, the result will be obtained by applying - func to each ok_with value + Parameters + ---------- + func : + function callable. + ok_with : + iterables of values that work fine. + raise_with : + iterables of values that raise exceptions. (Default value = ()) + results : + iterable of results. + If None, the result will be obtained by applying + func to each ok_with value (Default value = None) + + Returns + ------- + """ self._test1(func, ok_with, raise_with, output_units=None, results=results) @@ -103,18 +125,32 @@ def _test1_2o( ): """Test functions that takes a single argument and return two Quantities. - :param func: function callable. - :param ok_with: iterables of values that work fine. - :param raise_with: iterables of values that raise exceptions. - :param output_units: tuple of units to be used when building the result tuple. - 'same': ok_with[n].units (default). - is float: ok_with[n].units ** output_units. - None: no output units, the result should be an ndarray. - Other value will be parsed as unit. - :param results: iterable of results. - If None, the result will be obtained by applying - func to each ok_with value - :param rtol: relative tolerance. + Parameters + ---------- + func : + function callable. + ok_with : + iterables of values that work fine. + raise_with : + iterables of values that raise exceptions. (Default value = ()) + output_units : + tuple of units to be used when building the result tuple. + 'same': ok_with[n].units (default). + is float: ok_with[n].units ** output_units. + None: no output units, the result should be an ndarray. + Other value will be parsed as unit. + results : + iterable of results. + If None, the result will be obtained by applying + func to each ok_with value (Default value = None) + rtol : + relative tolerance. (Default value = 1e-6) + "same") : + + + Returns + ------- + """ if results is None: @@ -152,19 +188,32 @@ def _test2( ): """Test function that takes two arguments and return a Quantity. - :param func: function callable. - :param x1: first argument of func. - :param ok_with: iterables of values that work fine. - :param raise_with: iterables of values that raise exceptions. - :param output_units: units to be used when building results. - 'same': x1.units (default). - 'prod': x1.units * ok_with[n].units - 'div': x1.units / ok_with[n].units - 'second': x1.units * ok_with[n] - None: no output units, the result should be an ndarray. - Other value will be parsed as unit. - :param rtol: relative tolerance. - :param convert2: if the ok_with[n] should be converted to x1.units. + Parameters + ---------- + func : + function callable. + x1 : + first argument of func. + ok_with : + iterables of values that work fine. + raise_with : + iterables of values that raise exceptions. (Default value = ()) + output_units : + units to be used when building results. + 'same': x1.units (default). + 'prod': x1.units * ok_with[n].units + 'div': x1.units / ok_with[n].units + 'second': x1.units * ok_with[n] + None: no output units, the result should be an ndarray. + Other value will be parsed as unit. + rtol : + relative tolerance. (Default value = 1e-6) + convert2 : + if the ok_with[n] should be converted to x1.units. (Default value = True) + + Returns + ------- + """ for x2 in ok_with: err_msg = "At {} with {} and {}".format(func.__name__, x1, x2) @@ -201,10 +250,20 @@ def _test2( def _testn2(self, func, x1, ok_with, raise_with=()): """Test function that takes two arguments and return a ndarray. - :param func: function callable. - :param x1: first argument of func. - :param ok_with: iterables of values that work fine. - :param raise_with: iterables of values that raise exceptions. + Parameters + ---------- + func : + function callable. + x1 : + first argument of func. + ok_with : + iterables of values that work fine. + raise_with : + iterables of values that raise exceptions. (Default value = ()) + + Returns + ------- + """ self._test2(func, x1, ok_with, raise_with, output_units=None) @@ -212,9 +271,9 @@ def _testn2(self, func, x1, ok_with, raise_with=()): @helpers.requires_numpy() class TestMathUfuncs(TestUFuncs): """Universal functions (ufunc) > Math operations - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#math-operations - + add(x1, x2[, out]) Add arguments element-wise. subtract(x1, x2[, out]) Subtract arguments, element-wise. multiply(x1, x2[, out]) Multiply arguments element-wise. @@ -243,6 +302,13 @@ class TestMathUfuncs(TestUFuncs): square(x[, out]) Return the element-wise square of the input. reciprocal(x[, out]) Return the reciprocal of the argument, element-wise. ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array. + + Parameters + ---------- + + Returns + ------- + """ def test_add(self): @@ -353,9 +419,9 @@ def test_reciprocal(self): @helpers.requires_numpy() class TestTrigUfuncs(TestUFuncs): """Universal functions (ufunc) > Trigonometric functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#trigonometric-functions - + sin(x[, out]) Trigonometric sine, element-wise. cos(x[, out]) Cosine elementwise. tan(x[, out]) Compute tangent element-wise. @@ -372,6 +438,13 @@ class TestTrigUfuncs(TestUFuncs): arctanh(x[, out]) Inverse hyperbolic tangent elementwise. deg2rad(x[, out]) Convert angles from degrees to radians. rad2deg(x[, out]) Convert angles from radians to degrees. + + Parameters + ---------- + + Returns + ------- + """ def test_sin(self): @@ -603,15 +676,22 @@ def test_rad2deg(self): class TestComparisonUfuncs(TestUFuncs): """Universal functions (ufunc) > Comparison functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#comparison-functions - + greater(x1, x2[, out]) Return the truth value of (x1 > x2) element-wise. greater_equal(x1, x2[, out]) Return the truth value of (x1 >= x2) element-wise. less(x1, x2[, out]) Return the truth value of (x1 < x2) element-wise. less_equal(x1, x2[, out]) Return the truth value of (x1 =< x2) element-wise. not_equal(x1, x2[, out]) Return (x1 != x2) element-wise. equal(x1, x2[, out]) Return (x1 == x2) element-wise. + + Parameters + ---------- + + Returns + ------- + """ def test_greater(self): @@ -635,9 +715,9 @@ def test_equal(self): class TestFloatingUfuncs(TestUFuncs): """Universal functions (ufunc) > Floating functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#floating-functions - + isreal(x) Returns a bool array, where True if input element is real. iscomplex(x) Returns a bool array, where True if input element is complex. isfinite(x[, out]) Test element-wise for finite-ness (not infinity or not Not a Number). @@ -653,6 +733,13 @@ class TestFloatingUfuncs(TestUFuncs): floor(x[, out]) Return the floor of the input, element-wise. ceil(x[, out]) Return the ceiling of the input, element-wise. trunc(x[, out]) Return the truncated value of the input, element-wise. + + Parameters + ---------- + + Returns + ------- + """ def test_isreal(self): diff --git a/pint/unit.py b/pint/unit.py index 18a66da23..96066527e 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -20,11 +20,7 @@ class Unit(PrettyIPython, SharedRegistryObject): - """Implements a class to describe a unit supporting math operations. - - :type units: UnitsContainer, str, Unit or Quantity. - - """ + """Implements a class to describe a unit supporting math operations.""" #: Default formatting string. default_format = "" @@ -120,16 +116,12 @@ def format_babel(self, spec="", **kwspec): @property def dimensionless(self): - """Return true if the Unit is dimensionless. - - """ + """ """ return not bool(self.dimensionality) @property def dimensionality(self): - """Unit's dimensionality (e.g. {length: 1, time: -1}) - - """ + """Unit's dimensionality (e.g. {length: 1, time: -1})""" try: return self._dimensionality except AttributeError: @@ -263,12 +255,20 @@ def systems(self): def from_(self, value, strict=True, name="value"): """Converts a numerical value or quantity to this unit - :param value: a Quantity (or numerical value if strict=False) to convert - :param strict: boolean to indicate that only quanities are accepted - :param name: descriptive name to use if an exception occurs - :return: The converted value as this unit - :raises: - :class:`ValueError` if strict and one of the arguments is not a Quantity. + Parameters + ---------- + value : + a Quantity (or numerical value if strict=False) to convert + strict : + boolean to indicate that only quanities are accepted (Default value = True) + name : + descriptive name to use if an exception occurs (Default value = "value") + + Returns + ------- + type + The converted value as this unit + """ if self._check(value): if not isinstance(value, self._REGISTRY.Quantity): @@ -283,12 +283,20 @@ def m_from(self, value, strict=True, name="value"): """Converts a numerical value or quantity to this unit, then returns the magnitude of the converted value - :param value: a Quantity (or numerical value if strict=False) to convert - :param strict: boolean to indicate that only quanities are accepted - :param name: descriptive name to use if an exception occurs - :return: The magnitude of the converted value - :raises: - :class:`ValueError` if strict and one of the arguments is not a Quantity. + Parameters + ---------- + value : + a Quantity (or numerical value if strict=False) to convert + strict : + boolean to indicate that only quanities are accepted (Default value = True) + name : + descriptive name to use if an exception occurs (Default value = "value") + + Returns + ------- + type + The magnitude of the converted value + """ return self.from_(value, strict=strict, name=name).magnitude diff --git a/pint/util.py b/pint/util.py index 44bd91ce4..d9bb611f1 100644 --- a/pint/util.py +++ b/pint/util.py @@ -33,6 +33,21 @@ def matrix_to_string( matrix, row_headers=None, col_headers=None, fmtfun=lambda x: str(int(x)) ): """Takes a 2D matrix (as nested list) and returns a string. + + Parameters + ---------- + matrix : + + row_headers : + (Default value = None) + col_headers : + (Default value = None) + fmtfun : + (Default value = lambda x: str(int(x))) + + Returns + ------- + """ ret = [] if col_headers: @@ -50,6 +65,15 @@ def matrix_to_string( def transpose(matrix): """Takes a 2D matrix (as nested list) and returns the transposed version. + + Parameters + ---------- + matrix : + + + Returns + ------- + """ return [list(val) for val in zip(*matrix)] @@ -57,10 +81,20 @@ def transpose(matrix): def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): """Calculates the column echelon form using Gaussian elimination. - :param matrix: a 2D matrix as nested list. - :param ntype: the numerical type to use in the calculation. - :param transpose_result: indicates if the returned matrix should be transposed. - :return: column echelon form, transformed identity matrix, swapped rows + Parameters + ---------- + matrix : + a 2D matrix as nested list. + ntype : + the numerical type to use in the calculation. (Default value = Fraction) + transpose_result : + indicates if the returned matrix should be transposed. (Default value = False) + + Returns + ------- + type + column echelon form, transformed identity matrix, swapped rows + """ lead = 0 @@ -124,9 +158,18 @@ def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): def pi_theorem(quantities, registry=None): """Builds dimensionless quantities using the Buckingham π theorem - :param quantities: mapping between variable name and units - :type quantities: dict - :return: a list of dimensionless quantities expressed as dicts + Parameters + ---------- + quantities : dict + mapping between variable name and units + registry : + (Default value = None) + + Returns + ------- + type + a list of dimensionless quantities expressed as dicts + """ # Preprocess input and build the dimensionality Matrix @@ -189,12 +232,18 @@ def pi_theorem(quantities, registry=None): def solve_dependencies(dependencies): """Solve a dependency graph. - :param dependencies: + Parameters + ---------- + dependencies : dependency dictionary. For each key, the value is an iterable indicating its dependencies. - :return: + + Returns + ------- + type iterator of sets, each containing keys of independents tasks dependent only of the previous tasks in the list. + """ while dependencies: # values not in keys (items without dep) @@ -244,9 +293,7 @@ def find_connected_nodes(graph, start, visited=None): class udict(dict): - """ Custom dict implementing __missing__. - - """ + """Custom dict implementing __missing__.""" def __missing__(self, key): return 0.0 @@ -258,9 +305,16 @@ def copy(self): class UnitsContainer(Mapping): """The UnitsContainer stores the product of units and their respective exponent and implements the corresponding operations. - + UnitsContainer is a read-only mapping. All operations (even in place ones) - return new instances. + + Parameters + ---------- + + Returns + ------- + type + """ @@ -292,7 +346,15 @@ def add(self, key, value): return new def remove(self, keys): - """ Create a new UnitsContainer purged from given keys. + """Create a new UnitsContainer purged from given keys. + + Parameters + ---------- + keys : + + + Returns + ------- """ new = self.copy() @@ -302,7 +364,17 @@ def remove(self, keys): return new def rename(self, oldkey, newkey): - """ Create a new UnitsContainer in which an entry has been renamed. + """Create a new UnitsContainer in which an entry has been renamed. + + Parameters + ---------- + oldkey : + + newkey : + + + Returns + ------- """ new = self.copy() @@ -416,15 +488,20 @@ def __rtruediv__(self, other): class ParserHelper(UnitsContainer): - """ The ParserHelper stores in place the product of variables and + """The ParserHelper stores in place the product of variables and their respective exponent and implements the corresponding operations. - + ParserHelper is a read-only mapping. All operations (even in place ones) - return new instances. - WARNING : The hash value used does not take into account the scale - attribute so be careful if you use it as a dict key and then two unequal - object can have the same hash. + Parameters + ---------- + + Returns + ------- + type + WARNING : The hash value used does not take into account the scale + attribute so be careful if you use it as a dict key and then two unequal + object can have the same hash. """ @@ -437,9 +514,17 @@ def __init__(self, scale=1, *args, **kwargs): @classmethod def from_word(cls, input_word): """Creates a ParserHelper object with a single variable with exponent one. - + Equivalent to: ParserHelper({'word': 1}) + Parameters + ---------- + input_word : + + + Returns + ------- + """ return cls(1, [(input_word, 1)]) @@ -463,6 +548,15 @@ def eval_token(cls, token, use_decimal=False): @lru_cache() def from_string(cls, input_string): """Parse linear expression mathematical units and return a quantity object. + + Parameters + ---------- + input_string : + + + Returns + ------- + """ if not input_string: return cls() @@ -642,10 +736,16 @@ def _is_dim(name): class SharedRegistryObject: """Base class for object keeping a reference to the registree. - + Such object are for now Quantity and Unit, in a number of places it is that an object from this class has a '_units' attribute. + Parameters + ---------- + + Returns + ------- + """ def __new__(cls, *args, **kwargs): @@ -662,9 +762,16 @@ def _check(self, other): """Check if the other object use a registry and if so that it is the same registry. - Return True is both use a registry and they use the same, False is - other don't use a registry and raise ValueError if other don't use the - same unit registry. + Parameters + ---------- + other : + + + Returns + ------- + type + other don't use a registry and raise ValueError if other don't use the + same unit registry. """ if self._REGISTRY is getattr(other, "_REGISTRY", None): @@ -702,7 +809,17 @@ def _repr_pretty_(self, p, cycle): def to_units_container(unit_like, registry=None): - """ Convert a unit compatible type to a UnitsContainer. + """Convert a unit compatible type to a UnitsContainer. + + Parameters + ---------- + unit_like : + + registry : + (Default value = None) + + Returns + ------- """ mro = type(unit_like).mro() @@ -720,7 +837,19 @@ def to_units_container(unit_like, registry=None): def infer_base_unit(q): - """Return UnitsContainer of q with all prefixes stripped.""" + """ + + Parameters + ---------- + q : + + + Returns + ------- + type + + + """ d = udict() for unit_name, power in q._units.items(): candidates = q._REGISTRY.parse_unit_name(unit_name) @@ -735,6 +864,15 @@ 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. + + Parameters + ---------- + item : + + + Returns + ------- + """ # Double-underscore attributes are tricky to detect because they are # automatically prefixed with the class name - which may be a subclass of self @@ -744,15 +882,21 @@ def getattr_maybe_raise(self, item): class SourceIterator: """Iterator to facilitate reading the definition files. - + Accepts any sequence (like a list of lines, a file or another SourceIterator) - + The iterator yields the line number and line (skipping comments and empty lines) and stripping white spaces. - + for lineno, line in SourceIterator(sequence): # do something here + Parameters + ---------- + + Returns + ------- + """ def __new__(cls, sequence): @@ -782,14 +926,20 @@ def __next__(self): next = __next__ def block_iter(self): - """Iterate block including header. - """ + """Iterate block including header.""" return BlockIterator(self) class BlockIterator(SourceIterator): """Like SourceIterator but stops when it finds '@end' It also raises an error if another '@' directive is found inside. + + Parameters + ---------- + + Returns + ------- + """ def __new__(cls, line_iterator): @@ -817,12 +967,22 @@ def __next__(self): def iterable(y): """Check whether or not an object can be iterated over. - + Vendored from numpy under the terms of the BSD 3-Clause License. (Copyright (c) 2005-2019, NumPy Developers.) - :param value: Input object. - :param type: object + Parameters + ---------- + value : + Input object. + type : + object + y : + + + Returns + ------- + """ try: iter(y) @@ -834,8 +994,18 @@ def iterable(y): def sized(y): """Check whether or not an object has a defined length. - :param value: Input object. - :param type: object + Parameters + ---------- + value : + Input object. + type : + object + y : + + + Returns + ------- + """ try: len(y) From 511efdd9f5eb2fb030cab4f46314dcf74e27b799 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 01:48:51 -0300 Subject: [PATCH 257/612] Manual edits after pyment --- bench/bench.py | 8 +- pint/__init__.py | 27 +--- pint/compat.py | 27 ++-- pint/context.py | 162 +++++-------------- pint/converters.py | 4 +- pint/definitions.py | 29 ++-- pint/errors.py | 10 +- pint/formatting.py | 41 ++--- pint/matplotlib.py | 56 +------ pint/numpy_func.py | 131 +++++---------- pint/pint_eval.py | 97 ++++------- pint/quantity.py | 155 +++++------------- pint/registry.py | 338 +++++---------------------------------- pint/registry_helpers.py | 21 +-- pint/systems.py | 85 +--------- pint/unit.py | 2 +- pint/util.py | 21 --- 17 files changed, 253 insertions(+), 961 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index 9f48c5c22..9995b88bb 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -12,13 +12,13 @@ def time_stmt(stmt="pass", setup="pass", number=0, repeat=3): Parameters ---------- - stmt : + stmt : str (Default value = "pass") - setup : + setup : str (Default value = "pass") - number : + number : int (Default value = 0) - repeat : + repeat : int (Default value = 3) Returns diff --git a/pint/__init__.py b/pint/__init__.py index ab3e99924..04ddf1d37 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -63,13 +63,12 @@ def _unpickle(cls, *args): Parameters ---------- - cls : - Quantity, Magnitude, or Unit - *args : - + cls : Quantity, Magnitude, or Unit + *args Returns ------- + object of type cls """ from .unit import UnitsContainer @@ -91,11 +90,7 @@ def set_application_registry(registry): Parameters ---------- - registry : - a UnitRegistry instance. - - Returns - ------- + registry : UnitRegistry """ if not isinstance(registry, (LazyRegistry, UnitRegistry)): @@ -110,29 +105,19 @@ def get_application_registry(): invoked, return a registry built using :file:`defaults_en.txt` embedded in the pint package. - Parameters - ---------- - registry : - a UnitRegistry instance. - Returns ------- - + UnitRegistry """ return _APP_REGISTRY def test(): """Run all tests. - - :return: a :class:`unittest.TestResult` object - - Parameters - ---------- Returns ------- - + unittest.TestResult """ from .testsuite import run diff --git a/pint/compat.py b/pint/compat.py index 213a4f0bc..7642b15f4 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -153,38 +153,37 @@ def _to_magnitude(value, force_ndarray=False): def is_upcast_type(other): - """check if the type object is a upcast type + """Check if the type object is a upcast type. Parameters ---------- - other : - type + other : object Returns ------- - + bool """ # Check if class name is in preset list return other.__name__ in ("PintArray", "Series", "DataArray") -def eq(first, second, check_all): - """Comparison of scalars and arrays +def eq(lhs, rhs, check_all): + """Comparison of scalars and arrays. Parameters ---------- - first : - - second : - - check_all : - + lhs : object + left-hand side + rhs : object + right-hand side + check_all : bool + if True, reduce sequence to single bool. Returns ------- - + bool or array_like of bool """ - out = first == second + out = lhs == rhs if check_all and isinstance(out, ndarray): return np.all(out) return out diff --git a/pint/context.py b/pint/context.py index 0a0b73b32..0034925a6 100644 --- a/pint/context.py +++ b/pint/context.py @@ -37,7 +37,7 @@ class Context: dimension to another. Each Dimension are specified using a UnitsContainer. Simple transformation are given with a function taking a single parameter. - + Conversion functions may take optional keyword arguments and the context can have default values for these arguments. @@ -51,30 +51,35 @@ class Context: Parameters ---------- - - Returns + name : str or None (default), optional + Name of the context (must be unique within the registry). + Use None for anonymous Context. + aliases : iterable of str + Other names for the context. + defaults : None or dict + Maps variable names to values. + + Example ------- >>> from pint.util import UnitsContainer - >>> timedim = UnitsContainer({'[time]': 1}) - >>> spacedim = UnitsContainer({'[length]': 1}) - >>> def f(time): - ... 'Time to length converter' - ... return 3. * time - >>> c = Context() - >>> c.add_transformation(timedim, spacedim, f) - >>> c.transform(timedim, spacedim, 2) - 6 - - >>> def f(time, n): - ... 'Time to length converter, n is the index of refraction of the material' - ... return 3. * time / n - >>> c = Context(n=3) - >>> c.add_transformation(timedim, spacedim, f) - >>> c.transform(timedim, spacedim, 2) - 2 - - >>> c.redefine("pound = 0.5 kg") + >>> timedim = UnitsContainer({'[time]': 1}) + >>> spacedim = UnitsContainer({'[length]': 1}) + >>> def f(time): + ... 'Time to length converter' + ... return 3. * time + >>> c = Context() + >>> c.add_transformation(timedim, spacedim, f) + >>> c.transform(timedim, spacedim, 2) + 6 + >>> def f(time, n): + ... 'Time to length converter, n is the index of refraction of the material' + ... return 3. * time / n + >>> c = Context(n=3) + >>> c.add_transformation(timedim, spacedim, f) + >>> c.transform(timedim, spacedim, 2) + 2 + >>> c.redefine("pound = 0.5 kg") """ def __init__(self, name=None, aliases=(), defaults=None): @@ -108,13 +113,14 @@ def from_context(cls, context, **defaults): Parameters ---------- - context : - - **defaults : + context : Context + Original context. + **defaults Returns ------- + Context """ if defaults: @@ -212,38 +218,16 @@ def to_num(val): def add_transformation(self, src, dst, func): """Add a transformation function to the context. - - Parameters - ---------- - src : - - dst : - - func : - - - Returns - ------- - """ + _key = self.__keytransform__(src, dst) self.funcs[_key] = func self.relation_to_context[_key] = self def remove_transformation(self, src, dst): """Add a transformation function to the context. - - Parameters - ---------- - src : - - dst : - - - Returns - ------- - """ + _key = self.__keytransform__(src, dst) del self.funcs[_key] del self.relation_to_context[_key] @@ -254,22 +238,8 @@ def __keytransform__(src, dst): def transform(self, src, dst, registry, value): """Transform a value. - - Parameters - ---------- - src : - - dst : - - registry : - - value : - - - Returns - ------- - """ + _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) @@ -278,15 +248,10 @@ def redefine(self, definition: str) -> None: Parameters ---------- - definition : - unit> = ``, e.g. ``pound = 0.5 kg`` - definition: str : - - - Returns - ------- - + definition : str + = ``, e.g. ``pound = 0.5 kg`` """ + for line in definition.splitlines(): d = Definition.from_string(line) if not isinstance(d, UnitDefinition): @@ -306,12 +271,9 @@ def hashable(self): be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. - Parameters - ---------- - Returns ------- - + tuple """ return ( self.name, @@ -325,13 +287,6 @@ def hashable(self): class ContextChain(ChainMap): """A specialized ChainMap for contexts that simplifies finding rules to transform from one dimension to another. - - Parameters - ---------- - - Returns - ------- - """ def __init__(self): @@ -346,16 +301,8 @@ def insert_contexts(self, *contexts): To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. - - Parameters - ---------- - *contexts : - - - Returns - ------- - """ + self.contexts = list(reversed(contexts)) + self.contexts self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps self._graph = None @@ -365,13 +312,10 @@ def remove_contexts(self, n: int = None): Parameters ---------- - n: int : - (Default value = None) - - Returns - ------- - + n: int + (Default value = None) """ + del self.contexts[:n] del self.maps[:n] self._graph = None @@ -394,21 +338,6 @@ def graph(self): def transform(self, src, dst, registry, value): """Transform the value, finding the rule in the chained context. (A rule in last context will take precedence) - - Parameters - ---------- - src : - - dst : - - registry : - - value : - - - Returns - ------- - """ return self[(src, dst)].transform(src, dst, registry, value) @@ -416,12 +345,5 @@ def hashable(self): """Generate a unique hashable and comparable representation of self, which can be used as a key in a dict. This class cannot define ``__hash__`` because it is mutable, and the Python interpreter does cache the output of ``__hash__``. - - Parameters - ---------- - - Returns - ------- - """ return tuple(ctx.hashable() for ctx in self.contexts) diff --git a/pint/converters.py b/pint/converters.py index bcb659e89..927335016 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -22,7 +22,7 @@ def from_reference(self, value, inplace=False): class ScaleConverter(Converter): - """A linear transformation""" + """A linear transformation.""" is_multiplicative = True @@ -47,7 +47,7 @@ def from_reference(self, value, inplace=False): class OffsetConverter(Converter): - """An affine transformation""" + """An affine transformation.""" def __init__(self, scale, offset): self.scale = scale diff --git a/pint/definitions.py b/pint/definitions.py index 930488d6c..947a189a7 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -32,18 +32,14 @@ class Definition: Parameters ---------- - name : - name. - symbol : - a short name or symbol for the definition - aliases : - iterable of other names. - converter : + name : str + Canonical name of the unit/prefix/etc. + symbol : str or None + A short name or symbol for the definition. + aliases : iterable of str + Other names for the unit/prefix/etc. + converter : callable an instance of Converter. - - Returns - ------- - """ def __init__(self, name, symbol, aliases, converter): @@ -144,13 +140,10 @@ class UnitDefinition(Definition): Parameters ---------- - reference : - Units container with reference units. - is_base : - indicates if it is a base unit. - - Returns - ------- + reference : UnitsContainer + Reference units. + is_base : bool + Indicates if it is a base unit. """ diff --git a/pint/errors.py b/pint/errors.py index f2be370ef..4ce1eaf3f 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -21,7 +21,7 @@ def _file_prefix(filename=None, lineno=None): class DefinitionSyntaxError(SyntaxError): - """ """ + """Raised when a textual definition has a syntax error.""" def __init__(self, msg, *, filename=None, lineno=None): super().__init__(msg) @@ -43,7 +43,7 @@ def __reduce__(self): class RedefinitionError(ValueError): - """ """ + """Raised when a unit or prefix is redefined.""" def __init__(self, name, definition_type, *, filename=None, lineno=None): super().__init__(name, definition_type) @@ -59,7 +59,7 @@ def __reduce__(self): class UndefinedUnitError(AttributeError): - """ """ + """Raised when the units are not defined in the unit registry.""" def __init__(self, *unit_names): if len(unit_names) == 1 and not isinstance(unit_names[0], str): @@ -77,7 +77,7 @@ class PintTypeError(TypeError): class DimensionalityError(PintTypeError): - """ """ + """Raised when trying to convert between incompatible units.""" def __init__(self, units1, units2, dim1="", dim2="", *, extra_msg=""): super().__init__() @@ -105,7 +105,7 @@ def __reduce__(self): class OffsetUnitCalculusError(PintTypeError): - """ """ + """Raised on ambiguous operations with offset units.""" def __str__(self): return ( diff --git a/pint/formatting.py b/pint/formatting.py index f4320e42f..4bcdf0b2b 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -25,13 +25,14 @@ def _join(fmt, iterable): Parameters ---------- - fmt : + fmt : str iterable : Returns ------- + str """ if not iterable: @@ -54,11 +55,11 @@ def _pretty_fmt_exponent(num): Parameters ---------- - num : - + num : int Returns ------- + str """ # TODO: Will not work for decimals @@ -132,33 +133,33 @@ def formatter( Parameters ---------- - items : + items : list a list of (name, exponent) pairs. - as_ratio : + as_ratio : bool, optional True to display as ratio, False as negative powers. (Default value = True) - single_denominator : + single_denominator : bool, optional all with terms with negative exponents are collected together. (Default value = False) - product_fmt : + product_fmt : str the format used for multiplication. (Default value = " * ") - division_fmt : + division_fmt : str the format used for division. (Default value = " / ") - power_fmt : + power_fmt : str the format used for exponentiation. (Default value = "{} ** {}") - parentheses_fmt : + parentheses_fmt : str the format used for parenthesis. (Default value = "({0})") - locale : + locale : str the locale object as defined in babel. (Default value = None) - babel_length : + babel_length : str the length of the translated unit, as defined in babel cldr. (Default value = "long") - babel_plural_form : + babel_plural_form : str the plural form, calculated as defined in babel. (Default value = "one") - exp_call : + exp_call : callable (Default value = lambda x: f"{x:n}") Returns ------- - type + str the formula as a string. """ @@ -279,16 +280,8 @@ def format_unit(unit, spec, **kwspec): def siunitx_format_unit(units): """Returns LaTeX code for the unit that can be put into an siunitx command. - - Parameters - ---------- - units : - - - Returns - ------- - """ + # NOTE: unit registry is required to identify unit prefixes. registry = units._REGISTRY diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 68a74fcf1..912474246 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -31,19 +31,6 @@ def __init__(self, registry): def convert(self, value, unit, axis): """Convert :`Quantity` instances for matplotlib to use. - - Parameters - ---------- - value : - - unit : - - axis : - - - Returns - ------- - """ if iterable(value): return [self._convert_value(v, unit, axis) for v in value] @@ -52,19 +39,6 @@ def convert(self, value, unit, axis): def _convert_value(self, value, unit, axis): """Handle converting using attached unit or falling back to axis units. - - Parameters - ---------- - value : - - unit : - - axis : - - - Returns - ------- - """ if hasattr(value, "units"): return value.to(unit).magnitude @@ -73,37 +47,13 @@ def _convert_value(self, value, unit, axis): @staticmethod def axisinfo(unit, axis): - """ + """Return axis information for this particular unit.""" - Parameters - ---------- - unit : - - axis : - - - Returns - ------- - type - - - """ return PintAxisInfo(unit) @staticmethod def default_units(x, axis): """Get the default unit to use for the given combination of unit and axis. - - Parameters - ---------- - x : - - axis : - - - Returns - ------- - """ if iterable(x) and sized(x): return getattr(x[0], "units", None) @@ -116,9 +66,9 @@ def setup_matplotlib_handlers(registry, enable): Parameters ---------- registry : UnitRegistry - the registry that will be used + The registry that will be used. enable : bool - whether support should be enabled or disabled + Whether support should be enabled or disabled. Returns ------- diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 8c50bea43..ed91df1cb 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -21,57 +21,47 @@ # Shared Implementation Utilities -def _is_quantity(arg): +def _is_quantity(obj): """Test for _units and _magnitude attrs. This is done in place of isinstance(Quantity, arg), which would cause a circular import. Parameters ---------- - arg : + obj : Object Returns ------- - + bool """ - return hasattr(arg, "_units") and hasattr(arg, "_magnitude") + return hasattr(obj, "_units") and hasattr(obj, "_magnitude") -def _is_quantity_sequence(arg): +def _is_quantity_sequence(obj): """Test for sequences of quantities. Parameters ---------- - arg : + obj : object Returns ------- - + bool """ return ( - iterable(arg) - and sized(arg) - and not isinstance(arg, str) - and all(_is_quantity(item) for item in arg) + iterable(obj) + and sized(obj) + and not isinstance(obj, str) + and all(_is_quantity(item) for item in obj) ) -def _get_first_input_units(args, kwargs={}): +def _get_first_input_units(args, kwargs=None): """Obtain the first valid unit from a collection of args and kwargs. - - Parameters - ---------- - args : - - kwargs : - (Default value = {}) - - Returns - ------- - """ + kwargs = kwargs or {} for arg in chain(args, kwargs.values()): if _is_quantity(arg): return arg.units @@ -85,16 +75,6 @@ def convert_arg(arg, pre_calc_units): Helper function for convert_to_consistent_units. pre_calc_units must be given as a pint Unit or None. - Parameters - ---------- - arg : - - pre_calc_units : - - - Returns - ------- - """ if pre_calc_units is not None: if _is_quantity(arg): @@ -122,18 +102,6 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless Quantities. If pre_calc_units is None, units are simply stripped. - Parameters - ---------- - *args : - - pre_calc_units : - (Default value = None) - **kwargs : - - - Returns - ------- - """ return ( tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), @@ -150,14 +118,6 @@ def unwrap_and_wrap_consistent_units(*args): Returns the given args as parsed by convert_to_consistent_units assuming units of first arg with units, along with a wrapper to restore that unit to the output. - Parameters - ---------- - *args : - - - Returns - ------- - """ first_input_units = _get_first_input_units(args) args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) @@ -167,7 +127,7 @@ def unwrap_and_wrap_consistent_units(*args): ) -def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): +def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): """Determine resulting unit from given operation. Options for `unit_op`: @@ -193,7 +153,7 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): first_input_units : all_args : - (Default value = []) + (Default value = None) size : (Default value = None) @@ -201,6 +161,8 @@ def get_op_output_unit(unit_op, first_input_units, all_args=[], size=None): ------- """ + all_args = all_args or [] + if unit_op == "sum": result_unit = (1 * first_input_units + 1 * first_input_units).units elif unit_op == "mul": @@ -249,16 +211,6 @@ def implements(numpy_func_string, func_type): """Register an __array_function__/__array_ufunc__ implementation for Quantity objects. - Parameters - ---------- - numpy_func_string : - - func_type : - - - Returns - ------- - """ def decorator(func): @@ -278,17 +230,24 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): Parameters ---------- - func_type : - - func_str : - - input_units : - (Default value = None) - output_unit : - (Default value = None) - - Returns - ------- + func_type : str + "function" for NumPy functions, "ufunc" for NumPy ufuncs + func_str : str + String representing the name of the NumPy function/ufunc to add + input_units : pint.Unit or str or None + Parameter to control how the function downcasts to magnitudes of arguments. If + `pint.Unit`, converts all args and kwargs to this unit before downcasting to + magnitude. If "all_consistent", converts all args and kwargs to the unit of the + first Quantity in args and kwargs before downcasting to magnitude. If some + other string, the string is parsed as a unit, and all args and kwargs are + converted to that unit. If None, units are stripped without conversion. + output_unit : pint.Unit or str or None + Parameter to control the unit of the output. If `pint.Unit`, output is wrapped + with that unit. If "match_input", output is wrapped with the unit of the first + Quantity in args and kwargs. If a string representing a unit operation defined + in `get_op_output_unit`, output is wrapped by the unit determined by + `get_op_output_unit`. If some other string, the string is parsed as a unit, + which becomes the unit of the output. If None, the bare magnitude is returned. """ @@ -881,27 +840,9 @@ def implementation(a, *args, **kwargs): def numpy_wrap(func_type, func, args, kwargs, types): + """Return the result from a NumPy function/ufunc as wrapped by Pint. """ - Parameters - ---------- - func_type : - - func : - - args : - - kwargs : - - types : - - - Returns - ------- - type - - - """ if func_type == "function": handled = HANDLED_FUNCTIONS elif func_type == "ufunc": diff --git a/pint/pint_eval.py b/pint/pint_eval.py index 4916da9c2..130e64193 100644 --- a/pint/pint_eval.py +++ b/pint/pint_eval.py @@ -38,19 +38,15 @@ class EvalTreeNode: - def __init__(self, left, operator=None, right=None): - """left + operator + right --> binary op - left + operator --> unary op - left + right --> implicit op - left --> single value - - Parameters - ---------- - - Returns - ------- + """Single node within an evaluation tree + left + operator + right --> binary op + left + operator --> unary op + left + right --> implicit op + left --> single value """ + + def __init__(self, left, operator=None, right=None): self.left = left self.operator = operator self.right = right @@ -68,19 +64,16 @@ def to_string(self): return self.left[1] return "(%s)" % " ".join(comps) - def evaluate( - self, define_op, bin_op=_BINARY_OPERATOR_MAP, un_op=_UNARY_OPERATOR_MAP - ): - """define_op is a callable that translates tokens into objects - bin_op and un_op provide functions for performing binary and unary operations + def evaluate(self, define_op, bin_op=None, un_op=None): + """Evaluate node. Parameters ---------- - define_op : - - bin_op : + define_op : callable + Translates tokens into objects. + bin_op : dict or None, optional (Default value = _BINARY_OPERATOR_MAP) - un_op : + un_op : dict or None, optional (Default value = _UNARY_OPERATOR_MAP) Returns @@ -88,6 +81,9 @@ def evaluate( """ + bin_op = bin_op or _BINARY_OPERATOR_MAP + un_op = un_op or _UNARY_OPERATOR_MAP + if self.right: # binary or implicit operator op_text = self.operator[1] if self.operator else "" @@ -107,49 +103,24 @@ def evaluate( def build_eval_tree(tokens, op_priority=_OP_PRIORITY, index=0, depth=0, prev_op=None): - """ - - Parameters - ---------- - Index : - depth - Tokens : - is an iterable of tokens from an expression to be evaluated - Transform : - the tokens from an expression into a recursive parse tree - of : - operations - unary : - ops - General : - Strategy - 1 : - Get left side of operator - 2 : - If no tokens left - 3 : - Get operator - 4 : - Use recursion to create tree starting at token on right side of operator - 4 : - - 5 : - Combine left side - 6 : - Go back to step - tokens : - - op_priority : - (Default value = _OP_PRIORITY) - index : - (Default value = 0) - depth : - (Default value = 0) - prev_op : - (Default value = None) - - Returns - ------- + """Build an evaluation tree from a set of tokens. + + Params: + Index, depth, and prev_op used recursively, so don't touch. + Tokens is an iterable of tokens from an expression to be evaluated. + + Transform the tokens from an expression into a recursive parse tree, following order + of operations. Operations can include binary ops (3 + 4), implicit ops (3 kg), or + unary ops (-1). + + General Strategy: + 1) Get left side of operator + 2) If no tokens left, return final result + 3) Get operator + 4) Use recursion to create tree starting at token on right side of operator (start at step #1) + 4.1) If recursive call encounters an operator with lower or equal priority to step #2, exit recursion + 5) Combine left side, operator, and right side into a new left side + 6) Go back to step #2 """ diff --git a/pint/quantity.py b/pint/quantity.py index 9c2d4fcc6..1e03df81c 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -112,17 +112,6 @@ def wrapped(self, *args, **kwargs): def printoptions(*args, **kwargs): """Numpy printoptions context manager released with version 1.15.0 https://docs.scipy.org/doc/numpy/reference/generated/numpy.printoptions.html - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - """ opts = np.get_printoptions() @@ -140,9 +129,9 @@ class Quantity(PrettyIPython, SharedRegistryObject): Parameters ---------- value : str, pint.Quantity or any numeric type - value of the physical quantity to be created + Value of the physical quantity to be created. units : UnitsContainer, str or pint.Quantity - units of the physical quantity to be created + Units of the physical quantity to be created. Returns ------- @@ -420,18 +409,7 @@ def dimensionality(self): return self._dimensionality def check(self, dimension): - """ - - Parameters - ---------- - dimension : - - - Returns - ------- - type - - + """Return true if the quantity's dimension matches passed dimension. """ return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) @@ -446,14 +424,14 @@ def from_list(cls, quant_list, units=None): Parameters ---------- - quant_list : list: list of pint.Quantity + quant_list : list of pint.Quantity list of pint.Quantity units : UnitsContainer, str or pint.Quantity units of the physical quantity to be created (Default value = None) Returns ------- - + pint.Quantity """ return cls.from_sequence(quant_list, units=units) @@ -474,7 +452,7 @@ def from_sequence(cls, seq, units=None): Returns ------- - + pint.Quantity """ len_seq = len(seq) @@ -531,14 +509,11 @@ def ito(self, other=None, *contexts, **ctx_kwargs): Parameters ---------- other : pint.Quantity, str or dict - destination units. (Default value = None) - *contexts : - + Destination units. (Default value = None) + *contexts : str or Context + Contexts to use in the transformation. **ctx_kwargs : - - - Returns - ------- + Values for the Context/s """ other = to_units_container(other, self._REGISTRY) @@ -555,13 +530,14 @@ def to(self, other=None, *contexts, **ctx_kwargs): ---------- other : pint.Quantity, str or dict destination units. (Default value = None) - *contexts : - + *contexts : str or Context + Contexts to use in the transformation. **ctx_kwargs : - + Values for the Context/s Returns ------- + pint.Quantity """ other = to_units_container(other, self._REGISTRY) @@ -571,7 +547,7 @@ def to(self, other=None, *contexts, **ctx_kwargs): return self.__class__(magnitude, other) def ito_root_units(self): - """ """ + """Return Quantity rescaled to root units.""" _, other = self._REGISTRY._get_root_units(self._units) @@ -581,7 +557,8 @@ def ito_root_units(self): return None def to_root_units(self): - """ """ + """Return Quantity rescaled to root units.""" + _, other = self._REGISTRY._get_root_units(self._units) magnitude = self._convert_magnitude_not_inplace(other) @@ -589,7 +566,7 @@ def to_root_units(self): return self.__class__(magnitude, other) def ito_base_units(self): - """ """ + """Return Quantity rescaled to base units.""" _, other = self._REGISTRY._get_base_units(self._units) @@ -599,7 +576,8 @@ def ito_base_units(self): return None def to_base_units(self): - """ """ + """Return Quantity rescaled to base units.""" + _, other = self._REGISTRY._get_base_units(self._units) magnitude = self._convert_magnitude_not_inplace(other) @@ -607,18 +585,11 @@ def to_base_units(self): return self.__class__(magnitude, other) def ito_reduced_units(self): + """Return Quantity scaled in place to reduced units, i.e. one unit per + dimension. This will not reduce compound units (intentionally), nor + can it make use of contexts at this time. """ - Parameters - ---------- - - Returns - ------- - type - dimension. This will not reduce compound units (intentionally), nor - can it make use of contexts at this time. - - """ # shortcuts in case we're dimensionless or only a single unit if self.dimensionless: return self.ito({}) @@ -639,35 +610,25 @@ def ito_reduced_units(self): return self.ito(newunits) def to_reduced_units(self): + """Return Quantity scaled in place to reduced units, i.e. one unit per + dimension. This will not reduce compound units (intentionally), nor + can it make use of contexts at this time. """ - Parameters - ---------- - Returns - ------- - type - dimension. This will not reduce compound units (intentionally), nor - can it make use of contexts at this time. - - """ # can we make this more efficient? newq = copy.copy(self) newq.ito_reduced_units() return newq def to_compact(self, unit=None): - """ + """"Return Quantity rescaled to compact, human-readable units. - Parameters - ---------- - unit : - (Default value = None) + To get output in terms of a different unit, use the unit parameter. - Returns + + Example ------- - type - To get output in terms of a different unit, use the unit parameter. >>> import pint >>> ureg = pint.UnitRegistry() @@ -676,6 +637,7 @@ def to_compact(self, unit=None): >>> (1e-2*ureg('kg m/s^2')).to_compact('N') """ + if not isinstance(self.magnitude, numbers.Number): msg = ( "to_compact applied to non numerical types " @@ -761,9 +723,6 @@ def _iadd_sub(self, other, op): op : function operator function (e.g. operator.add, operator.isub) - Returns - ------- - """ if not self._check(other): # other not from same Registry or not a Quantity @@ -875,8 +834,6 @@ def _add_sub(self, other, op): op : function operator function (e.g. operator.add, operator.isub) - Returns - ------- """ if not self._check(other): @@ -1520,20 +1477,8 @@ def __array_function__(self, func, types, args, kwargs): def _numpy_method_wrap(self, func, *args, **kwargs): """Convenience method to wrap on the fly NumPy ndarray methods taking care of the units. - - Parameters - ---------- - func : - - *args : - - **kwargs : - - - Returns - ------- - """ + # Set input units if needed if func.__name__ in set_units_ufuncs: self.__ito_if_needed(set_units_ufuncs[func.__name__][0]) @@ -1655,16 +1600,8 @@ def dot(self, b): """Dot product of two arrays. Wraps np.dot(). - - Parameters - ---------- - b : - - - Returns - ------- - """ + return np.dot(self, b) def __ito_if_needed(self, to_units): @@ -1800,7 +1737,7 @@ def _is_multiplicative(self): return not self._get_non_multiplicative_units() def _get_non_multiplicative_units(self): - """ """ + """Return a list of the of non-multiplicative units of the Quantity object.""" offset_units = [ unit for unit in self._units.keys() @@ -1809,21 +1746,12 @@ def _get_non_multiplicative_units(self): return offset_units def _get_delta_units(self): - """ """ + """Return list of delta units ot the Quantity object.""" delta_units = [u for u in self._units.keys() if u.startswith("delta_")] return delta_units def _has_compatible_delta(self, unit): """"Check if Quantity object has a delta_unit that is compatible with unit - - Parameters - ---------- - unit : - - - Returns - ------- - """ deltas = self._get_delta_units() if "delta_" + unit in deltas: @@ -1837,19 +1765,8 @@ def _has_compatible_delta(self, unit): def _ok_for_muldiv(self, no_offset_units=None): """Checks if Quantity object can be multiplied or divided - - :q: pint.Quantity object that is checked - :no_offset_units: number of offset units in q - - Parameters - ---------- - no_offset_units : - (Default value = None) - - Returns - ------- - """ + is_ok = True if no_offset_units is None: no_offset_units = len(self._get_non_multiplicative_units()) diff --git a/pint/registry.py b/pint/registry.py index 591538091..d4f75d3d5 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -86,13 +86,6 @@ class RegistryMeta(type): """This is just to call after_init at the right time instead of asking the developer to do it when subclassing. - - Parameters - ---------- - - Returns - ------- - """ def __call__(self, *args, **kwargs): @@ -118,13 +111,6 @@ def __init__(self): class ContextCacheOverlay: """Layer on top of the base UnitRegistry cache, specific to a combination of active contexts which contain unit redefinitions. - - Parameters - ---------- - - Returns - ------- - """ def __init__(self, registry_cache: RegistryCache): @@ -152,7 +138,7 @@ class BaseRegistry(metaclass=RegistryMeta): filename : str or None path of the units definition file to load or line iterable object. Empty to load the default definition file. None to leave the UnitRegistry empty. - force_ndarray : + force_ndarray : bool convert any input, scalar or not to a numpy.ndarray. on_redefinition : str action to take in case a unit is redefined: 'warn', 'raise', 'ignore' @@ -164,9 +150,6 @@ class BaseRegistry(metaclass=RegistryMeta): fmt_locale : locale identifier string, used in `format_babel` - Returns - ------- - """ #: Map context prefix to function @@ -276,15 +259,6 @@ def _register_parsers(self): def _parse_defaults(self, ifile): """Loader for a @default section. - - Parameters - ---------- - ifile : - - - Returns - ------- - """ next(ifile) for lineno, part in ifile.block_iter(): @@ -316,12 +290,8 @@ def set_fmt_locale(self, loc): Parameters ---------- - loc : + loc : str or None None` (do not translate), 'sys' (detect the system locale) or a locale id string. - - Returns - ------- - """ if isinstance(loc, str): if loc == "sys": @@ -349,10 +319,6 @@ def define(self, definition): ---------- definition : str or Definition a dimension, unit or prefix definition. - - Returns - ------- - """ if isinstance(definition, str): @@ -450,19 +416,6 @@ def _define(self, definition): def _define_adder(self, definition, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. It stores the definition under its name, symbol and aliases. - - Parameters - ---------- - definition : - - unit_dict : - - casei_unit_dict : - - - Returns - ------- - """ self._define_single_adder( definition.name, definition, unit_dict, casei_unit_dict @@ -483,21 +436,6 @@ def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. It warns or raise error on redefinition. - - Parameters - ---------- - key : - - value : - - unit_dict : - - casei_unit_dict : - - - Returns - ------- - """ if key in unit_dict: if self._on_redefinition == "raise": @@ -652,20 +590,7 @@ def _build_cache(self): logger.warning(f"Could not resolve {unit_name}: {exc!r}") def get_name(self, name_or_alias, case_sensitive=True): - """ - - Parameters - ---------- - name_or_alias : - - case_sensitive : - (Default value = True) - - Returns - ------- - type - - + """Return the canonical name of a unit. """ if name_or_alias == "dimensionless": @@ -700,18 +625,7 @@ def get_name(self, name_or_alias, case_sensitive=True): return unit_name def get_symbol(self, name_or_alias): - """ - - Parameters - ---------- - name_or_alias : - - - Returns - ------- - type - - + """Return the preferred alias for a unit. """ candidates = self.parse_unit_name(name_or_alias) if not candidates: @@ -733,17 +647,6 @@ def _get_symbol(self, name): def get_dimensionality(self, input_units): """Convert unit or dict of units or dimensions to a dict of base dimensions dimensions - - Parameters - ---------- - input_units : - return: dimensionality - - Returns - ------- - type - dimensionality - """ input_units = to_units_container(input_units) @@ -751,17 +654,6 @@ def get_dimensionality(self, input_units): def _get_dimensionality(self, input_units): """Convert a UnitsContainer to base dimensions. - - Parameters - ---------- - input_units : - return: dimensionality - - Returns - ------- - type - dimensionality - """ if not input_units: return UnitsContainer() @@ -811,7 +703,7 @@ def _get_dimensionality_ratio(self, unit1, unit2): Returns ------- - type + number or None exponential proportionality or None if the units cannot be converted """ @@ -839,14 +731,14 @@ def get_root_units(self, input_units, check_nonmult=True): ---------- input_units : UnitsContainer or str units - check_nonmult : + check_nonmult : bool if True, None will be returned as the multiplicative factor if a non-multiplicative units is found in the final Units. (Default value = True) Returns ------- - type + number, Unit multiplicative factor, base units """ @@ -866,14 +758,14 @@ def _get_root_units(self, input_units, check_nonmult=True): ---------- input_units : UnitsContainer or dict units - check_nonmult : + check_nonmult : bool if True, None will be returned as the multiplicative factor if a non-multiplicative units is found in the final Units. (Default value = True) Returns ------- - type + number, Unit multiplicative factor, base units """ @@ -919,7 +811,7 @@ def get_base_units(self, input_units, check_nonmult=True, system=None): Returns ------- - type + Number, Unit multiplicative factor, base units """ @@ -940,17 +832,6 @@ def _get_root_units_recurse(self, ref, exp, accumulators): def get_compatible_units(self, input_units, group_or_system=None): """ - - Parameters - ---------- - input_units : - - group_or_system : - (Default value = None) - - Returns - ------- - """ input_units = to_units_container(input_units) @@ -960,17 +841,6 @@ def get_compatible_units(self, input_units, group_or_system=None): def _get_compatible_units(self, input_units, group_or_system): """ - - Parameters - ---------- - input_units : - - group_or_system : - - - Returns - ------- - """ if not input_units: return frozenset() @@ -1073,7 +943,7 @@ def parse_unit_name(self, unit_name, case_sensitive=True): Returns ------- - str, str, str), ...) + tuple of (str, str, str) all non-equivalent combinations of (prefix, unit name, suffix) """ @@ -1082,18 +952,7 @@ def parse_unit_name(self, unit_name, case_sensitive=True): ) def _parse_unit_name(self, unit_name, case_sensitive=True): - """Helper of parse_unit_name - - Parameters - ---------- - unit_name : - - case_sensitive : - (Default value = True) - - Returns - ------- - + """Helper of parse_unit_name. """ stw = unit_name.startswith edw = unit_name.endswith @@ -1127,15 +986,6 @@ def _dedup_candidates(candidates): different names but equal value, preferring those with a prefix. e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') - - Parameters - ---------- - candidates : - - - Returns - ------- - """ candidates = dict.fromkeys(candidates) # ordered set for cp, cu, cs in list(candidates): @@ -1155,11 +1005,10 @@ def parse_units(self, input_string, as_delta=None): Parameters ---------- - as_delta : + input_string : str + as_delta : bool or None if the expression has multiple units, the parser will interpret non multiplicative units as their `delta_` counterparts. (Default value = None) - input_string : - Returns ------- @@ -1171,19 +1020,10 @@ def parse_units(self, input_string, as_delta=None): return self.Unit(units) def _parse_units(self, input_string, as_delta=True): + """Parse a units expression and returns a UnitContainer with + the canonical names. """ - Parameters - ---------- - input_string : - - as_delta : - (Default value = True) - - Returns - ------- - - """ cache = self._cache.parse_unit if as_delta: try: @@ -1291,16 +1131,13 @@ class NonMultiplicativeRegistry(BaseRegistry): Parameters ---------- - default_as_delta : + default_as_delta : bool If True, non-multiplicative units are interpreted as their *delta* counterparts in multiplications. - autoconvert_offset_to_baseunit : + autoconvert_offset_to_baseunit : bool If True, non-multiplicative units are converted to base units in multiplications. - Returns - ------- - """ def __init__( @@ -1318,17 +1155,6 @@ def __init__( def _parse_units(self, input_string, as_delta=None): """ - - Parameters - ---------- - input_string : - - as_delta : - (Default value = None) - - Returns - ------- - """ if as_delta is None: as_delta = self.default_as_delta @@ -1343,8 +1169,8 @@ def _define(self, definition): Parameters ---------- - definition : str | Definition - a dimension, unit or prefix definition. + definition : str or Definition + A dimension, unit or prefix definition. Returns ------- @@ -1487,13 +1313,6 @@ class ContextRegistry(BaseRegistry): - Register contexts. - Enable and disable contexts. - Parse @context directive. - - Parameters - ---------- - - Returns - ------- - """ def __init__(self, **kwargs): @@ -1530,14 +1349,6 @@ def add_context(self, context: Context) -> None: Notice that this method will NOT enable the context. Use `enable_contexts`. - Parameters - ---------- - context: Context : - - - Returns - ------- - """ if not context.name: raise ValueError("Can't add unnamed context to registry") @@ -1559,14 +1370,6 @@ def remove_context(self, name_or_alias: str) -> Context: Notice that this methods will not disable the context. Use `disable_contexts`. - Parameters - ---------- - name_or_alias: str : - - - Returns - ------- - """ context = self._contexts[name_or_alias] @@ -1586,12 +1389,6 @@ def _switch_context_cache_and_units(self) -> None: The next time this method is invoked with the same combination of contexts, reuse the same variant self._cache and self._units as in the previous time. - Parameters - ---------- - - Returns - ------- - """ del self._units.maps[:-1] units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) @@ -1626,15 +1423,6 @@ def _switch_context_cache_and_units(self) -> None: def _redefine(self, definition: UnitDefinition) -> None: """Redefine a unit from a context - - Parameters - ---------- - definition: UnitDefinition : - - - Returns - ------- - """ # Find original definition in the UnitRegistry candidates = self.parse_unit_name(definition.name) @@ -1688,10 +1476,6 @@ def enable_contexts(self, *names_or_contexts, **kwargs) -> None: *names_or_contexts : **kwargs : - - - Returns - ------- """ @@ -1769,27 +1553,23 @@ def context(self, *names, **kwargs): *names : **kwargs : - - Returns + + Example ------- >>> with ureg.context('one'): - ... pass - - >>> with ureg.context('one', n=1): - ... pass - - >>> with ureg.context('one', 'two', n=1): - ... pass - - >>> with ureg.context('one', n=1): - ... with ureg.context('two', n=2): - ... pass - - >>> with ureg.context('one', n=1): - ... with ureg.context('two'): # Here n takes the value of the upper context - ... pass + ... pass + >>> with ureg.context('one', n=1): + ... pass + >>> with ureg.context('one', 'two', n=1): + ... pass + >>> with ureg.context('one', n=1): + ... with ureg.context('two', n=2): + ... pass + >>> with ureg.context('one', n=1): + ... with ureg.context('two'): # Here n takes the value of the upper context + ... pass """ # Enable the contexts. @@ -1823,9 +1603,11 @@ def with_context(self, name, **kw): Returns ------- - type + callable the wrapped function. + Example + ------- >>> @ureg.with_context('sp') ... def my_cool_fun(wavelenght): ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz')) @@ -1868,7 +1650,7 @@ def _convert(self, value, src, dst, inplace=False): Returns ------- - type + callable converted value """ @@ -1893,17 +1675,6 @@ def _convert(self, value, src, dst, inplace=False): def _get_compatible_units(self, input_units, group_or_system): """ - - Parameters - ---------- - input_units : - - group_or_system : - - - Returns - ------- - """ src_dim = self._get_dimensionality(input_units) @@ -1933,12 +1704,6 @@ class SystemRegistry(BaseRegistry): - Get or get the default system. - Parse @system and @group directive. - Parameters - ---------- - - Returns - ------- - """ def __init__(self, system=None, **kwargs): @@ -1968,13 +1733,6 @@ def _after_init(self): Create default group. Add all orphan units to it. Set default system. - - Parameters - ---------- - - Returns - ------- - """ super()._after_init() @@ -2162,19 +1920,6 @@ def _get_base_units(self, input_units, check_nonmult=True, system=None): return base_factor, destination_units def _get_compatible_units(self, input_units, group_or_system): - """ - - Parameters - ---------- - input_units : - - group_or_system : - - - Returns - ------- - - """ if group_or_system is None: group_or_system = self._default_system @@ -2223,10 +1968,6 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): or unit string fmt_locale : locale identifier string, used in `format_babel`. Default to None - - Returns - ------- - """ def __init__( @@ -2264,7 +2005,7 @@ def pi_theorem(self, quantities): Returns ------- - type + list a list of dimensionless quantities expressed as dicts """ @@ -2278,9 +2019,6 @@ def setup_matplotlib(self, enable=True): enable : bool whether support should be enabled or disabled (Default value = True) - Returns - ------- - """ # Delays importing matplotlib until it's actually requested from .matplotlib import setup_matplotlib_handlers diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 14e68f9ff..6bd21dd52 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -51,7 +51,7 @@ def _to_units_container(a, registry=None): Returns ------- - type + UnitsContainer, bool """ @@ -157,19 +157,6 @@ def _apply_defaults(func, args, kwargs): Named keywords may have been left blank. This function applies the default values so that every argument is defined. - - Parameters - ---------- - func : - - args : - - kwargs : - - - Returns - ------- - """ sig = signature(func) @@ -203,12 +190,12 @@ def wraps(ureg, ret, args, strict=True): output units. args : iterable of input units. - strict : - boolean to indicate that only quantities are accepted. (Default value = True) + strict : bool + Indicates that only quantities are accepted. (Default value = True) Returns ------- - type + callable the wrapped function. """ diff --git a/pint/systems.py b/pint/systems.py index 64d90114d..198baf218 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -41,13 +41,6 @@ class Group(SharedRegistryObject): ... @end - - Parameters - ---------- - - Returns - ------- - """ #: Regex to match the header parts of a definition. @@ -96,12 +89,6 @@ def members(self): Calculated to include to all units in all included _used_groups. - Parameters - ---------- - - Returns - ------- - """ if self._computed_members is None: self._computed_members = set(self._unit_names) @@ -137,15 +124,6 @@ def is_used_group(self, group_name): def add_units(self, *unit_names): """Add units to group. - - Parameters - ---------- - *unit_names : - - - Returns - ------- - """ for unit_name in unit_names: self._unit_names.add(unit_name) @@ -158,15 +136,6 @@ def non_inherited_unit_names(self): def remove_units(self, *unit_names): """Remove units from group. - - Parameters - ---------- - *unit_names : - - - Returns - ------- - """ for unit_name in unit_names: self._unit_names.remove(unit_name) @@ -175,15 +144,6 @@ def remove_units(self, *unit_names): def add_groups(self, *group_names): """Add groups to group. - - Parameters - ---------- - *group_names : - - - Returns - ------- - """ d = self._REGISTRY._groups for group_name in group_names: @@ -203,15 +163,6 @@ def add_groups(self, *group_names): def remove_groups(self, *group_names): """Remove groups from group. - - Parameters - ---------- - *group_names : - - - Returns - ------- - """ d = self._REGISTRY._groups for group_name in group_names: @@ -314,13 +265,6 @@ class System(SharedRegistryObject): - new_unit_name: a non root unit which is going to replace the old_unit. If the new_unit_name and the old_unit_name, the later and the colon can be ommited. - - Parameters - ---------- - - Returns - ------- - """ #: Regex to match the header parts of a context. @@ -390,15 +334,6 @@ def invalidate_members(self): def add_groups(self, *group_names): """Add groups to group. - - Parameters - ---------- - *group_names : - - - Returns - ------- - """ self._used_groups |= set(group_names) @@ -406,31 +341,13 @@ def add_groups(self, *group_names): def remove_groups(self, *group_names): """Remove groups from group. - - Parameters - ---------- - *group_names : - - - Returns - ------- - """ self._used_groups -= set(group_names) self.invalidate_members() def format_babel(self, locale): - """translate the name of the system - - Parameters - ---------- - locale : - - - Returns - ------- - + """translate the name of the system. """ if locale and self.name in _babel_systems: name = _babel_systems[self.name] diff --git a/pint/unit.py b/pint/unit.py index 96066527e..10ebd71e2 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -116,7 +116,7 @@ def format_babel(self, spec="", **kwspec): @property def dimensionless(self): - """ """ + """Return true if the Unit is dimensionless.""" return not bool(self.dimensionality) @property diff --git a/pint/util.py b/pint/util.py index d9bb611f1..fe3c17241 100644 --- a/pint/util.py +++ b/pint/util.py @@ -891,12 +891,6 @@ class SourceIterator: for lineno, line in SourceIterator(sequence): # do something here - Parameters - ---------- - - Returns - ------- - """ def __new__(cls, sequence): @@ -933,13 +927,6 @@ def block_iter(self): class BlockIterator(SourceIterator): """Like SourceIterator but stops when it finds '@end' It also raises an error if another '@' directive is found inside. - - Parameters - ---------- - - Returns - ------- - """ def __new__(cls, line_iterator): @@ -978,10 +965,6 @@ def iterable(y): type : object y : - - - Returns - ------- """ try: @@ -1001,10 +984,6 @@ def sized(y): type : object y : - - - Returns - ------- """ try: From f18183495d36f7c161a3a455f4c4700115ac12a1 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 01:53:21 -0300 Subject: [PATCH 258/612] LinterS --- pint/quantity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 1e03df81c..1bf3fb9dc 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -615,7 +615,6 @@ def to_reduced_units(self): can it make use of contexts at this time. """ - # can we make this more efficient? newq = copy.copy(self) newq.ito_reduced_units() From 3ff439e386191b77cb5186bd98478595aa035355 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 01:54:27 -0300 Subject: [PATCH 259/612] Updated CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index b68178450..20689cdbf 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Moved docstrings to Numpy Docs - Added tests for immutability of the magnitude's type under common operations (Issue #956, Thanks Jon Thielen) - Switched test configuration to pytest and added tests of Pint's matplotlib support. From 58956fa57fcafce0716f265f9ec81703cb3f547e Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 10:54:45 -0300 Subject: [PATCH 260/612] Fixes W293 --- bench/bench.py | 4 +- pint/context.py | 16 +++---- pint/definitions.py | 2 +- pint/formatting.py | 6 +-- pint/numpy_func.py | 22 ++++----- pint/quantity.py | 6 +-- pint/registry.py | 84 ++++++++++++++++----------------- pint/registry_helpers.py | 18 +++---- pint/systems.py | 28 +++++------ pint/testsuite/__init__.py | 4 +- pint/testsuite/parameterized.py | 6 +-- pint/testsuite/test_issues.py | 12 ++--- pint/testsuite/test_numpy.py | 4 +- pint/testsuite/test_umath.py | 18 +++---- pint/util.py | 42 ++++++++--------- 15 files changed, 136 insertions(+), 136 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index 9995b88bb..c3c9323b2 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -89,13 +89,13 @@ def time_task(name, stmt="pass", setup="pass", number=0, repeat=3, stmts="", bas def time_file(filename, name="", setup="", number=0, repeat=3): """Open a yaml benchmark file an time each statement, - + yields a tuple with filename, task name, time in seconds. Parameters ---------- filename : - + name : (Default value = "") setup : diff --git a/pint/context.py b/pint/context.py index 0034925a6..1ceb35969 100644 --- a/pint/context.py +++ b/pint/context.py @@ -36,15 +36,15 @@ class Context: """A specialized container that defines transformation functions from one dimension to another. Each Dimension are specified using a UnitsContainer. Simple transformation are given with a function taking a single parameter. - + Conversion functions may take optional keyword arguments and the context can have default values for these arguments. - - + + Additionally, a context may host redefinitions: - - + + A redefinition must be performed among units that already exist in the registry. It cannot change the dimensionality of a unit. The symbol and aliases are automatically inherited from the registry. @@ -108,7 +108,7 @@ def from_context(cls, context, **defaults): """Creates a new context that shares the funcs dictionary with the original context. The default values are copied from the original context and updated with the new defaults. - + If defaults is empty, return the same context. Parameters @@ -116,7 +116,7 @@ def from_context(cls, context, **defaults): context : Context Original context. **defaults - + Returns ------- @@ -298,7 +298,7 @@ def __init__(self): def insert_contexts(self, *contexts): """Insert one or more contexts in reversed order the chained map. (A rule in last context will take precedence) - + To facilitate the identification of the context with the matching rule, the *relation_to_context* dictionary of the context is used. """ diff --git a/pint/definitions.py b/pint/definitions.py index 947a189a7..16dd840ec 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -59,7 +59,7 @@ def from_string(cls, definition): Parameters ---------- definition : - + Returns ------- diff --git a/pint/formatting.py b/pint/formatting.py index 4bcdf0b2b..0e84f8289 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -18,7 +18,7 @@ def _join(fmt, iterable): """Join an iterable with the format specified in fmt. - + The format can be specified in two ways: - PEP3101 format with two replacement fields (eg. '{} * {}') - The concatenating string (eg. ' * ') @@ -26,9 +26,9 @@ def _join(fmt, iterable): Parameters ---------- fmt : str - + iterable : - + Returns ------- diff --git a/pint/numpy_func.py b/pint/numpy_func.py index ed91df1cb..0dcb878d7 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -23,13 +23,13 @@ def _is_quantity(obj): """Test for _units and _magnitude attrs. - + This is done in place of isinstance(Quantity, arg), which would cause a circular import. Parameters ---------- obj : Object - + Returns ------- @@ -44,7 +44,7 @@ def _is_quantity_sequence(obj): Parameters ---------- obj : object - + Returns ------- @@ -71,7 +71,7 @@ def _get_first_input_units(args, kwargs=None): def convert_arg(arg, pre_calc_units): """Convert quantities and sequences of quantities to pre_calc_units and strip units. - + Helper function for convert_to_consistent_units. pre_calc_units must be given as a pint Unit or None. @@ -96,7 +96,7 @@ def convert_arg(arg, pre_calc_units): def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): """Prepare args and kwargs for wrapping by unit conversion and stripping. - + If pre_calc_units is not None, takes the args and kwargs for a NumPy function and converts any Quantity or Sequence of Quantities into the units of the first Quantiy/Sequence of Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless @@ -114,7 +114,7 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): def unwrap_and_wrap_consistent_units(*args): """Strip units from args while providing a rewrapping function. - + Returns the given args as parsed by convert_to_consistent_units assuming units of first arg with units, along with a wrapper to restore that unit to the output. @@ -129,9 +129,9 @@ def unwrap_and_wrap_consistent_units(*args): def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): """Determine resulting unit from given operation. - + Options for `unit_op`: - + - "sum": `first_input_units`, unless non-multiplicative, which raises OffsetUnitCalculusError - "mul": product of all units in `all_args` @@ -149,9 +149,9 @@ def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): Parameters ---------- unit_op : - + first_input_units : - + all_args : (Default value = None) size : @@ -249,7 +249,7 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): `get_op_output_unit`. If some other string, the string is parsed as a unit, which becomes the unit of the output. If None, the bare magnitude is returned. - + """ # If NumPy is not available, do not attempt implement that which does not exist if np is None: diff --git a/pint/quantity.py b/pint/quantity.py index 1bf3fb9dc..8bdf737dc 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -418,7 +418,7 @@ def from_list(cls, quant_list, units=None): """Transforms a list of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. Same as from_sequence. - + If units is not specified and list is empty, the unit cannot be determined and a ValueError is raised. @@ -439,7 +439,7 @@ def from_list(cls, quant_list, units=None): def from_sequence(cls, seq, units=None): """Transforms a sequence of Quantities into an numpy.array quantity. If no units are specified, the unit of the first element will be used. - + If units is not specified and sequence is empty, the unit cannot be determined and a ValueError is raised. @@ -1597,7 +1597,7 @@ def searchsorted(self, v, side="left", sorter=None): def dot(self, b): """Dot product of two arrays. - + Wraps np.dot(). """ diff --git a/pint/registry.py b/pint/registry.py index d4f75d3d5..cfade8b51 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -122,9 +122,9 @@ def __init__(self, registry_cache: RegistryCache): class BaseRegistry(metaclass=RegistryMeta): """Base class for all registries. - + Capabilities: - + - Register units, prefixes, and dimensions, and their relations. - Convert between units. - Find dimensionality of a unit. @@ -329,7 +329,7 @@ def define(self, definition): def _define(self, definition): """Add unit to the registry. - + This method defines only multiplicative units, converting any other type to `delta_` units. @@ -434,7 +434,7 @@ def _define_adder(self, definition, unit_dict, casei_unit_dict): def _define_single_adder(self, key, value, unit_dict, casei_unit_dict): """Helper function to store a definition in the internal dictionaries. - + It warns or raise error on redefinition. """ if key in unit_dict: @@ -723,7 +723,7 @@ def _get_dimensionality_ratio(self, unit1, unit2): def get_root_units(self, input_units, check_nonmult=True): """Convert unit or dict of units to the root units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. @@ -750,7 +750,7 @@ def get_root_units(self, input_units, check_nonmult=True): def _get_root_units(self, input_units, check_nonmult=True): """Convert unit or dict of units to the root units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. @@ -794,7 +794,7 @@ def _get_root_units(self, input_units, check_nonmult=True): def get_base_units(self, input_units, check_nonmult=True, system=None): """Convert unit or dict of units to the base units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. @@ -937,7 +937,7 @@ def parse_unit_name(self, unit_name, case_sensitive=True): Parameters ---------- unit_name : - + case_sensitive : (Default value = True) @@ -981,10 +981,10 @@ def _parse_unit_name(self, unit_name, case_sensitive=True): @staticmethod def _dedup_candidates(candidates): """Helper of parse_unit_name. - + Given an iterable of unit triplets (prefix, name, suffix), remove those with different names but equal value, preferring those with a prefix. - + e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') """ candidates = dict.fromkeys(candidates) # ordered set @@ -1000,7 +1000,7 @@ def _dedup_candidates(candidates): def parse_units(self, input_string, as_delta=None): """Parse a units expression and returns a UnitContainer with the canonical names. - + The expression can only contain products, ratios and powers of units. Parameters @@ -1085,20 +1085,20 @@ def parse_expression( self, input_string, case_sensitive=True, use_decimal=False, **values ): """Parse a mathematical expression including units and return a quantity object. - + Numerical constants can be specified as keyword arguments and will take precedence over the names defined in the registry. Parameters ---------- input_string : - + case_sensitive : (Default value = True) use_decimal : (Default value = False) **values : - + Returns ------- @@ -1124,7 +1124,7 @@ def parse_expression( class NonMultiplicativeRegistry(BaseRegistry): """Handle of non multiplicative units (e.g. Temperature). - + Capabilities: - Register non-multiplicative units and their relations. - Convert between non-multiplicative units. @@ -1163,7 +1163,7 @@ def _parse_units(self, input_string, as_delta=None): def _define(self, definition): """Add unit to the registry. - + In addition to what is done by the BaseRegistry, registers also non-multiplicative units. @@ -1232,7 +1232,7 @@ def _validate_and_extract(self, units): def _convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. - + In addition to what is done by the BaseRegistry, converts between non-multiplicative units. @@ -1304,11 +1304,11 @@ def _convert(self, value, src, dst, inplace=False): class ContextRegistry(BaseRegistry): """Handle of Contexts. - + Conversion between units with different dimenstions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) - + Capabilities: - Register contexts. - Enable and disable contexts. @@ -1344,9 +1344,9 @@ def _parse_context(self, ifile): def add_context(self, context: Context) -> None: """Add a context object to the registry. - + The context will be accessible by its name and aliases. - + Notice that this method will NOT enable the context. Use `enable_contexts`. """ @@ -1367,7 +1367,7 @@ def add_context(self, context: Context) -> None: def remove_context(self, name_or_alias: str) -> Context: """Remove a context from the registry and return it. - + Notice that this methods will not disable the context. Use `disable_contexts`. """ @@ -1474,7 +1474,7 @@ def enable_contexts(self, *names_or_contexts, **kwargs) -> None: kwargs : keyword arguments for the context *names_or_contexts : - + **kwargs : """ @@ -1535,23 +1535,23 @@ def context(self, *names, **kwargs): name of the context. kwargs : keyword arguments for the contexts. - + Context are called by their name:: - - + + If the context has an argument, you can specify its value as a keyword argument:: - - + + Multiple contexts can be entered in single call: - - + + or nested allowing you to give different values to the same keyword argument:: - - + + A nested context inherits the defaults from the containing context:: *names : - + **kwargs : @@ -1586,7 +1586,7 @@ def context(self, *names, **kwargs): def with_context(self, name, **kw): """Decorator to wrap a function call in a Pint context. - + Use it to ensure that a certain context is active when calling a function:: @@ -1597,9 +1597,9 @@ def with_context(self, name, **kw): kwargs : keyword arguments for the contexts. name : - + **kw : - + Returns ------- @@ -1632,7 +1632,7 @@ def wrapper(*values, **kwargs): def _convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. - + In addition to what is done by the BaseRegistry, converts between units with different dimensions by following transformation rules defined in the context. @@ -1693,11 +1693,11 @@ def _get_compatible_units(self, input_units, group_or_system): class SystemRegistry(BaseRegistry): """Handle of Systems and Groups. - + Conversion between units with different dimenstions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) - + Capabilities: - Register systems and groups. - List systems @@ -1729,7 +1729,7 @@ def _init_dynamic_classes(self): def _after_init(self): """After init function - + Create default group. Add all orphan units to it. Set default system. @@ -1847,10 +1847,10 @@ def _define(self, definition): def get_base_units(self, input_units, check_nonmult=True, system=None): """Convert unit or dict of units to the base units. - + If any unit is non multiplicative and check_converter is True, then None is returned as the multiplicative factor. - + Unlike BaseRegistry, in this registry root_units might be different from base_units diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 6bd21dd52..d51daba9e 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -45,14 +45,14 @@ def _to_units_container(a, registry=None): Parameters ---------- a : - + registry : (Default value = None) Returns ------- UnitsContainer, bool - + """ if isinstance(a, str) and "=" in a: @@ -154,7 +154,7 @@ def _converter(ureg, values, strict): def _apply_defaults(func, args, kwargs): """Apply default keyword arguments. - + Named keywords may have been left blank. This function applies the default values so that every argument is defined. """ @@ -170,15 +170,15 @@ def _apply_defaults(func, args, kwargs): def wraps(ureg, ret, args, strict=True): """Wraps a function to become pint-aware. - + Use it when a function requires a numerical value but in some specific units. The wrapper function will take a pint quantity, convert to the units specified in `args` and then call the wrapped function with the resulting magnitude. - + The value returned by the wrapped function will be converted to the units specified in `ret`. - + Use None to skip argument conversion. Set strict to False, to accept also numerical values. @@ -256,10 +256,10 @@ def wrapper(*values, **kw): def check(ureg, *args): """Decorator to for quantity type checking for function inputs. - + Use it to ensure that the decorated function input parameters match the expected type of pint quantity. - + Use None to skip argument checking. Parameters @@ -269,7 +269,7 @@ def check(ureg, *args): args : iterable of input units. *args : - + Returns ------- diff --git a/pint/systems.py b/pint/systems.py index 198baf218..fc8c53271 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -26,16 +26,16 @@ class Group(SharedRegistryObject): """A group is a set of units. - + Units can be added directly or by including other groups. - + Members are computed dynamically, that is if a unit is added to a group X all groups that include X are affected. - + The group belongs to one Registry. - + It can be specified in the definition file as:: - + @group [using , ..., ] ... @@ -86,7 +86,7 @@ def __init__(self, name): @property def members(self): """Names of the units that are members of the group. - + Calculated to include to all units in all included _used_groups. """ @@ -242,28 +242,28 @@ def __getattr__(self, item): class System(SharedRegistryObject): """A system is a Group plus a set of base units. - + Members are computed dynamically, that is if a unit is added to a group X all groups that include X are affected. - + The System belongs to one Registry. - + It can be specified in the definition file as:: - + @system [using , ..., ] ... @end - + The syntax for the rule is: - + new_unit_name : old_unit_name - + where: - old_unit_name: a root unit part which is going to be removed from the system. - new_unit_name: a non root unit which is going to replace the old_unit. - + If the new_unit_name and the old_unit_name, the later and the colon can be ommited. """ diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index bd384d612..1614f5988 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -143,7 +143,7 @@ def main(): def run(): """Run all tests. - + :return: a :class:`unittest.TestResult` object Parameters @@ -173,7 +173,7 @@ def add_docs(suite): Parameters ---------- suite : - + Returns ------- diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py index ee4917d2a..8e5df98a0 100644 --- a/pint/testsuite/parameterized.py +++ b/pint/testsuite/parameterized.py @@ -109,7 +109,7 @@ def parameterize( cls, param_names, data, func_name_format="{func_name}_{case_num:05d}" ): """Decorator for parameterizing a test method - example: - + @ParameterizedTestCase.parameterize( ("isbn", "expected_title"), [ ("0262033844", "Introduction to Algorithms"), @@ -118,9 +118,9 @@ def parameterize( Parameters ---------- param_names : - + data : - + func_name_format : (Default value = "{func_name}_{case_num:05d}") diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 04aa31dbb..8d6bd7e16 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -527,21 +527,21 @@ def test_issue625a(self): @ureg.wraps(ureg.second, (ureg.meters, ureg.meters / ureg.second ** 2)) def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): """Calculate time to fall from a height h with a default gravity. - + By default, the gravity is assumed to be earth gravity, but it can be modified. - + d = .5 * g * t**2 t = sqrt(2 * d / g) Parameters ---------- height : - + gravity : (Default value = Q_(9.8) "m/s^2") : - + Returns ------- @@ -568,11 +568,11 @@ def get_displacement(time, rate=Q_(1, "m/s")): Parameters ---------- time : - + rate : (Default value = Q_(1) "m/s") : - + Returns ------- diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 72fa70e85..3f2bce18b 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1124,9 +1124,9 @@ def pad_with(vector, pad_width, iaxis, kwargs): @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): """Universal functions (ufuncs) > Bittwiddling functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#bittwiddlingfunctions - + bitwise_and(x1, x2[, out]) Compute the bitwise AND of two arrays elementwise. bitwise_or(x1, x2[, out]) Compute the bitwise OR of two arrays elementwise. bitwise_xor(x1, x2[, out]) Compute the bitwise XOR of two arrays elementwise. diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index 60559ed93..75b73659f 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -146,7 +146,7 @@ def _test1_2o( rtol : relative tolerance. (Default value = 1e-6) "same") : - + Returns ------- @@ -271,9 +271,9 @@ def _testn2(self, func, x1, ok_with, raise_with=()): @helpers.requires_numpy() class TestMathUfuncs(TestUFuncs): """Universal functions (ufunc) > Math operations - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#math-operations - + add(x1, x2[, out]) Add arguments element-wise. subtract(x1, x2[, out]) Subtract arguments, element-wise. multiply(x1, x2[, out]) Multiply arguments element-wise. @@ -419,9 +419,9 @@ def test_reciprocal(self): @helpers.requires_numpy() class TestTrigUfuncs(TestUFuncs): """Universal functions (ufunc) > Trigonometric functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#trigonometric-functions - + sin(x[, out]) Trigonometric sine, element-wise. cos(x[, out]) Cosine elementwise. tan(x[, out]) Compute tangent element-wise. @@ -676,9 +676,9 @@ def test_rad2deg(self): class TestComparisonUfuncs(TestUFuncs): """Universal functions (ufunc) > Comparison functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#comparison-functions - + greater(x1, x2[, out]) Return the truth value of (x1 > x2) element-wise. greater_equal(x1, x2[, out]) Return the truth value of (x1 >= x2) element-wise. less(x1, x2[, out]) Return the truth value of (x1 < x2) element-wise. @@ -715,9 +715,9 @@ def test_equal(self): class TestFloatingUfuncs(TestUFuncs): """Universal functions (ufunc) > Floating functions - + http://docs.scipy.org/doc/numpy/reference/ufuncs.html#floating-functions - + isreal(x) Returns a bool array, where True if input element is real. iscomplex(x) Returns a bool array, where True if input element is complex. isfinite(x[, out]) Test element-wise for finite-ness (not infinity or not Not a Number). diff --git a/pint/util.py b/pint/util.py index fe3c17241..aff507bb9 100644 --- a/pint/util.py +++ b/pint/util.py @@ -37,7 +37,7 @@ def matrix_to_string( Parameters ---------- matrix : - + row_headers : (Default value = None) col_headers : @@ -69,7 +69,7 @@ def transpose(matrix): Parameters ---------- matrix : - + Returns ------- @@ -305,7 +305,7 @@ def copy(self): class UnitsContainer(Mapping): """The UnitsContainer stores the product of units and their respective exponent and implements the corresponding operations. - + UnitsContainer is a read-only mapping. All operations (even in place ones) Parameters @@ -314,7 +314,7 @@ class UnitsContainer(Mapping): Returns ------- type - + """ @@ -351,7 +351,7 @@ def remove(self, keys): Parameters ---------- keys : - + Returns ------- @@ -369,9 +369,9 @@ def rename(self, oldkey, newkey): Parameters ---------- oldkey : - + newkey : - + Returns ------- @@ -490,7 +490,7 @@ def __rtruediv__(self, other): class ParserHelper(UnitsContainer): """The ParserHelper stores in place the product of variables and their respective exponent and implements the corresponding operations. - + ParserHelper is a read-only mapping. All operations (even in place ones) Parameters @@ -514,13 +514,13 @@ def __init__(self, scale=1, *args, **kwargs): @classmethod def from_word(cls, input_word): """Creates a ParserHelper object with a single variable with exponent one. - + Equivalent to: ParserHelper({'word': 1}) Parameters ---------- input_word : - + Returns ------- @@ -552,7 +552,7 @@ def from_string(cls, input_string): Parameters ---------- input_string : - + Returns ------- @@ -736,7 +736,7 @@ def _is_dim(name): class SharedRegistryObject: """Base class for object keeping a reference to the registree. - + Such object are for now Quantity and Unit, in a number of places it is that an object from this class has a '_units' attribute. @@ -765,7 +765,7 @@ def _check(self, other): Parameters ---------- other : - + Returns ------- @@ -814,7 +814,7 @@ def to_units_container(unit_like, registry=None): Parameters ---------- unit_like : - + registry : (Default value = None) @@ -842,12 +842,12 @@ def infer_base_unit(q): Parameters ---------- q : - + Returns ------- type - + """ d = udict() @@ -868,7 +868,7 @@ def getattr_maybe_raise(self, item): Parameters ---------- item : - + Returns ------- @@ -882,12 +882,12 @@ def getattr_maybe_raise(self, item): class SourceIterator: """Iterator to facilitate reading the definition files. - + Accepts any sequence (like a list of lines, a file or another SourceIterator) - + The iterator yields the line number and line (skipping comments and empty lines) and stripping white spaces. - + for lineno, line in SourceIterator(sequence): # do something here @@ -954,7 +954,7 @@ def __next__(self): def iterable(y): """Check whether or not an object can be iterated over. - + Vendored from numpy under the terms of the BSD 3-Clause License. (Copyright (c) 2005-2019, NumPy Developers.) From 044df85d7b644c849dbbde8d1d9b6d3f3b927951 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 21:23:42 -0600 Subject: [PATCH 261/612] Add upcast type check on Quantity creation, and make list extensible --- pint/compat.py | 3 ++- pint/quantity.py | 4 +++- pint/testsuite/test_quantity.py | 9 +++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 7642b15f4..87cc5b0f1 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -150,6 +150,7 @@ def _to_magnitude(value, force_ndarray=False): # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and # downcast/wrappable types +upcast_types = ["PintArray", "Series", "DataArray"] def is_upcast_type(other): @@ -164,7 +165,7 @@ def is_upcast_type(other): bool """ # Check if class name is in preset list - return other.__name__ in ("PintArray", "Series", "DataArray") + return other.__name__ in upcast_types def eq(lhs, rhs, check_all): diff --git a/pint/quantity.py b/pint/quantity.py index 8bdf737dc..59194ae71 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -158,7 +158,9 @@ def __reduce__(self): def __new__(cls, value, units=None): global SKIP_ARRAY_FUNCTION_CHANGE_WARNING - if units is None: + if is_upcast_type(type(value)): + raise TypeError(f"Quantity cannot wrap upcast type {type(value)}") + elif units is None: if isinstance(value, str): if value == "": raise ValueError( diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index d82096422..a6b9c3ba5 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -536,6 +536,15 @@ def test_array_function_warning_on_creation(self): def test_no_ndarray_coercion_without_numpy(self): self.assertRaises(ValueError, self.Q_(1, "m").__array__) + @patch("pint.compat.upcast_types", ['FakeWrapper']) + def test_upcast_type_rejection_on_creation(self): + class FakeWrapper: + def __init__(self, q): + self.q = q + + self.assertRaises(TypeError, self.Q_, FakeWrapper(42), 'm') + self.assertEqual(FakeWrapper(self.Q_(42, 'm')).q, self.Q_(42, 'm')) + class TestQuantityToCompact(QuantityTestCase): def assertQuantityAlmostIdentical(self, q1, q2): From 6f3f96183af4afc91acc132435cc7e7f0b7ed6ef Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 26 Dec 2019 21:46:48 -0600 Subject: [PATCH 262/612] Add upcast tests for xarray, with additional implementation checks --- .travis.yml | 1 + pint/compat.py | 2 +- pint/quantity.py | 5 ++ pint/testsuite/test_compat_upcast.py | 110 +++++++++++++++++++++++++++ pint/testsuite/test_quantity.py | 6 +- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 pint/testsuite/test_compat_upcast.py diff --git a/.travis.yml b/.travis.yml index 4f8613f24..a6cf799ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ env: - PKGS="python=3.6 numpy uncertainties" - PKGS="python=3.7 numpy uncertainties" - PKGS="python=3.8 numpy uncertainties" + - PKGS="python xarray netCDF4" # TODO: pandas tests # - PKGS="python=3.7 numpy pandas uncertainties pandas" diff --git a/pint/compat.py b/pint/compat.py index 87cc5b0f1..b4cacdc0d 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -150,7 +150,7 @@ def _to_magnitude(value, force_ndarray=False): # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and # downcast/wrappable types -upcast_types = ["PintArray", "Series", "DataArray"] +upcast_types = ["PintArray", "Series", "DataArray", "Dataset", "Variable"] def is_upcast_type(other): diff --git a/pint/quantity.py b/pint/quantity.py index 59194ae71..1d6a37297 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -714,6 +714,7 @@ def __complex__(self): return complex(self._convert_magnitude_not_inplace(UnitsContainer())) raise DimensionalityError(self._units, "dimensionless") + @check_implemented def _iadd_sub(self, other, op): """Perform addition or subtraction operation in-place and return the result. @@ -966,6 +967,7 @@ def __rsub__(self, other): else: return -self._add_sub(other, operator.sub) + @check_implemented @ireduce_dimensions def _imul_div(self, other, magnitude_op, units_op=None): """Perform multiplication or division operation in-place and return the @@ -1185,6 +1187,7 @@ def __rfloordiv__(self, other): raise DimensionalityError(self._units, "dimensionless") return self.__class__(magnitude, UnitsContainer({})) + @check_implemented def __imod__(self, other): if not self._check(other): other = self.__class__(other, UnitsContainer({})) @@ -1228,6 +1231,7 @@ def __rdivmod__(self, other): raise DimensionalityError(self._units, "dimensionless") return (self.__class__(q, UnitsContainer({})), self.__class__(r, unit)) + @check_implemented def __ipow__(self, other): if not isinstance(self._magnitude, ndarray): return self.__pow__(other) @@ -1408,6 +1412,7 @@ def __eq__(self, other): except DimensionalityError: return False + @check_implemented def __ne__(self, other): out = self.__eq__(other) if isinstance(out, ndarray): diff --git a/pint/testsuite/test_compat_upcast.py b/pint/testsuite/test_compat_upcast.py new file mode 100644 index 000000000..116606da5 --- /dev/null +++ b/pint/testsuite/test_compat_upcast.py @@ -0,0 +1,110 @@ +import pytest + +from pint import UnitRegistry + +# Conditionally import NumPy and any upcast type libraries +np = pytest.importorskip("numpy", reason="NumPy is not available") +xr = pytest.importorskip("xarray", reason="xarray is not available") + +# Set up unit registry and sample +ureg = UnitRegistry() +q = [[1.0, 2.0], [3.0, 4.0]] * ureg.m + + +@pytest.fixture +def da(): + return xr.DataArray(q.copy()) + + +@pytest.fixture +def ds(): + return xr.tutorial.load_dataset("air_temperature") + + +def test_xarray_quantity_creation(): + with pytest.raises(TypeError) as exc: + ureg.Quantity(xr.DataArray(np.arange(4)), "m") + assert "Quantity cannot wrap upcast type" in str(exc) + assert xr.DataArray(q).data is q + + +def test_quantification(ds): + da = ds["air"][0] + da.data = ureg.Quantity(da.values, da.attrs.pop("units")) + mean = da.mean().item() + assert mean.units == ureg.K + assert np.isclose(mean, 274.166259765625 * ureg.K) + + +@pytest.mark.parametrize( + "op", + [ + lambda x, y: x + y, + lambda x, y: x - (-y), + lambda x, y: x * y, + lambda x, y: x / (y ** -1), + ], +) +@pytest.mark.parametrize( + "pair", + [ + (q, xr.DataArray(q)), + ( + xr.DataArray([1.0, 2.0] * ureg.m, dims=("y",)), + xr.DataArray( + np.arange(6, dtype="float").reshape(3, 2, 1), dims=("z", "y", "x") + ) + * ureg.km, + ), + (1 * ureg.m, xr.DataArray(q)), + ], +) +def test_binary_arithmatic_commutivity(op, pair): + z0 = op(*pair) + z1 = op(*pair[::-1]) + z1 = z1.transpose(*z0.dims) + assert np.all(np.isclose(z0.data, z1.data.to(z0.data.units))) + + +def test_eq_commutivity(da): + assert np.all((q.T == da) == (da.transpose() == q)) + + +def test_ne_commutivity(da): + assert np.all((q != da.transpose()) == (da != q.T)) + + +def test_dataset_operation_with_unit(ds): + ds0 = ureg.K * ds.isel(time=0) + ds1 = (ds * ureg.K).isel(time=0) + xr.testing.assert_identical(ds0, ds1) + assert np.isclose(ds0["air"].mean().item(), 274.166259765625 * ureg.K) + + +def test_dataarray_inplace_arithmatic_roundtrip(da): + da_original = da.copy() + q_to_modify = q.copy() + da += q + xr.testing.assert_identical(da, xr.DataArray([[2, 4], [6, 8]] * ureg.m)) + da -= q + xr.testing.assert_identical(da, da_original) + da *= ureg.m + xr.testing.assert_identical(da, xr.DataArray(q * ureg.m)) + da /= ureg.m + xr.testing.assert_identical(da, da_original) + # Operating inplace with DataArray converts to DataArray + q_to_modify += da + q_to_modify -= da + assert np.all(np.isclose(q_to_modify.data, q)) + + +def test_dataarray_inequalities(da): + xr.testing.assert_identical( + 2 * ureg.m > da, xr.DataArray([[True, False], [False, False]]) + ) + xr.testing.assert_identical( + 2 * ureg.m < da, xr.DataArray([[False, False], [True, True]]) + ) + with pytest.raises(ValueError) as exc: + da > 2 + assert "Cannot compare Quantity and " in str(exc) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index a6b9c3ba5..29bc886e3 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -536,14 +536,14 @@ def test_array_function_warning_on_creation(self): def test_no_ndarray_coercion_without_numpy(self): self.assertRaises(ValueError, self.Q_(1, "m").__array__) - @patch("pint.compat.upcast_types", ['FakeWrapper']) + @patch("pint.compat.upcast_types", ["FakeWrapper"]) def test_upcast_type_rejection_on_creation(self): class FakeWrapper: def __init__(self, q): self.q = q - self.assertRaises(TypeError, self.Q_, FakeWrapper(42), 'm') - self.assertEqual(FakeWrapper(self.Q_(42, 'm')).q, self.Q_(42, 'm')) + self.assertRaises(TypeError, self.Q_, FakeWrapper(42), "m") + self.assertEqual(FakeWrapper(self.Q_(42, "m")).q, self.Q_(42, "m")) class TestQuantityToCompact(QuantityTestCase): From 434852cb88821258e01e349d02674d1fbe730d9c Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Fri, 27 Dec 2019 11:02:06 -0600 Subject: [PATCH 263/612] Documentation updates for upcast type updates --- CHANGES | 9 ++++++++- docs/numpy.rst | 6 +++++- pint/testsuite/test_compat_upcast.py | 8 ++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 20689cdbf..414d37713 100644 --- a/CHANGES +++ b/CHANGES @@ -4,9 +4,16 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Improved compatbility for upcast types like xarray's DataArray or Dataset, to which + Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of + basic tests for proper deferral has been added (for full integration tests, see + xarray's test suite). The list of names of upcast types is available at + `pint.compat.upcast_types` in the API. + (Issue #959, Thanks Jon Thielen) - Moved docstrings to Numpy Docs + (Issue #958) - Added tests for immutability of the magnitude's type under common operations - (Issue #956, Thanks Jon Thielen) + (Issue #957, Thanks Jon Thielen) - Switched test configuration to pytest and added tests of Pint's matplotlib support. (Issue #954, Thanks Jon Thielen) - Deprecate array protocol fallback except where explicitly defined (`__array__`, diff --git a/docs/numpy.rst b/docs/numpy.rst index f849af23b..33056d949 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -144,7 +144,11 @@ upcast types to which Pint defers (see - ``PintArray``, as defined by pint-pandas - ``Series``, as defined by pandas -- ``DataArray``, as defined by xarray +- ``DataArray``, ``Dataset``, and ``Variable``, as defined by xarray + +If your application requires extension of this collection of types, the collection of +type names is available in Pint's API at ``pint.compat.upcast_types``. Note that these +are also the types to which a Quantity object will defer for arithmetic operations. To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that diff --git a/pint/testsuite/test_compat_upcast.py b/pint/testsuite/test_compat_upcast.py index 116606da5..108582e7b 100644 --- a/pint/testsuite/test_compat_upcast.py +++ b/pint/testsuite/test_compat_upcast.py @@ -59,18 +59,18 @@ def test_quantification(ds): (1 * ureg.m, xr.DataArray(q)), ], ) -def test_binary_arithmatic_commutivity(op, pair): +def test_binary_arithmetic_commutativity(op, pair): z0 = op(*pair) z1 = op(*pair[::-1]) z1 = z1.transpose(*z0.dims) assert np.all(np.isclose(z0.data, z1.data.to(z0.data.units))) -def test_eq_commutivity(da): +def test_eq_commutativity(da): assert np.all((q.T == da) == (da.transpose() == q)) -def test_ne_commutivity(da): +def test_ne_commutativity(da): assert np.all((q != da.transpose()) == (da != q.T)) @@ -81,7 +81,7 @@ def test_dataset_operation_with_unit(ds): assert np.isclose(ds0["air"].mean().item(), 274.166259765625 * ureg.K) -def test_dataarray_inplace_arithmatic_roundtrip(da): +def test_dataarray_inplace_arithmetic_roundtrip(da): da_original = da.copy() q_to_modify = q.copy() da += q From 5ce5c468d99f13032be845ef16d06e026495110f Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Fri, 27 Dec 2019 12:43:46 -0600 Subject: [PATCH 264/612] Refactor upcast type check to check actual types rather than names --- CHANGES | 2 +- docs/numpy.rst | 6 +++--- pint/compat.py | 33 ++++++++++++++++++++++++++++----- pint/testsuite/test_quantity.py | 12 +++++++----- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/CHANGES b/CHANGES index 414d37713..c131d45a9 100644 --- a/CHANGES +++ b/CHANGES @@ -7,7 +7,7 @@ Pint Changelog - Improved compatbility for upcast types like xarray's DataArray or Dataset, to which Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of basic tests for proper deferral has been added (for full integration tests, see - xarray's test suite). The list of names of upcast types is available at + xarray's test suite). The list of upcast types is available at `pint.compat.upcast_types` in the API. (Issue #959, Thanks Jon Thielen) - Moved docstrings to Numpy Docs diff --git a/docs/numpy.rst b/docs/numpy.rst index 33056d949..c9a46b99c 100644 --- a/docs/numpy.rst +++ b/docs/numpy.rst @@ -146,9 +146,9 @@ upcast types to which Pint defers (see - ``Series``, as defined by pandas - ``DataArray``, ``Dataset``, and ``Variable``, as defined by xarray -If your application requires extension of this collection of types, the collection of -type names is available in Pint's API at ``pint.compat.upcast_types``. Note that these -are also the types to which a Quantity object will defer for arithmetic operations. +If your application requires extension of this collection of types, it is available in +Pint's API at ``pint.compat.upcast_types``. Note that these are also the types to which +a Quantity object will defer for arithmetic operations. To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that diff --git a/pint/compat.py b/pint/compat.py index b4cacdc0d..e87c26c23 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -149,12 +149,36 @@ def _to_magnitude(value, force_ndarray=False): babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 # Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and -# downcast/wrappable types -upcast_types = ["PintArray", "Series", "DataArray", "Dataset", "Variable"] +# downcast/wrappable types using guarded imports +upcast_types = [] + +# pint-pandas (PintArray) +try: + from pintpandas import PintArray + + upcast_types.append(PintArray) +except ImportError: + pass + +# Pandas (Series) +try: + from pandas import Series + + upcast_types.append(Series) +except ImportError: + pass + +# xarray (DataArray, Dataset, Variable) +try: + from xarray import DataArray, Dataset, Variable + + upcast_types += [DataArray, Dataset, Variable] +except ImportError: + pass def is_upcast_type(other): - """Check if the type object is a upcast type. + """Check if the type object is a upcast type using preset list. Parameters ---------- @@ -164,8 +188,7 @@ def is_upcast_type(other): ------- bool """ - # Check if class name is in preset list - return other.__name__ in upcast_types + return other in upcast_types def eq(lhs, rhs, check_all): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 29bc886e3..9d61bbe8f 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -12,6 +12,12 @@ from pint.unit import UnitsContainer +class FakeWrapper: + # Used in test_upcast_type_rejection_on_creation + def __init__(self, q): + self.q = q + + class TestQuantity(QuantityTestCase): FORCE_NDARRAY = False @@ -536,12 +542,8 @@ def test_array_function_warning_on_creation(self): def test_no_ndarray_coercion_without_numpy(self): self.assertRaises(ValueError, self.Q_(1, "m").__array__) - @patch("pint.compat.upcast_types", ["FakeWrapper"]) + @patch("pint.compat.upcast_types", [FakeWrapper]) def test_upcast_type_rejection_on_creation(self): - class FakeWrapper: - def __init__(self, q): - self.q = q - self.assertRaises(TypeError, self.Q_, FakeWrapper(42), "m") self.assertEqual(FakeWrapper(self.Q_(42, "m")).q, self.Q_(42, "m")) From fd960931d6d70a6755d146cdcc11ad1bb59964bf Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Fri, 27 Dec 2019 13:48:56 -0600 Subject: [PATCH 265/612] Add array function/ufunc deferral tests (xref #951) --- pint/testsuite/test_compat_upcast.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pint/testsuite/test_compat_upcast.py b/pint/testsuite/test_compat_upcast.py index 108582e7b..7e3cd1023 100644 --- a/pint/testsuite/test_compat_upcast.py +++ b/pint/testsuite/test_compat_upcast.py @@ -108,3 +108,20 @@ def test_dataarray_inequalities(da): with pytest.raises(ValueError) as exc: da > 2 assert "Cannot compare Quantity and " in str(exc) + + +def test_array_function_deferral(da): + lower = 2 * ureg.m + upper = 3 * ureg.m + args = (da, lower, upper) + assert ( + lower.__array_function__( + np.clip, tuple(set(type(arg) for arg in args)), args, {} + ) + is NotImplemented + ) + + +def test_array_ufunc_deferral(da): + lower = 2 * ureg.m + assert lower.__array_ufunc__(np.maximum, "__call__", lower, da) is NotImplemented From f4f6eaca91f91aca88e5fa78e9d3cf9632e0ba3b Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 23:28:54 -0300 Subject: [PATCH 266/612] Add rps to definitions Close #653 --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index b2b6ca289..6d5fe6aa4 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -218,6 +218,7 @@ stere = meter ** 3 [frequency] = 1 / [time] hertz = 1 / second = Hz revolutions_per_minute = revolution / minute = rpm +revolutions_per_second = revolution / second = rps counts_per_second = count / second = cps # Wavenumber From a08fda75a0162b35d0a6864150b063eea15f36b8 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 27 Dec 2019 23:32:07 -0300 Subject: [PATCH 267/612] Updated CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index c131d45a9..8a2ba2d60 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Add revolutions per second (rps) - Improved compatbility for upcast types like xarray's DataArray or Dataset, to which Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of basic tests for proper deferral has been added (for full integration tests, see From e51af44fc38bc43a03bac8685ed845d1beca0e20 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 02:09:07 -0300 Subject: [PATCH 268/612] Add verification of number of args/params in check and wrap --- pint/registry_helpers.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index d51daba9e..c9dde5792 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -214,6 +214,14 @@ def wraps(ureg, ret, args, strict=True): container, ret = False, _to_units_container(ret, ureg) def decorator(func): + + count_params = len(signature(func).parameters) + if len(args) != count_params: + raise TypeError( + "%s takes %i parameters, but %i units were passed" + % (func.__name__, count_params, len(args)) + ) + assigned = tuple( attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) ) @@ -287,6 +295,14 @@ def check(ureg, *args): ] def decorator(func): + + count_params = len(signature(func).parameters) + if len(dimensions) != count_params: + raise TypeError( + "%s takes %i parameters, but %i dimensions were passed" + % (func.__name__, count_params, len(dimensions)) + ) + assigned = tuple( attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) ) @@ -297,11 +313,7 @@ def decorator(func): @functools.wraps(func, assigned=assigned, updated=updated) def wrapper(*args, **kwargs): list_args, empty = _apply_defaults(func, args, kwargs) - if len(dimensions) > len(list_args): - raise TypeError( - "%s takes %i parameters, but %i dimensions were passed" - % (func.__name__, len(list_args), len(dimensions)) - ) + for dim, value in zip(dimensions, list_args): if dim is None: From c4da2323db12327cff4a401724551f464be2471c Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 02:09:46 -0300 Subject: [PATCH 269/612] Streamline code --- pint/registry_helpers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index c9dde5792..36264a5b6 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -205,13 +205,11 @@ def wraps(ureg, ret, args, strict=True): converter = _parse_wrap_args(args) - if isinstance(ret, (list, tuple)): - container, ret = ( - True, - ret.__class__([_to_units_container(arg, ureg) for arg in ret]), - ) + is_ret_container = isinstance(ret, (list, tuple)) + if is_ret_container: + ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) else: - container, ret = False, _to_units_container(ret, ureg) + ret = _to_units_container(ret, ureg) def decorator(func): @@ -240,7 +238,7 @@ def wrapper(*values, **kw): result = func(*new_values, **kw) - if container: + if is_ret_container: out_units = ( _replace_units(r, values_by_name) if is_ref else r for (r, is_ref) in ret From d7b864d1db56b0bf8e9da59d2760b0d4d50d9a71 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 02:10:39 -0300 Subject: [PATCH 270/612] Improved docs check/wraps --- pint/registry_helpers.py | 45 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 36264a5b6..686c4f626 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -179,24 +179,28 @@ def wraps(ureg, ret, args, strict=True): The value returned by the wrapped function will be converted to the units specified in `ret`. - Use None to skip argument conversion. - Set strict to False, to accept also numerical values. - Parameters ---------- - ureg : + ureg : UnitRegistry a UnitRegistry instance. - ret : - output units. - args : - iterable of input units. + ret : iterable of str or iterable of Unit + Units of each of the return values. Use `None` to skip argument conversion. + args : iterable of str or iterable of Unit + Units of each of the input arguments. Use `None` to skip argument conversion. strict : bool Indicates that only quantities are accepted. (Default value = True) Returns ------- callable - the wrapped function. + the wrapper function. + + Raises + ------ + TypeError + if the number of given dimensions does not match the number of function parameters. + ValueError + if the any of the provided dimensions cannot be parsed as a dimension. """ @@ -264,28 +268,27 @@ def check(ureg, *args): """Decorator to for quantity type checking for function inputs. Use it to ensure that the decorated function input parameters match - the expected type of pint quantity. + the expected dimension of pint quantity. - Use None to skip argument checking. + The wrapper function raises: + - `pint.DimensionalityError` if an argument doesn't match the required dimensions. - Parameters - ---------- - ureg : + ureg : UnitRegistry a UnitRegistry instance. - args : - iterable of input units. - *args : - + *args : iterable of str or iterable of UnitContainer + Dimensions of each of the input arguments. Use `None` to skip argument conversion. Returns ------- - type + callable the wrapped function. Raises ------ - pint.DimensionalityError - if the parameters don't match dimensions + TypeError + if the number of given dimensions does not match the number of function parameters. + ValueError + if the any of the provided dimensions cannot be parsed as a dimension. """ dimensions = [ From 2d48e6b0911c65ccfb27f82e15376c828f491def Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 02:17:36 -0300 Subject: [PATCH 271/612] wraps(strict=True) now accept strings as arguments in the wrapper function Closes #711, #723 --- pint/registry_helpers.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 686c4f626..18089aeec 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -139,13 +139,20 @@ def _converter(ureg, values, strict): ) else: if strict: - raise ValueError( - "A wrapped function using strict=True requires " - "quantity for all arguments with not None units. " - "(error found for {}, {})".format( - args_as_uc[ndx][0], new_values[ndx] + if isinstance(values[ndx], str): + # if the value is a string, we try to parse it + tmp_value = ureg.parse_expression(values[ndx]) + new_values[ndx] = ureg._convert( + tmp_value._magnitude, tmp_value._units, args_as_uc[ndx][0] + ) + else: + raise ValueError( + "A wrapped function using strict=True requires " + "quantity or a string for all arguments with not None units. " + "(error found for {}, {})".format( + args_as_uc[ndx][0], new_values[ndx] + ) ) - ) return new_values, values_by_name From a79aaeda590d047d10efb86e456e06efe2729d48 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 02:31:45 -0300 Subject: [PATCH 272/612] wraps enforces str or Unit as arguments Close #489, #490 --- pint/registry_helpers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 18089aeec..4eb9ae758 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -205,23 +205,31 @@ def wraps(ureg, ret, args, strict=True): Raises ------ TypeError - if the number of given dimensions does not match the number of function parameters. - ValueError - if the any of the provided dimensions cannot be parsed as a dimension. + if the number of given arguments does not match the number of function parameters. + if the any of the provided arguments is not a unit a string or Quantity """ if not isinstance(args, (list, tuple)): args = (args,) + for arg in args: + if not isinstance(arg, (ureg.Unit, str)): + raise TypeError("wraps arguments must by of type str or Unit, not %s (%s)" % (type(arg), arg)) + converter = _parse_wrap_args(args) is_ret_container = isinstance(ret, (list, tuple)) if is_ret_container: + for arg in ret: + if not isinstance(arg, (ureg.Unit, str)): + raise TypeError("wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(arg), arg)) ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) else: + if not isinstance(ret, (ureg.Unit, str)): + raise TypeError("wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(ret), ret)) ret = _to_units_container(ret, ureg) - + def decorator(func): count_params = len(signature(func).parameters) From 2d5374f9ab5157a546d43b444d5ac92fa05a8892 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 03:07:54 -0300 Subject: [PATCH 273/612] Linters --- pint/registry_helpers.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 4eb9ae758..52170952d 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -215,7 +215,10 @@ def wraps(ureg, ret, args, strict=True): for arg in args: if not isinstance(arg, (ureg.Unit, str)): - raise TypeError("wraps arguments must by of type str or Unit, not %s (%s)" % (type(arg), arg)) + raise TypeError( + "wraps arguments must by of type str or Unit, not %s (%s)" + % (type(arg), arg) + ) converter = _parse_wrap_args(args) @@ -223,13 +226,19 @@ def wraps(ureg, ret, args, strict=True): if is_ret_container: for arg in ret: if not isinstance(arg, (ureg.Unit, str)): - raise TypeError("wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(arg), arg)) + raise TypeError( + "wraps 'ret' argument must by of type str or Unit, not %s (%s)" + % (type(arg), arg) + ) ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) else: if not isinstance(ret, (ureg.Unit, str)): - raise TypeError("wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(ret), ret)) + raise TypeError( + "wraps 'ret' argument must by of type str or Unit, not %s (%s)" + % (type(ret), ret) + ) ret = _to_units_container(ret, ureg) - + def decorator(func): count_params = len(signature(func).parameters) From 3a582af9e288a761b3c15d9c38fc205b232f39f5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 03:19:00 -0300 Subject: [PATCH 274/612] Add None case to valid values --- pint/registry_helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 52170952d..970f2efa0 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -214,7 +214,7 @@ def wraps(ureg, ret, args, strict=True): args = (args,) for arg in args: - if not isinstance(arg, (ureg.Unit, str)): + if arg is not None and not isinstance(arg, (ureg.Unit, str)): raise TypeError( "wraps arguments must by of type str or Unit, not %s (%s)" % (type(arg), arg) @@ -225,14 +225,14 @@ def wraps(ureg, ret, args, strict=True): is_ret_container = isinstance(ret, (list, tuple)) if is_ret_container: for arg in ret: - if not isinstance(arg, (ureg.Unit, str)): + if arg is not None and not isinstance(arg, (ureg.Unit, str)): raise TypeError( "wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(arg), arg) ) ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) else: - if not isinstance(ret, (ureg.Unit, str)): + if ret is not None and not isinstance(ret, (ureg.Unit, str)): raise TypeError( "wraps 'ret' argument must by of type str or Unit, not %s (%s)" % (type(ret), ret) From 8c82eae54deb0c04f3c2c672a1eab16abe8c82e9 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 28 Dec 2019 10:48:12 -0300 Subject: [PATCH 275/612] Updated CHANGES to new exceptions --- CHANGES | 7 +++++++ pint/testsuite/test_unit.py | 26 +++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 8a2ba2d60..0395b7fc0 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,13 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Improvements to wraps and check: + - fail upon decoration (not execution) by checking wrapped function signature against + wraps/check arguments. + (might BREAK test code) + - wraps only accepts strings and Units (not quantities) to avoid confusion with magnitude. + (might BREAK code not conforming to documentation) + - when strict=True, strings that can be parsed to quantities are accepted as arguments. - Add revolutions per second (rps) - Improved compatbility for upcast types like xarray's DataArray or Dataset, to which Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 7f64473cc..0b6c086a8 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -433,6 +433,9 @@ def func(x): ureg = self.ureg + self.assertRaises(TypeError, ureg.wraps, (3 * ureg.meter, [None])) + self.assertRaises(TypeError, ureg.wraps, (None, [3 * ureg.meter])) + f0 = ureg.wraps(None, [None])(func) self.assertEqual(f0(3.0), 3.0) @@ -451,6 +454,16 @@ def func(x): self.assertEqual(f1b(3.0 * ureg.meter), 3.0) self.assertRaises(DimensionalityError, f1b, 3 * ureg.second) + f1c = ureg.wraps("meter", [ureg.meter])(func) + self.assertEqual(f1c(3.0 * ureg.centimeter), 0.03 * ureg.meter) + self.assertEqual(f1c(3.0 * ureg.meter), 3.0 * ureg.meter) + self.assertRaises(DimensionalityError, f1c, 3 * ureg.second) + + f1d = ureg.wraps(ureg.meter, [ureg.meter])(func) + self.assertEqual(f1d(3.0 * ureg.centimeter), 0.03 * ureg.meter) + self.assertEqual(f1d(3.0 * ureg.meter), 3.0 * ureg.meter) + self.assertRaises(DimensionalityError, f1d, 3 * ureg.second) + f1 = ureg.wraps(None, "meter")(func) self.assertRaises(ValueError, f1, 3.0) self.assertEqual(f1(3.0 * ureg.centimeter), 0.03) @@ -565,17 +578,8 @@ def gfunc(x, y): 1 * ureg.meter / ureg.second ** 2, ) - g2 = ureg.check("[speed]")(gfunc) - self.assertRaises(DimensionalityError, g2, 3.0, 1) - self.assertRaises(TypeError, g2, 2 * ureg.parsec) - self.assertRaises(DimensionalityError, g2, 2 * ureg.parsec, 1.0) - self.assertEqual(g2(2.0 * ureg.km / ureg.hour, 2), 1 * ureg.km / ureg.hour) - - g3 = ureg.check("[speed]", "[time]", "[mass]")(gfunc) - self.assertRaises(TypeError, g3, 1 * ureg.parsec, 1 * ureg.angstrom) - self.assertRaises( - TypeError, g3, 1 * ureg.parsec, 1 * ureg.angstrom, 1 * ureg.kilogram - ) + self.assertRaises(TypeError, ureg.check("[speed]"), gfunc) + self.assertRaises(TypeError, ureg.check("[speed]", "[time]", "[mass]"), gfunc) def test_to_ref_vs_to(self): self.ureg.autoconvert_offset_to_baseunit = True From 2027686a10d8e4297281718f0e879bfd93a22796 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sat, 28 Dec 2019 16:25:43 -0600 Subject: [PATCH 276/612] Switch numpy doc page to notebook and add array type compat information --- docs/numpy.ipynb | 763 +++++++++++++++++++++++++++++++++++++++++++++++ docs/numpy.rst | 181 ----------- 2 files changed, 763 insertions(+), 181 deletions(-) create mode 100644 docs/numpy.ipynb delete mode 100644 docs/numpy.rst diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb new file mode 100644 index 000000000..02f8f3fce --- /dev/null +++ b/docs/numpy.ipynb @@ -0,0 +1,763 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "NumPy Support\n", + "=============\n", + "\n", + "The magnitude of a Pint quantity can be of any numerical scalar type, and you are free\n", + "to choose it according to your needs. For numerical applications requiring arrays, it is\n", + "quite convenient to use [NumPy ndarray](http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html) (or [ndarray-like types supporting NEP-18](https://numpy.org/neps/nep-0018-array-function-protocol.html)),\n", + "and therefore these are the array types supported by Pint.\n", + "\n", + "First, we import the relevant packages:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Import NumPy\n", + "import numpy as np\n", + "\n", + "# Disable Pint's old fallback behavior (must come before importing Pint)\n", + "import os\n", + "os.environ['PINT_ARRAY_PROTOCOL_FALLBACK'] = \"0\"\n", + "\n", + "# Import Pint\n", + "import pint\n", + "ureg = pint.UnitRegistry()\n", + "Q_ = ureg.Quantity\n", + "\n", + "# Silence NEP 18 warning\n", + "import warnings\n", + "with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\")\n", + " Q_([])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and then we create a quantity the standard way" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[3.0 4.0] meter\n" + ] + } + ], + "source": [ + "legs1 = Q_(np.asarray([3., 4.]), 'meter')\n", + "print(legs1)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[3.0 4.0] meter\n" + ] + } + ], + "source": [ + "legs1 = [3., 4.] * ureg.meter\n", + "print(legs1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All usual Pint methods can be used with this quantity. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.003 0.004] kilometer\n" + ] + } + ], + "source": [ + "print(legs1.to('kilometer'))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[length]\n" + ] + } + ], + "source": [ + "print(legs1.dimensionality)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cannot convert from 'meter' ([length]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2)\n" + ] + } + ], + "source": [ + "try:\n", + " legs1.to('joule')\n", + "except pint.DimensionalityError as exc:\n", + " print(exc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "NumPy functions are supported by Pint. For example if we define:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[400.0 300.0] centimeter\n" + ] + } + ], + "source": [ + "legs2 = [400., 300.] * ureg.centimeter\n", + "print(legs2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "we can calculate the hypotenuse of the right triangles with legs1 and legs2." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[5.0 5.0] meter\n" + ] + } + ], + "source": [ + "hyps = np.hypot(legs1, legs2)\n", + "print(hyps)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice that before the `np.hypot` was used, the numerical value of legs2 was\n", + "internally converted to the units of legs1 as expected.\n", + "\n", + "Similarly, when you apply a function that expects angles in radians, a conversion\n", + "is applied before the requested calculation:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.6435011087932843 0.9272952180016123] radian\n" + ] + } + ], + "source": [ + "angles = np.arccos(legs2/hyps)\n", + "print(angles)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can convert the result to degrees using usual unit conversion:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[36.86989764584401 53.13010235415599] degree\n" + ] + } + ], + "source": [ + "print(angles.to('degree'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Applying a function that expects angles to a quantity with a different dimensionality\n", + "results in an error:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cannot convert from 'centimeter' ([length]) to 'dimensionless' (dimensionless)\n" + ] + } + ], + "source": [ + "try:\n", + " np.arccos(legs2)\n", + "except pint.DimensionalityError as exc:\n", + " print(exc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Function/Method Support\n", + "-----------------------\n", + "\n", + "The following [ufuncs](http://docs.scipy.org/doc/numpy/reference/ufuncs.html) can be applied to a Quantity object:\n", + "\n", + "- **Math operations**: `add`, `subtract`, `multiply`, `divide`, `logaddexp`, `logaddexp2`, `true_divide`, `floor_divide`, `negative`, `remainder`, `mod`, `fmod`, `absolute`, `rint`, `sign`, `conj`, `exp`, `exp2`, `log`, `log2`, `log10`, `expm1`, `log1p`, `sqrt`, `square`, `reciprocal`\n", + "- **Trigonometric functions**: `sin`, `cos`, `tan`, `arcsin`, `arccos`, `arctan`, `arctan2`, `hypot`, `sinh`, `cosh`, `tanh`, `arcsinh`, `arccosh`, `arctanh`\n", + "- **Comparison functions**: `greater`, `greater_equal`, `less`, `less_equal`, `not_equal`, `equal`\n", + "- **Floating functions**: `isreal`, `iscomplex`, `isfinite`, `isinf`, `isnan`, `signbit`, `copysign`, `nextafter`, `modf`, `ldexp`, `frexp`, `fmod`, `floor`, `ceil`, `trunc`\n", + "\n", + "And the following NumPy functions:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['alen', 'amax', 'amin', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" + ] + } + ], + "source": [ + "from pint.numpy_func import HANDLED_FUNCTIONS\n", + "print(sorted(list(HANDLED_FUNCTIONS)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And the following [NumPy ndarray methods](http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods):\n", + "\n", + "- `argmax`, `argmin`, `argsort`, `astype`, `clip`, `compress`, `conj`, `conjugate`, `cumprod`, `cumsum`, `diagonal`, `dot`, `fill`, `flatten`, `flatten`, `item`, `max`, `mean`, `min`, `nonzero`, `prod`, `ptp`, `put`, `ravel`, `repeat`, `reshape`, `round`, `searchsorted`, `sort`, `squeeze`, `std`, `sum`, `take`, `trace`, `transpose`, `var`\n", + "\n", + "Pull requests are welcome for any NumPy function, ufunc, or method that is not currently\n", + "supported.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Array Type Support\n", + "------------------\n", + "\n", + "### Overview\n", + "\n", + "When not wrapping a scalar type, a Pint `Quantity` can be considered a [\"duck array\"](https://numpy.org/neps/nep-0022-ndarray-duck-typing-overview.html), that is, an array-like type that implements (all or most of) NumPy's API for `ndarray`. Many other such duck arrays exist in the Python ecosystem, and Pint aims to work with as many of them as reasonably possible. To date, the following are specifically tested and known to work:\n", + "\n", + "- xarray: `DataArray`, `Dataset`, and `Variable`\n", + "- Sparse: `COO`\n", + "- NumPy: masked arrays\n", + "\n", + "and the following have partial support, with full integration planned:\n", + "\n", + "- Dask arrays\n", + "- CuPy arrays\n", + "\n", + "### Technical Commentary\n", + "\n", + "Starting with version 0.10, Pint aims to interoperate with other duck arrays in a well-defined and well-supported fashion. Part of this support lies in implementing [`__array_ufunc__` to support NumPy ufuncs](https://numpy.org/neps/nep-0013-ufunc-overrides.html) and [`__array_function__` to support NumPy functions](https://numpy.org/neps/nep-0018-array-function-protocol.html). However, the central component to this interoperability is respecting a [type casting hierarchy](https://numpy.org/neps/nep-0018-array-function-protocol.html) of duck arrays. When all types in the hierarchy properly defer to those above it (in wrapping, arithmetic, and NumPy operations), a well-defined nesting and operator precedence order exists. When they don't, the graph of relations becomes cyclic, and the expected result of mixed-type operations becomes ambiguous.\n", + "\n", + "For Pint, following this hierarchy means declaring a list of types that are above it in the hierarchy and to which it defers (\"upcast types\") and assuming all others are below it and wrappable by it (\"downcast types\"). To date, Pint's declared upcast types are:\n", + "\n", + "- `PintArray`, as defined by pint-pandas\n", + "- `Series`, as defined by Pandas\n", + "- `DataArray`, `Dataset`, and `Variable`, as defined by xarray\n", + "\n", + "(Note: if your application requires extension of this collection of types, it is available in Pint's API at `pint.compat.upcast_types`.)\n", + "\n", + "While Pint assumes it can wrap any other duck array (meaning, for now, those that implement `__array_function__`, `shape`, `ndim`, and `dtype`, at least until [NEP 30](https://numpy.org/neps/nep-0030-duck-array-protocol.html) is implemented), there are a few common types that Pint explicitly tests (or plans to test) for optimal interoperability. These are listed above in the overview section and included in the below chart.\n", + "\n", + "This type casting hierarchy of ndarray-like types can be shown by the below acyclic graph, where solid lines represent declared support, and dashed lines represent planned support:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "Dask array\n", + "\n", + "Dask array\n", + "\n", + "\n", + "NumPy ndarray\n", + "\n", + "NumPy ndarray\n", + "\n", + "\n", + "Dask array->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "CuPy ndarray\n", + "\n", + "CuPy ndarray\n", + "\n", + "\n", + "Dask array->CuPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Sparse COO\n", + "\n", + "Sparse COO\n", + "\n", + "\n", + "Dask array->Sparse COO\n", + "\n", + "\n", + "\n", + "\n", + "NumPy masked array\n", + "\n", + "NumPy masked array\n", + "\n", + "\n", + "Dask array->NumPy masked array\n", + "\n", + "\n", + "\n", + "\n", + "CuPy ndarray->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Sparse COO->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "NumPy masked array->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Jax array\n", + "\n", + "Jax array\n", + "\n", + "\n", + "Jax array->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Pint Quantity\n", + "\n", + "Pint Quantity\n", + "\n", + "\n", + "Pint Quantity->Dask array\n", + "\n", + "\n", + "\n", + "\n", + "Pint Quantity->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Pint Quantity->CuPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "Pint Quantity->Sparse COO\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable\n", + "\n", + "xarray Dataset/DataArray/Variable\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->Dask array\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->NumPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->CuPy ndarray\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->Sparse COO\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->NumPy masked array\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->Jax array\n", + "\n", + "\n", + "\n", + "\n", + "xarray Dataset/DataArray/Variable->Pint Quantity\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from graphviz import Digraph\n", + "\n", + "g = Digraph(graph_attr={'size': '8,5'}, node_attr={'fontname': 'courier'})\n", + "g.edge('Dask array', 'NumPy ndarray')\n", + "g.edge('Dask array', 'CuPy ndarray')\n", + "g.edge('Dask array', 'Sparse COO')\n", + "g.edge('Dask array', 'NumPy masked array')\n", + "g.edge('CuPy ndarray', 'NumPy ndarray')\n", + "g.edge('Sparse COO', 'NumPy ndarray')\n", + "g.edge('NumPy masked array', 'NumPy ndarray')\n", + "g.edge('Jax array', 'NumPy ndarray')\n", + "g.edge('Pint Quantity', 'Dask array', style='dashed')\n", + "g.edge('Pint Quantity', 'NumPy ndarray')\n", + "g.edge('Pint Quantity', 'CuPy ndarray', style='dashed')\n", + "g.edge('Pint Quantity', 'Sparse COO')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'Dask array')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'CuPy ndarray', style='dashed')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'Sparse COO')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'NumPy ndarray')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'NumPy masked array', style='dashed')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'Pint Quantity')\n", + "g.edge('xarray Dataset/DataArray/Variable', 'Jax array', style='dashed')\n", + "g" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Examples\n", + "\n", + "**xarray wrapping Pint Quantity**" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Coordinates:\n", + " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", + " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", + " time datetime64[ns] 2013-01-01\n", + "Attributes:\n", + " long_name: 4xDaily Air temperature at sigma level 995\n", + " precision: 2\n", + " GRIB_id: 11\n", + " GRIB_name: TMP\n", + " var_desc: Air temperature\n", + " dataset: NMC Reanalysis\n", + " level_desc: Surface\n", + " statistic: Individual Obs\n", + " parent_stat: Other\n", + " actual_range: [185.16 322.1 ]\n", + "\n", + "\n", + "\n", + "Coordinates:\n", + " time datetime64[ns] 2013-01-01\n" + ] + } + ], + "source": [ + "import xarray as xr\n", + "\n", + "# Load tutorial data\n", + "air = xr.tutorial.load_dataset('air_temperature')['air'][0]\n", + "\n", + "# Convert to Quantity\n", + "air.data = Q_(air.data, air.attrs.pop('units', ''))\n", + "\n", + "print(air)\n", + "print()\n", + "print(air.max())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Pint Quantity wrapping Sparse COO**" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " meter\n", + "\n", + "0.0952163965972451 meter\n" + ] + } + ], + "source": [ + "from sparse import COO\n", + "\n", + "x = np.random.random((100, 100, 100))\n", + "x[x < 0.9] = 0 # fill most of the array with zeros\n", + "s = COO(x)\n", + "\n", + "q = Q_(s, 'm')\n", + "\n", + "print(q)\n", + "print()\n", + "print(np.mean(q))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**xarray wrapping Pint Quantity wrapping Dask array wrapping Sparse COO**" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + ", 'meter')>\n", + "Coordinates:\n", + " * z (z) int64 0 1 2 3 4 5 6 7 8 9 10 ... 90 91 92 93 94 95 96 97 98 99\n", + " * y (y) int64 -50 -49 -48 -47 -46 -45 -44 -43 ... 43 44 45 46 47 48 49\n", + " * x (x) float64 -20.0 -18.5 -17.0 -15.5 ... 124.0 125.5 127.0 128.5\n", + "\n", + "\n", + ", 'meter')>\n", + "Coordinates:\n", + " y int64 -46\n", + " x float64 125.5\n" + ] + } + ], + "source": [ + "import dask.array as da\n", + "\n", + "x = da.random.random((100, 100, 100), chunks=(100, 1, 1))\n", + "x[x < 0.95] = 0\n", + "\n", + "data = xr.DataArray(\n", + " Q_(x.map_blocks(COO), 'm'),\n", + " dims=('z', 'y', 'x'),\n", + " coords={\n", + " 'z': np.arange(100),\n", + " 'y': np.arange(100) - 50,\n", + " 'x': np.arange(100) * 1.5 - 20\n", + " },\n", + " name='test'\n", + ")\n", + "\n", + "print(data)\n", + "print()\n", + "print(data.sel(x=125.5, y=-46).mean())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compatibility Packages\n", + "\n", + "To aid in integration between various array types and Pint (such as by providing convenience methods), the following compatibility packages are available:\n", + "\n", + "- [pint-pandas](https://github.com/hgrecco/pint-pandas)\n", + "- pint-xarray ([in development](https://github.com/hgrecco/pint/issues/849), initial alpha release planned for January 2020)\n", + "\n", + "(Note: if you have developed a compatibility package for Pint, please submit a pull request to add it to this list!)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Additional Comments\n", + "\n", + "What follows is a short discussion about how NumPy support is implemented in Pint's `Quantity` Object.\n", + "\n", + "For the supported functions, Pint expects certain units and attempts to convert the input (or inputs). For example, the argument of the exponential function (`numpy.exp`) must be dimensionless. Units will be simplified (converting the magnitude appropriately) and `numpy.exp` will be applied to the resulting magnitude. If the input is not dimensionless, a `DimensionalityError` exception will be raised.\n", + "\n", + "In some functions that take 2 or more arguments (e.g. `arctan2`), the second argument is converted to the units of the first. Again, a `DimensionalityError` exception will be raised if this is not possible. ndarray or downcast type arguments are generally treated as if they were dimensionless quantities, whereas Pint defers to its declared upcast types by always returning `NotImplemented` when they are encountered (see above).\n", + "\n", + "To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that functions and ufuncs that Pint does not explicitly handle will error, rather than return a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more\n", + "information on these protocols, see .\n", + "\n", + "This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see [wrapping](wrapping.html)).\n", + "\n", + "Array interface protocol attributes (such as `__array_struct__` and\n", + "`__array_interface__`) are available on Pint Quantities by deferring to the corresponding `__array_*` attribute on the magnitude as casted to an ndarray. This has been found to be potentially incorrect and to cause unexpected behavior, and has therefore been deprecated. As of the next minor version of Pint (or when the `PINT_ARRAY_PROTOCOL_FALLBACK` environment variable is set to 0 prior to importing Pint as done at the beginning of this page), attempting to access these attributes will instead raise an AttributeError." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/numpy.rst b/docs/numpy.rst deleted file mode 100644 index c9a46b99c..000000000 --- a/docs/numpy.rst +++ /dev/null @@ -1,181 +0,0 @@ -.. _numpy: - - -NumPy Support -============= - -The magnitude of a Pint quantity can be of any numerical scalar type, and you are free -to choose it according to your needs. For numerical applications requiring arrays, it is -quite convenient to use `NumPy ndarray`_ (or `ndarray-like types supporting NEP-18`_), -and therefore these are the array types supported by Pint. - -First, we import the relevant packages: - -.. doctest:: - - >>> import numpy as np - >>> from pint import UnitRegistry - >>> ureg = UnitRegistry() - >>> Q_ = ureg.Quantity - -.. testsetup:: * - - import numpy as np - from pint import UnitRegistry - ureg = UnitRegistry() - Q_ = ureg.Quantity - -and then we create a quantity the standard way - -.. doctest:: - - >>> legs1 = Q_(np.asarray([3., 4.]), 'meter') - >>> print(legs1) - [ 3. 4.] meter - -or we use the property that Pint converts iterables into NumPy ndarrays to simply write: - -.. doctest:: - - >>> legs1 = [3., 4.] * ureg.meter - >>> print(legs1) - [ 3. 4.] meter - -All usual Pint methods can be used with this quantity. For example: - -.. doctest:: - - >>> print(legs1.to('kilometer')) - [ 0.003 0.004] kilometer - >>> print(legs1.dimensionality) - [length] - >>> legs1.to('joule') - Traceback (most recent call last): - ... - DimensionalityError: Cannot convert from 'meter' ([length]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) - -NumPy functions are supported by Pint. For example if we define: - -.. doctest:: - - >>> legs2 = [400., 300.] * ureg.centimeter - >>> print(legs2) - [ 400. 300.] centimeter - -we can calculate the hypotenuse of the right triangles with legs1 and legs2. - -.. doctest:: - - >>> hyps = np.hypot(legs1, legs2) - >>> print(hyps) - [ 5. 5.] meter - -Notice that before the `np.hypot` was used, the numerical value of legs2 was -internally converted to the units of legs1 as expected. - -Similarly, when you apply a function that expects angles in radians, a conversion -is applied before the requested calculation: - -.. doctest:: - - >>> angles = np.arccos(legs2/hyps) - >>> print(angles) - [ 0.64350111 0.92729522] radian - -You can convert the result to degrees using usual unit conversion: - -.. doctest:: - - >>> print(angles.to('degree')) - [ 36.86989765 53.13010235] degree - -Applying a function that expects angles to a quantity with a different dimensionality -results in an error: - -.. doctest:: - - >>> np.arccos(legs2) - Traceback (most recent call last): - ... - DimensionalityError: Cannot convert from 'centimeter' ([length]) to 'dimensionless' (dimensionless) - - -Support --------- - -The following ufuncs_ can be applied to a Quantity object: - -- **Math operations**: add, subtract, multiply, divide, logaddexp, logaddexp2, true_divide, floor_divide, negative, remainder mod, fmod, absolute, rint, sign, conj, exp, exp2, log, log2, log10, expm1, log1p, sqrt, square, reciprocal -- **Trigonometric functions**: sin, cos, tan, arcsin, arccos, arctan, arctan2, hypot, sinh, cosh, tanh, arcsinh, arccosh, arctanh, deg2rad, rad2deg -- **Comparison functions**: greater, greater_equal, less, less_equal, not_equal, equal -- **Floating functions**: isreal,iscomplex, isfinite, isinf, isnan, signbit, copysign, nextafter, modf, ldexp, frexp, fmod, floor, ceil, trunc - -And the following NumPy functions: - -- alen, amax, amin, append, argmax, argmin, argsort, around, atleast_1d, atleast_2d, atleast_3d, average, block, broadcast_to, clip, column_stack, compress, concatenate, copy, copyto, count_nonzero, cross, cumprod, cumproduct, cumsum, diagonal, diff, dot, dstack, ediff1d, einsum, empty_like, expand_dims, fix, flip, full_like, gradient, hstack, insert, interp, isclose, iscomplex, isin, isreal, linspace, mean, median, meshgrid, moveaxis, nan_to_num, nanargmax, nanargmin, nancumprod, nancumsum, nanmax, nanmean, nanmedian, nanmin, nanpercentile, nanstd, nanvar, ndim, nonzero, ones_like, pad, percentile, ptp, ravel, resize, result_type, rollaxis, rot90, round\_, searchsorted, shape, size, sort, squeeze, stack, std, sum, swapaxes, tile, transpose, trapz, trim_zeros, unwrap, var, vstack, where, zeros_like - -And the following `NumPy ndarray methods`_: - -- argmax, argmin, argsort, astype, clip, compress, conj, conjugate, cumprod, cumsum, diagonal, dot, fill, flatten, flatten, item, max, mean, min, nonzero, prod, ptp, put, ravel, repeat, reshape, round, searchsorted, sort, squeeze, std, sum, take, trace, transpose, var - -Pull requests are welcome for any NumPy function, ufunc, or method that is not currently -supported. - - -Comments --------- - -What follows is a short discussion about how NumPy support is implemented in -Pint's `Quantity` Object. - -For the supported functions, Pint expects certain units and attempts to convert -the input (or inputs). For example, the argument of the exponential function -(`numpy.exp`) must be dimensionless. Units will be simplified (converting the -magnitude appropriately) and `numpy.exp` will be applied to the resulting -magnitude. If the input is not dimensionless, a `DimensionalityError` exception -will be raised. - -In some functions that take 2 or more arguments (e.g. `arctan2`), the second -argument is converted to the units of the first. Again, a `DimensionalityError` -exception will be raised if this is not possible. ndarray or ndarray-like arguments -are generally treated as if they were dimensionless quantities, except for declared -upcast types to which Pint defers (see -). To date, these "upcast types" are: - -- ``PintArray``, as defined by pint-pandas -- ``Series``, as defined by pandas -- ``DataArray``, ``Dataset``, and ``Variable``, as defined by xarray - -If your application requires extension of this collection of types, it is available in -Pint's API at ``pint.compat.upcast_types``. Note that these are also the types to which -a Quantity object will defer for arithmetic operations. - -To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and -``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that -functions and ufuncs that Pint does not explicitly handle will error, rather than return -a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more -information on these protocols, see -. - -This behaviour introduces some performance penalties and increased memory -usage. Quantities that must be converted to other units require additional -memory and CPU cycles. Therefore, for numerically intensive code, you -might want to convert the objects first and then use directly the magnitude, -such as by using Pint's `wraps` utility (see :ref:`wrapping`). - -Array interface protocol attributes (such as `__array_struct__` and -`__array_interface__`) are available on Pint Quantities by deferring to the -corresponding `__array_*` attribute on the magnitude as casted to an ndarray. This -has been found to be potentially incorrect and to cause unexpected behavior, and has -therefore been deprecated. As of the next minor version of Pint (or when the -`PINT_ARRAY_PROTOCOL_FALLBACK` environment variable is set to 0 prior to importing -Pint), attempting to access these attributes will instead raise an AttributeError. - - - - - -.. _`NumPy ndarray`: http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html -.. _`ndarray-like types supporting NEP-18`: https://numpy.org/neps/nep-0018-array-function-protocol.html -.. _ufuncs: http://docs.scipy.org/doc/numpy/reference/ufuncs.html -.. _`NumPy ndarray methods`: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods From ec0fe532da1583ab39b1170cb703c534eb72a97e Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 29 Dec 2019 14:36:36 -0600 Subject: [PATCH 277/612] Add explicit support for duck arrays/downcast types along with force_ndarray_like option --- pint/compat.py | 40 ++++++++++++--- pint/numpy_func.py | 3 +- pint/quantity.py | 123 +++++++++++++++++++++++++++++---------------- pint/registry.py | 10 +++- pint/unit.py | 34 +++++++++---- 5 files changed, 148 insertions(+), 62 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index e87c26c23..6fb27ee5a 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -66,14 +66,16 @@ class BehaviorChangeWarning(UserWarning): NUMPY_VER = np.__version__ NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) - def _to_magnitude(value, force_ndarray=False): + def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): if isinstance(value, (dict, bool)) or value is None: raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value)) elif isinstance(value, str) and value == "": raise ValueError("Quantity magnitude cannot be an empty string.") elif isinstance(value, (list, tuple)): return np.asarray(value) - if force_ndarray: + if force_ndarray or ( + force_ndarray_like and not is_duck_array_type(type(value)) + ): return np.asarray(value) return value @@ -112,9 +114,11 @@ class ndarray: NP_NO_VALUE = None ARRAY_FALLBACK = False - def _to_magnitude(value, force_ndarray=False): - if force_ndarray: - raise ValueError("Cannot force to ndarray when NumPy is not present.") + def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): + if force_ndarray or force_ndarray_like: + raise ValueError( + "Cannot force to ndarray or ndarray-like when NumPy is not present." + ) elif isinstance(value, (dict, bool)) or value is None: raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value)) elif isinstance(value, str) and value == "": @@ -148,8 +152,8 @@ def _to_magnitude(value, force_ndarray=False): if not HAS_BABEL: babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 -# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and -# downcast/wrappable types using guarded imports +# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast +# types using guarded imports upcast_types = [] # pint-pandas (PintArray) @@ -191,6 +195,28 @@ def is_upcast_type(other): return other in upcast_types +def is_duck_array_type(other): + """Check if the type object represents a (non-Quantity) duck array type. + + Parameters + ---------- + other : object + + Returns + ------- + bool + """ + # TODO (NEP 30): replace duck array check with hasattr(other, "__duckarray__") + return other is ndarray or ( + not hasattr(other, "_magnitude") + and not hasattr(other, "_units") + and HAS_NUMPY_ARRAY_FUNCTION + and hasattr(other, "__array_function__") + and hasattr(other, "ndim") + and hasattr(other, "dtype") + ) + + def eq(lhs, rhs, check_all): """Comparison of scalars and arrays. diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 0dcb878d7..1a9221caa 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -424,7 +424,7 @@ def implementation(*args, **kwargs): implement_func("ufunc", ufunc_str, input_units=None, output_unit=None) for ufunc_str in matching_input_bare_output_ufuncs: - # Require all inputs to match units, but output base ndarray + # Require all inputs to match units, but output base ndarray/duck array implement_func("ufunc", ufunc_str, input_units="all_consistent", output_unit=None) for ufunc_str, out_unit in matching_input_set_units_output_ufuncs.items(): @@ -744,6 +744,7 @@ def implementation(*args, **kwargs): ("rot90", "m", True), ("insert", ["arr", "values"], True), ("resize", "a", True), + ("reshape", "a", True), ]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) diff --git a/pint/quantity.py b/pint/quantity.py index 91abfb77f..ba4a26431 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -29,6 +29,7 @@ array_function_change_msg, babel_parse, eq, + is_duck_array_type, is_upcast_type, ndarray, np, @@ -145,6 +146,10 @@ class Quantity(PrettyIPython, SharedRegistryObject): def force_ndarray(self): return self._REGISTRY.force_ndarray + @property + def force_ndarray_like(self): + return self._REGISTRY.force_ndarray_like + def __reduce__(self): """Allow pickling quantities. Since UnitRegistries are not pickled, upon unpickling the new object is always attached to the application registry. @@ -173,15 +178,21 @@ def __new__(cls, value, units=None): inst = copy.copy(value) else: inst = SharedRegistryObject.__new__(cls) - inst._magnitude = _to_magnitude(value, inst.force_ndarray) + inst._magnitude = _to_magnitude( + value, inst.force_ndarray, inst.force_ndarray_like + ) inst._units = UnitsContainer() elif isinstance(units, (UnitsContainer, UnitDefinition)): inst = SharedRegistryObject.__new__(cls) - inst._magnitude = _to_magnitude(value, inst.force_ndarray) + inst._magnitude = _to_magnitude( + value, inst.force_ndarray, inst.force_ndarray_like + ) inst._units = units elif isinstance(units, str): inst = SharedRegistryObject.__new__(cls) - inst._magnitude = _to_magnitude(value, inst.force_ndarray) + inst._magnitude = _to_magnitude( + value, inst.force_ndarray, inst.force_ndarray_like + ) inst._units = inst._REGISTRY.parse_units(units)._units elif isinstance(units, SharedRegistryObject): if isinstance(units, Quantity) and units.magnitude != 1: @@ -192,7 +203,9 @@ def __new__(cls, value, units=None): else: inst = SharedRegistryObject.__new__(cls) inst._units = units._units - inst._magnitude = _to_magnitude(value, inst.force_ndarray) + inst._magnitude = _to_magnitude( + value, inst.force_ndarray, inst.force_ndarray_like + ) else: raise TypeError( "units must be of type str, Quantity or " @@ -502,7 +515,7 @@ def _convert_magnitude(self, other, *contexts, **ctx_kwargs): self._magnitude, self._units, other, - inplace=isinstance(self._magnitude, ndarray), + inplace=is_duck_array_type(type(self._magnitude)), ) def ito(self, other=None, *contexts, **ctx_kwargs): @@ -729,7 +742,9 @@ def _iadd_sub(self, other, op): if not self._check(other): # other not from same Registry or not a Quantity try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) except PintTypeError: raise except TypeError: @@ -848,12 +863,14 @@ def _add_sub(self, other, op): # the operation. units = self._units magnitude = op( - self._magnitude, _to_magnitude(other, self.force_ndarray) + self._magnitude, + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), ) elif self.dimensionless: units = UnitsContainer() magnitude = op( - self.to(units)._magnitude, _to_magnitude(other, self.force_ndarray) + self.to(units)._magnitude, + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), ) else: raise DimensionalityError(self._units, "dimensionless") @@ -939,10 +956,10 @@ def _add_sub(self, other, op): def __iadd__(self, other): if isinstance(other, datetime.datetime): return self.to_timedelta() + other - elif not isinstance(self._magnitude, ndarray): - return self._add_sub(other, operator.add) - else: + elif is_duck_array_type(type(self._magnitude)): return self._iadd_sub(other, operator.iadd) + else: + return self._add_sub(other, operator.add) def __add__(self, other): if isinstance(other, datetime.datetime): @@ -953,10 +970,10 @@ def __add__(self, other): __radd__ = __add__ def __isub__(self, other): - if not isinstance(self._magnitude, ndarray): - return self._add_sub(other, operator.sub) - else: + if is_duck_array_type(type(self._magnitude)): return self._iadd_sub(other, operator.isub) + else: + return self._add_sub(other, operator.sub) def __sub__(self, other): return self._add_sub(other, operator.sub) @@ -1007,7 +1024,9 @@ def _imul_div(self, other, magnitude_op, units_op=None): self._units, getattr(other, "units", "") ) try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) except PintTypeError: raise except TypeError: @@ -1075,7 +1094,9 @@ def _mul_div(self, other, magnitude_op, units_op=None): self._units, getattr(other, "units", "") ) try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) except PintTypeError: raise except TypeError: @@ -1109,10 +1130,10 @@ def _mul_div(self, other, magnitude_op, units_op=None): return self.__class__(magnitude, units) def __imul__(self, other): - if not isinstance(self._magnitude, ndarray): - return self._mul_div(other, operator.mul) - else: + if is_duck_array_type(type(self._magnitude)): return self._imul_div(other, operator.imul) + else: + return self._mul_div(other, operator.mul) def __mul__(self, other): return self._mul_div(other, operator.mul) @@ -1129,17 +1150,19 @@ def __matmul__(self, other): __rmatmul__ = __matmul__ def __itruediv__(self, other): - if not isinstance(self._magnitude, ndarray): - return self._mul_div(other, operator.truediv) - else: + if is_duck_array_type(type(self._magnitude)): return self._imul_div(other, operator.itruediv) + else: + return self._mul_div(other, operator.truediv) def __truediv__(self, other): return self._mul_div(other, operator.truediv) def __rtruediv__(self, other): try: - other_magnitude = _to_magnitude(other, self.force_ndarray) + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) except PintTypeError: raise except TypeError: @@ -1233,11 +1256,11 @@ def __rdivmod__(self, other): @check_implemented def __ipow__(self, other): - if not isinstance(self._magnitude, ndarray): + if not is_duck_array_type(type(self._magnitude)): return self.__pow__(other) try: - _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) except PintTypeError: raise except TypeError: @@ -1246,7 +1269,7 @@ def __ipow__(self, other): if not self._ok_for_muldiv: raise OffsetUnitCalculusError(self._units) - if isinstance(getattr(other, "_magnitude", other), ndarray): + if is_duck_array_type(type(getattr(other, "_magnitude", other))): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units # unless the base is dimensionless. @@ -1286,13 +1309,15 @@ def __ipow__(self, other): else: self._units **= other - self._magnitude **= _to_magnitude(other, self.force_ndarray) + self._magnitude **= _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) return self @check_implemented def __pow__(self, other): try: - _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) except PintTypeError: raise except TypeError: @@ -1301,7 +1326,7 @@ def __pow__(self, other): if not self._ok_for_muldiv: raise OffsetUnitCalculusError(self._units) - if isinstance(getattr(other, "_magnitude", other), ndarray): + if is_duck_array_type(type(getattr(other, "_magnitude", other))): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units # unless the base is dimensionless. @@ -1339,7 +1364,9 @@ def __pow__(self, other): elif not getattr(other, "dimensionless", True): raise DimensionalityError(other._units, "dimensionless") else: - exponent = _to_magnitude(other, self.force_ndarray) + exponent = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) units = new_self._units ** exponent magnitude = new_self._magnitude ** exponent @@ -1348,7 +1375,7 @@ def __pow__(self, other): @check_implemented def __rpow__(self, other): try: - _to_magnitude(other, self.force_ndarray) + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) except PintTypeError: raise except TypeError: @@ -1356,7 +1383,7 @@ def __rpow__(self, other): else: if not self.dimensionless: raise DimensionalityError(self._units, "dimensionless") - if isinstance(self._magnitude, ndarray): + if is_duck_array_type(type(self._magnitude)): if np.size(self._magnitude) > 1: raise DimensionalityError(self._units, "dimensionless") new_self = self.to_root_units() @@ -1415,7 +1442,7 @@ def __eq__(self, other): @check_implemented def __ne__(self, other): out = self.__eq__(other) - if isinstance(out, ndarray): + if is_duck_array_type(type(out)): return np.logical_not(out) return not out @@ -1637,12 +1664,12 @@ def __getattr__(self, item): stacklevel=2, ) - if isinstance(self._magnitude, ndarray): + 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_` attributes is requested but the magnitude is not an ndarray, - # we convert the magnitude to a numpy ndarray. - # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays + # 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 ) @@ -1651,13 +1678,23 @@ def __getattr__(self, item): # TODO (next minor version): ARRAY_FALLBACK is removed and this becomes the standard behavior raise AttributeError(f"Array protocol attribute {item} not available.") elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods: - # TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars - magnitude_as_array = _to_magnitude(self._magnitude, True) - attr = getattr(magnitude_as_array, item) - if callable(attr): + magnitude_as_duck_array = _to_magnitude( + self._magnitude, force_ndarray_like=True + ) + try: + attr = getattr(magnitude_as_duck_array, item) return functools.partial(self._numpy_method_wrap, attr) - else: - raise AttributeError("NumPy method {} was not callable.".format(item)) + except AttributeError: + raise AttributeError( + f"NumPy method {item} not available on {type(magnitude_as_duck_array)}" + ) + except TypeError as exc: + if "not callable" in str(exc): + raise AttributeError( + f"NumPy method {item} not callable on {type(magnitude_as_duck_array)}" + ) + else: + raise exc try: return getattr(self._magnitude, item) diff --git a/pint/registry.py b/pint/registry.py index cdb85dd93..90d595be6 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -140,6 +140,8 @@ class BaseRegistry(metaclass=RegistryMeta): the default definition file. None to leave the UnitRegistry empty. force_ndarray : bool convert any input, scalar or not to a numpy.ndarray. + force_ndarray_like : bool + convert all inputs other than duck arrays to a numpy.ndarray. on_redefinition : str action to take in case a unit is redefined: 'warn', 'raise', 'ignore' auto_reduce_dimensions : @@ -179,6 +181,7 @@ def __init__( self, filename="", force_ndarray=False, + force_ndarray_like=False, on_redefinition="warn", auto_reduce_dimensions=False, preprocessors=None, @@ -189,6 +192,7 @@ def __init__( self._filename = filename self.force_ndarray = force_ndarray + self.force_ndarray_like = force_ndarray_like self.preprocessors = preprocessors or [] #: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore' @@ -1950,8 +1954,10 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): path of the units definition file to load or line-iterable object. Empty to load the default definition file. None to leave the UnitRegistry empty. - force_ndarray : + force_ndarray : bool convert any input, scalar or not to a numpy.ndarray. + force_ndarray_like : bool + convert all inputs other than duck arrays to a numpy.ndarray. default_as_delta : In the context of a multiplication of units, interpret non-multiplicative units as their *delta* counterparts. @@ -1975,6 +1981,7 @@ def __init__( self, filename="", force_ndarray=False, + force_ndarray_like=False, default_as_delta=True, autoconvert_offset_to_baseunit=False, on_redefinition="warn", @@ -1987,6 +1994,7 @@ def __init__( super().__init__( filename=filename, force_ndarray=force_ndarray, + force_ndarray_like=force_ndarray_like, on_redefinition=on_redefinition, default_as_delta=default_as_delta, autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, diff --git a/pint/unit.py b/pint/unit.py index 10ebd71e2..b61cfe6fa 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -13,7 +13,7 @@ import operator from numbers import Number -from .compat import NUMERIC_TYPES +from .compat import NUMERIC_TYPES, is_upcast_type from .definitions import UnitDefinition from .formatting import siunitx_format_unit from .util import PrettyIPython, SharedRegistryObject, UnitsContainer @@ -230,18 +230,32 @@ def __complex__(self): __array_priority__ = 17 - def __array_prepare__(self, array, context=None): - return 1 + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method != "__call__": + # Only handle ufuncs as callables + return NotImplemented - def __array_wrap__(self, array, context=None): - uf, objs, huh = context + # Check types and return NotImplemented when upcast type encountered + types = set( + type(arg) + for arg in list(inputs) + list(kwargs.values()) + if hasattr(arg, "__array_ufunc__") + ) + if any(is_upcast_type(other) for other in types): + return NotImplemented - if uf.__name__ in ("true_divide", "divide", "floor_divide"): - return self._REGISTRY.Quantity(array, 1 / self._units) - elif uf.__name__ in ("multiply",): - return self._REGISTRY.Quantity(array, self._units) + # Act on limited implementations by conversion to multiplicative identity + # Quantity + if ufunc.__name__ in ("true_divide", "divide", "floor_divide", "multiply"): + return ufunc( + *tuple( + self._REGISTRY.Quantity(1, self._units) if arg is self else arg + for arg in inputs + ), + **kwargs, + ) else: - raise ValueError("Unsupproted operation for Unit") + return NotImplemented @property def systems(self): From c029e443fdff0e98a9585d6ec4827f6ba5242e36 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 29 Dec 2019 22:22:25 -0600 Subject: [PATCH 278/612] Add downcast compat tests --- pint/testsuite/test_compat_downcast.py | 108 +++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 pint/testsuite/test_compat_downcast.py diff --git a/pint/testsuite/test_compat_downcast.py b/pint/testsuite/test_compat_downcast.py new file mode 100644 index 000000000..e85369a14 --- /dev/null +++ b/pint/testsuite/test_compat_downcast.py @@ -0,0 +1,108 @@ +import pytest + +from pint import UnitRegistry + +# Conditionally import NumPy and any upcast type libraries +np = pytest.importorskip("numpy", reason="NumPy is not available") +sparse = pytest.importorskip("sparse", reason="sparse is not available") + +# Set up unit registry and sample +ureg = UnitRegistry(force_ndarray_like=True) +q_base = (np.arange(25).reshape(5, 5).T + 1) * ureg.kg + + +# Define identity function for use in tests +def identity(x): + return x + + +@pytest.fixture(params=["sparse", "masked_array"]) +def array(request): + """Generate 5x5 arrays of given type for tests.""" + if request.param == "sparse": + # Create sample sparse COO as a permutation matrix. + coords = [[0, 1, 2, 3, 4], [1, 3, 0, 2, 4]] + data = [1.0] * 5 + return sparse.COO(coords, data, shape=(5, 5)) + elif request.param == "masked_array": + # Create sample masked array as an upper triangular matrix. + return np.ma.masked_array( + np.arange(25, dtype=np.float).reshape((5, 5)), + mask=np.logical_not(np.triu(np.ones((5, 5)))), + ) + + +@pytest.mark.parametrize( + "op, magnitude_op, unit_op", + [ + pytest.param(identity, identity, identity, id="identity"), + pytest.param( + lambda x: x + 1 * ureg.m, lambda x: x + 1, identity, id="addition" + ), + pytest.param( + lambda x: x - 20 * ureg.cm, lambda x: x - 0.2, identity, id="subtraction" + ), + pytest.param( + lambda x: x * (2 * ureg.s), + lambda x: 2 * x, + lambda u: u * ureg.s, + id="multiplication", + ), + pytest.param( + lambda x: x / (1 * ureg.s), identity, lambda u: u / ureg.s, id="division" + ), + pytest.param(lambda x: x ** 2, lambda x: x ** 2, lambda u: u ** 2, id="square"), + pytest.param(lambda x: x.T, lambda x: x.T, identity, id="transpose"), + pytest.param(np.mean, np.mean, identity, id="mean ufunc"), + pytest.param(np.sum, np.sum, identity, id="sum ufunc"), + pytest.param(np.sqrt, np.sqrt, lambda u: u ** 0.5, id="sqrt ufunc"), + pytest.param( + lambda x: np.reshape(x, 25), + lambda x: np.reshape(x, 25), + identity, + id="reshape function", + ), + pytest.param(np.amax, np.amax, identity, id="amax function"), + ], +) +def test_univariate_op_consistency(op, magnitude_op, unit_op, array): + q = ureg.Quantity(array, "meter") + res = op(q) + assert np.all(res.magnitude == magnitude_op(array)) # Magnitude check + assert res.units == unit_op(q.units) # Unit check + assert q.magnitude is array # Immutability check + + +@pytest.mark.parametrize( + "op, unit", + [ + pytest.param(lambda x, y: x * y, ureg("kg m"), id="multiplication"), + pytest.param(lambda x, y: x / y, ureg("m / kg"), id="division"), + pytest.param(np.multiply, ureg("kg m"), id="multiply ufunc"), + ], +) +def test_bivariate_op_consistency(op, unit, array): + q = ureg.Quantity(array, "meter") + res = op(q, q_base) + assert np.all(res.magnitude == op(array, q_base.magnitude)) # Magnitude check + assert res.units == unit # Unit check + assert q.magnitude is array # Immutability check + + +@pytest.mark.parametrize( + "op", + [ + pytest.param( + lambda a, u: a * u, + id="array-first", + marks=pytest.mark.xfail(reason="upstream issue numpy/numpy#15200"), + ), + pytest.param(lambda a, u: u * a, id="unit-first"), + ], +) +@pytest.mark.parametrize( + "unit", + [pytest.param(ureg.m, id="Unit"), pytest.param(ureg("meter"), id="Quantity")], +) +def test_array_quantity_creation_by_multiplication(op, unit, array): + assert type(op(array, unit)) == ureg.Quantity From bd1897ede79e94fd3864e383e77c506e998512d2 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 29 Dec 2019 22:43:42 -0600 Subject: [PATCH 279/612] Update CHANGES for downcast type/doc updates --- CHANGES | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 0395b7fc0..1f0a95379 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,13 @@ Pint Changelog 0.10 (unreleased) ----------------- +- Documentation on Pint's array type compatibility has been added to the NumPy support + page, including a graph of the duck array type casting hierarchy as understood by Pint + for N-dimensional arrays. + (Issue #963, Thanks Jon Thielen, Stephan Hoyer, and Guido Imperiale) +- Improved compatibility for downcast duck array types like Sparse and Masked Arrays. A + collection of basic tests has been added. + (Issue #963, Thanks Jon Thielen) - Improvements to wraps and check: - fail upon decoration (not execution) by checking wrapped function signature against wraps/check arguments. @@ -12,7 +19,7 @@ Pint Changelog (might BREAK code not conforming to documentation) - when strict=True, strings that can be parsed to quantities are accepted as arguments. - Add revolutions per second (rps) -- Improved compatbility for upcast types like xarray's DataArray or Dataset, to which +- Improved compatibility for upcast types like xarray's DataArray or Dataset, to which Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of basic tests for proper deferral has been added (for full integration tests, see xarray's test suite). The list of upcast types is available at From f7efb18534404939bcfddccae6a8801ed7f3c9b1 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 30 Dec 2019 14:16:05 -0600 Subject: [PATCH 280/612] Raise ValueError for ambiguity when attempting to cast Quantity with offset unit to boolean --- CHANGES | 4 ++++ pint/quantity.py | 6 +++++- pint/testsuite/test_quantity.py | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 1f0a95379..78c4fa118 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ Pint Changelog 0.10 (unreleased) ----------------- +- **BREAKING CHANGE**: + Boolean value of Quantities with offsets units is ambiguous, and so, now a ValueError + is raised when attempting to cast such a Quantity to boolean. + (Issue #965, Thanks Jon Thielen) - Documentation on Pint's array type compatibility has been added to the NumPy support page, including a graph of the duck array type casting hierarchy as understood by Pint for N-dimensional arrays. diff --git a/pint/quantity.py b/pint/quantity.py index ba4a26431..715ab6d80 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1481,7 +1481,11 @@ def compare(self, other, op): __gt__ = lambda self, other: self.compare(other, op=operator.gt) def __bool__(self): - return bool(self._magnitude) + # Only cast when non-ambiguous (when multiplicative unit) + if self._is_multiplicative: + return bool(self._magnitude) + else: + raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") __nonzero__ = __bool__ diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 9d61bbe8f..fbfd773b3 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -54,6 +54,8 @@ def test_quantity_bool(self): self.assertTrue(self.Q_(1, "meter")) self.assertFalse(self.Q_(0, None)) self.assertFalse(self.Q_(0, "meter")) + self.assertRaises(ValueError, bool, self.Q_(0, "degC")) + self.assertFalse(self.Q_(0, "delta_degC")) def test_quantity_comparison(self): x = self.Q_(4.2, "meter") From 97e9a866ad1aef3ae96c1b7ad0f4e99754a77d9a Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 30 Dec 2019 14:52:31 -0600 Subject: [PATCH 281/612] Add any, all, cbrt, linalg.solve --- docs/numpy.ipynb | 4 ++-- pint/numpy_func.py | 38 +++++++++++++++++++++++++++++++++--- pint/testsuite/test_numpy.py | 27 +++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 02f8f3fce..646a0b3c2 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -284,7 +284,7 @@ "\n", "The following [ufuncs](http://docs.scipy.org/doc/numpy/reference/ufuncs.html) can be applied to a Quantity object:\n", "\n", - "- **Math operations**: `add`, `subtract`, `multiply`, `divide`, `logaddexp`, `logaddexp2`, `true_divide`, `floor_divide`, `negative`, `remainder`, `mod`, `fmod`, `absolute`, `rint`, `sign`, `conj`, `exp`, `exp2`, `log`, `log2`, `log10`, `expm1`, `log1p`, `sqrt`, `square`, `reciprocal`\n", + "- **Math operations**: `add`, `subtract`, `multiply`, `divide`, `logaddexp`, `logaddexp2`, `true_divide`, `floor_divide`, `negative`, `remainder`, `mod`, `fmod`, `absolute`, `rint`, `sign`, `conj`, `exp`, `exp2`, `log`, `log2`, `log10`, `expm1`, `log1p`, `sqrt`, `square`, `cbrt`, `reciprocal`\n", "- **Trigonometric functions**: `sin`, `cos`, `tan`, `arcsin`, `arccos`, `arctan`, `arctan2`, `hypot`, `sinh`, `cosh`, `tanh`, `arcsinh`, `arccosh`, `arctanh`\n", "- **Comparison functions**: `greater`, `greater_equal`, `less`, `less_equal`, `not_equal`, `equal`\n", "- **Floating functions**: `isreal`, `iscomplex`, `isfinite`, `isinf`, `isnan`, `signbit`, `copysign`, `nextafter`, `modf`, `ldexp`, `frexp`, `fmod`, `floor`, `ceil`, `trunc`\n", @@ -301,7 +301,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['alen', 'amax', 'amin', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" + "['alen', 'all', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" ] } ], diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 1a9221caa..059ffaa6e 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -194,6 +194,8 @@ def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): result_unit = first_input_units ** 2 elif unit_op == "sqrt": result_unit = first_input_units ** 0.5 + elif unit_op == "cbrt": + result_unit = first_input_units ** (1 / 3) elif unit_op == "reciprocal": result_unit = first_input_units ** -1 elif unit_op == "size": @@ -255,7 +257,11 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): if np is None: return - func = getattr(np, func_str) + # Handle functions in submodules + func_str_split = func_str.split(".") + func = getattr(np, func_str_split[0]) + for func_str_piece in func_str_split[1:]: + func = getattr(func, func_str_piece) @implements(func_str, func_type) def implementation(*args, **kwargs): @@ -295,6 +301,7 @@ def implementation(*args, **kwargs): "variance", "square", "sqrt", + "cbrt", "reciprocal", "size", ]: @@ -408,6 +415,7 @@ def implementation(*args, **kwargs): "divide": "div", "floor_divide": "div", "sqrt": "sqrt", + "cbrt": "cbrt", "square": "square", "reciprocal": "reciprocal", "std": "sum", @@ -665,6 +673,24 @@ def _recursive_convert(arg, unit): ) +@implements("any", "function") +def _any(a, *args, **kwargs): + # Only valid when multiplicative unit/no offset + if a._is_multiplicative: + return np.any(a._magnitude, *args, **kwargs) + else: + raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") + + +@implements("all", "function") +def _all(a, *args, **kwargs): + # Only valid when multiplicative unit/no offset + if a._is_multiplicative: + return np.all(a._magnitude, *args, **kwargs) + else: + raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") + + # Implement simple matching-unit or stripped-unit functions based on signature @@ -836,6 +862,8 @@ def implementation(a, *args, **kwargs): implement_func("function", func_str, input_units=None, output_unit="delta") for func_str in ["gradient"]: implement_func("function", func_str, input_units=None, output_unit="delta,div") +for func_str in ["linalg.solve"]: + implement_func("function", func_str, input_units=None, output_unit="div") for func_str in ["var", "nanvar"]: implement_func("function", func_str, input_units=None, output_unit="variance") @@ -846,11 +874,15 @@ def numpy_wrap(func_type, func, args, kwargs, types): if func_type == "function": handled = HANDLED_FUNCTIONS + # Need to handle functions in submodules + name = ".".join(func.__module__.split(".")[1:] + [func.__name__]) elif func_type == "ufunc": handled = HANDLED_UFUNCS + # ufuncs do not have func.__module__ + name = func.__name__ else: raise ValueError("Invalid func_type {}".format(func_type)) - if func.__name__ not in handled or any(is_upcast_type(t) for t in types): + if name not in handled or any(is_upcast_type(t) for t in types): return NotImplemented - return handled[func.__name__](*args, **kwargs) + return handled[name](*args, **kwargs) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 3f2bce18b..0c9d09022 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -374,6 +374,13 @@ def test_einsum(self): np.array([30, 80, 130, 180, 230]) * self.ureg.m ** 2, ) + @helpers.requires_array_function_protocol() + def test_solve(self): + self.assertQuantityAlmostEqual( + np.linalg.solve(self.q, [[3], [7]] * self.ureg.s), + self.Q_([[1], [1]], "m / s"), + ) + # Arithmetic operations def test_addition_with_scalar(self): a = np.array([0, 1, 2]) @@ -414,6 +421,14 @@ def test_power(self): ) self.assertNDArrayEqual(arr ** self.Q_(2), np.array([0, 1, 4])) + def test_sqrt(self): + q = self.Q_(100, "m**2") + self.assertQuantityEqual(np.sqrt(q), self.Q_(10, "m")) + + def test_cbrt(self): + q = self.Q_(1000, "m**3") + self.assertQuantityEqual(np.cbrt(q), self.Q_(10, "m")) + @unittest.expectedFailure @helpers.requires_numpy() def test_exponentiation_array_exp_2(self): @@ -537,6 +552,18 @@ def test_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m self.assertNDArrayEqual(np.nonzero(q)[0], [0, 2, 3, 5]) + @helpers.requires_array_function_protocol() + def test_any_numpy_func(self): + q = [0, 1] * self.ureg.m + self.assertTrue(np.any(q)) + self.assertRaises(ValueError, np.any, self.q_temperature) + + @helpers.requires_array_function_protocol() + def test_all_numpy_func(self): + q = [0, 1] * self.ureg.m + self.assertFalse(np.all(q)) + self.assertRaises(ValueError, np.all, self.q_temperature) + @helpers.requires_array_function_protocol() def test_count_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m From 58cce931deaa8811bd4ef613da28847d5c591254 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Mon, 30 Dec 2019 18:29:02 -0600 Subject: [PATCH 282/612] Update documentation to address MaskedArray breaking change --- CHANGES | 13 +++- docs/numpy.ipynb | 192 +++++++++++++++++++++++++++++------------------ 2 files changed, 129 insertions(+), 76 deletions(-) diff --git a/CHANGES b/CHANGES index 78c4fa118..35ef06b4c 100644 --- a/CHANGES +++ b/CHANGES @@ -8,12 +8,21 @@ Pint Changelog Boolean value of Quantities with offsets units is ambiguous, and so, now a ValueError is raised when attempting to cast such a Quantity to boolean. (Issue #965, Thanks Jon Thielen) +- **BREAKING CHANGE**: + `__array_ufunc__` has been implemented on `pint.Unit` to permit + multiplication/division by units on the right of ufunc-reliant array types (like + Sparse) with proper respect for the type casting hierarchy. However, until [an + upstream issue with NumPy is resolved](https://github.com/numpy/numpy/issues/15200), + this breaks creation of Masked Array Quantities by multiplication on the right. + Read Pint's [NumPy support + documentation](https://pint.readthedocs.io/en/latest/numpy.html) for more details. + (Issues #963 and #966, Thanks Jon Thielen) - Documentation on Pint's array type compatibility has been added to the NumPy support page, including a graph of the duck array type casting hierarchy as understood by Pint for N-dimensional arrays. (Issue #963, Thanks Jon Thielen, Stephan Hoyer, and Guido Imperiale) -- Improved compatibility for downcast duck array types like Sparse and Masked Arrays. A - collection of basic tests has been added. +- Improved compatibility for downcast duck array types like Sparse.COO. A collection + of basic tests has been added. (Issue #963, Thanks Jon Thielen) - Improvements to wraps and check: - fail upon decoration (not execution) by checking wrapped function signature against diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 646a0b3c2..8a66c64be 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -301,7 +301,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['alen', 'all', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" + "['alen', 'all', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'reshape', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" ] } ], @@ -335,10 +335,10 @@ "\n", "- xarray: `DataArray`, `Dataset`, and `Variable`\n", "- Sparse: `COO`\n", - "- NumPy: masked arrays\n", "\n", "and the following have partial support, with full integration planned:\n", "\n", + "- NumPy masked arrays (NOTE: Masked Array compatibility has changed with Pint 0.10 and versions of NumPy up to at least 1.18, see the example below)\n", "- Dask arrays\n", "- CuPy arrays\n", "\n", @@ -373,151 +373,156 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "%3\n", - "\n", + "\n", "\n", "Dask array\n", - "\n", - "Dask array\n", + "\n", + "Dask array\n", "\n", "\n", "NumPy ndarray\n", - "\n", - "NumPy ndarray\n", + "\n", + "NumPy ndarray\n", "\n", "\n", "Dask array->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "CuPy ndarray\n", - "\n", - "CuPy ndarray\n", + "\n", + "CuPy ndarray\n", "\n", "\n", "Dask array->CuPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Sparse COO\n", - "\n", - "Sparse COO\n", + "\n", + "Sparse COO\n", "\n", "\n", "Dask array->Sparse COO\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "NumPy masked array\n", - "\n", - "NumPy masked array\n", + "\n", + "NumPy masked array\n", "\n", "\n", "Dask array->NumPy masked array\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "CuPy ndarray->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Sparse COO->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "NumPy masked array->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Jax array\n", - "\n", - "Jax array\n", + "\n", + "Jax array\n", "\n", "\n", "Jax array->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Pint Quantity\n", - "\n", - "Pint Quantity\n", + "\n", + "Pint Quantity\n", "\n", "\n", "Pint Quantity->Dask array\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Pint Quantity->NumPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Pint Quantity->CuPy ndarray\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "Pint Quantity->Sparse COO\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "Pint Quantity->NumPy masked array\n", + "\n", + "\n", "\n", "\n", "xarray Dataset/DataArray/Variable\n", - "\n", - "xarray Dataset/DataArray/Variable\n", + "\n", + "xarray Dataset/DataArray/Variable\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->Dask array\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->Dask array\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->NumPy ndarray\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->NumPy ndarray\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->CuPy ndarray\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->CuPy ndarray\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->Sparse COO\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->Sparse COO\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->NumPy masked array\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->NumPy masked array\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->Jax array\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->Jax array\n", + "\n", + "\n", "\n", "\n", - "xarray Dataset/DataArray/Variable->Pint Quantity\n", - "\n", - "\n", + "xarray Dataset/DataArray/Variable->Pint Quantity\n", + "\n", + "\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 13, @@ -532,7 +537,7 @@ "g.edge('Dask array', 'NumPy ndarray')\n", "g.edge('Dask array', 'CuPy ndarray')\n", "g.edge('Dask array', 'Sparse COO')\n", - "g.edge('Dask array', 'NumPy masked array')\n", + "g.edge('Dask array', 'NumPy masked array', style='dashed')\n", "g.edge('CuPy ndarray', 'NumPy ndarray')\n", "g.edge('Sparse COO', 'NumPy ndarray')\n", "g.edge('NumPy masked array', 'NumPy ndarray')\n", @@ -541,6 +546,7 @@ "g.edge('Pint Quantity', 'NumPy ndarray')\n", "g.edge('Pint Quantity', 'CuPy ndarray', style='dashed')\n", "g.edge('Pint Quantity', 'Sparse COO')\n", + "g.edge('Pint Quantity', 'NumPy masked array', style='dashed')\n", "g.edge('xarray Dataset/DataArray/Variable', 'Dask array')\n", "g.edge('xarray Dataset/DataArray/Variable', 'CuPy ndarray', style='dashed')\n", "g.edge('xarray Dataset/DataArray/Variable', 'Sparse COO')\n", @@ -630,9 +636,9 @@ "name": "stdout", "output_type": "stream", "text": [ - " meter\n", + " meter\n", "\n", - "0.0952163965972451 meter\n" + "0.09488747484058625 meter\n" ] } ], @@ -643,7 +649,7 @@ "x[x < 0.9] = 0 # fill most of the array with zeros\n", "s = COO(x)\n", "\n", - "q = Q_(s, 'm')\n", + "q = s * ureg.m\n", "\n", "print(q)\n", "print()\n", @@ -654,13 +660,51 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**xarray wrapping Pint Quantity wrapping Dask array wrapping Sparse COO**" + "**Pint Quantity wrapping NumPy Masked Array**" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "masked_array(data=[, --, , --],\n", + " mask=[False, True, False, True],\n", + " fill_value='?',\n", + " dtype=object)\n" + ] + } + ], + "source": [ + "m = np.ma.masked_array([2, 3, 5, 7], mask=[False, True, False, True])\n", + "\n", + "# Must create using Quantity class\n", + "print(repr(ureg.Quantity(m, 'm')))\n", + "print()\n", + "\n", + "# DO NOT create using multiplication until\n", + "# https://github.com/numpy/numpy/issues/15200 is resolved, as\n", + "# unexpected behavior may result\n", + "print(repr(m * ureg.m))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**xarray wrapping Pint Quantity wrapping Dask array wrapping Sparse COO**" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -755,7 +799,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.6.7" } }, "nbformat": 4, From 402c6233cb828fa005c026261e1ba5abf4f54cee Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 22:13:07 -0600 Subject: [PATCH 283/612] Updated CHANGES --- CHANGES | 85 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/CHANGES b/CHANGES index 35ef06b4c..82faedbaf 100644 --- a/CHANGES +++ b/CHANGES @@ -100,8 +100,10 @@ Pint Changelog (Issue #915, Thanks Guido Imperiale) - All Exceptions can now be pickled and can be accessed from the top-level package. (Issue #915, Thanks Guido Imperiale) -- TODO #913 -- TODO #911 +- Mark regex as raw strings to avoid unnecessary warnings. + (Issue #913, Thanks keewis) +- Implement registry-based string preprocessing as list of callables. + (Issues #429 and #851, thanks Jon Thielen) - Context activation and deactivation is now instantaneous; drastically reduced memory footprint of a context (it used to be ~1.6MB per context; now it's a few bytes) (Issues #909 / #923 / #938, Thanks Guido Imperiale) @@ -111,12 +113,14 @@ Pint Changelog Pint now adheres to NEP-29 as a rolling dependencies version policy. (Issues #908 and #910, Thanks Guido Imperiale) -- TODO #907 -- TODO #891 -- TODO #890 -- TODO #889 -- TODO #888 -- TODO #884 +- Show proper code location of UnitStrippedWarning exception. + (Issue #907, thanks Martin K. Scherer) +- Reimplement _Quantity.__iter__ to return an iterator. + (Issues #751 and #760, Thanks Jon Thielen) +- Add http://www.dimensionalanalysis.org/ to README + (Thanks Shiri Avni) +- Allow for user defined units formatting. + (Issue #873, Thanks Ryan Clary) - Quantity, Unit, and Measurement are now accessible as top-level classes (pint.Quantity, pint.Unit, pint.Measurement) and can be instantiated without explicitly creating a UnitRegistry @@ -133,35 +137,42 @@ Pint Changelog (Issue #863, Thanks Guido Imperiale) - Document the '_' symbols found in the definitions files (Issue #862, Thanks Guido Imperiale) -- TODO #846 -- TODO #837 -- TODO #835 -- TODO #834 -- TODO #830 -- TODO #829 -- TODO #825 -- TODO #824 -- TODO #822 -- TODO #813 -- TODO #811 -- TODO #809 -- TODO #805 -- TODO #802 -- TODO #798 -- TODO #796 -- TODO #791 -- TODO #788 -- TODO #781 -- TODO #776 -- TODO #784 -- TODO #773 -- TODO #768 -- TODO #767 -- TODO #762 -- TODO #759 -- TODO #756 -- TODO #754 -- TODO #680 +- Improve OffsetUnitCalculusError message. + (Issue #839, Thanks Christoph Buchner) +- Atomic units for intensity and electric field. + (Issue #834, Thanks Øyvind Sigmundson Schøyen) +- Allow np arrays of scalar quantities to be plotted. + (Issue #825, Thanks andrewgsavage) +- Updated gravitational constant to CODATA 2018. + (Issue #816, Thanks Jellby) +- Update to new SI definition and CODATA 2018. + (Issue #811, Thanks Jellby) +- Allow units with aliases but no symbol. + (Issue #808, Thanks Jellby) +- Fix definition of dimensionless units and constants. + (Issue #805, Thanks Jellby) +- Added RKM unit (used in textile industry). + (Issue #802, Thanks Giuseppe Corbelli) +- Remove __name__ method definition in BaseRegistry. + (Issue #787, Thanks Carlos Pascual) +- Added t_force, short_ton_force and long_ton_force. + (Issue #796, Thanks Jan Hein de Jong) +- Fixed error message of DefinitionSyntaxError + (Issue #791, Thanks Clément Pit-Claudel) +- Expanded the potential use of Decimal type to parsing. + (Issue #788, Thanks Francisco Couzo) +- Fixed gram name to allow translation by babel. + (Issue #776, Thanks Hervé Cauwelier) +- Default group should only have orphan units. + (Issue #766, Thanks Jules Chéron) +- Added custom constructors from_sequence and from_list. + (Issue #761, Thanks deniz195) +- Add quantity formatting with ndarray. + (Issue #559, Thanks Jules Chéron) +- Add pint-pandas notebook docs + (Issue #754, Thanks andrewgsavage) +- Use µ as default abbreviation for micro. + (Issue #666, Thanks Eric Prestat) 0.9 (2019-01-12) From 9da654fc18baea0c84a30e289b7577dd9d3941fe Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 22:17:45 -0600 Subject: [PATCH 284/612] Updated README and index.rst --- README.rst | 58 +++++++++++++++++++++++++++++++++++--------------- docs/index.rst | 7 +++--- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 6e5f270f0..68d8261b2 100644 --- a/README.rst +++ b/README.rst @@ -90,32 +90,56 @@ Full documentation is available at http://pint.readthedocs.org/ GUI Website ----------- -This [website](www.dimensionalanalysis.org) wraps Pint's "dimensional analysis" methods to provide a GUI. +This Website_ wraps Pint's "dimensional analysis" methods to provide a GUI. Design principles ----------------- Although there are already a few very good Python packages to handle physical -quantities, no one was really fitting my needs. Like most developers, I programed -Pint to scratch my own itches. +quantities, no one was really fitting my needs. Like most developers, I +programmed Pint to scratch my own itches. -- Unit parsing: prefixed and pluralized forms of units are recognized without - explicitly defining them. In other words: as the prefix *kilo* and the unit *meter* - are defined, Pint understands *kilometers*. This results in a much shorter and - maintainable unit definition list as compared to other packages. +**Unit parsing**: prefixed and pluralized forms of units are recognized without +explicitly defining them. In other words: as the prefix *kilo* and the unit +*meter* are defined, Pint understands *kilometers*. This results in a much +shorter and maintainable unit definition list as compared to other packages. -- Standalone unit definitions: units definitions are loaded from simple and - easy to edit text file. Adding and changing units and their definitions does - not involve changing the code. +**Standalone unit definitions**: units definitions are loaded from a text file +which is simple and easy to edit. Adding and changing units and their +definitions does not involve changing the code. -- Advanced string formatting: a quantity can be formatted into string using - PEP 3101 syntax. Extended conversion flags are given to provide latex and pretty - formatting. +**Advanced string formatting**: a quantity can be formatted into string using +`PEP 3101`_ syntax. Extended conversion flags are given to provide symbolic, +LaTeX and pretty formatting. Unit name translation is available if Babel_ is +installed. -- Small codebase: small and easy to maintain with a flat hierarchy. +**Free to choose the numerical type**: You can use any numerical type +(`fraction`, `float`, `decimal`, `numpy.ndarray`, etc). NumPy_ is not required +but supported. -- Dependency free: it depends only on Python and its standard library. +**Awesome NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and +ufuncs are supported including automatic conversion of units. For example +`numpy.arccos(q)` will require a dimensionless `q` and the units of the output +quantity will be radian. -- Advanced NumPy support: While NumPy is not a requirement for Pint, - when available ndarray methods and ufuncs can be used in Quantity objects. +**Uncertainties integration**: transparently handles calculations with +quantities with uncertainties (like 3.14±0.01) meter via the `uncertainties +package`_. + +**Handle temperature**: conversion between units with different reference +points, like positions on a map or absolute temperature scales. + +**Dependency free**: it depends only on Python and its standard library. It interacts with other packages +like numpy and uncertainties if they are installed + +**Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. + + +When you choose to use a NumPy_ ndarray, its methods and +ufuncs are supported including automatic conversion of units. For example +`numpy.arccos(q)` will require a dimensionless `q` and the units of the output +quantity will be radian. + + +.. _Website: http://www.dimensionalanalysis.org/ \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index b3245abf3..e72ed87f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,7 +83,7 @@ installed. (`fraction`, `float`, `decimal`, `numpy.ndarray`, etc). NumPy_ is not required but supported. -**NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and +**Awesome NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and ufuncs are supported including automatic conversion of units. For example `numpy.arccos(q)` will require a dimensionless `q` and the units of the output quantity will be radian. @@ -95,9 +95,8 @@ package`_. **Handle temperature**: conversion between units with different reference points, like positions on a map or absolute temperature scales. -**Small codebase**: easy to maintain codebase with a flat hierarchy. - -**Dependency free**: it depends only on Python and its standard library. +**Dependency free**: it depends only on Python and its standard library. It interacts with other packages +like numpy and uncertainties if they are installed **Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. From cbf12688823f58c738cd24e3691d102dc6e2356f Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 22:18:13 -0600 Subject: [PATCH 285/612] Removed README to avoid having to keep it in sync with README.rst --- README | 92 ------------------------------------------------------- setup.cfg | 2 +- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 README diff --git a/README b/README deleted file mode 100644 index a63127937..000000000 --- a/README +++ /dev/null @@ -1,92 +0,0 @@ -Pint: makes units easy -====================== - -Pint is a Python package to define, operate and manipulate physical -quantities: the product of a numerical value and a unit of measurement. -It allows arithmetic operations between them and conversions from and -to different units. - -It is distributed with a comprehensive list of physical units, prefixes -and constants. Due to its modular design, you can extend (or even rewrite!) -the complete list without changing the source code. It supports a lot of -numpy mathematical operations **without monkey patching or wrapping numpy**. - -It has a complete test coverage. It runs in Python 3.6+ with no other dependency. -If you need Python 2.7 or 3.4/3.5 compatibility, use Pint 0.9. -It is licensed under BSD. - -It is extremely easy and natural to use: - -.. code-block:: python - - >>> import pint - >>> ureg = pint.UnitRegistry() - >>> 3 * ureg.meter + 4 * ureg.cm - - -and you can make good use of numpy if you want: - -.. code-block:: python - - >>> import numpy as np - >>> [3, 4] * ureg.meter + [4, 3] * ureg.cm - - >>> np.sum(_) - - - -Quick Installation ------------------- - -To install Pint, simply: - -.. code-block:: bash - - $ pip install pint - -or utilizing conda with, the conda-forge channel: - -.. code-block:: bash - - $ conda install -c conda-forge pint - -and then simply enjoy it! - - -Documentation -------------- - -Full documentation is available at http://pint.readthedocs.org/ - -GUI Website ------------ - -This [website](www.dimensionalanalysis.org) wraps Pint's "dimensional analysis" methods to provide a GUI. - - -Design principles ------------------ - -Although there are already a few very good Python packages to handle physical -quantities, no one was really fitting my needs. Like most developers, I programed -Pint to scratch my own itches. - -- Unit parsing: prefixed and pluralized forms of units are recognized without - explicitly defining them. In other words: as the prefix *kilo* and the unit *meter* - are defined, Pint understands *kilometers*. This results in a much shorter and - maintainable unit definition list as compared to other packages. - -- Standalone unit definitions: units definitions are loaded from simple and - easy to edit text file. Adding and changing units and their definitions does - not involve changing the code. - -- Advanced string formatting: a quantity can be formatted into string using - PEP 3101 syntax. Extended conversion flags are given to provide latex and pretty - formatting. - -- Small codebase: small and easy to maintain with a flat hierarchy. - -- Dependency free: it depends only on Python and its standard library. - -- Advanced NumPy support: While NumPy is not a requirement for Pint, - when available ndarray methods and ufuncs can be used in Quantity objects. diff --git a/setup.cfg b/setup.cfg index 6c086432e..20ad8f08f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ author = Hernan E. Grecco author_email = hernan.grecco@gmail.com license = BSD description = Physical quantities module -long_description = file: README, AUTHORS, CHANGES +long_description = file: README.rst, AUTHORS, CHANGES keywords = physical, quantities, unit, conversion, science url = https://github.com/hgrecco/pint classifiers = From e405ececf2b95ce7ebd2b9bc446115d4dac49690 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 22:51:30 -0600 Subject: [PATCH 286/612] Release related fixes --- CHANGES | 2 ++ MANIFEST.in | 3 ++- README.rst | 9 ++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 82faedbaf..085003878 100644 --- a/CHANGES +++ b/CHANGES @@ -25,12 +25,14 @@ Pint Changelog of basic tests has been added. (Issue #963, Thanks Jon Thielen) - Improvements to wraps and check: + - fail upon decoration (not execution) by checking wrapped function signature against wraps/check arguments. (might BREAK test code) - wraps only accepts strings and Units (not quantities) to avoid confusion with magnitude. (might BREAK code not conforming to documentation) - when strict=True, strings that can be parsed to quantities are accepted as arguments. + - Add revolutions per second (rps) - Improved compatibility for upcast types like xarray's DataArray or Dataset, to which Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of diff --git a/MANIFEST.in b/MANIFEST.in index e8e05bbdc..05c3182e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,8 @@ -include README AUTHORS CHANGES LICENSE README.rst BADGES.rst +include AUTHORS CHANGES LICENSE README.rst BADGES.rst recursive-include pint * recursive-include docs * recursive-include bench * prune docs/_build prune docs/_themes/.git +exclude .editorconfig bors.toml pull_request_template.md requirements_docs.txt global-exclude *.pyc *~ .DS_Store *__pycache__* *.pyo .travis-exclude.yml diff --git a/README.rst b/README.rst index 68d8261b2..6c994665f 100644 --- a/README.rst +++ b/README.rst @@ -142,4 +142,11 @@ ufuncs are supported including automatic conversion of units. For example quantity will be radian. -.. _Website: http://www.dimensionalanalysis.org/ \ No newline at end of file +.. _Website: http://www.dimensionalanalysis.org/ +.. _`comprehensive list of physical units, prefixes and constants`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt +.. _`uncertainties package`: https://pythonhosted.org/uncertainties/ +.. _`NumPy`: http://www.numpy.org/ +.. _`PEP 3101`: https://www.python.org/dev/peps/pep-3101/ +.. _`Babel`: http://babel.pocoo.org/ +.. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types +.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb From d5669bb5b779496107ca5594c350f1dc3af231b7 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 23:05:03 -0600 Subject: [PATCH 287/612] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a25db6c0..d2d7e8917 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ dist/ MANIFEST *pytest_cache* +.eggs # WebDAV file system cache files .DAV/ From 6dc78679ee0ad5a324cf1e9d364fb55d83c0966c Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 23:02:43 -0600 Subject: [PATCH 288/612] Add version.py to make zest.releaser work with setuptools_scm --- MANIFEST.in | 4 ++-- setup.cfg | 3 +++ version.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 version.py diff --git a/MANIFEST.in b/MANIFEST.in index 05c3182e3..63e342183 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ -include AUTHORS CHANGES LICENSE README.rst BADGES.rst +include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt recursive-include pint * recursive-include docs * recursive-include bench * prune docs/_build prune docs/_themes/.git -exclude .editorconfig bors.toml pull_request_template.md requirements_docs.txt +exclude .editorconfig bors.toml pull_request_template.md requirements_docs.txt version.py global-exclude *.pyc *~ .DS_Store *__pycache__* *.pyo .travis-exclude.yml diff --git a/setup.cfg b/setup.cfg index 20ad8f08f..a95b2ca1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,3 +72,6 @@ include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 + +[zest.releaser] +python-file-with-version = version.py \ No newline at end of file diff --git a/version.py b/version.py new file mode 100644 index 000000000..12509a0aa --- /dev/null +++ b/version.py @@ -0,0 +1,3 @@ +# This is just for zest.releaser. Do not touch + +__version__ = "0.10.dev0" From a91762071e3cecc8d995c993a3175f84ed9ec804 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 23:17:51 -0600 Subject: [PATCH 289/612] Preparing release 0.10 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 085003878..9aac65737 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.10 (unreleased) +0.10 (2020-01-05) ----------------- - **BREAKING CHANGE**: diff --git a/version.py b/version.py index 12509a0aa..86fecbbb5 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ # This is just for zest.releaser. Do not touch -__version__ = "0.10.dev0" +__version__ = '0.10' From 57b80ec270d4b23b984609db2bcd597dd2a1e972 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 23:18:22 -0600 Subject: [PATCH 290/612] Back to development: 0.11 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 9aac65737..bd1d9835d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.11 (unreleased) +----------------- + +- Nothing changed yet. + + 0.10 (2020-01-05) ----------------- diff --git a/version.py b/version.py index 86fecbbb5..be336e326 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ # This is just for zest.releaser. Do not touch -__version__ = '0.10' +__version__ = '0.11.dev0' From 57ff2dff14bc2e71aeadd5a4ad5baa6303d5de0f Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 5 Jan 2020 23:29:16 -0600 Subject: [PATCH 291/612] Fixed zest releaser string convention --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index be336e326..2af39f951 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ # This is just for zest.releaser. Do not touch -__version__ = '0.11.dev0' +__version__ = "0.11.dev0" From 079559c41a3bbc96307f7047c19e5b1665aa54c6 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 14:43:55 +0000 Subject: [PATCH 292/612] Sphinx fixes --- docs/conf.py | 1 + pint/__init__.py | 5 +- pint/context.py | 9 +- pint/matplotlib.py | 2 +- pint/quantity.py | 13 +-- pint/registry.py | 183 ++++++++++++++++++--------------------- pint/registry_helpers.py | 17 ++-- pint/systems.py | 5 +- pint/unit.py | 10 ++- 9 files changed, 119 insertions(+), 126 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 211ff0b10..1b772ba3c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,7 @@ "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.coverage", + "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx.ext.mathjax", "matplotlib.sphinxext.plot_directive", diff --git a/pint/__init__.py b/pint/__init__.py index 04ddf1d37..a6f59c1ec 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -90,8 +90,7 @@ def set_application_registry(registry): Parameters ---------- - registry : UnitRegistry - + registry : pint.UnitRegistry """ if not isinstance(registry, (LazyRegistry, UnitRegistry)): raise TypeError("Expected UnitRegistry; got %s" % type(registry)) @@ -107,7 +106,7 @@ def get_application_registry(): Returns ------- - UnitRegistry + pint.UnitRegistry """ return _APP_REGISTRY diff --git a/pint/context.py b/pint/context.py index 1ceb35969..bcc34c372 100644 --- a/pint/context.py +++ b/pint/context.py @@ -51,9 +51,9 @@ class Context: Parameters ---------- - name : str or None (default), optional + name : str or None, optional Name of the context (must be unique within the registry). - Use None for anonymous Context. + Use None for anonymous Context. (Default value = None). aliases : iterable of str Other names for the context. defaults : None or dict @@ -113,15 +113,14 @@ def from_context(cls, context, **defaults): Parameters ---------- - context : Context + context : pint.Context Original context. **defaults Returns ------- - Context - + pint.Context """ if defaults: newdef = dict(context.defaults, **defaults) diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 912474246..914c62f3c 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -65,7 +65,7 @@ def setup_matplotlib_handlers(registry, enable): Parameters ---------- - registry : UnitRegistry + registry : pint.UnitRegistry The registry that will be used. enable : bool Whether support should be enabled or disabled. diff --git a/pint/quantity.py b/pint/quantity.py index 715ab6d80..747432c72 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -417,7 +417,12 @@ def dimensionless(self): @property def dimensionality(self): - """Quantity's dimensionality (e.g. {length: 1, time: -1})""" + """ + Returns + ------- + dict + Dimensionality of the Quantity, e.g. ``{length: 1, time: -1}`` + """ if self._dimensionality is None: self._dimensionality = self._REGISTRY._get_dimensionality(self._units) @@ -525,11 +530,10 @@ def ito(self, other=None, *contexts, **ctx_kwargs): ---------- other : pint.Quantity, str or dict Destination units. (Default value = None) - *contexts : str or Context + *contexts : str or pint.Context Contexts to use in the transformation. **ctx_kwargs : Values for the Context/s - """ other = to_units_container(other, self._REGISTRY) @@ -545,7 +549,7 @@ def to(self, other=None, *contexts, **ctx_kwargs): ---------- other : pint.Quantity, str or dict destination units. (Default value = None) - *contexts : str or Context + *contexts : str or pint.Context Contexts to use in the transformation. **ctx_kwargs : Values for the Context/s @@ -553,7 +557,6 @@ def to(self, other=None, *contexts, **ctx_kwargs): Returns ------- pint.Quantity - """ other = to_units_container(other, self._REGISTRY) diff --git a/pint/registry.py b/pint/registry.py index 90d595be6..4ff979042 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1,34 +1,36 @@ """ - pint.registry - ~~~~~~~~~~~~~ +pint.registry +~~~~~~~~~~~~~ - Defines the Registry, a class to contain units and their relations. +Defines the Registry, a class to contain units and their relations. - The module actually defines 5 registries with different capabilites: +The module actually defines 5 registries with different capabilites: - - BaseRegistry: Basic unit definition and querying. - Conversion between multiplicative units. +- BaseRegistry: Basic unit definition and querying. + Conversion between multiplicative units. - - NonMultiplicativeRegistry: Conversion between non multiplicative (offset) units. - (e.g. Temperature) +- NonMultiplicativeRegistry: Conversion between non multiplicative (offset) units. + (e.g. Temperature) - * Inherits from BaseRegistry + * Inherits from BaseRegistry - - ContextRegisty: Conversion between units with different dimenstions according - to previously established relations (contexts). - (e.g. in the spectroscopy, conversion between frequency and energy is possible) +- ContextRegisty: Conversion between units with different dimensions according + to previously established relations (contexts) - e.g. in spectroscopy, + conversion between frequency and energy is possible. May also override + conversions between units on the same dimension - e.g. different + rounding conventions. - * Inherits from BaseRegistry + * Inherits from BaseRegistry - - SystemRegistry: Group unit and changing of base units. - (e.g. in MKS, meter, kilogram and second are base units.) +- SystemRegistry: Group unit and changing of base units. + (e.g. in MKS, meter, kilogram and second are base units.) - * Inherits from BaseRegistry + * Inherits from BaseRegistry - - UnitRegistry: Combine all previous capabilities, it is exposed by Pint. +- UnitRegistry: Combine all previous capabilities, it is exposed by Pint. - :copyright: 2016 by Pint Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. +:copyright: 2016 by Pint Authors, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. """ import copy @@ -742,7 +744,7 @@ def get_root_units(self, input_units, check_nonmult=True): Returns ------- - number, Unit + Number, pint.Unit multiplicative factor, base units """ @@ -806,16 +808,16 @@ def get_base_units(self, input_units, check_nonmult=True, system=None): ---------- input_units : UnitsContainer or str units - check_nonmult : - if True, None will be returned as the - multiplicative factor if a non-multiplicative - units is found in the final Units. (Default value = True) + check_nonmult : bool + If True, None will be returned as the multiplicative factor if + non-multiplicative units are found in the final Units. + (Default value = True) system : (Default value = None) Returns ------- - Number, Unit + Number, pint.Unit multiplicative factor, base units """ @@ -947,9 +949,8 @@ def parse_unit_name(self, unit_name, case_sensitive=True): Returns ------- - tuple of (str, str, str) + tuple of tuples (str, str, str) all non-equivalent combinations of (prefix, unit name, suffix) - """ return self._dedup_candidates( self._parse_unit_name(unit_name, case_sensitive=case_sensitive) @@ -1314,6 +1315,7 @@ class ContextRegistry(BaseRegistry): (e.g. in the spectroscopy, conversion between frequency and energy is possible) Capabilities: + - Register contexts. - Enable and disable contexts. - Parse @context directive. @@ -1351,8 +1353,8 @@ def add_context(self, context: Context) -> None: The context will be accessible by its name and aliases. - Notice that this method will NOT enable the context. Use `enable_contexts`. - + Notice that this method will NOT enable the context; + see :meth:`enable_contexts`. """ if not context.name: raise ValueError("Can't add unnamed context to registry") @@ -1372,8 +1374,8 @@ def add_context(self, context: Context) -> None: def remove_context(self, name_or_alias: str) -> Context: """Remove a context from the registry and return it. - Notice that this methods will not disable the context. Use `disable_contexts`. - + Notice that this methods will not disable the context; + see :meth:`disable_contexts`. """ context = self._contexts[name_or_alias] @@ -1392,7 +1394,6 @@ def _switch_context_cache_and_units(self) -> None: and self._units specific to the combination of active contexts. The next time this method is invoked with the same combination of contexts, reuse the same variant self._cache and self._units as in the previous time. - """ del self._units.maps[:-1] units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) @@ -1473,14 +1474,14 @@ def enable_contexts(self, *names_or_contexts, **kwargs) -> None: Parameters ---------- - names_or_contexts : - sequence of the contexts or contexts names/alias - kwargs : - keyword arguments for the context *names_or_contexts : - + one or more contexts or context names/aliases **kwargs : + keyword arguments for the context(s) + Examples + -------- + See :meth:`context` """ # If present, copy the defaults from the containing contexts @@ -1519,12 +1520,8 @@ def disable_contexts(self, n: int = None) -> None: Parameters ---------- - n: int : - (Default value = None) - - Returns - ------- - + n : int + Number of contexts to disable. Default: disable all contexts. """ self._active_ctx.remove_contexts(n) self._switch_context_cache_and_units() @@ -1536,47 +1533,41 @@ def context(self, *names, **kwargs): Parameters ---------- - names : - name of the context. - kwargs : + *names : + name(s) of the context(s). + **kwargs : keyword arguments for the contexts. - Context are called by their name:: + Examples + -------- + Context can be called by their name:: + >>> with ureg.context('one'): + ... pass - If the context has an argument, you can specify its value as a keyword - argument:: + If a context has an argument, you can specify its value as a keyword argument:: + >>> with ureg.context('one', n=1): + ... pass - Multiple contexts can be entered in single call: + Multiple contexts can be entered in single call: + >>> with ureg.context('one', 'two', n=1): + ... pass - or nested allowing you to give different values to the same keyword argument:: + Or nested allowing you to give different values to the same keyword argument:: + >>> with ureg.context('one', n=1): + ... with ureg.context('two', n=2): + ... pass - A nested context inherits the defaults from the containing context:: - *names : - - **kwargs : + A nested context inherits the defaults from the containing context:: - - Example - ------- - - >>> with ureg.context('one'): - ... pass - >>> with ureg.context('one', n=1): - ... pass - >>> with ureg.context('one', 'two', n=1): - ... pass - >>> with ureg.context('one', n=1): - ... with ureg.context('two', n=2): - ... pass - >>> with ureg.context('one', n=1): - ... with ureg.context('two'): # Here n takes the value of the upper context - ... pass + >>> with ureg.context('one', n=1): + ... # Here n takes the value of the outer context + ... with ureg.context('two'): + ... pass """ - # Enable the contexts. self.enable_contexts(*names, **kwargs) @@ -1589,7 +1580,7 @@ def context(self, *names, **kwargs): # the added contexts are removed from the active one. self.disable_contexts(len(names)) - def with_context(self, name, **kw): + def with_context(self, name, **kwargs): """Decorator to wrap a function call in a Pint context. Use it to ensure that a certain context is active when @@ -1597,13 +1588,10 @@ def with_context(self, name, **kw): Parameters ---------- - names : - name of the context. - kwargs : - keyword arguments for the contexts. name : - - **kw : + name of the context. + **kwargs : + keyword arguments for the context Returns @@ -1627,9 +1615,9 @@ def decorator(func): ) @functools.wraps(func, assigned=assigned, updated=updated) - def wrapper(*values, **kwargs): - with self.context(name, **kw): - return func(*values, **kwargs) + def wrapper(*values, **wrapper_kwargs): + with self.context(name, **kwargs): + return func(*values, **wrapper_kwargs) return wrapper @@ -1657,9 +1645,7 @@ def _convert(self, value, src, dst, inplace=False): ------- callable converted value - """ - # If there is an active context, we look for a path connecting source and # destination dimensionality. If it exists, we transform the source value # by applying sequentially each transformation of the path. @@ -1679,9 +1665,6 @@ def _convert(self, value, src, dst, inplace=False): return super()._convert(value, src, dst, inplace) def _get_compatible_units(self, input_units, group_or_system): - """ - """ - src_dim = self._get_dimensionality(input_units) ret = super()._get_compatible_units(input_units, group_or_system) @@ -1704,11 +1687,11 @@ class SystemRegistry(BaseRegistry): (e.g. in the spectroscopy, conversion between frequency and energy is possible) Capabilities: + - Register systems and groups. - List systems - Get or get the default system. - Parse @system and @group directive. - """ def __init__(self, system=None, **kwargs): @@ -1733,11 +1716,10 @@ def _init_dynamic_classes(self): self.System = systems.build_system_class(self) def _after_init(self): - """After init function + """Invoked at the end of ``__init__``. - Create default group. - Add all orphan units to it. - Set default system. + - Create default group and add all orphan units to it + - Set default system """ super()._after_init() @@ -1776,16 +1758,16 @@ def get_group(self, name, create_if_needed=True): Parameters ---------- - name : + name : str Name of the group to be - create_if_needed : - Create a group if not Found. If False, raise an Exception. (Default value = True) + create_if_needed : bool + If True, create a group if not found. If False, raise an Exception. + (Default value = True) Returns ------- type Group - """ if name in self._groups: return self._groups[name] @@ -1818,10 +1800,11 @@ def get_system(self, name, create_if_needed=True): Parameters ---------- - name : + name : str Name of the group to be - create_if_needed : - Create a group if not Found. If False, raise an Exception. (Default value = True) + create_if_needed : bool + If True, create a group if not found. If False, raise an Exception. + (Default value = True) Returns ------- @@ -1863,7 +1846,7 @@ def get_base_units(self, input_units, check_nonmult=True, system=None): ---------- input_units : UnitsContainer or str units - check_nonmult : + check_nonmult : bool if True, None will be returned as the multiplicative factor if a non-multiplicative units is found in the final Units. (Default value = True) diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py index 970f2efa0..4b7adc30e 100644 --- a/pint/registry_helpers.py +++ b/pint/registry_helpers.py @@ -188,11 +188,11 @@ def wraps(ureg, ret, args, strict=True): Parameters ---------- - ureg : UnitRegistry + ureg : pint.UnitRegistry a UnitRegistry instance. - ret : iterable of str or iterable of Unit + ret : str, pint.Unit, iterable of str, or iterable of pint.Unit Units of each of the return values. Use `None` to skip argument conversion. - args : iterable of str or iterable of Unit + args : str, pint.Unit, iterable of str, or iterable of pint.Unit Units of each of the input arguments. Use `None` to skip argument conversion. strict : bool Indicates that only quantities are accepted. (Default value = True) @@ -299,8 +299,9 @@ def check(ureg, *args): ureg : UnitRegistry a UnitRegistry instance. - *args : iterable of str or iterable of UnitContainer - Dimensions of each of the input arguments. Use `None` to skip argument conversion. + args : str or UnitContainer or None + Dimensions of each of the input arguments. + Use `None` to skip argument conversion. Returns ------- @@ -310,10 +311,10 @@ def check(ureg, *args): Raises ------ TypeError - if the number of given dimensions does not match the number of function parameters. + If the number of given dimensions does not match the number of function + parameters. ValueError - if the any of the provided dimensions cannot be parsed as a dimension. - + If the any of the provided dimensions cannot be parsed as a dimension. """ dimensions = [ ureg.get_dimensionality(dim) if dim is not None else None for dim in args diff --git a/pint/systems.py b/pint/systems.py index fc8c53271..8627ff623 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -181,8 +181,9 @@ def from_lines(cls, lines, define_func): ---------- lines : list[str] iterable - define_func : str -> None - Function to define a unit in the registry. + define_func : callable + Function to define a unit in the registry; it must accept a single string as + a parameter. Returns ------- diff --git a/pint/unit.py b/pint/unit.py index b61cfe6fa..14bf19b95 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -116,12 +116,18 @@ def format_babel(self, spec="", **kwspec): @property def dimensionless(self): - """Return true if the Unit is dimensionless.""" + """Return True if the Unit is dimensionless; False otherwise. + """ return not bool(self.dimensionality) @property def dimensionality(self): - """Unit's dimensionality (e.g. {length: 1, time: -1})""" + """ + Returns + ------- + dict + Dimensionality of the Unit, e.g. ``{length: 1, time: -1}`` + """ try: return self._dimensionality except AttributeError: From e88926f9f47194b89325f5a3ee8ef008d81cfed7 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 14:54:16 +0000 Subject: [PATCH 293/612] CI integration --- .travis.yml | 9 ++++++--- docs/requirements.yml | 9 +++++++++ readthedocs.yml | 9 +++++++++ requirements_docs.txt | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 docs/requirements.yml create mode 100644 readthedocs.yml diff --git a/.travis.yml b/.travis.yml index a6cf799ed..337065903 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ env: # for min/max Python versions supported by uncertainties - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly + - PKGS="python=3.7 sphinx nbsphinx matplotlib numpy" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -57,7 +58,8 @@ install: - source activate travis - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - - if [[ $PKGS =~ matplotlib ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi + - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi + - if [[ $PKGS =~ matplotlib && DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list @@ -68,9 +70,10 @@ script: # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi # test notebooks too if pandas available # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - - if [[ $PANDAS == 0 && $LINT == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi + - if [[ $PANDAS == 0 && $LINT == 0 && DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - if [[ $LINT == 0 ]]; then coverage report -m; fi + - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi + - if [[ $LINT == 0 && DOCS == 0 ]]; then coverage report -m; fi after_success: - coveralls --verbose diff --git a/docs/requirements.yml b/docs/requirements.yml new file mode 100644 index 000000000..e99a59059 --- /dev/null +++ b/docs/requirements.yml @@ -0,0 +1,9 @@ +name: docs +channels: + - conda-forge +dependencies: + - python=3.7 + - sphinx + - nbsphinx + - numpy + - matplotlib diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 000000000..f992e7b61 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,9 @@ +version: 2 +sphinx: + configuration: docs/conf.py + fail_on_warning: false +conda: + environment: docs/requirements.yml +python: + version: 3.7 + setup_py_install: true diff --git a/requirements_docs.txt b/requirements_docs.txt index 1cbe5b677..4cbfa226a 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -matplotlib>=2 +matplotlib numpy nbsphinx \ No newline at end of file From efe5f47bfbf7281b5fac25afe6a6a825d2b511ce Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:03:25 +0000 Subject: [PATCH 294/612] CI integration bugfixes --- .travis.yml | 6 +++--- CHANGES | 4 ++-- docs/requirements.yml | 6 ++++-- requirements_docs.txt | 4 +++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 337065903..501f383ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: # for min/max Python versions supported by uncertainties - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly - - PKGS="python=3.7 sphinx nbsphinx matplotlib numpy" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy sphinx" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -70,10 +70,10 @@ script: # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi # test notebooks too if pandas available # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - - if [[ $PANDAS == 0 && $LINT == 0 && DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi + - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi - - if [[ $LINT == 0 && DOCS == 0 ]]; then coverage report -m; fi + - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi after_success: - coveralls --verbose diff --git a/CHANGES b/CHANGES index bd1d9835d..30c8e7dd8 100644 --- a/CHANGES +++ b/CHANGES @@ -4,8 +4,8 @@ Pint Changelog 0.11 (unreleased) ----------------- -- Nothing changed yet. - +- Fixed several Sphinx issues. Fixed intersphinx hooks to all classes missing. + (Issue #881, Thanks Guido Imperiale) 0.10 (2020-01-05) ----------------- diff --git a/docs/requirements.yml b/docs/requirements.yml index e99a59059..ba3fdb1b2 100644 --- a/docs/requirements.yml +++ b/docs/requirements.yml @@ -3,7 +3,9 @@ channels: - conda-forge dependencies: - python=3.7 - - sphinx + - ipython + - matplotlib - nbsphinx - numpy - - matplotlib + - pytest + - sphinx diff --git a/requirements_docs.txt b/requirements_docs.txt index 4cbfa226a..06e3532e9 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,5 @@ +ipython matplotlib +nbsphinx numpy -nbsphinx \ No newline at end of file +pytest From e7ce5cee1ee06b390160f4755b576220e9398969 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:12:07 +0000 Subject: [PATCH 295/612] RTD fixes --- readthedocs.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/readthedocs.yml b/readthedocs.yml index f992e7b61..7944809d1 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,4 +1,6 @@ version: 2 +build: + image: stable sphinx: configuration: docs/conf.py fail_on_warning: false @@ -6,4 +8,6 @@ conda: environment: docs/requirements.yml python: version: 3.7 - setup_py_install: true + install: + - method: pip + path: . From 9d1a00000018d3e6a23d91df3737607d7fbe1749 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:17:07 +0000 Subject: [PATCH 296/612] RTD fixes --- docs/requirements.yml | 11 ----------- readthedocs.yml | 6 +++--- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 docs/requirements.yml diff --git a/docs/requirements.yml b/docs/requirements.yml deleted file mode 100644 index ba3fdb1b2..000000000 --- a/docs/requirements.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: docs -channels: - - conda-forge -dependencies: - - python=3.7 - - ipython - - matplotlib - - nbsphinx - - numpy - - pytest - - sphinx diff --git a/readthedocs.yml b/readthedocs.yml index 7944809d1..58c7175f1 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,13 +1,13 @@ version: 2 build: - image: stable + image: latest sphinx: configuration: docs/conf.py fail_on_warning: false -conda: - environment: docs/requirements.yml python: version: 3.7 install: - method: pip path: . + extra_requirements: + - requirements_docs.txt From 433e4e7de86b8aa08a7aea1403e2284421aeecef Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:23:42 +0000 Subject: [PATCH 297/612] RTD fixes --- readthedocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readthedocs.yml b/readthedocs.yml index 58c7175f1..79392e542 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -7,7 +7,6 @@ sphinx: python: version: 3.7 install: + - requirements: requirements_docs.txt - method: pip path: . - extra_requirements: - - requirements_docs.txt From 4f854266376d88e584282f2eb0294d4895a39acc Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:34:52 +0000 Subject: [PATCH 298/612] Align travis to RTD --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 501f383ac..fd7a13f93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,8 @@ env: # for min/max Python versions supported by uncertainties - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy sphinx" + # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" From 3ceea95dfba7833d0ca1c47e03137546272bbb39 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 6 Jan 2020 16:40:02 +0000 Subject: [PATCH 299/612] travis fix --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fd7a13f93..2859e831d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,7 +60,7 @@ install: - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - - if [[ $PKGS =~ matplotlib && DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi + - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list From 068da487cdaaa6774dd8c831046e09eac059fff6 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Tue, 7 Jan 2020 18:31:31 -0700 Subject: [PATCH 300/612] Fix __array__ signature (Fixes #974) According to numpy docs, __array__ can optionally be given a dtype. We do don't do anything with it, but everything seems to work regardless. --- CHANGES | 1 + pint/quantity.py | 2 +- pint/testsuite/test_numpy.py | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 30c8e7dd8..31b438207 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Pint Changelog - Fixed several Sphinx issues. Fixed intersphinx hooks to all classes missing. (Issue #881, Thanks Guido Imperiale) +- Fixed __array__ signature to match numpy docs (Issue #974, Thanks Ryan May) 0.10 (2020-01-05) ----------------- diff --git a/pint/quantity.py b/pint/quantity.py index 747432c72..09f7aed04 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1551,7 +1551,7 @@ def _numpy_method_wrap(self, func, *args, **kwargs): else: return value - def __array__(self): + def __array__(self, t=None): warnings.warn( "The unit of the quantity is stripped when downcasting to ndarray.", UnitStrippedWarning, diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0c9d09022..13a773faf 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1036,6 +1036,12 @@ 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__"): From 1ebc4b09949d7d0a63ca58fbef1d8037836a3f30 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 7 Jan 2020 21:33:18 -0600 Subject: [PATCH 301/612] Rename sequence-of-quantities helper, with tests for size zero Quantity creation from ndarray by multiplication --- pint/numpy_func.py | 11 ++++++----- pint/testsuite/test_issues.py | 9 +++++++++ pint/testsuite/test_numpy_func.py | 24 +++++++++++++++--------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 059ffaa6e..2eda4fa05 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -38,7 +38,7 @@ def _is_quantity(obj): return hasattr(obj, "_units") and hasattr(obj, "_magnitude") -def _is_quantity_sequence(obj): +def _is_sequence_with_quantity_elements(obj): """Test for sequences of quantities. Parameters @@ -54,6 +54,7 @@ def _is_quantity_sequence(obj): iterable(obj) and sized(obj) and not isinstance(obj, str) + and len(obj) > 0 and all(_is_quantity(item) for item in obj) ) @@ -65,7 +66,7 @@ def _get_first_input_units(args, kwargs=None): for arg in chain(args, kwargs.values()): if _is_quantity(arg): return arg.units - elif _is_quantity_sequence(arg): + elif _is_sequence_with_quantity_elements(arg): return arg[0].units @@ -79,7 +80,7 @@ def convert_arg(arg, pre_calc_units): if pre_calc_units is not None: if _is_quantity(arg): return arg.m_as(pre_calc_units) - elif _is_quantity_sequence(arg): + elif _is_sequence_with_quantity_elements(arg): return [item.m_as(pre_calc_units) for item in arg] elif arg is not None: if pre_calc_units.dimensionless: @@ -89,7 +90,7 @@ def convert_arg(arg, pre_calc_units): else: if _is_quantity(arg): return arg.m - elif _is_quantity_sequence(arg): + elif _is_sequence_with_quantity_elements(arg): return [item.m for item in arg] return arg @@ -626,7 +627,7 @@ def _isin(element, test_elements, assume_unique=False, invert=False): except DimensionalityError: # Incompatible unit test elements cannot be in element return np.full(element.shape, False) - elif _is_quantity_sequence(test_elements): + elif _is_sequence_with_quantity_elements(test_elements): compatible_test_elements = [] for test_element in test_elements: try: diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 8d6bd7e16..3036022dd 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -733,6 +733,15 @@ def test_issue925(callable, q): callable(q) assert isinstance(q._magnitude, type_before) + @pytest.mark.skipif(np is None, reason="NumPy is not available") + def test_issue973(): + """Verify that an empty array Quantity can be created through multiplication.""" + q0 = np.array([]) * ureg.m # by Unit + q1 = np.array([]) * ureg("m") # by Quantity + assert isinstance(q0, ureg.Quantity) + assert isinstance(q1, ureg.Quantity) + assert len(q0) == len(q1) == 0 + except AttributeError: # Calling attributes on np will fail if NumPy is not available diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 59f0a2c13..fb36c74dc 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -5,7 +5,7 @@ from pint.compat import np from pint.numpy_func import ( _is_quantity, - _is_quantity_sequence, + _is_sequence_with_quantity_elements, convert_to_consistent_units, get_op_output_unit, implements, @@ -48,14 +48,20 @@ def test_is_quantity(self): self.assertFalse(_is_quantity("not-a-quantity")) # TODO (#905 follow-up): test other duck arrays that wrap or are wrapped by Pint - def test_is_quantity_sequence(self): - self.assertTrue(_is_quantity_sequence((self.Q_(0, "m"), self.Q_(32.0, "degF")))) - self.assertTrue(_is_quantity_sequence(np.arange(4) * self.ureg.m)) - self.assertFalse(_is_quantity_sequence((self.Q_(0), True))) - self.assertFalse(_is_quantity_sequence([1, 3, 5])) - self.assertFalse(_is_quantity_sequence(9 * self.ureg.m)) - self.assertFalse(_is_quantity_sequence(np.arange(4))) - self.assertFalse(_is_quantity_sequence("0123")) + def test_is_sequence_with_quantity_elements(self): + self.assertTrue( + _is_sequence_with_quantity_elements( + (self.Q_(0, "m"), self.Q_(32.0, "degF")) + ) + ) + self.assertTrue(_is_sequence_with_quantity_elements(np.arange(4) * self.ureg.m)) + self.assertFalse(_is_sequence_with_quantity_elements((self.Q_(0), True))) + self.assertFalse(_is_sequence_with_quantity_elements([1, 3, 5])) + self.assertFalse(_is_sequence_with_quantity_elements(9 * self.ureg.m)) + self.assertFalse(_is_sequence_with_quantity_elements(np.arange(4))) + self.assertFalse(_is_sequence_with_quantity_elements("0123")) + self.assertFalse(_is_sequence_with_quantity_elements([])) + self.assertFalse(_is_sequence_with_quantity_elements(np.array([]))) def test_convert_to_consistent_units_with_pre_calc_units(self): args, kwargs = convert_to_consistent_units( From 0c8d9f0d33f781aac827376b21077088ee963482 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 7 Jan 2020 21:36:29 -0600 Subject: [PATCH 302/612] Update CHANGES for bugfix --- CHANGES | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 31b438207..415a9cb1c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,9 +1,12 @@ Pint Changelog ============== -0.11 (unreleased) +0.10.1 (unreleased) ----------------- +- Fixed bug introduced in 0.10 that prevented creation of size-zero Quantities + from NumPy arrays by multiplication. + (Issue #977, Thanks Jon Thielen) - Fixed several Sphinx issues. Fixed intersphinx hooks to all classes missing. (Issue #881, Thanks Guido Imperiale) - Fixed __array__ signature to match numpy docs (Issue #974, Thanks Ryan May) From 1f34e42e8406af92ef26e6b7a766d302be602c5f Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 7 Jan 2020 23:25:04 -0600 Subject: [PATCH 303/612] Revert version change to CHANGES. zest.releaser takes care of that --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 415a9cb1c..c947acd6e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.10.1 (unreleased) +0.11 (unreleased) ----------------- - Fixed bug introduced in 0.10 that prevented creation of size-zero Quantities From 3864d25bff471960ece8fd61de1120f01ba701b5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 7 Jan 2020 23:29:32 -0600 Subject: [PATCH 304/612] Updated MANIFEST.in --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 63e342183..0cf68650a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt +include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt readthedocs.yml recursive-include pint * recursive-include docs * recursive-include bench * From 82b2c2f97b3f929568a07850de4f75b0d16f3c56 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 7 Jan 2020 23:47:17 -0600 Subject: [PATCH 305/612] Preparing release 0.10.1 --- CHANGES | 4 ++-- version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index c947acd6e..669661687 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,8 @@ Pint Changelog ============== -0.11 (unreleased) ------------------ +0.10.1 (2020-01-07) +------------------- - Fixed bug introduced in 0.10 that prevented creation of size-zero Quantities from NumPy arrays by multiplication. diff --git a/version.py b/version.py index 2af39f951..45afe7c5b 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ # This is just for zest.releaser. Do not touch -__version__ = "0.11.dev0" +__version__ = '0.10.1' From 919399901cdb68247e467875c280371c251e16c0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 7 Jan 2020 23:47:59 -0600 Subject: [PATCH 306/612] Back to development: 0.11 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 669661687..9534bbdcd 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.11 (unreleased) +----------------- + +- Nothing changed yet. + + 0.10.1 (2020-01-07) ------------------- diff --git a/version.py b/version.py index 45afe7c5b..be336e326 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,3 @@ # This is just for zest.releaser. Do not touch -__version__ = '0.10.1' +__version__ = '0.11.dev0' From 5da3a2da7f3a4f25cd9da4065f4d3cc7a671a1f0 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 8 Jan 2020 00:00:03 -0600 Subject: [PATCH 307/612] Make version.py black and flake8 friendly --- version.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/version.py b/version.py index be336e326..17a86c71f 100644 --- a/version.py +++ b/version.py @@ -1,3 +1,6 @@ # This is just for zest.releaser. Do not touch +# flake8: noqa +# fmt: off __version__ = '0.11.dev0' +# fmt: on From 6a0098a26e7811df09e2b0f40f0a28aed14f6233 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Wed, 8 Jan 2020 11:23:39 -0600 Subject: [PATCH 308/612] Add implementations and tests for allclose and intersect1d --- .gitignore | 1 + CHANGES | 4 ++-- docs/numpy.ipynb | 10 ++++++---- pint/numpy_func.py | 2 ++ pint/testsuite/test_numpy.py | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index d2d7e8917..d3e751fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ .DS_Store docs/_build/ .idea +.vscode build/ dist/ MANIFEST diff --git a/CHANGES b/CHANGES index 9534bbdcd..6fdb38de7 100644 --- a/CHANGES +++ b/CHANGES @@ -4,8 +4,8 @@ Pint Changelog 0.11 (unreleased) ----------------- -- Nothing changed yet. - +- Added additional NumPy function implementations (allclose, intersect1d) + (Issue #979, Thanks Jon Thielen) 0.10.1 (2020-01-07) ------------------- diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 8a66c64be..2dc6078d2 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -301,7 +301,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['alen', 'all', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'reshape', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" + "['alen', 'all', 'allclose', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'intersect1d', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'reshape', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" ] } ], @@ -522,7 +522,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 13, @@ -636,15 +636,17 @@ "name": "stdout", "output_type": "stream", "text": [ - " meter\n", + " meter\n", "\n", - "0.09488747484058625 meter\n" + "0.09462606529121113 meter\n" ] } ], "source": [ "from sparse import COO\n", "\n", + "np.random.seed(80243963)\n", + "\n", "x = np.random.random((100, 100, 100))\n", "x[x < 0.9] = 0 # fill most of the array with zeros\n", "s = COO(x)\n", diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 2eda4fa05..1fd7d8cdb 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -772,6 +772,8 @@ def implementation(*args, **kwargs): ("insert", ["arr", "values"], True), ("resize", "a", True), ("reshape", "a", True), + ("allclose", ["a", "b"], False), + ("intersect1d", ["ar1", "ar2"], True), ]: implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 13a773faf..0efb4763f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1153,6 +1153,22 @@ def pad_with(vector, pad_width, iaxis, kwargs): ), ) # Note: Does not support Quantity pad_with vectorized callable use + @helpers.requires_array_function_protocol() + def test_allclose(self): + self.assertTrue( + np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.m) + ) + self.assertFalse( + np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.mm) + ) + + @helpers.requires_array_function_protocol() + def test_intersect1d(self): + self.assertQuantityEqual( + np.intersect1d([1, 3, 4, 3] * self.ureg.m, [3, 1, 2, 1] * self.ureg.m), + [1, 3] * self.ureg.m, + ) + @unittest.skip class TestBitTwiddlingUfuncs(TestUFuncs): From 9f42c8dd3d8e6d5ba50b6ac5f205a59be3c44230 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 13 Jan 2020 22:35:29 -0600 Subject: [PATCH 309/612] Changed pytest arguments to ignores testsuite in coverage --- .coveragerc | 3 +++ .travis.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..73fab9eb7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = pint/testsuite/* + diff --git a/.travis.yml b/.travis.yml index 2859e831d..ec164c35c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ before_install: # But broke travis 2019-08 # - sudo rm -rf /dev/shm # - sudo ln -s /run/shm /dev/shm - - export TEST_OPTS="-rfsxEX -s --cov=pint" + - export TEST_OPTS="-rfsxEX -s --cov=pint --cov-config=.coveragerc" install: - conda create -n travis $PKGS pytest pytest-cov coveralls From d931ffa4d54716d15a85d24a89520e6da0f841cf Mon Sep 17 00:00:00 2001 From: Will Stott Date: Tue, 14 Jan 2020 14:41:46 +0000 Subject: [PATCH 310/612] minor typo fix --- pint/default_en.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index 6d5fe6aa4..8bd413313 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]` # # From 547bc938aa36e32443c65ff49914466974452a2d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:10:32 -0600 Subject: [PATCH 311/612] Don't raise error if key starts with _[digit] --- pint/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pint/util.py b/pint/util.py index aff507bb9..95badf1dd 100644 --- a/pint/util.py +++ b/pint/util.py @@ -876,7 +876,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.startswith("_") + and not item.lstrip('_')[0].isdigit() + or item.endswith("__") + ): raise AttributeError("%r object has no attribute %r" % (self, item)) From f6ffaf4eb5aa16108b0bc8c061820c1942776975 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:17:42 -0600 Subject: [PATCH 312/612] Update docstring for getattr_maybe_raise --- pint/util.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pint/util.py b/pint/util.py index 95badf1dd..d3c06aca9 100644 --- a/pint/util.py +++ b/pint/util.py @@ -862,12 +862,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 From 774fc6f4ffe97837796358322cdfaa9d28ba4036 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:34:23 -0600 Subject: [PATCH 313/612] Add test for no attribute error in _100km --- pint/testsuite/test_issues.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 3036022dd..79c9d6b2f 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -697,6 +697,14 @@ def test_issue932(self): with self.assertRaises(DimensionalityError): q.to("joule") + def test_issue507(self): + # leading underscore in unit works with numbers + u.define('_100km = 100 * kilometer') + battery_ec = 16 * u.kWh / u._100km + # ... but not with text + u.define('_home = 4700 * kWh / year') + with self.assertRaises(AttributeError): + home_elec_power = 1 * u._home try: From 2b10cbd4972f689555d28af29e1ce862e9e5bb44 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:35:58 -0600 Subject: [PATCH 314/612] Format strings according to black --- pint/testsuite/test_issues.py | 5 +++-- pint/util.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 79c9d6b2f..0f9da3518 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -699,13 +699,14 @@ def test_issue932(self): def test_issue507(self): # leading underscore in unit works with numbers - u.define('_100km = 100 * kilometer') + u.define("_100km = 100 * kilometer") battery_ec = 16 * u.kWh / u._100km # ... but not with text - u.define('_home = 4700 * kWh / year') + u.define("_home = 4700 * kWh / year") with self.assertRaises(AttributeError): home_elec_power = 1 * u._home + try: @pytest.mark.skipif(np is None, reason="NumPy is not available") diff --git a/pint/util.py b/pint/util.py index d3c06aca9..55ed7e2c9 100644 --- a/pint/util.py +++ b/pint/util.py @@ -882,7 +882,7 @@ def getattr_maybe_raise(self, item): # automatically prefixed with the class name - which may be a subclass of self if ( item.startswith("_") - and not item.lstrip('_')[0].isdigit() + and not item.lstrip("_")[0].isdigit() or item.endswith("__") ): raise AttributeError("%r object has no attribute %r" % (self, item)) From f912b9375006154eeffa929333a2d0a8052a2ca8 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:45:47 -0600 Subject: [PATCH 315/612] Add example to documentation --- docs/defining.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/defining.rst b/docs/defining.rst index 7aba5bbdf..152893a86 100644 --- a/docs/defining.rst +++ b/docs/defining.rst @@ -138,3 +138,17 @@ Same for aliases and derived dimensions: .. warning:: Units, prefixes, aliases and dimensions added programmatically are forgotten when the program ends. + + +Units with constants +-------------------- + +Some units, such as ``L/100km``, contain constants. These can be defined with a +leading underscore: + +.. doctest:: + + >>> ureg.define('_100km = 100 * kilometer') + >>> ureg.define('mpg = 1 * mile / gallon') + >>> fuel_ec_europe = 5 * ureg.L / ureg._100km + >>> fuel_ec_us = (1 / fuel_ec_europe).to(ureg.mpg) From 275d23908fa0d3289f3e6e4cc97534db4f1d8f64 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 02:47:27 -0600 Subject: [PATCH 316/612] Add entry to CHANGES file --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 6fdb38de7..6c645b933 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,8 @@ Pint Changelog - Added additional NumPy function implementations (allclose, intersect1d) (Issue #979, Thanks Jon Thielen) +- Allow constants in units by using a leading underscore (Issue #989, Thanks + Juan Nunez-Iglesias) 0.10.1 (2020-01-07) ------------------- From d0d8674286fd54ac40d1485d899fc62868b565b2 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 21:58:20 -0600 Subject: [PATCH 317/612] Fix incorrect use of u instead of ureg in tests --- pint/testsuite/test_issues.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 0f9da3518..29b777170 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -699,12 +699,12 @@ def test_issue932(self): def test_issue507(self): # leading underscore in unit works with numbers - u.define("_100km = 100 * kilometer") - battery_ec = 16 * u.kWh / u._100km + ureg.define("_100km = 100 * kilometer") + battery_ec = 16 * ureg.kWh / ureg._100km # ... but not with text - u.define("_home = 4700 * kWh / year") + ureg.define("_home = 4700 * kWh / year") with self.assertRaises(AttributeError): - home_elec_power = 1 * u._home + home_elec_power = 1 * ureg._home try: From f28923e64e494948261735918da9d84c49f49bd8 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 22:03:09 -0600 Subject: [PATCH 318/612] Check for attributes containing only '_' --- pint/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/util.py b/pint/util.py index 55ed7e2c9..c31458317 100644 --- a/pint/util.py +++ b/pint/util.py @@ -880,10 +880,10 @@ 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 ( + if item.endswith("__") or ( item.startswith("_") + and len(item.lstrip("_")) > 1 and not item.lstrip("_")[0].isdigit() - or item.endswith("__") ): raise AttributeError("%r object has no attribute %r" % (self, item)) From e40f2eb31af985b865ca5e464210fbaee38fdadb Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 22:09:08 -0600 Subject: [PATCH 319/612] Add test for attribute _ and fix logic ;) --- pint/testsuite/test_issues.py | 4 ++++ pint/util.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 29b777170..451498957 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -705,6 +705,10 @@ def test_issue507(self): ureg.define("_home = 4700 * kWh / year") with self.assertRaises(AttributeError): home_elec_power = 1 * ureg._home + # ... or with *only* underscores + ureg.define("_ = 45 * km") + with self.assertRaises(AttributeError): + one_blank = 1 * ureg._ try: diff --git a/pint/util.py b/pint/util.py index c31458317..71078d3db 100644 --- a/pint/util.py +++ b/pint/util.py @@ -880,10 +880,10 @@ 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.endswith("__") or ( - item.startswith("_") - and len(item.lstrip("_")) > 1 - and not item.lstrip("_")[0].isdigit() + 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)) From 9b022e29daf8e87b97d4d3626e88992e84658038 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 Jan 2020 23:24:29 -0600 Subject: [PATCH 320/612] Ignore unassigned variables in flake8 in test suite --- pint/testsuite/test_issues.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 451498957..cc04e9f8f 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -700,15 +700,15 @@ def test_issue932(self): def test_issue507(self): # leading underscore in unit works with numbers ureg.define("_100km = 100 * kilometer") - battery_ec = 16 * ureg.kWh / ureg._100km + 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 + home_elec_power = 1 * ureg._home # noqa: F841 # ... or with *only* underscores ureg.define("_ = 45 * km") with self.assertRaises(AttributeError): - one_blank = 1 * ureg._ + one_blank = 1 * ureg._ # noqa: F841 try: From 7df09524bf16fdb8a215ad10c38627e5221de146 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 17 Jan 2020 18:34:12 +0100 Subject: [PATCH 321/612] import the code from the issue --- pint/quantity.py | 18 ++++++++++++++++++ pint/registry.py | 11 +++++++++++ pint/unit.py | 19 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index 09f7aed04..64bae6fd2 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -504,6 +504,24 @@ def compatible_units(self, *contexts): return self._REGISTRY.get_compatible_units(self._units) + def is_compatible_with(self, other, *contexts, **ctx_kwargs): + 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): diff --git a/pint/registry.py b/pint/registry.py index 4ff979042..755de87d1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -854,6 +854,17 @@ def _get_compatible_units(self, input_units, group_or_system): src_dim = self._get_dimensionality(input_units) return self._cache.dimensional_equivalents[src_dim] + def is_compatible_with(self, obj1, obj2, *contexts, **ctx_kwargs): + 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. diff --git a/pint/unit.py b/pint/unit.py index 14bf19b95..505f97565 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -15,6 +15,7 @@ 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,24 @@ def compatible_units(self, *contexts): return self._REGISTRY.get_compatible_units(self) + def is_compatible_with(self, other, *contexts, **ctx_kwargs): + 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__): From 73898ca52acc5ba5ea96963a70a1dcd67967b6a8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 17 Jan 2020 22:39:13 +0100 Subject: [PATCH 322/612] add docstrings --- pint/quantity.py | 16 ++++++++++++++++ pint/registry.py | 16 ++++++++++++++++ pint/unit.py | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index 64bae6fd2..8a3494ffb 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -505,6 +505,22 @@ def compatible_units(self, *contexts): 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) diff --git a/pint/registry.py b/pint/registry.py index 755de87d1..01b640b79 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -855,6 +855,22 @@ def _get_compatible_units(self, input_units, group_or_system): 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) diff --git a/pint/unit.py b/pint/unit.py index 505f97565..ef7aeef50 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -145,6 +145,22 @@ def compatible_units(self, *contexts): 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) From ad45eb4fcea94784ab51d163c2caa3a65f70e822 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 18 Jan 2020 10:48:56 -0600 Subject: [PATCH 323/612] Add `from_string` to all Definition subclasses. Until this commit, parsing a definition was done by partly `Definition.from_string` which then called the constructor of each Definition subclass. At the constructor level, each the string parsing continued. Now, each Definition subclass has a `from_string` method that handles all the parsing steps and the constructor should only be used with the parsed parts. --- pint/definitions.py | 301 ++++++++++++++++++++--------- pint/testsuite/test_definitions.py | 2 +- 2 files changed, 213 insertions(+), 90 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 16dd840ec..f12d02b28 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -8,17 +8,79 @@ :license: BSD, see LICENSE for more details. """ +from collections import namedtuple + from .converters import OffsetConverter, ScaleConverter from .errors import DefinitionSyntaxError from .util import ParserHelper, UnitsContainer, _is_dim +class ParsedDefinition( + namedtuple("ParsedDefinition", "name symbol aliases value rhs_parts") +): + """Splits a definition into the constitutive parts. + + A definition is given as a string with equalities in a single line. + + ---------------> rhs + a = b = c = d = e + | | | -------> aliases (optional) + | | | + | | -----------> symbol (use "_" to + | | + | ---------------> value + | + -------------------> name + + Attributes + ---------- + name : str + value : str + symbol : str or None + aliases : tuple of str + rhs : tuple of str + """ + + @classmethod + def from_string(cls, definition): + name, definition = definition.split("=", 1) + name = name.strip() + + rhs_parts = tuple(res.strip() for res in definition.split("=")) + + value, aliases = rhs_parts[0], tuple([x for x in rhs_parts[1:] if x != ""]) + symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) + if symbol == "_": + symbol = None + aliases = tuple([x for x in aliases if x != "_"]) + + return cls(name, symbol, aliases, value, rhs_parts) + + class _NotNumeric(Exception): + """Internal exception. Do not expose outside Pint + """ + def __init__(self, value): self.value = value def numeric_parse(s): + """Try parse a string into a number (without using eval). + + Parameters + ---------- + s : str + + Returns + ------- + Number + + Raises + ------ + _NotNumeric + If the string cannot be parsed as a number. + """ ph = ParserHelper.from_string(s) if len(ph): @@ -38,11 +100,16 @@ class Definition: A short name or symbol for the definition. aliases : iterable of str Other names for the unit/prefix/etc. - converter : callable - an instance of Converter. + converter : callable or Converter or None """ def __init__(self, name, symbol, aliases, converter): + + if isinstance(converter, str): + raise TypeError( + "The converter parameter cannot be an instance of `str`. Use `from_string` method" + ) + self._name = name self._symbol = symbol self._aliases = aliases @@ -54,40 +121,28 @@ def is_multiplicative(self): @classmethod def from_string(cls, definition): - """Parse a definition + """Parse a definition. Parameters ---------- - definition : - + definition : str or ParsedDefinition Returns ------- - + Definition or subclass of Definition """ - name, definition = definition.split("=", 1) - name = name.strip() - result = [res.strip() for res in definition.split("=")] + if isinstance(definition, str): + definition = ParsedDefinition.from_string(definition) - # @alias name = alias1 = alias2 = ... - if name.startswith("@alias "): - name = name[len("@alias ") :].lstrip() - return AliasDefinition(name, tuple(result)) - - value, aliases = result[0], tuple([x for x in result[1:] if x != ""]) - symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases) - if symbol == "_": - symbol = None - aliases = tuple([x for x in aliases if x != "_"]) - - if name.startswith("["): - return DimensionDefinition(name, symbol, aliases, value) - elif name.endswith("-"): - name = name.rstrip("-") - return PrefixDefinition(name, symbol, aliases, value) + if definition.name.startswith("@alias "): + return AliasDefinition.from_string(definition) + elif definition.name.startswith("["): + return DimensionDefinition.from_string(definition) + elif definition.name.endswith("-"): + return PrefixDefinition.from_string(definition) else: - return UnitDefinition(name, symbol, aliases, value) + return UnitDefinition.from_string(definition) @property def name(self): @@ -118,26 +173,43 @@ def __str__(self): class PrefixDefinition(Definition): - """Definition of a prefix.""" + """Definition of a prefix. - def __init__(self, name, symbol, aliases, converter): - if isinstance(converter, str): - try: - converter = ScaleConverter(numeric_parse(converter)) - except _NotNumeric as ex: - raise ValueError( - f"Prefix definition ('{name}') must contain only numbers, not {ex.value}" - ) + - = [= ] [= ] [ = ] [...] - aliases = tuple(alias.strip("-") for alias in aliases) - if symbol: - symbol = symbol.strip("-") - super().__init__(name, symbol, aliases, converter) + Example: + deca- = 1e+1 = da- = deka- + """ + + @classmethod + def from_string(cls, definition): + if isinstance(definition, str): + definition = ParsedDefinition.from_string(definition) + + aliases = tuple(alias.strip("-") for alias in definition.aliases) + if definition.symbol: + symbol = definition.symbol.strip("-") + else: + symbol = definition.symbol + + try: + converter = ScaleConverter(numeric_parse(definition.value)) + except _NotNumeric as ex: + raise ValueError( + f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}" + ) + + return cls(definition.name.rstrip("-"), symbol, aliases, converter) class UnitDefinition(Definition): """Definition of a unit. + = [= ] [= ] [ = ] [...] + + Example: + millennium = 1e3 * year = _ = millennia + Parameters ---------- reference : UnitsContainer @@ -150,70 +222,121 @@ class UnitDefinition(Definition): def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base - if isinstance(converter, str): - if ";" in converter: - [converter, modifiers] = converter.split(";", 2) - - try: - modifiers = dict( - (key.strip(), numeric_parse(value)) - for key, value in ( - part.split(":") for part in modifiers.split(";") - ) - ) - except _NotNumeric as ex: - raise ValueError( - f"Unit definition ('{name}') must contain only numbers in modifier, not {ex.value}" - ) - - else: - modifiers = {} - - converter = ParserHelper.from_string(converter) - if not any(_is_dim(key) for key in converter.keys()): - self.is_base = False - elif all(_is_dim(key) for key in converter.keys()): - self.is_base = True - else: - raise DefinitionSyntaxError( - "Cannot mix dimensions and units in the same definition. " - "Base units must be referenced only to dimensions. " - "Derived units must be referenced only to units." - ) - self.reference = UnitsContainer(converter) - if modifiers.get("offset", 0.0) != 0.0: - converter = OffsetConverter(converter.scale, modifiers["offset"]) - else: - converter = ScaleConverter(converter.scale) super().__init__(name, symbol, aliases, converter) + @classmethod + def from_string(cls, definition): + if isinstance(definition, str): + definition = ParsedDefinition.from_string(definition) + + if ";" in definition.value: + [converter, modifiers] = definition.value.split(";", 2) + + try: + modifiers = dict( + (key.strip(), numeric_parse(value)) + for key, value in (part.split(":") for part in modifiers.split(";")) + ) + except _NotNumeric as ex: + raise ValueError( + f"Unit definition ('{definition.name}') must contain only numbers in modifier, not {ex.value}" + ) + + else: + converter = definition.value + modifiers = {} + + converter = ParserHelper.from_string(converter) + + if not any(_is_dim(key) for key in converter.keys()): + is_base = False + elif all(_is_dim(key) for key in converter.keys()): + is_base = True + else: + raise DefinitionSyntaxError( + "Cannot mix dimensions and units in the same definition. " + "Base units must be referenced only to dimensions. " + "Derived units must be referenced only to units." + ) + reference = UnitsContainer(converter) + + if modifiers.get("offset", 0.0) != 0.0: + converter = OffsetConverter(converter.scale, modifiers["offset"]) + else: + converter = ScaleConverter(converter.scale) + + return cls( + definition.name, + definition.symbol, + definition.aliases, + converter, + reference, + is_base, + ) + class DimensionDefinition(Definition): - """Definition of a dimension.""" + """Definition of a dimension. + + [dimension name] = + + Example: + [density] = [mass] / [volume] + """ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False): self.reference = reference self.is_base = is_base - if isinstance(converter, str): - converter = ParserHelper.from_string(converter) - if not converter: - self.is_base = True - elif all(_is_dim(key) for key in converter.keys()): - self.is_base = False - else: - raise DefinitionSyntaxError( - "Base dimensions must be referenced to None. " - "Derived dimensions must only be referenced " - "to dimensions." - ) - self.reference = UnitsContainer(converter) super().__init__(name, symbol, aliases, converter=None) + @classmethod + def from_string(cls, definition): + if isinstance(definition, str): + definition = ParsedDefinition.from_string(definition) + + converter = ParserHelper.from_string(definition.value) + + if not converter: + is_base = True + elif all(_is_dim(key) for key in converter.keys()): + is_base = False + else: + raise DefinitionSyntaxError( + "Base dimensions must be referenced to None. " + "Derived dimensions must only be referenced " + "to dimensions." + ) + reference = UnitsContainer(converter) + + return cls( + definition.name, + definition.symbol, + definition.aliases, + converter, + reference, + is_base, + ) + class AliasDefinition(Definition): - """Additional alias(es) for an already existing unit""" + """Additional alias(es) for an already existing unit. + + @alias = [ = ] [...] + + Example: + @alias meter = my_meter + """ def __init__(self, name, aliases): super().__init__(name=name, symbol=None, aliases=aliases, converter=None) + + @classmethod + def from_string(cls, definition): + + if isinstance(definition, str): + definition = ParsedDefinition.from_string(definition) + + name = definition.name[len("@alias ") :].lstrip() + return AliasDefinition(name, tuple(definition.rhs_parts)) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index a5e008666..ac8e47226 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -95,7 +95,7 @@ def test_unit_definition(self): ) def test_dimension_definition(self): - x = DimensionDefinition("[time]", "", (), converter="") + x = DimensionDefinition("[time]", "", (), None, is_base=True) self.assertTrue(x.is_base) self.assertEqual(x.name, "[time]") From 7befc5bcaae53449c9229a4d3ea6648698ab9678 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 20 Jan 2020 14:11:04 +0100 Subject: [PATCH 324/612] change the implementation of `pad` to treat non-quantities as dimensionless --- pint/numpy_func.py | 10 +++++++--- pint/testsuite/test_numpy.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 1fd7d8cdb..63c16b347 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -654,10 +654,14 @@ 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 + if not _is_quantity(arg): + if arg == 0: + 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 diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0efb4763f..5c0174d13 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1067,9 +1067,18 @@ def test_resize(self): def test_pad(self): # Tests reproduced with modification from NumPy documentation a = [1, 2, 3, 4, 5] * self.ureg.m + b = self.Q_([4, 6, 8, 9, -3], "degC") + + 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=self.Q_(10, "degC")), + self.Q_([10, 10, 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 From 0226bf46972a6297ce06b3f683f802961fa68bd8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 20 Jan 2020 14:17:36 +0100 Subject: [PATCH 325/612] remove the unneeded else: --- pint/numpy_func.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 63c16b347..1073643f8 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -654,14 +654,13 @@ 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) - else: - if not _is_quantity(arg): - if arg == 0: - arg = unit._REGISTRY.Quantity(arg, unit) - else: - arg = unit._REGISTRY.Quantity(arg, "dimensionless") + elif not _is_quantity(arg): + if arg == 0: + arg = unit._REGISTRY.Quantity(arg, unit) + else: + arg = unit._REGISTRY.Quantity(arg, "dimensionless") - return arg.m_as(unit) + return arg.m_as(unit) # pad only dispatches on array argument, so we know it is a Quantity units = array.units From 90bad48e56e6cbbbb146d1abc6f3c95aa40dcf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Tue, 21 Jan 2020 21:46:40 +0100 Subject: [PATCH 326/612] Fixed #960 - Handles unit in to_compact. - Use same function infer_base_unit on Quantity based on input unit. - Add unit test in test_issues.py --- AUTHORS | 1 + CHANGES | 1 + pint/quantity.py | 2 ++ pint/testsuite/test_issues.py | 5 +++++ 4 files changed, 9 insertions(+) diff --git a/AUTHORS b/AUTHORS index 21ba865f8..db3229e6d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Other contributors, listed alphabetically, are: * Joel B. Mohler * John David Reaver * Jonas Olson +* Jules Chéron * Kaido Kert * Kenneth D. Mankoff * Kevin Davies diff --git a/CHANGES b/CHANGES index 6c645b933..115edcada 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Pint Changelog (Issue #979, Thanks Jon Thielen) - Allow constants in units by using a leading underscore (Issue #989, Thanks Juan Nunez-Iglesias) +- Fixed bug where to_compact handled prefix units incorrectly (Issue #960) 0.10.1 (2020-01-07) ------------------- diff --git a/pint/quantity.py b/pint/quantity.py index 09f7aed04..051fc849d 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -689,6 +689,8 @@ def to_compact(self, unit=None): if unit is None: unit = infer_base_unit(self) + else: + unit = infer_base_unit(self.__class__(1, unit)) q_base = self.to(unit) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index cc04e9f8f..5f4271701 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -710,6 +710,11 @@ def test_issue507(self): 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: From 43bbbba4777499eda1f670b49c3b134821cd1e54 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 19 Jan 2020 00:11:41 -0600 Subject: [PATCH 327/612] Replace 1.0 and 0.0 (floats) by 1 and 0 (integers) In the original codebase, many accumulators, default values and so on were initialized with floats (instead fo integers). The reason behind this decision was that in Python 2 the division (/) was by default an integer division if all values were ints. Therefore, using a float ensured "the right result". Now that the codebase is Python 3, we can safely use integers everywhere. This wil be helpful when a different non integer type is desired. --- pint/definitions.py | 2 +- pint/registry.py | 16 ++++++++-------- pint/systems.py | 4 ++-- pint/unit.py | 4 ++-- pint/util.py | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index f12d02b28..8ffa15322 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -261,7 +261,7 @@ def from_string(cls, definition): ) reference = UnitsContainer(converter) - if modifiers.get("offset", 0.0) != 0.0: + if modifiers.get("offset", 0) != 0: converter = OffsetConverter(converter.scale, modifiers["offset"]) else: converter = ScaleConverter(converter.scale) diff --git a/pint/registry.py b/pint/registry.py index 4ff979042..ceb47ef15 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -384,7 +384,7 @@ def _define(self, definition): raise TypeError("{} is not a valid definition.".format(definition)) # define "delta_" units for units with an offset - if getattr(definition.converter, "offset", 0.0) != 0.0: + if getattr(definition.converter, "offset", 0) != 0: if definition.name.startswith("["): d_name = "[delta_" + definition.name[1:] @@ -671,13 +671,13 @@ def _get_dimensionality(self, input_units): except KeyError: pass - accumulator = defaultdict(float) - self._get_dimensionality_recurse(input_units, 1.0, accumulator) + accumulator = defaultdict(int) + self._get_dimensionality_recurse(input_units, 1, accumulator) if "[]" in accumulator: del accumulator["[]"] - dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0.0}) + dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0}) cache[input_units] = dims @@ -776,7 +776,7 @@ def _get_root_units(self, input_units, check_nonmult=True): """ if not input_units: - return 1.0, UnitsContainer() + return 1, UnitsContainer() cache = self._cache.root_units try: @@ -784,8 +784,8 @@ def _get_root_units(self, input_units, check_nonmult=True): except KeyError: pass - accumulators = [1.0, defaultdict(float)] - self._get_root_units_recurse(input_units, 1.0, accumulators) + accumulators = [1, defaultdict(int)] + self._get_root_units_recurse(input_units, 1, accumulators) factor = accumulators[0] units = UnitsContainer({k: v for k, v in accumulators[1].items() if v != 0}) @@ -1187,7 +1187,7 @@ def _define(self, definition): definition, d, di = super()._define(definition) # define additional units for units with an offset - if getattr(definition.converter, "offset", 0.0) != 0.0: + if getattr(definition.converter, "offset", 0) != 0: self._define_adder(definition, d, di) return definition, d, di diff --git a/pint/systems.py b/pint/systems.py index 8627ff623..05d2b6fd5 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -408,7 +408,7 @@ def from_lines(cls, lines, get_root_func): # Here we invert the equation, in other words # we write old units in terms new unit and expansion new_unit_dict = { - new_unit: -1.0 / value + new_unit: -1 / value for new_unit, value in new_unit_expanded.items() if new_unit != old_unit } @@ -430,7 +430,7 @@ def from_lines(cls, lines, get_root_func): old_unit, value = dict(old_unit_dict).popitem() - base_unit_names[old_unit] = {new_unit: 1.0 / value} + base_unit_names[old_unit] = {new_unit: 1 / value} system = cls(name) diff --git a/pint/unit.py b/pint/unit.py index 14bf19b95..5db27cd4e 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -148,7 +148,7 @@ def __mul__(self, other): if isinstance(other, self.__class__): return self.__class__(self._units * other._units) else: - qself = self._REGISTRY.Quantity(1.0, self._units) + qself = self._REGISTRY.Quantity(1, self._units) return qself * other if isinstance(other, Number) and other == 1: @@ -163,7 +163,7 @@ def __truediv__(self, other): if isinstance(other, self.__class__): return self.__class__(self._units / other._units) else: - qself = 1.0 * self + qself = 1 * self return qself / other return self._REGISTRY.Quantity(1 / other, self._units) diff --git a/pint/util.py b/pint/util.py index aff507bb9..8601cfe96 100644 --- a/pint/util.py +++ b/pint/util.py @@ -296,7 +296,7 @@ class udict(dict): """Custom dict implementing __missing__.""" def __missing__(self, key): - return 0.0 + return 0 def copy(self): return udict(self) @@ -601,8 +601,8 @@ def copy(self): return self.__copy__() def __hash__(self): - if self.scale != 1.0: - mess = "Only scale 1.0 ParserHelper instance should be considered hashable" + if self.scale != 1: + mess = "Only scale 1 ParserHelper instance should be considered hashable" raise ValueError(mess) return super().__hash__() @@ -614,7 +614,7 @@ def __eq__(self, other): elif isinstance(other, Number): return self.scale == other and not len(self._d) else: - return self.scale == 1.0 and super().__eq__(other) + return self.scale == 1 and super().__eq__(other) def operate(self, items, op=operator.iadd, cleanup=True): d = udict(self._d) From 04b15c85eca0fca8ca55633b045b637ca0d3d11f Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 20 Jan 2020 22:07:06 -0600 Subject: [PATCH 328/612] Moved pi from _after_init to the constants file In this way, an empty registry will be really empty. --- pint/constants_en.txt | 2 +- pint/registry.py | 1 - pint/testsuite/test_systems.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index a7c22fcaa..fa485fadf 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/registry.py b/pint/registry.py index ceb47ef15..64b01b6cf 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -250,7 +250,6 @@ def _init_dynamic_classes(self): def _after_init(self): """This should be called after all __init__""" - self.define(UnitDefinition("pi", "π", (), ScaleConverter(math.pi))) if self._filename == "": self.load_definitions("default_en.txt", True) diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index 008d299f5..ffe084dff 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -6,7 +6,6 @@ class TestGroup(QuantityTestCase): def _build_empty_reg_root(self): ureg = UnitRegistry(None) grp = ureg.get_group("root") - grp.remove_units("pi") grp.invalidate_members() return ureg, ureg.get_group("root") @@ -163,7 +162,6 @@ class TestSystem(QuantityTestCase): def _build_empty_reg_root(self): ureg = UnitRegistry(None) grp = ureg.get_group("root") - grp.remove_units("pi") grp.invalidate_members() return ureg, ureg.get_group("root") From 98a846321f599df467192e69fbe7e5b1f68dfc77 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 21 Jan 2020 22:51:11 -0600 Subject: [PATCH 329/612] Updated CHANGES --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 6fdb38de7..df05274cf 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,13 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Moved Pi to defintions files. +- Use ints (not floats) a defaults at many points in the codebase as in Python 3 + the true division is the default one. +- **BREAKING CHANGE**: + Added `from_string` method to all Definitions subclasses. The value/converter + argument of the constructor no longer accepts an string. + It is unlikely that this change affects the end user. - Added additional NumPy function implementations (allclose, intersect1d) (Issue #979, Thanks Jon Thielen) From 19a44f697d9c2cc4999dec2f7ae65b2ca65ad1d2 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 21 Jan 2020 23:00:31 -0600 Subject: [PATCH 330/612] Removed unused import --- pint/registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 64b01b6cf..7a33ac764 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -37,7 +37,6 @@ import functools import itertools import locale -import math import os import re from collections import ChainMap, defaultdict From 032c6bd4e93f86046a702d6ff42afcf48d12e597 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 22 Jan 2020 01:04:36 -0600 Subject: [PATCH 331/612] Add support for other numerical types at the registry level Until this commit, `int` and `float` were special types in Pint (just as they are in Python). Numbers were parsed from strings as int/float from the definition files and from user provided strings; and exponents of units were also stored as int/float. This commit change this by adding a new argument (`non_int_type`) to classes and methods. It indicates how numeric values will be parsed and defaulted. Any numerical class can be used such as `float` (default), Decimal, Fraction. This argument will be found in the following places 1. UnitRegistry: used for parsing the definition files and any value provided as a string. 2. UnitsContainer: used to compare equality with strings, multiply and divide by strings (which is equivalent to parse the string) 3. All methods OUTSIDE the UnitRegistry/Quantity that can parse strings have a `non_int_type` argument. (e.g. Definition.from_string) Tests have been added for by duplicating most cases in `test_quantity.py`. (Some tests have been deleted such as those dealing with numpy.) The new file `test_non_int.py` run the tests for `Decimal`, `Fraction` and `float` (which is redundant but is kept as a crosscheck for the implementation of this testsuite) BREAKING CHANGE: `use_decimal` is deprecated. Use `non_int_type` keyword argument when instantiating the registry. >>> from decimal import Decimal >>> ureg = UnitRegistry(non_int_type=Decimal) --- CHANGES | 4 + pint/context.py | 12 +- pint/definitions.py | 34 +- pint/quantity.py | 49 +- pint/registry.py | 94 ++- pint/systems.py | 14 +- pint/testsuite/__init__.py | 8 +- pint/testsuite/test_non_int.py | 1076 ++++++++++++++++++++++++++++++++ pint/util.py | 91 ++- 9 files changed, 1284 insertions(+), 98 deletions(-) create mode 100644 pint/testsuite/test_non_int.py diff --git a/CHANGES b/CHANGES index df05274cf..a263a989e 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Add full support for Decimal and Fraction at the registry level. + **BREAKING CHANGE**: + `use_decimal` is deprecated. Use `non_int_type=Decimal` when instantiating + the registry. - Moved Pi to defintions files. - Use ints (not floats) a defaults at many points in the codebase as in Python 3 the true division is the default one. diff --git a/pint/context.py b/pint/context.py index bcc34c372..e1405d5fc 100644 --- a/pint/context.py +++ b/pint/context.py @@ -133,7 +133,7 @@ def from_context(cls, context, **defaults): return context @classmethod - def from_lines(cls, lines, to_base_func=None): + def from_lines(cls, lines, to_base_func=None, non_int_type=float): lines = SourceIterator(lines) lineno, header = next(lines) @@ -186,14 +186,20 @@ def to_num(val): func = _expression_to_function(eq) if "<->" in rel: - src, dst = (ParserHelper.from_string(s) for s in rel.split("<->")) + src, dst = ( + ParserHelper.from_string(s, non_int_type) + for s in rel.split("<->") + ) if to_base_func: src = to_base_func(src) dst = to_base_func(dst) ctx.add_transformation(src, dst, func) ctx.add_transformation(dst, src, func) elif "->" in rel: - src, dst = (ParserHelper.from_string(s) for s in rel.split("->")) + src, dst = ( + ParserHelper.from_string(s, non_int_type) + for s in rel.split("->") + ) if to_base_func: src = to_base_func(src) dst = to_base_func(dst) diff --git a/pint/definitions.py b/pint/definitions.py index 8ffa15322..327cc87cb 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -65,12 +65,13 @@ def __init__(self, value): self.value = value -def numeric_parse(s): +def numeric_parse(s, non_int_type=float): """Try parse a string into a number (without using eval). Parameters ---------- s : str + non_int_type : type Returns ------- @@ -81,7 +82,7 @@ def numeric_parse(s): _NotNumeric If the string cannot be parsed as a number. """ - ph = ParserHelper.from_string(s) + ph = ParserHelper.from_string(s, non_int_type) if len(ph): raise _NotNumeric(s) @@ -120,12 +121,13 @@ def is_multiplicative(self): return self._converter.is_multiplicative @classmethod - def from_string(cls, definition): + def from_string(cls, definition, non_int_type=float): """Parse a definition. Parameters ---------- definition : str or ParsedDefinition + non_int_type : type Returns ------- @@ -136,13 +138,13 @@ def from_string(cls, definition): definition = ParsedDefinition.from_string(definition) if definition.name.startswith("@alias "): - return AliasDefinition.from_string(definition) + return AliasDefinition.from_string(definition, non_int_type) elif definition.name.startswith("["): - return DimensionDefinition.from_string(definition) + return DimensionDefinition.from_string(definition, non_int_type) elif definition.name.endswith("-"): - return PrefixDefinition.from_string(definition) + return PrefixDefinition.from_string(definition, non_int_type) else: - return UnitDefinition.from_string(definition) + return UnitDefinition.from_string(definition, non_int_type) @property def name(self): @@ -182,7 +184,7 @@ class PrefixDefinition(Definition): """ @classmethod - def from_string(cls, definition): + def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): definition = ParsedDefinition.from_string(definition) @@ -193,7 +195,7 @@ def from_string(cls, definition): symbol = definition.symbol try: - converter = ScaleConverter(numeric_parse(definition.value)) + converter = ScaleConverter(numeric_parse(definition.value, non_int_type)) except _NotNumeric as ex: raise ValueError( f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}" @@ -226,7 +228,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal super().__init__(name, symbol, aliases, converter) @classmethod - def from_string(cls, definition): + def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): definition = ParsedDefinition.from_string(definition) @@ -235,7 +237,7 @@ def from_string(cls, definition): try: modifiers = dict( - (key.strip(), numeric_parse(value)) + (key.strip(), numeric_parse(value, non_int_type)) for key, value in (part.split(":") for part in modifiers.split(";")) ) except _NotNumeric as ex: @@ -247,7 +249,7 @@ def from_string(cls, definition): converter = definition.value modifiers = {} - converter = ParserHelper.from_string(converter) + converter = ParserHelper.from_string(converter, non_int_type) if not any(_is_dim(key) for key in converter.keys()): is_base = False @@ -292,11 +294,11 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal super().__init__(name, symbol, aliases, converter=None) @classmethod - def from_string(cls, definition): + def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): definition = ParsedDefinition.from_string(definition) - converter = ParserHelper.from_string(definition.value) + converter = ParserHelper.from_string(definition.value, non_int_type) if not converter: is_base = True @@ -308,7 +310,7 @@ def from_string(cls, definition): "Derived dimensions must only be referenced " "to dimensions." ) - reference = UnitsContainer(converter) + reference = UnitsContainer(converter, non_int_type=non_int_type) return cls( definition.name, @@ -333,7 +335,7 @@ def __init__(self, name, aliases): super().__init__(name=name, symbol=None, aliases=aliases, converter=None) @classmethod - def from_string(cls, definition): + def from_string(cls, definition, non_int_type=float): if isinstance(definition, str): definition = ParsedDefinition.from_string(definition) diff --git a/pint/quantity.py b/pint/quantity.py index 09f7aed04..1d9ed219e 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -150,6 +150,10 @@ def force_ndarray(self): def force_ndarray_like(self): return self._REGISTRY.force_ndarray_like + @property + def UnitsContainer(self): + return self._REGISTRY.UnitsContainer + def __reduce__(self): """Allow pickling quantities. Since UnitRegistries are not pickled, upon unpickling the new object is always attached to the application registry. @@ -181,7 +185,7 @@ def __new__(cls, value, units=None): inst._magnitude = _to_magnitude( value, inst.force_ndarray, inst.force_ndarray_like ) - inst._units = UnitsContainer() + inst._units = inst.UnitsContainer() elif isinstance(units, (UnitsContainer, UnitDefinition)): inst = SharedRegistryObject.__new__(cls) inst._magnitude = _to_magnitude( @@ -492,7 +496,7 @@ def from_sequence(cls, seq, units=None): @classmethod def from_tuple(cls, tup): - return cls(tup[0], UnitsContainer(tup[1])) + return cls(tup[0], cls._REGISTRY.UnitsContainer(tup[1])) def to_tuple(self): return self.m, tuple(self._units.items()) @@ -760,7 +764,7 @@ def _iadd_sub(self, other, op): # the operation. self._magnitude = op(self._magnitude, other_magnitude) elif self.dimensionless: - self.ito(UnitsContainer()) + self.ito(self.UnitsContainer()) self._magnitude = op(self._magnitude, other_magnitude) else: raise DimensionalityError(self._units, "dimensionless") @@ -870,7 +874,7 @@ def _add_sub(self, other, op): _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), ) elif self.dimensionless: - units = UnitsContainer() + units = self.UnitsContainer() magnitude = op( self.to(units)._magnitude, _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), @@ -1035,7 +1039,7 @@ def _imul_div(self, other, magnitude_op, units_op=None): except TypeError: return NotImplemented self._magnitude = magnitude_op(self._magnitude, other_magnitude) - self._units = units_op(self._units, UnitsContainer()) + self._units = units_op(self._units, self.UnitsContainer()) return self if isinstance(other, self._REGISTRY.Unit): @@ -1106,7 +1110,7 @@ def _mul_div(self, other, magnitude_op, units_op=None): return NotImplemented magnitude = magnitude_op(self._magnitude, other_magnitude) - units = units_op(self._units, UnitsContainer()) + units = units_op(self._units, self.UnitsContainer()) return self.__class__(magnitude, units) @@ -1190,7 +1194,7 @@ def __ifloordiv__(self, other): self._magnitude = self.to("")._magnitude // other else: raise DimensionalityError(self._units, "dimensionless") - self._units = UnitsContainer({}) + self._units = self.UnitsContainer({}) return self @check_implemented @@ -1201,7 +1205,7 @@ def __floordiv__(self, other): magnitude = self.to("")._magnitude // other else: raise DimensionalityError(self._units, "dimensionless") - return self.__class__(magnitude, UnitsContainer({})) + return self.__class__(magnitude, self.UnitsContainer({})) @check_implemented def __rfloordiv__(self, other): @@ -1211,19 +1215,19 @@ def __rfloordiv__(self, other): magnitude = other // self.to("")._magnitude else: raise DimensionalityError(self._units, "dimensionless") - return self.__class__(magnitude, UnitsContainer({})) + return self.__class__(magnitude, self.UnitsContainer({})) @check_implemented def __imod__(self, other): if not self._check(other): - other = self.__class__(other, UnitsContainer({})) + other = self.__class__(other, self.UnitsContainer({})) self._magnitude %= other.to(self._units)._magnitude return self @check_implemented def __mod__(self, other): if not self._check(other): - other = self.__class__(other, UnitsContainer({})) + other = self.__class__(other, self.UnitsContainer({})) magnitude = self._magnitude % other.to(self._units)._magnitude return self.__class__(magnitude, self._units) @@ -1234,16 +1238,19 @@ def __rmod__(self, other): return self.__class__(magnitude, other._units) elif self.dimensionless: magnitude = other % self.to("")._magnitude - return self.__class__(magnitude, UnitsContainer({})) + return self.__class__(magnitude, self.UnitsContainer({})) else: raise DimensionalityError(self._units, "dimensionless") @check_implemented def __divmod__(self, other): if not self._check(other): - other = self.__class__(other, UnitsContainer({})) + other = self.__class__(other, self.UnitsContainer({})) q, r = divmod(self._magnitude, other.to(self._units)._magnitude) - return (self.__class__(q, UnitsContainer({})), self.__class__(r, self._units)) + return ( + self.__class__(q, self.UnitsContainer({})), + self.__class__(r, self._units), + ) @check_implemented def __rdivmod__(self, other): @@ -1252,10 +1259,10 @@ def __rdivmod__(self, other): unit = other._units elif self.dimensionless: q, r = divmod(other, self.to("")._magnitude) - unit = UnitsContainer({}) + unit = self.UnitsContainer({}) else: raise DimensionalityError(self._units, "dimensionless") - return (self.__class__(q, UnitsContainer({})), self.__class__(r, unit)) + return (self.__class__(q, self.UnitsContainer({})), self.__class__(r, unit)) @check_implemented def __ipow__(self, other): @@ -1296,7 +1303,7 @@ def __ipow__(self, other): if other == 1: return self elif other == 0: - self._units = UnitsContainer() + self._units = self.UnitsContainer() else: if not self._is_multiplicative: if self._REGISTRY.autoconvert_offset_to_baseunit: @@ -1353,7 +1360,7 @@ def __pow__(self, other): return self elif other == 0: exponent = 0 - units = UnitsContainer() + units = self.UnitsContainer() else: if not self._is_multiplicative: if self._REGISTRY.autoconvert_offset_to_baseunit: @@ -1424,7 +1431,7 @@ def __eq__(self, other): raise OffsetUnitCalculusError(self._units) return self.dimensionless and eq( - self._convert_magnitude(UnitsContainer()), other, False + self._convert_magnitude(self.UnitsContainer()), other, False ) if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): @@ -1453,7 +1460,9 @@ def __ne__(self, other): def compare(self, other, op): if not isinstance(other, self.__class__): if self.dimensionless: - return op(self._convert_magnitude_not_inplace(UnitsContainer()), other) + return op( + self._convert_magnitude_not_inplace(self.UnitsContainer()), other + ) elif eq(other, 0, True): # Handle the special case in which we compare to zero # (or an array of zeros) diff --git a/pint/registry.py b/pint/registry.py index 7a33ac764..9379770a9 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -152,6 +152,8 @@ class BaseRegistry(metaclass=RegistryMeta): string fmt_locale : locale identifier string, used in `format_babel` + non_int_type : type + numerical type used for non integer values. (Default: float) """ @@ -164,6 +166,7 @@ class BaseRegistry(metaclass=RegistryMeta): _dir = [ "Quantity", "Unit", + "UnitsContainer", "Measurement", "define", "load_definitions", @@ -187,6 +190,7 @@ def __init__( auto_reduce_dimensions=False, preprocessors=None, fmt_locale=None, + non_int_type=float, ): self._register_parsers() self._init_dynamic_classes() @@ -205,6 +209,9 @@ def __init__( #: Default locale identifier string, used when calling format_babel without explicit locale. self.fmt_locale = self.set_fmt_locale(fmt_locale) + #: Numerical type used for non integer values. + self.non_int_type = non_int_type + #: Map between name (string) and value (string) of defaults stored in the #: definitions file. self._defaults = {} @@ -306,6 +313,9 @@ def set_fmt_locale(self, loc): self.fmt_locale = loc + def UnitsContainer(self, *args, **kwargs): + return UnitsContainer(*args, non_int_type=self.non_int_type, **kwargs) + @property def default_format(self): """Default formatting string for quantities.""" @@ -327,7 +337,7 @@ def define(self, definition): if isinstance(definition, str): for line in definition.split("\n"): - self._define(Definition.from_string(line)) + self._define(Definition.from_string(line, self.non_int_type)) else: self._define(definition) @@ -398,7 +408,7 @@ def _define(self, definition): "delta_" + alias for alias in definition.aliases ) - d_reference = UnitsContainer( + d_reference = self.UnitsContainer( {ref: value for ref, value in definition.reference.items()} ) @@ -548,7 +558,7 @@ def load_definitions(self, file, is_resource=False): raise ex else: try: - self.define(Definition.from_string(line)) + self.define(Definition.from_string(line, self.non_int_type)) except DefinitionSyntaxError as ex: if ex.lineno is None: ex.lineno = no @@ -576,7 +586,7 @@ def _build_cache(self): prefix, base_name = "", unit_name try: - uc = ParserHelper.from_word(base_name) + uc = ParserHelper.from_word(base_name, self.non_int_type) bu = self._get_root_units(uc) di = self._get_dimensionality(uc) @@ -622,7 +632,11 @@ def get_name(self, name_or_alias, case_sensitive=True): symbol = self.get_symbol(name) prefix_def = self._prefixes[prefix] self._units[name] = UnitDefinition( - name, symbol, (), prefix_def.converter, UnitsContainer({unit_name: 1}) + name, + symbol, + (), + prefix_def.converter, + self.UnitsContainer({unit_name: 1}), ) return prefix + unit_name @@ -652,6 +666,9 @@ def get_dimensionality(self, input_units): """Convert unit or dict of units or dimensions to a dict of base dimensions dimensions """ + + # TODO: This should be to_units_container(input_units, self) + # but this tries to reparse and fail for dimensions. input_units = to_units_container(input_units) return self._get_dimensionality(input_units) @@ -660,7 +677,7 @@ def _get_dimensionality(self, input_units): """Convert a UnitsContainer to base dimensions. """ if not input_units: - return UnitsContainer() + return self.UnitsContainer() cache = self._cache.dimensionality @@ -675,7 +692,7 @@ def _get_dimensionality(self, input_units): if "[]" in accumulator: del accumulator["[]"] - dims = UnitsContainer({k: v for k, v in accumulator.items() if v != 0}) + dims = self.UnitsContainer({k: v for k, v in accumulator.items() if v != 0}) cache[input_units] = dims @@ -746,7 +763,7 @@ def get_root_units(self, input_units, check_nonmult=True): multiplicative factor, base units """ - input_units = to_units_container(input_units) + input_units = to_units_container(input_units, self) f, units = self._get_root_units(input_units, check_nonmult) @@ -774,7 +791,7 @@ def _get_root_units(self, input_units, check_nonmult=True): """ if not input_units: - return 1, UnitsContainer() + return 1, self.UnitsContainer() cache = self._cache.root_units try: @@ -786,7 +803,9 @@ def _get_root_units(self, input_units, check_nonmult=True): self._get_root_units_recurse(input_units, 1, accumulators) factor = accumulators[0] - units = UnitsContainer({k: v for k, v in accumulators[1].items() if v != 0}) + units = self.UnitsContainer( + {k: v for k, v in accumulators[1].items() if v != 0} + ) # Check if any of the final units is non multiplicative and return None instead. if check_nonmult: @@ -1035,12 +1054,12 @@ def _parse_units(self, input_string, as_delta=True): pass if not input_string: - return UnitsContainer() + return self.UnitsContainer() # Sanitize input_string with whitespaces. input_string = input_string.strip() - units = ParserHelper.from_string(input_string) + units = ParserHelper.from_string(input_string, self.non_int_type) if units.scale != 1: raise ValueError("Unit expression cannot have a scaling factor.") @@ -1057,14 +1076,25 @@ def _parse_units(self, input_string, as_delta=True): cname = "delta_" + cname ret[cname] = value - ret = UnitsContainer(ret) + ret = self.UnitsContainer(ret) if as_delta: cache[input_string] = ret return ret - def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): + def _eval_token( + self, token, case_sensitive=True, use_decimal=False, **values, + ): + + # TODO: remove this code when use_decimal is deprecated + if use_decimal: + raise DeprecationWarning( + "`use_decimal` is deprecated, use `non_int_type` keyword argument when instantiating the registry.\n" + ">>> from decimal import Decimal\n" + ">>> ureg = UnitRegistry(non_int_type=Decimal)" + ) + token_type = token[0] token_text = token[1] if token_type == NAME: @@ -1075,17 +1105,17 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): else: return self.Quantity( 1, - UnitsContainer( + self.UnitsContainer( {self.get_name(token_text, case_sensitive=case_sensitive): 1} ), ) elif token_type == NUMBER: - return ParserHelper.eval_token(token, use_decimal=use_decimal) + return ParserHelper.eval_token(token, non_int_type=self.non_int_type) else: raise Exception("unknown token type") def parse_expression( - self, input_string, case_sensitive=True, use_decimal=False, **values + self, input_string, case_sensitive=True, use_decimal=False, **values, ): """Parse a mathematical expression including units and return a quantity object. @@ -1108,6 +1138,14 @@ def parse_expression( """ + # TODO: remove this code when use_decimal is deprecated + if use_decimal: + raise DeprecationWarning( + "`use_decimal` is deprecated, use `non_int_type` keyword argument when instantiating the registry.\n" + ">>> from decimal import Decimal\n" + ">>> ureg = UnitRegistry(non_int_type=Decimal)" + ) + if not input_string: return self.Quantity(1) @@ -1117,9 +1155,7 @@ def parse_expression( gen = tokenizer(input_string) return build_eval_tree(gen).evaluate( - lambda x: self._eval_token( - x, case_sensitive=case_sensitive, use_decimal=use_decimal, **values - ) + lambda x: self._eval_token(x, case_sensitive=case_sensitive, **values) ) __call__ = parse_expression @@ -1341,7 +1377,11 @@ def _register_parsers(self): def _parse_context(self, ifile): try: self.add_context( - Context.from_lines(ifile.block_iter(), self.get_dimensionality) + Context.from_lines( + ifile.block_iter(), + self.get_dimensionality, + non_int_type=self.non_int_type, + ) ) except KeyError as e: raise DefinitionSyntaxError(f"unknown dimension {e} in context") @@ -1746,10 +1786,12 @@ def _register_parsers(self): self._register_parser("@system", self._parse_system) def _parse_group(self, ifile): - self.Group.from_lines(ifile.block_iter(), self.define) + self.Group.from_lines(ifile.block_iter(), self.define, self.non_int_type) def _parse_system(self, ifile): - self.System.from_lines(ifile.block_iter(), self.get_root_units) + self.System.from_lines( + ifile.block_iter(), self.get_root_units, self.non_int_type + ) def get_group(self, name, create_if_needed=True): """Return a Group. @@ -1886,7 +1928,7 @@ def _get_base_units(self, input_units, check_nonmult=True, system=None): # as it has a UnitsContainer intermediate units = to_units_container(units, self) - destination_units = UnitsContainer() + destination_units = self.UnitsContainer() bu = self.get_system(system, False).base_units @@ -1896,7 +1938,7 @@ def _get_base_units(self, input_units, check_nonmult=True, system=None): new_unit = to_units_container(new_unit, self) destination_units *= new_unit ** value else: - destination_units *= UnitsContainer({unit: value}) + destination_units *= self.UnitsContainer({unit: value}) base_factor = self.convert(factor, units, destination_units) @@ -1970,6 +2012,7 @@ def __init__( auto_reduce_dimensions=False, preprocessors=None, fmt_locale=None, + non_int_type=float, ): super().__init__( @@ -1983,6 +2026,7 @@ def __init__( auto_reduce_dimensions=auto_reduce_dimensions, preprocessors=preprocessors, fmt_locale=fmt_locale, + non_int_type=non_int_type, ) def pi_theorem(self, quantities): diff --git a/pint/systems.py b/pint/systems.py index 05d2b6fd5..166de9115 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -174,7 +174,7 @@ def remove_groups(self, *group_names): self.invalidate_members() @classmethod - def from_lines(cls, lines, define_func): + def from_lines(cls, lines, define_func, non_int_type=float): """Return a Group object parsing an iterable of lines. Parameters @@ -208,7 +208,7 @@ def from_lines(cls, lines, define_func): for lineno, line in lines: if "=" in line: # Is a definition - definition = Definition.from_string(line) + definition = Definition.from_string(line, non_int_type=non_int_type) if not isinstance(definition, UnitDefinition): raise DefinitionSyntaxError( "Only UnitDefinition are valid inside _used_groups, not " @@ -357,7 +357,7 @@ def format_babel(self, locale): return self.name @classmethod - def from_lines(cls, lines, get_root_func): + def from_lines(cls, lines, get_root_func, non_int_type=float): lines = SourceIterator(lines) lineno, header = next(lines) @@ -399,7 +399,9 @@ def from_lines(cls, lines, get_root_func): ) # Here we find new_unit expanded in terms of root_units - new_unit_expanded = to_units_container(get_root_func(new_unit)[1]) + new_unit_expanded = to_units_container( + get_root_func(new_unit)[1], cls._REGISTRY + ) # We require that the old unit is present in the new_unit expanded if old_unit not in new_unit_expanded: @@ -421,7 +423,9 @@ def from_lines(cls, lines, get_root_func): # old_unit is inferred as the root unit with the same dimensionality. new_unit = line - old_unit_dict = to_units_container(get_root_func(line)[1]) + old_unit_dict = to_units_container( + get_root_func(line)[1], cls._REGISTRY + ) if len(old_unit_dict) != 1: raise ValueError( diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 1614f5988..4aa3e324a 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -106,7 +106,13 @@ def assertQuantityEqual(self, first, second, msg=None): def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None): if msg is None: - msg = "Comparing %r and %r. " % (first, second) + try: + msg = "Comparing %r and %r. " % (first, second) + except TypeError: + try: + msg = "Comparing %s and %s. " % (first, second) + except Exception: + msg = "Comparing" m1, m2 = self._get_comparable_magnitudes(first, second, msg) diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py new file mode 100644 index 000000000..47409f2e4 --- /dev/null +++ b/pint/testsuite/test_non_int.py @@ -0,0 +1,1076 @@ +import copy +import math +import operator as op +from decimal import Decimal +from fractions import Fraction + +from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry +from pint.testsuite import QuantityTestCase +from pint.testsuite.parameterized import ParameterizedTestMixin as ParameterizedTestCase +from pint.unit import UnitsContainer + + +class FakeWrapper: + # Used in test_upcast_type_rejection_on_creation + def __init__(self, q): + self.q = q + + +class NonIntTypeQuantityTestCase(QuantityTestCase): + + NON_INT_TYPE = None + + @classmethod + def setUpClass(cls): + cls.ureg = UnitRegistry( + force_ndarray=cls.FORCE_NDARRAY, non_int_type=cls.NON_INT_TYPE + ) + cls.Q_ = cls.ureg.Quantity + cls.U_ = cls.ureg.Unit + + def assertQuantityAlmostEqual( + self, first, second, rtol="1e-07", atol="0", msg=None + ): + + if isinstance(first, self.Q_): + self.assertIsInstance(first.m, (self.NON_INT_TYPE, int)) + else: + self.assertIsInstance(first, (self.NON_INT_TYPE, int)) + + if isinstance(second, self.Q_): + self.assertIsInstance(second.m, (self.NON_INT_TYPE, int)) + else: + self.assertIsInstance(second, (self.NON_INT_TYPE, int)) + super().assertQuantityAlmostEqual( + first, second, self.NON_INT_TYPE(rtol), self.NON_INT_TYPE(atol), msg + ) + + def QP_(self, value, units): + self.assertIsInstance(value, str) + return self.Q_(self.NON_INT_TYPE(value), units) + + +class _TestBasic: + def test_quantity_creation(self): + + value = self.NON_INT_TYPE("4.2") + + for args in ( + (value, "meter"), + (value, UnitsContainer(meter=1)), + (value, self.ureg.meter), + ("4.2*meter",), + ("4.2/meter**(-1)",), + (self.Q_(value, "meter"),), + ): + x = self.Q_(*args) + self.assertEqual(x.magnitude, value) + self.assertEqual(x.units, self.ureg.UnitsContainer(meter=1)) + + x = self.Q_(value, UnitsContainer(length=1)) + y = self.Q_(x) + self.assertEqual(x.magnitude, y.magnitude) + self.assertEqual(x.units, y.units) + self.assertIsNot(x, y) + + x = self.Q_(value, None) + self.assertEqual(x.magnitude, value) + self.assertEqual(x.units, UnitsContainer()) + + with self.capture_log() as buffer: + self.assertEqual( + value * self.ureg.meter, + self.Q_(value, self.NON_INT_TYPE("2") * self.ureg.meter), + ) + self.assertEqual(len(buffer), 1) + + def test_quantity_comparison(self): + x = self.QP_("4.2", "meter") + y = self.QP_("4.2", "meter") + z = self.QP_("5", "meter") + j = self.QP_("5", "meter*meter") + + # identity for single object + self.assertTrue(x == x) + self.assertFalse(x != x) + + # identity for multiple objects with same value + self.assertTrue(x == y) + self.assertFalse(x != y) + + self.assertTrue(x <= y) + self.assertTrue(x >= y) + self.assertFalse(x < y) + self.assertFalse(x > y) + + self.assertFalse(x == z) + self.assertTrue(x != z) + self.assertTrue(x < z) + + self.assertTrue(z != j) + + self.assertNotEqual(z, j) + self.assertEqual(self.QP_("0", "meter"), self.QP_("0", "centimeter")) + self.assertNotEqual(self.QP_("0", "meter"), self.QP_("0", "second")) + + self.assertLess(self.QP_("10", "meter"), self.QP_("5", "kilometer")) + + def test_quantity_comparison_convert(self): + self.assertEqual(self.QP_("1000", "millimeter"), self.QP_("1", "meter")) + self.assertEqual( + self.QP_("1000", "millimeter/min"), + self.Q_( + self.NON_INT_TYPE("1000") / self.NON_INT_TYPE("60"), "millimeter/s" + ), + ) + + def test_quantity_hash(self): + x = self.QP_("4.2", "meter") + x2 = self.QP_("4200", "millimeter") + y = self.QP_("2", "second") + z = self.QP_("0.5", "hertz") + self.assertEqual(hash(x), hash(x2)) + + # Dimensionless equality + self.assertEqual(hash(y * z), hash(1.0)) + + # Dimensionless equality from a different unit registry + ureg2 = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) + y2 = ureg2.Quantity(self.NON_INT_TYPE("2"), "second") + z2 = ureg2.Quantity(self.NON_INT_TYPE("0.5"), "hertz") + self.assertEqual(hash(y * z), hash(y2 * z2)) + + def test_to_base_units(self): + x = self.Q_("1*inch") + self.assertQuantityAlmostEqual(x.to_base_units(), self.QP_("0.0254", "meter")) + x = self.Q_("1*inch*inch") + self.assertQuantityAlmostEqual( + x.to_base_units(), + self.Q_( + self.NON_INT_TYPE("0.0254") ** self.NON_INT_TYPE("2.0"), "meter*meter" + ), + ) + x = self.Q_("1*inch/minute") + self.assertQuantityAlmostEqual( + x.to_base_units(), + self.Q_( + self.NON_INT_TYPE("0.0254") / self.NON_INT_TYPE("60"), "meter/second" + ), + ) + + def test_convert(self): + self.assertQuantityAlmostEqual( + self.Q_("2 inch").to("meter"), + self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"), + ) + self.assertQuantityAlmostEqual( + self.Q_("2 meter").to("inch"), + self.Q_(self.NON_INT_TYPE("2") / self.NON_INT_TYPE("0.0254"), "inch"), + ) + self.assertQuantityAlmostEqual( + self.Q_("2 sidereal_year").to("second"), self.QP_("63116297.5325", "second") + ) + self.assertQuantityAlmostEqual( + self.Q_("2.54 centimeter/second").to("inch/second"), + self.Q_("1 inch/second"), + ) + self.assertAlmostEqual(self.Q_("2.54 centimeter").to("inch").magnitude, 1) + self.assertAlmostEqual(self.Q_("2 second").to("millisecond").magnitude, 2000) + + def test_convert_from(self): + x = self.Q_("2*inch") + meter = self.ureg.meter + + # from quantity + self.assertQuantityAlmostEqual( + meter.from_(x), + self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"), + ) + self.assertQuantityAlmostEqual( + meter.m_from(x), self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254") + ) + + # from unit + self.assertQuantityAlmostEqual( + meter.from_(self.ureg.inch), self.QP_("0.0254", "meter") + ) + self.assertQuantityAlmostEqual( + meter.m_from(self.ureg.inch), self.NON_INT_TYPE("0.0254") + ) + + # from number + self.assertQuantityAlmostEqual( + meter.from_(2, strict=False), self.QP_("2", "meter") + ) + self.assertQuantityAlmostEqual( + meter.m_from(self.NON_INT_TYPE("2"), strict=False), self.NON_INT_TYPE("2") + ) + + # from number (strict mode) + self.assertRaises(ValueError, meter.from_, self.NON_INT_TYPE("2")) + self.assertRaises(ValueError, meter.m_from, self.NON_INT_TYPE("2")) + + def test_context_attr(self): + self.assertEqual(self.ureg.meter, self.QP_("1", "meter")) + + def test_both_symbol(self): + self.assertEqual(self.QP_("2", "ms"), self.QP_("2", "millisecond")) + self.assertEqual(self.QP_("2", "cm"), self.QP_("2", "centimeter")) + + def test_dimensionless_units(self): + twopi = self.NON_INT_TYPE("2") * self.ureg.pi + self.assertAlmostEqual( + self.QP_("360", "degree").to("radian").magnitude, twopi, + ) + self.assertAlmostEqual(self.Q_(twopi, "radian"), self.QP_("360", "degree")) + self.assertEqual(self.QP_("1", "radian").dimensionality, UnitsContainer()) + self.assertTrue(self.QP_("1", "radian").dimensionless) + self.assertFalse(self.QP_("1", "radian").unitless) + + self.assertEqual(self.QP_("1", "meter") / self.QP_("1", "meter"), 1) + self.assertEqual((self.QP_("1", "meter") / self.QP_("1", "mm")).to(""), 1000) + + self.assertEqual(self.Q_(10) // self.QP_("360", "degree"), 1) + self.assertEqual(self.QP_("400", "degree") // self.Q_(twopi), 1) + self.assertEqual(self.QP_("400", "degree") // twopi, 1) + self.assertEqual(7 // self.QP_("360", "degree"), 1) + + def test_offset(self): + self.assertQuantityAlmostEqual( + self.QP_("0", "kelvin").to("kelvin"), self.QP_("0", "kelvin") + ) + self.assertQuantityAlmostEqual( + self.QP_("0", "degC").to("kelvin"), self.QP_("273.15", "kelvin") + ) + self.assertQuantityAlmostEqual( + self.QP_("0", "degF").to("kelvin"), + self.QP_("255.372222", "kelvin"), + rtol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.QP_("100", "kelvin").to("kelvin"), self.QP_("100", "kelvin") + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "degC").to("kelvin"), self.QP_("373.15", "kelvin") + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "degF").to("kelvin"), + self.QP_("310.92777777", "kelvin"), + rtol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.QP_("0", "kelvin").to("degC"), self.QP_("-273.15", "degC") + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "kelvin").to("degC"), self.QP_("-173.15", "degC") + ) + self.assertQuantityAlmostEqual( + self.QP_("0", "kelvin").to("degF"), self.QP_("-459.67", "degF"), rtol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "kelvin").to("degF"), self.QP_("-279.67", "degF"), rtol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.QP_("32", "degF").to("degC"), self.QP_("0", "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "degC").to("degF"), self.QP_("212", "degF"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.QP_("54", "degF").to("degC"), self.QP_("12.2222", "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("12", "degC").to("degF"), self.QP_("53.6", "degF"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.QP_("12", "kelvin").to("degC"), self.QP_("-261.15", "degC"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("12", "degC").to("kelvin"), self.QP_("285.15", "kelvin"), atol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.QP_("12", "kelvin").to("degR"), self.QP_("21.6", "degR"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("12", "degR").to("kelvin"), + self.QP_("6.66666667", "kelvin"), + atol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.QP_("12", "degC").to("degR"), self.QP_("513.27", "degR"), atol=0.01 + ) + self.assertQuantityAlmostEqual( + self.QP_("12", "degR").to("degC"), + self.QP_("-266.483333", "degC"), + atol=0.01, + ) + + def test_offset_delta(self): + self.assertQuantityAlmostEqual( + self.QP_("0", "delta_degC").to("kelvin"), self.QP_("0", "kelvin") + ) + self.assertQuantityAlmostEqual( + self.QP_("0", "delta_degF").to("kelvin"), self.QP_("0", "kelvin"), rtol=0.01 + ) + + self.assertQuantityAlmostEqual( + self.QP_("100", "kelvin").to("delta_degC"), self.QP_("100", "delta_degC") + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "kelvin").to("delta_degF"), + self.QP_("180", "delta_degF"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "delta_degF").to("kelvin"), + self.QP_("55.55555556", "kelvin"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "delta_degC").to("delta_degF"), + self.QP_("180", "delta_degF"), + rtol=0.01, + ) + self.assertQuantityAlmostEqual( + self.QP_("100", "delta_degF").to("delta_degC"), + self.QP_("55.55555556", "delta_degC"), + rtol=0.01, + ) + + self.assertQuantityAlmostEqual( + self.QP_("12.3", "delta_degC").to("delta_degF"), + self.QP_("22.14", "delta_degF"), + rtol=0.01, + ) + + def test_pickle(self): + import pickle + + def pickle_test(q): + self.assertEqual(q, pickle.loads(pickle.dumps(q))) + + pickle_test(self.QP_("32", "")) + pickle_test(self.QP_("2.4", "")) + pickle_test(self.QP_("32", "m/s")) + pickle_test(self.QP_("2.4", "m/s")) + + def test_notiter(self): + # Verify that iter() crashes immediately, without needing to draw any + # element from it, if the magnitude isn't iterable + x = self.QP_("1", "m") + with self.assertRaises(TypeError): + iter(x) + + +class _TestQuantityBasicMath: + + FORCE_NDARRAY = False + + def _test_inplace(self, operator, value1, value2, expected_result, unit=None): + if isinstance(value1, str): + value1 = self.Q_(value1) + if isinstance(value2, str): + value2 = self.Q_(value2) + if isinstance(expected_result, str): + expected_result = self.Q_(expected_result) + + if unit is not None: + value1 = value1 * unit + value2 = value2 * unit + expected_result = expected_result * unit + + value1 = copy.copy(value1) + value2 = copy.copy(value2) + id1 = id(value1) + id2 = id(value2) + value1 = operator(value1, value2) + value2_cpy = copy.copy(value2) + self.assertQuantityAlmostEqual(value1, expected_result) + self.assertEqual(id1, id(value1)) + self.assertQuantityAlmostEqual(value2, value2_cpy) + self.assertEqual(id2, id(value2)) + + def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None): + if isinstance(value1, str): + value1 = self.Q_(value1) + if isinstance(value2, str): + value2 = self.Q_(value2) + if isinstance(expected_result, str): + expected_result = self.Q_(expected_result) + + if unit is not None: + value1 = value1 * unit + value2 = value2 * unit + expected_result = expected_result * unit + + id1 = id(value1) + id2 = id(value2) + + value1_cpy = copy.copy(value1) + value2_cpy = copy.copy(value2) + + result = operator(value1, value2) + + self.assertQuantityAlmostEqual(expected_result, result) + self.assertQuantityAlmostEqual(value1, value1_cpy) + self.assertQuantityAlmostEqual(value2, value2_cpy) + self.assertNotEqual(id(result), id1) + self.assertNotEqual(id(result), id2) + + def _test_quantity_add_sub(self, unit, func): + x = self.Q_(unit, "centimeter") + y = self.Q_(unit, "inch") + z = self.Q_(unit, "second") + a = self.Q_(unit, None) + + func(op.add, x, x, self.Q_(unit + unit, "centimeter")) + func( + op.add, x, y, self.Q_(unit + self.NON_INT_TYPE("2.54") * unit, "centimeter") + ) + func( + op.add, + y, + x, + self.Q_(unit + unit / (self.NON_INT_TYPE("2.54") * unit), "inch"), + ) + func(op.add, a, unit, self.Q_(unit + unit, None)) + self.assertRaises(DimensionalityError, op.add, self.NON_INT_TYPE("10"), x) + self.assertRaises(DimensionalityError, op.add, x, self.NON_INT_TYPE("10")) + self.assertRaises(DimensionalityError, op.add, x, z) + + func(op.sub, x, x, self.Q_(unit - unit, "centimeter")) + func( + op.sub, x, y, self.Q_(unit - self.NON_INT_TYPE("2.54") * unit, "centimeter") + ) + func( + op.sub, + y, + x, + self.Q_(unit - unit / (self.NON_INT_TYPE("2.54") * unit), "inch"), + ) + func(op.sub, a, unit, self.Q_(unit - unit, None)) + self.assertRaises(DimensionalityError, op.sub, self.NON_INT_TYPE("10"), x) + self.assertRaises(DimensionalityError, op.sub, x, self.NON_INT_TYPE("10")) + self.assertRaises(DimensionalityError, op.sub, x, z) + + def _test_quantity_iadd_isub(self, unit, func): + x = self.Q_(unit, "centimeter") + y = self.Q_(unit, "inch") + z = self.Q_(unit, "second") + a = self.Q_(unit, None) + + func(op.iadd, x, x, self.Q_(unit + unit, "centimeter")) + func( + op.iadd, + x, + y, + self.Q_(unit + self.NON_INT_TYPE("2.54") * unit, "centimeter"), + ) + func(op.iadd, y, x, self.Q_(unit + unit / self.NON_INT_TYPE("2.54"), "inch")) + func(op.iadd, a, unit, self.Q_(unit + unit, None)) + self.assertRaises(DimensionalityError, op.iadd, self.NON_INT_TYPE("10"), x) + self.assertRaises(DimensionalityError, op.iadd, x, self.NON_INT_TYPE("10")) + self.assertRaises(DimensionalityError, op.iadd, x, z) + + func(op.isub, x, x, self.Q_(unit - unit, "centimeter")) + func(op.isub, x, y, self.Q_(unit - self.NON_INT_TYPE("2.54"), "centimeter")) + func(op.isub, y, x, self.Q_(unit - unit / self.NON_INT_TYPE("2.54"), "inch")) + func(op.isub, a, unit, self.Q_(unit - unit, None)) + self.assertRaises(DimensionalityError, op.sub, self.NON_INT_TYPE("10"), x) + self.assertRaises(DimensionalityError, op.sub, x, self.NON_INT_TYPE("10")) + self.assertRaises(DimensionalityError, op.sub, x, z) + + def _test_quantity_mul_div(self, unit, func): + func(op.mul, unit * self.NON_INT_TYPE("10"), "4.2*meter", "42*meter", unit) + func(op.mul, "4.2*meter", unit * self.NON_INT_TYPE("10"), "42*meter", unit) + func(op.mul, "4.2*meter", "10*inch", "42*meter*inch", unit) + func(op.truediv, unit * self.NON_INT_TYPE("42"), "4.2*meter", "10/meter", unit) + func( + op.truediv, "4.2*meter", unit * self.NON_INT_TYPE("10"), "0.42*meter", unit + ) + func(op.truediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit) + + def _test_quantity_imul_idiv(self, unit, func): + # func(op.imul, 10.0, '4.2*meter', '42*meter') + func(op.imul, "4.2*meter", self.NON_INT_TYPE("10"), "42*meter", unit) + func(op.imul, "4.2*meter", "10*inch", "42*meter*inch", unit) + # func(op.truediv, 42, '4.2*meter', '10/meter') + func( + op.itruediv, "4.2*meter", unit * self.NON_INT_TYPE("10"), "0.42*meter", unit + ) + func(op.itruediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit) + + def _test_quantity_floordiv(self, unit, func): + a = self.Q_("10*meter") + b = self.Q_("3*second") + self.assertRaises(DimensionalityError, op.floordiv, a, b) + self.assertRaises(DimensionalityError, op.floordiv, self.NON_INT_TYPE("3"), b) + self.assertRaises(DimensionalityError, op.floordiv, a, self.NON_INT_TYPE("3")) + self.assertRaises(DimensionalityError, op.ifloordiv, a, b) + self.assertRaises(DimensionalityError, op.ifloordiv, self.NON_INT_TYPE("3"), b) + self.assertRaises(DimensionalityError, op.ifloordiv, a, self.NON_INT_TYPE("3")) + func( + op.floordiv, + unit * self.NON_INT_TYPE("10"), + "4.2*meter/meter", + self.NON_INT_TYPE("2"), + unit, + ) + func(op.floordiv, "10*meter", "4.2*inch", self.NON_INT_TYPE("93"), unit) + + def _test_quantity_mod(self, unit, func): + a = self.Q_("10*meter") + b = self.Q_("3*second") + self.assertRaises(DimensionalityError, op.mod, a, b) + self.assertRaises(DimensionalityError, op.mod, 3, b) + self.assertRaises(DimensionalityError, op.mod, a, 3) + self.assertRaises(DimensionalityError, op.imod, a, b) + self.assertRaises(DimensionalityError, op.imod, 3, b) + self.assertRaises(DimensionalityError, op.imod, a, 3) + func( + op.mod, + unit * self.NON_INT_TYPE("10"), + "4.2*meter/meter", + self.NON_INT_TYPE("1.6"), + unit, + ) + + def _test_quantity_ifloordiv(self, unit, func): + func( + op.ifloordiv, + self.NON_INT_TYPE("10"), + "4.2*meter/meter", + self.NON_INT_TYPE("2"), + unit, + ) + func(op.ifloordiv, "10*meter", "4.2*inch", self.NON_INT_TYPE("93"), unit) + + def _test_quantity_divmod_one(self, a, b): + if isinstance(a, str): + a = self.Q_(a) + if isinstance(b, str): + b = self.Q_(b) + + q, r = divmod(a, b) + self.assertEqual(q, a // b) + self.assertEqual(r, a % b) + self.assertQuantityEqual(a, (q * b) + r) + self.assertEqual(q, math.floor(q)) + if b > (0 * b): + self.assertTrue((0 * b) <= r < b) + else: + self.assertTrue((0 * b) >= r > b) + if isinstance(a, self.Q_): + self.assertEqual(r.units, a.units) + else: + self.assertTrue(r.unitless) + self.assertTrue(q.unitless) + + copy_a = copy.copy(a) + a %= b + self.assertEqual(a, r) + copy_a //= b + self.assertEqual(copy_a, q) + + def _test_quantity_divmod(self): + self._test_quantity_divmod_one("10*meter", "4.2*inch") + + # Disabling these tests as it yields different results without Quantities + # >>> from decimal import Decimal as D + # >>> divmod(-D('100'), D('3')) + # (Decimal('-33'), Decimal('-1')) + # >>> divmod(-100, 3) + # (-34, 2) + + # self._test_quantity_divmod_one("-10*meter", "4.2*inch") + # self._test_quantity_divmod_one("-10*meter", "-4.2*inch") + # self._test_quantity_divmod_one("10*meter", "-4.2*inch") + + self._test_quantity_divmod_one("400*degree", "3") + self._test_quantity_divmod_one("4", "180 degree") + self._test_quantity_divmod_one(4, "180 degree") + self._test_quantity_divmod_one("20", 4) + self._test_quantity_divmod_one("300*degree", "100 degree") + + a = self.Q_("10*meter") + b = self.Q_("3*second") + self.assertRaises(DimensionalityError, divmod, a, b) + self.assertRaises(DimensionalityError, divmod, 3, b) + self.assertRaises(DimensionalityError, divmod, a, 3) + + def _test_numeric(self, unit, ifunc): + self._test_quantity_add_sub(unit, self._test_not_inplace) + self._test_quantity_iadd_isub(unit, ifunc) + self._test_quantity_mul_div(unit, self._test_not_inplace) + self._test_quantity_imul_idiv(unit, ifunc) + self._test_quantity_floordiv(unit, self._test_not_inplace) + self._test_quantity_mod(unit, self._test_not_inplace) + self._test_quantity_divmod() + # self._test_quantity_ifloordiv(unit, ifunc) + + def test_quantity_abs_round(self): + + value = self.NON_INT_TYPE("4.2") + x = self.Q_(-value, "meter") + y = self.Q_(value, "meter") + + for fun in (abs, round, op.pos, op.neg): + zx = self.Q_(fun(x.magnitude), "meter") + zy = self.Q_(fun(y.magnitude), "meter") + rx = fun(x) + ry = fun(y) + self.assertEqual(rx, zx, "while testing {0}".format(fun)) + self.assertEqual(ry, zy, "while testing {0}".format(fun)) + self.assertIsNot(rx, zx, "while testing {0}".format(fun)) + self.assertIsNot(ry, zy, "while testing {0}".format(fun)) + + def test_quantity_float_complex(self): + x = self.QP_("-4.2", None) + y = self.QP_("4.2", None) + z = self.QP_("1", "meter") + for fun in (float, complex): + self.assertEqual(fun(x), fun(x.magnitude)) + self.assertEqual(fun(y), fun(y.magnitude)) + self.assertRaises(DimensionalityError, fun, z) + + def test_not_inplace(self): + self._test_numeric(self.NON_INT_TYPE("1.0"), self._test_not_inplace) + + +class _TestOffsetUnitMath(ParameterizedTestCase): + def setup(self): + self.ureg.autoconvert_offset_to_baseunit = False + self.ureg.default_as_delta = True + + additions = [ + # --- input tuple -------------------- | -- expected result -- + ((("100", "kelvin"), ("10", "kelvin")), ("110", "kelvin")), + ((("100", "kelvin"), ("10", "degC")), "error"), + ((("100", "kelvin"), ("10", "degF")), "error"), + ((("100", "kelvin"), ("10", "degR")), ("105.56", "kelvin")), + ((("100", "kelvin"), ("10", "delta_degC")), ("110", "kelvin")), + ((("100", "kelvin"), ("10", "delta_degF")), ("105.56", "kelvin")), + ((("100", "degC"), ("10", "kelvin")), "error"), + ((("100", "degC"), ("10", "degC")), "error"), + ((("100", "degC"), ("10", "degF")), "error"), + ((("100", "degC"), ("10", "degR")), "error"), + ((("100", "degC"), ("10", "delta_degC")), ("110", "degC")), + ((("100", "degC"), ("10", "delta_degF")), ("105.56", "degC")), + ((("100", "degF"), ("10", "kelvin")), "error"), + ((("100", "degF"), ("10", "degC")), "error"), + ((("100", "degF"), ("10", "degF")), "error"), + ((("100", "degF"), ("10", "degR")), "error"), + ((("100", "degF"), ("10", "delta_degC")), ("118", "degF")), + ((("100", "degF"), ("10", "delta_degF")), ("110", "degF")), + ((("100", "degR"), ("10", "kelvin")), ("118", "degR")), + ((("100", "degR"), ("10", "degC")), "error"), + ((("100", "degR"), ("10", "degF")), "error"), + ((("100", "degR"), ("10", "degR")), ("110", "degR")), + ((("100", "degR"), ("10", "delta_degC")), ("118", "degR")), + ((("100", "degR"), ("10", "delta_degF")), ("110", "degR")), + ((("100", "delta_degC"), ("10", "kelvin")), ("110", "kelvin")), + ((("100", "delta_degC"), ("10", "degC")), ("110", "degC")), + ((("100", "delta_degC"), ("10", "degF")), ("190", "degF")), + ((("100", "delta_degC"), ("10", "degR")), ("190", "degR")), + ((("100", "delta_degC"), ("10", "delta_degC")), ("110", "delta_degC")), + ((("100", "delta_degC"), ("10", "delta_degF")), ("105.56", "delta_degC")), + ((("100", "delta_degF"), ("10", "kelvin")), ("65.56", "kelvin")), + ((("100", "delta_degF"), ("10", "degC")), ("65.56", "degC")), + ((("100", "delta_degF"), ("10", "degF")), ("110", "degF")), + ((("100", "delta_degF"), ("10", "degR")), ("110", "degR")), + ((("100", "delta_degF"), ("10", "delta_degC")), ("118", "delta_degF")), + ((("100", "delta_degF"), ("10", "delta_degF")), ("110", "delta_degF")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) + def test_addition(self, input_tuple, expected): + self.ureg.autoconvert_offset_to_baseunit = False + qin1, qin2 = input_tuple + q1, q2 = self.QP_(*qin1), self.QP_(*qin2) + # update input tuple with new values to have correct values on failure + input_tuple = q1, q2 + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.add, q1, q2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.add(q1, q2).units, expected.units) + self.assertQuantityAlmostEqual(op.add(q1, q2), expected, atol="0.01") + + subtractions = [ + ((("100", "kelvin"), ("10", "kelvin")), ("90", "kelvin")), + ((("100", "kelvin"), ("10", "degC")), ("-183.15", "kelvin")), + ((("100", "kelvin"), ("10", "degF")), ("-160.93", "kelvin")), + ((("100", "kelvin"), ("10", "degR")), ("94.44", "kelvin")), + ((("100", "kelvin"), ("10", "delta_degC")), ("90", "kelvin")), + ((("100", "kelvin"), ("10", "delta_degF")), ("94.44", "kelvin")), + ((("100", "degC"), ("10", "kelvin")), ("363.15", "delta_degC")), + ((("100", "degC"), ("10", "degC")), ("90", "delta_degC")), + ((("100", "degC"), ("10", "degF")), ("112.22", "delta_degC")), + ((("100", "degC"), ("10", "degR")), ("367.59", "delta_degC")), + ((("100", "degC"), ("10", "delta_degC")), ("90", "degC")), + ((("100", "degC"), ("10", "delta_degF")), ("94.44", "degC")), + ((("100", "degF"), ("10", "kelvin")), ("541.67", "delta_degF")), + ((("100", "degF"), ("10", "degC")), ("50", "delta_degF")), + ((("100", "degF"), ("10", "degF")), ("90", "delta_degF")), + ((("100", "degF"), ("10", "degR")), ("549.67", "delta_degF")), + ((("100", "degF"), ("10", "delta_degC")), ("82", "degF")), + ((("100", "degF"), ("10", "delta_degF")), ("90", "degF")), + ((("100", "degR"), ("10", "kelvin")), ("82", "degR")), + ((("100", "degR"), ("10", "degC")), ("-409.67", "degR")), + ((("100", "degR"), ("10", "degF")), ("-369.67", "degR")), + ((("100", "degR"), ("10", "degR")), ("90", "degR")), + ((("100", "degR"), ("10", "delta_degC")), ("82", "degR")), + ((("100", "degR"), ("10", "delta_degF")), ("90", "degR")), + ((("100", "delta_degC"), ("10", "kelvin")), ("90", "kelvin")), + ((("100", "delta_degC"), ("10", "degC")), ("90", "degC")), + ((("100", "delta_degC"), ("10", "degF")), ("170", "degF")), + ((("100", "delta_degC"), ("10", "degR")), ("170", "degR")), + ((("100", "delta_degC"), ("10", "delta_degC")), ("90", "delta_degC")), + ((("100", "delta_degC"), ("10", "delta_degF")), ("94.44", "delta_degC")), + ((("100", "delta_degF"), ("10", "kelvin")), ("45.56", "kelvin")), + ((("100", "delta_degF"), ("10", "degC")), ("45.56", "degC")), + ((("100", "delta_degF"), ("10", "degF")), ("90", "degF")), + ((("100", "delta_degF"), ("10", "degR")), ("90", "degR")), + ((("100", "delta_degF"), ("10", "delta_degC")), ("82", "delta_degF")), + ((("100", "delta_degF"), ("10", "delta_degF")), ("90", "delta_degF")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) + def test_subtraction(self, input_tuple, expected): + self.ureg.autoconvert_offset_to_baseunit = False + qin1, qin2 = input_tuple + q1, q2 = self.QP_(*qin1), self.QP_(*qin2) + input_tuple = q1, q2 + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.sub, q1, q2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.sub(q1, q2).units, expected.units) + self.assertQuantityAlmostEqual(op.sub(q1, q2), expected, atol=0.01) + + multiplications = [ + ((("100", "kelvin"), ("10", "kelvin")), ("1000", "kelvin**2")), + ((("100", "kelvin"), ("10", "degC")), "error"), + ((("100", "kelvin"), ("10", "degF")), "error"), + ((("100", "kelvin"), ("10", "degR")), ("1000", "kelvin*degR")), + ((("100", "kelvin"), ("10", "delta_degC")), ("1000", "kelvin*delta_degC")), + ((("100", "kelvin"), ("10", "delta_degF")), ("1000", "kelvin*delta_degF")), + ((("100", "degC"), ("10", "kelvin")), "error"), + ((("100", "degC"), ("10", "degC")), "error"), + ((("100", "degC"), ("10", "degF")), "error"), + ((("100", "degC"), ("10", "degR")), "error"), + ((("100", "degC"), ("10", "delta_degC")), "error"), + ((("100", "degC"), ("10", "delta_degF")), "error"), + ((("100", "degF"), ("10", "kelvin")), "error"), + ((("100", "degF"), ("10", "degC")), "error"), + ((("100", "degF"), ("10", "degF")), "error"), + ((("100", "degF"), ("10", "degR")), "error"), + ((("100", "degF"), ("10", "delta_degC")), "error"), + ((("100", "degF"), ("10", "delta_degF")), "error"), + ((("100", "degR"), ("10", "kelvin")), ("1000", "degR*kelvin")), + ((("100", "degR"), ("10", "degC")), "error"), + ((("100", "degR"), ("10", "degF")), "error"), + ((("100", "degR"), ("10", "degR")), ("1000", "degR**2")), + ((("100", "degR"), ("10", "delta_degC")), ("1000", "degR*delta_degC")), + ((("100", "degR"), ("10", "delta_degF")), ("1000", "degR*delta_degF")), + ((("100", "delta_degC"), ("10", "kelvin")), ("1000", "delta_degC*kelvin")), + ((("100", "delta_degC"), ("10", "degC")), "error"), + ((("100", "delta_degC"), ("10", "degF")), "error"), + ((("100", "delta_degC"), ("10", "degR")), ("1000", "delta_degC*degR")), + ((("100", "delta_degC"), ("10", "delta_degC")), ("1000", "delta_degC**2")), + ( + (("100", "delta_degC"), ("10", "delta_degF")), + ("1000", "delta_degC*delta_degF"), + ), + ((("100", "delta_degF"), ("10", "kelvin")), ("1000", "delta_degF*kelvin")), + ((("100", "delta_degF"), ("10", "degC")), "error"), + ((("100", "delta_degF"), ("10", "degF")), "error"), + ((("100", "delta_degF"), ("10", "degR")), ("1000", "delta_degF*degR")), + ( + (("100", "delta_degF"), ("10", "delta_degC")), + ("1000", "delta_degF*delta_degC"), + ), + ((("100", "delta_degF"), ("10", "delta_degF")), ("1000", "delta_degF**2")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) + def test_multiplication(self, input_tuple, expected): + self.ureg.autoconvert_offset_to_baseunit = False + qin1, qin2 = input_tuple + q1, q2 = self.QP_(*qin1), self.QP_(*qin2) + input_tuple = q1, q2 + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.mul(q1, q2).units, expected.units) + self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + + divisions = [ + ((("100", "kelvin"), ("10", "kelvin")), ("10", "")), + ((("100", "kelvin"), ("10", "degC")), "error"), + ((("100", "kelvin"), ("10", "degF")), "error"), + ((("100", "kelvin"), ("10", "degR")), ("10", "kelvin/degR")), + ((("100", "kelvin"), ("10", "delta_degC")), ("10", "kelvin/delta_degC")), + ((("100", "kelvin"), ("10", "delta_degF")), ("10", "kelvin/delta_degF")), + ((("100", "degC"), ("10", "kelvin")), "error"), + ((("100", "degC"), ("10", "degC")), "error"), + ((("100", "degC"), ("10", "degF")), "error"), + ((("100", "degC"), ("10", "degR")), "error"), + ((("100", "degC"), ("10", "delta_degC")), "error"), + ((("100", "degC"), ("10", "delta_degF")), "error"), + ((("100", "degF"), ("10", "kelvin")), "error"), + ((("100", "degF"), ("10", "degC")), "error"), + ((("100", "degF"), ("10", "degF")), "error"), + ((("100", "degF"), ("10", "degR")), "error"), + ((("100", "degF"), ("10", "delta_degC")), "error"), + ((("100", "degF"), ("10", "delta_degF")), "error"), + ((("100", "degR"), ("10", "kelvin")), ("10", "degR/kelvin")), + ((("100", "degR"), ("10", "degC")), "error"), + ((("100", "degR"), ("10", "degF")), "error"), + ((("100", "degR"), ("10", "degR")), ("10", "")), + ((("100", "degR"), ("10", "delta_degC")), ("10", "degR/delta_degC")), + ((("100", "degR"), ("10", "delta_degF")), ("10", "degR/delta_degF")), + ((("100", "delta_degC"), ("10", "kelvin")), ("10", "delta_degC/kelvin")), + ((("100", "delta_degC"), ("10", "degC")), "error"), + ((("100", "delta_degC"), ("10", "degF")), "error"), + ((("100", "delta_degC"), ("10", "degR")), ("10", "delta_degC/degR")), + ((("100", "delta_degC"), ("10", "delta_degC")), ("10", "")), + ( + (("100", "delta_degC"), ("10", "delta_degF")), + ("10", "delta_degC/delta_degF"), + ), + ((("100", "delta_degF"), ("10", "kelvin")), ("10", "delta_degF/kelvin")), + ((("100", "delta_degF"), ("10", "degC")), "error"), + ((("100", "delta_degF"), ("10", "degF")), "error"), + ((("100", "delta_degF"), ("10", "degR")), ("10", "delta_degF/degR")), + ( + (("100", "delta_degF"), ("10", "delta_degC")), + ("10", "delta_degF/delta_degC"), + ), + ((("100", "delta_degF"), ("10", "delta_degF")), ("10", "")), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) + def test_truedivision(self, input_tuple, expected): + self.ureg.autoconvert_offset_to_baseunit = False + qin1, qin2 = input_tuple + q1, q2 = self.QP_(*qin1), self.QP_(*qin2) + input_tuple = q1, q2 + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.truediv, q1, q2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.truediv(q1, q2).units, expected.units) + self.assertQuantityAlmostEqual(op.truediv(q1, q2), expected, atol=0.01) + + multiplications_with_autoconvert_to_baseunit = [ + ((("100", "kelvin"), ("10", "degC")), ("28315.0", "kelvin**2")), + ((("100", "kelvin"), ("10", "degF")), ("26092.78", "kelvin**2")), + ((("100", "degC"), ("10", "kelvin")), ("3731.5", "kelvin**2")), + ((("100", "degC"), ("10", "degC")), ("105657.42", "kelvin**2")), + ((("100", "degC"), ("10", "degF")), ("97365.20", "kelvin**2")), + ((("100", "degC"), ("10", "degR")), ("3731.5", "kelvin*degR")), + ((("100", "degC"), ("10", "delta_degC")), ("3731.5", "kelvin*delta_degC")), + ((("100", "degC"), ("10", "delta_degF")), ("3731.5", "kelvin*delta_degF")), + ((("100", "degF"), ("10", "kelvin")), ("3109.28", "kelvin**2")), + ((("100", "degF"), ("10", "degC")), ("88039.20", "kelvin**2")), + ((("100", "degF"), ("10", "degF")), ("81129.69", "kelvin**2")), + ((("100", "degF"), ("10", "degR")), ("3109.28", "kelvin*degR")), + ((("100", "degF"), ("10", "delta_degC")), ("3109.28", "kelvin*delta_degC")), + ((("100", "degF"), ("10", "delta_degF")), ("3109.28", "kelvin*delta_degF")), + ((("100", "degR"), ("10", "degC")), ("28315.0", "degR*kelvin")), + ((("100", "degR"), ("10", "degF")), ("26092.78", "degR*kelvin")), + ((("100", "delta_degC"), ("10", "degC")), ("28315.0", "delta_degC*kelvin")), + ((("100", "delta_degC"), ("10", "degF")), ("26092.78", "delta_degC*kelvin")), + ((("100", "delta_degF"), ("10", "degC")), ("28315.0", "delta_degF*kelvin")), + ((("100", "delta_degF"), ("10", "degF")), ("26092.78", "delta_degF*kelvin")), + ] + + @ParameterizedTestCase.parameterize( + ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit + ) + def test_multiplication_with_autoconvert(self, input_tuple, expected): + self.ureg.autoconvert_offset_to_baseunit = True + qin1, qin2 = input_tuple + q1, q2 = self.QP_(*qin1), self.QP_(*qin2) + input_tuple = q1, q2 + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.mul(q1, q2).units, expected.units) + self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + + multiplications_with_scalar = [ + ((("10", "kelvin"), "2"), ("20.0", "kelvin")), + ((("10", "kelvin**2"), "2"), ("20.0", "kelvin**2")), + ((("10", "degC"), "2"), ("20.0", "degC")), + ((("10", "1/degC"), "2"), "error"), + ((("10", "degC**0.5"), "2"), "error"), + ((("10", "degC**2"), "2"), "error"), + ((("10", "degC**-2"), "2"), "error"), + ] + + @ParameterizedTestCase.parameterize( + ("input", "expected_output"), multiplications_with_scalar + ) + def test_multiplication_with_scalar(self, input_tuple, expected): + self.ureg.default_as_delta = False + in1, in2 = input_tuple + if type(in1) is tuple: + in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2) + else: + in1, in2 = in1, self.QP_(*in2) + input_tuple = in1, in2 # update input_tuple for better tracebacks + if expected == "error": + self.assertRaises(OffsetUnitCalculusError, op.mul, in1, in2) + else: + expected = self.QP_(*expected) + self.assertEqual(op.mul(in1, in2).units, expected.units) + self.assertQuantityAlmostEqual(op.mul(in1, in2), expected, atol="0.01") + + divisions_with_scalar = [ # without / with autoconvert to base unit + ((("10", "kelvin"), "2"), [("5.0", "kelvin"), ("5.0", "kelvin")]), + ((("10", "kelvin**2"), "2"), [("5.0", "kelvin**2"), ("5.0", "kelvin**2")]), + ((("10", "degC"), "2"), ["error", "error"]), + ((("10", "degC**2"), "2"), ["error", "error"]), + ((("10", "degC**-2"), "2"), ["error", "error"]), + (("2", ("10", "kelvin")), [("0.2", "1/kelvin"), ("0.2", "1/kelvin")]), + # (('2', ('10', "degC")), ["error", (2 / 283.15, "1/kelvin")]), + (("2", ("10", "degC**2")), ["error", "error"]), + (("2", ("10", "degC**-2")), ["error", "error"]), + ] + + @ParameterizedTestCase.parameterize( + ("input", "expected_output"), divisions_with_scalar + ) + def test_division_with_scalar(self, input_tuple, expected): + self.ureg.default_as_delta = False + in1, in2 = input_tuple + if type(in1) is tuple: + in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2) + else: + in1, in2 = self.NON_INT_TYPE(in1), self.QP_(*in2) + input_tuple = in1, in2 # update input_tuple for better tracebacks + expected_copy = expected[:] + for i, mode in enumerate([False, True]): + self.ureg.autoconvert_offset_to_baseunit = mode + if expected_copy[i] == "error": + self.assertRaises(OffsetUnitCalculusError, op.truediv, in1, in2) + else: + expected = self.QP_(*expected_copy[i]) + self.assertEqual(op.truediv(in1, in2).units, expected.units) + self.assertQuantityAlmostEqual(op.truediv(in1, in2), expected) + + exponentiation = [ # resuls without / with autoconvert + ((("10", "degC"), "1"), [("10", "degC"), ("10", "degC")]), + # ((('10', "degC"), 0.5), ["error", (283.15 ** '0.5', "kelvin**0.5")]), + ((("10", "degC"), "0"), [("1.0", ""), ("1.0", "")]), + # ((('10', "degC"), -1), ["error", (1 / (10 + 273.15), "kelvin**-1")]), + # ((('10', "degC"), -2), ["error", (1 / (10 + 273.15) ** 2.0, "kelvin**-2")]), + # ((('0', "degC"), -2), ["error", (1 / (273.15) ** 2, "kelvin**-2")]), + # ((('10', "degC"), ('2', "")), ["error", ((283.15) ** 2, "kelvin**2")]), + ((("10", "degC"), ("10", "degK")), ["error", "error"]), + ( + (("10", "kelvin"), ("2", "")), + [("100.0", "kelvin**2"), ("100.0", "kelvin**2")], + ), + (("2", ("2", "kelvin")), ["error", "error"]), + # (('2', ('500.0', "millikelvin/kelvin")), [2 ** 0.5, 2 ** 0.5]), + # (('2', ('0.5', "kelvin/kelvin")), [2 ** 0.5, 2 ** 0.5]), + # ( + # (('10', "degC"), ('500.0', "millikelvin/kelvin")), + # ["error", (283.15 ** '0.5', "kelvin**0.5")], + # ), + ] + + @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) + def test_exponentiation(self, input_tuple, expected): + self.ureg.default_as_delta = False + in1, in2 = input_tuple + if type(in1) is tuple and type(in2) is tuple: + in1, in2 = self.QP_(*in1), self.QP_(*in2) + elif not type(in1) is tuple and type(in2) is tuple: + in1, in2 = self.NON_INT_TYPE(in1), self.QP_(*in2) + else: + in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2) + input_tuple = in1, in2 + expected_copy = expected[:] + for i, mode in enumerate([False, True]): + self.ureg.autoconvert_offset_to_baseunit = mode + if expected_copy[i] == "error": + self.assertRaises( + (OffsetUnitCalculusError, DimensionalityError), op.pow, in1, in2 + ) + else: + if type(expected_copy[i]) is tuple: + expected = self.QP_(*expected_copy[i]) + self.assertEqual(op.pow(in1, in2).units, expected.units) + else: + expected = expected_copy[i] + self.assertQuantityAlmostEqual(op.pow(in1, in2), expected) + + +class NonIntTypeQuantityTestQuantityFloat(_TestBasic, NonIntTypeQuantityTestCase): + + NON_INT_TYPE = float + + +class NonIntTypeQuantityTestQuantityBasicMathFloat( + _TestQuantityBasicMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = float + + +class NonIntTypeQuantityTestOffsetUnitMathFloat( + _TestOffsetUnitMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = float + + +class NonIntTypeQuantityTestQuantityDecimal(_TestBasic, NonIntTypeQuantityTestCase): + + NON_INT_TYPE = Decimal + + +class NonIntTypeQuantityTestQuantityBasicMathDecimal( + _TestQuantityBasicMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = Decimal + + +class NonIntTypeQuantityTestOffsetUnitMathDecimal( + _TestOffsetUnitMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = Decimal + + +class NonIntTypeQuantityTestQuantityFraction(_TestBasic, NonIntTypeQuantityTestCase): + + NON_INT_TYPE = Fraction + + +class NonIntTypeQuantityTestQuantityBasicMathFraction( + _TestQuantityBasicMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = Fraction + + +class NonIntTypeQuantityTestOffsetUnitMathFraction( + _TestOffsetUnitMath, NonIntTypeQuantityTestCase +): + + NON_INT_TYPE = Fraction diff --git a/pint/util.py b/pint/util.py index 8601cfe96..68bed2e28 100644 --- a/pint/util.py +++ b/pint/util.py @@ -13,9 +13,8 @@ import operator import re from collections.abc import Mapping -from decimal import Decimal from fractions import Fraction -from functools import lru_cache +from functools import lru_cache, partial from logging import NullHandler from numbers import Number from token import NAME, NUMBER @@ -178,14 +177,16 @@ def pi_theorem(quantities, registry=None): if registry is None: getdim = lambda x: x + non_int_type = float else: getdim = registry.get_dimensionality + non_int_type = registry.non_int_type for name, value in quantities.items(): if isinstance(value, str): - value = ParserHelper.from_string(value) + value = ParserHelper.from_string(value, non_int_type=non_int_type) if isinstance(value, dict): - dims = getdim(UnitsContainer(value)) + dims = getdim(registry.UnitsContainer(value)) elif not hasattr(value, "dimensionality"): dims = getdim(value) else: @@ -318,9 +319,21 @@ class UnitsContainer(Mapping): """ - __slots__ = ("_d", "_hash") + __slots__ = ("_d", "_hash", "_one", "_non_int_type") def __init__(self, *args, **kwargs): + if args and isinstance(args[0], UnitsContainer): + default_non_int_type = args[0]._non_int_type + else: + default_non_int_type = float + + self._non_int_type = kwargs.pop("non_int_type", default_non_int_type) + + if self._non_int_type is float: + self._one = 1 + else: + self._one = self._non_int_type("1") + d = udict(*args, **kwargs) self._d = d for key, value in d.items(): @@ -328,8 +341,8 @@ def __init__(self, *args, **kwargs): raise TypeError("key must be a str, not {}".format(type(key))) if not isinstance(value, Number): raise TypeError("value must be a number, not {}".format(type(value))) - if not isinstance(value, float): - d[key] = float(value) + if not isinstance(value, int) and not isinstance(value, self._non_int_type): + d[key] = self._non_int_type(value) self._hash = None def copy(self): @@ -411,7 +424,7 @@ def __eq__(self, other): other = other._d elif isinstance(other, str): - other = ParserHelper.from_string(other) + other = ParserHelper.from_string(other, self._non_int_type) other = other._d return dict.__eq__(self._d, other) @@ -436,6 +449,8 @@ def __copy__(self): out = object.__new__(self.__class__) out._d = self._d.copy() out._hash = self._hash + out._non_int_type = self._non_int_type + out._one = self._one return out def __mul__(self, other): @@ -512,7 +527,7 @@ def __init__(self, scale=1, *args, **kwargs): self.scale = scale @classmethod - def from_word(cls, input_word): + def from_word(cls, input_word, non_int_type=float): """Creates a ParserHelper object with a single variable with exponent one. Equivalent to: ParserHelper({'word': 1}) @@ -526,27 +541,41 @@ def from_word(cls, input_word): ------- """ - return cls(1, [(input_word, 1)]) + if non_int_type is float: + return cls(1, [(input_word, 1)], non_int_type=non_int_type) + else: + ONE = non_int_type("1.0") + return cls(ONE, [(input_word, ONE)], non_int_type=non_int_type) @classmethod - def eval_token(cls, token, use_decimal=False): + def eval_token(cls, token, use_decimal=False, non_int_type=float): + + # TODO: remove this code when use_decimal is deprecated + if use_decimal: + raise DeprecationWarning( + "`use_decimal` is deprecated, use `non_int_type` keyword argument when instantiating the registry.\n" + ">>> from decimal import Decimal\n" + ">>> ureg = UnitRegistry(non_int_type=Decimal)" + ) + token_type = token.type token_text = token.string if token_type == NUMBER: - try: - return int(token_text) - except ValueError: - if use_decimal: - return Decimal(token_text) - return float(token_text) + if non_int_type is float: + try: + return int(token_text) + except ValueError: + return float(token_text) + else: + return non_int_type(token_text) elif token_type == NAME: - return ParserHelper.from_word(token_text) + return ParserHelper.from_word(token_text, non_int_type=non_int_type) else: raise Exception("unknown token type") @classmethod @lru_cache() - def from_string(cls, input_string): + def from_string(cls, input_string, non_int_type=float): """Parse linear expression mathematical units and return a quantity object. Parameters @@ -559,7 +588,7 @@ def from_string(cls, input_string): """ if not input_string: - return cls() + return cls(non_int_type=non_int_type) input_string = string_preprocessor(input_string) if "[" in input_string: @@ -571,10 +600,12 @@ def from_string(cls, input_string): reps = False gen = tokenizer(input_string) - ret = build_eval_tree(gen).evaluate(cls.eval_token) + ret = build_eval_tree(gen).evaluate( + partial(cls.eval_token, non_int_type=non_int_type) + ) if isinstance(ret, Number): - return ParserHelper(ret) + return ParserHelper(ret, non_int_type=non_int_type) if reps: ret = ParserHelper( @@ -583,6 +614,7 @@ def from_string(cls, input_string): key.replace("__obra__", "[").replace("__cbra__", "]"): value for key, value in ret.items() }, + non_int_type=non_int_type, ) for k in list(ret): @@ -610,7 +642,7 @@ def __eq__(self, other): if isinstance(other, ParserHelper): return self.scale == other.scale and super().__eq__(other) elif isinstance(other, str): - return self == ParserHelper.from_string(other) + return self == ParserHelper.from_string(other, self._non_int_type) elif isinstance(other, Number): return self.scale == other and not len(self._d) else: @@ -626,7 +658,7 @@ def operate(self, items, op=operator.iadd, cleanup=True): for key in keys: del d[key] - return self.__class__(self.scale, d) + return self.__class__(self.scale, d, non_int_type=self._non_int_type) def __str__(self): tmp = "{%s}" % ", ".join( @@ -642,7 +674,7 @@ def __repr__(self): def __mul__(self, other): if isinstance(other, str): - new = self.add(other, 1) + new = self.add(other, self._one) elif isinstance(other, Number): new = self.copy() new.scale *= other @@ -659,7 +691,7 @@ def __pow__(self, other): d = self._d.copy() for key in self._d: d[key] *= other - return self.__class__(self.scale ** other, d) + return self.__class__(self.scale ** other, d, non_int_type=self._non_int_type) def __truediv__(self, other): if isinstance(other, str): @@ -679,7 +711,7 @@ def __truediv__(self, other): def __rtruediv__(self, other): new = self.__pow__(-1) if isinstance(other, str): - new = new.add(other, 1) + new = new.add(other, self._one) elif isinstance(other, Number): new.scale *= other elif isinstance(other, self.__class__): @@ -833,7 +865,10 @@ def to_units_container(unit_like, registry=None): else: return ParserHelper.from_string(unit_like) elif dict in mro: - return UnitsContainer(unit_like) + if registry: + return registry.UnitsContainer(unit_like) + else: + return UnitsContainer(unit_like) def infer_base_unit(q): From 590841165af6a4281c657ca27be8a985d7cb47fd Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 22 Jan 2020 01:50:57 -0600 Subject: [PATCH 332/612] Removed passing registry to to_units_container in Systems.from_lines --- pint/systems.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pint/systems.py b/pint/systems.py index 166de9115..2a8f802f8 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -423,9 +423,7 @@ def from_lines(cls, lines, get_root_func, non_int_type=float): # old_unit is inferred as the root unit with the same dimensionality. new_unit = line - old_unit_dict = to_units_container( - get_root_func(line)[1], cls._REGISTRY - ) + old_unit_dict = to_units_container(get_root_func(line)[1]) if len(old_unit_dict) != 1: raise ValueError( From 36ebb0f0e280bbb5b1cd32475e4b13d0222a371c Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 22 Jan 2020 01:52:06 -0600 Subject: [PATCH 333/612] Adjusted tests to new int default for UnitsContainer and ParserHelper --- pint/testsuite/test_util.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 52c7c8239..5f7117312 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -2,7 +2,6 @@ import copy import math import operator as op -from decimal import Decimal from pint.testsuite import BaseTestCase, QuantityTestCase from pint.util import ( @@ -51,8 +50,8 @@ def _test_not_inplace(self, operator, value1, value2, expected_result): def test_unitcontainer_creation(self): x = UnitsContainer(meter=1, second=2) - y = UnitsContainer({"meter": 1.0, "second": 2.0}) - self.assertIsInstance(x["meter"], float) + y = UnitsContainer({"meter": 1, "second": 2}) + self.assertIsInstance(x["meter"], int) self.assertEqual(x, y) self.assertIsNot(x, y) z = copy.copy(x) @@ -68,10 +67,10 @@ def test_unitcontainer_repr(self): self.assertEqual(repr(x), "") x = UnitsContainer(meter=1, second=2) self.assertEqual(str(x), "meter * second ** 2") - self.assertEqual(repr(x), "") + self.assertEqual(repr(x), "") x = UnitsContainer(meter=1, second=2.5) self.assertEqual(str(x), "meter * second ** 2.5") - self.assertEqual(repr(x), "") + self.assertEqual(repr(x), "") def test_unitcontainer_bool(self): self.assertTrue(UnitsContainer(meter=1, second=2)) @@ -161,7 +160,7 @@ def test_basic(self): self.assertEqual(x(), xp()) self.assertNotEqual(x(), y()) self.assertEqual(ParserHelper.from_string(""), ParserHelper()) - self.assertEqual(repr(x()), "") + self.assertEqual(repr(x()), "") self.assertEqual(ParserHelper(2), 2) @@ -199,10 +198,7 @@ def _test_eval_token(self, expected, expression, use_decimal=False): def test_eval_token(self): self._test_eval_token(1000.0, "1e3") self._test_eval_token(1000.0, "1E3") - self._test_eval_token(Decimal(1000), "1e3", use_decimal=True) self._test_eval_token(1000, "1000") - # integer numbers are represented as ints, not Decimals - self._test_eval_token(1000, "1000", use_decimal=True) def test_nan(self): for s in ("nan", "NAN", "NaN", "123 NaN nan NAN 456"): From 17c5514dce35022566a7a00efc36b335767c1c63 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 29 Jan 2020 18:00:19 +0100 Subject: [PATCH 334/612] add install instructions and a warning about the status of pint-pandas --- docs/pint-pandas.ipynb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index 390831ed3..c1ac467d3 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -6,7 +6,22 @@ "source": [ "# Pandas support\n", "\n", - "It is convenient to use the Pandas package when dealing with numerical data, so Pint provides PintArray. A PintArray is a Pandas Extension Array, which allows Pandas to recognise the Quantity and store it in Pandas DataFrames and Series." + "

\n", + "\n", + "**Warning:** pandas support is currently experimental, don't expect everything to work.\n", + "\n", + "
\n", + "\n", + "It is convenient to use the Pandas package when dealing with numerical data, so Pint provides PintArray. A PintArray is a Pandas Extension Array, which allows Pandas to recognise the Quantity and store it in Pandas DataFrames and Series.\n", + "\n", + "
\n", + "\n", + "**Note:** Pandas support is provided by `pint-pandas`. However, it is not available on PyPI yet, install it with\n", + "```\n", + "python -m pip install git+https://github.com/hgrecco/pint-pandas.git\n", + "```\n", + "\n", + "
\n" ] }, { From 1673da70cdab6070643c1a93f3e19d625aeb3e53 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 29 Jan 2020 18:01:21 +0100 Subject: [PATCH 335/612] use pintpandas when accessing pandas functionality --- docs/pint-pandas.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index c1ac467d3..dd392c59f 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -1234,7 +1234,7 @@ } ], "source": [ - "pint.PintType.ureg.default_format = \"~P\"\n", + "pintpandas.PintType.ureg.default_format = \"~P\"\n", "df_.pint.dequantify()" ] }, @@ -1387,7 +1387,7 @@ "metadata": {}, "outputs": [], "source": [ - "PA_ = pint.PintArray" + "PA_ = pintpandas.PintArray" ] }, { @@ -1420,7 +1420,7 @@ "metadata": {}, "outputs": [], "source": [ - "pint.PintType.ureg = ureg" + "pintpandas.PintType.ureg = ureg" ] }, { From 9138c874a79717c95cfea33ee4ebdc6682fb3553 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 29 Jan 2020 18:02:10 +0100 Subject: [PATCH 336/612] style fixes --- docs/pint-pandas.ipynb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index dd392c59f..d56f26687 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -546,8 +546,8 @@ } ], "source": [ - "df = pd.read_csv(io.StringIO(test_data),header=[0,1])\n", - "# df = pd.read_csv(\"/path/to/test_data.csv\",header=[0,1])\n", + "df = pd.read_csv(io.StringIO(test_data), header=[0, 1])\n", + "# df = pd.read_csv(\"/path/to/test_data.csv\", header=[0, 1])\n", "df" ] }, @@ -705,7 +705,7 @@ } ], "source": [ - "df_.speed*df_.torque" + "df_.speed * df_.torque" ] }, { @@ -889,7 +889,7 @@ } ], "source": [ - "df_['mech power'] = df_.speed*df_.torque\n", + "df_['mech power'] = df_.speed * df_.torque\n", "df_['fluid power'] = df_['fuel flow rate'] * df_['rail pressure']\n", "df_" ] @@ -1403,8 +1403,8 @@ "metadata": {}, "outputs": [], "source": [ - "ureg=pint.UnitRegistry()\n", - "Q_=ureg.Quantity" + "ureg = pint.UnitRegistry()\n", + "Q_ = ureg.Quantity" ] }, { From 0a03462515bca32258f948ae17c97a30d2c5b51d Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 29 Jan 2020 18:27:33 +0100 Subject: [PATCH 337/612] fix a reference in the numpy page --- docs/numpy.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 2dc6078d2..4ed462b25 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -778,7 +778,7 @@ "To achive these function and ufunc overrides, Pint uses the ``__array_function__`` and ``__array_ufunc__`` protocols respectively, as recommened by NumPy. This means that functions and ufuncs that Pint does not explicitly handle will error, rather than return a value with units stripped (in contrast to Pint's behavior prior to v0.10). For more\n", "information on these protocols, see .\n", "\n", - "This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see [wrapping](wrapping.html)).\n", + "This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see [wrapping](wrapping.rst)).\n", "\n", "Array interface protocol attributes (such as `__array_struct__` and\n", "`__array_interface__`) are available on Pint Quantities by deferring to the corresponding `__array_*` attribute on the magnitude as casted to an ndarray. This has been found to be potentially incorrect and to cause unexpected behavior, and has therefore been deprecated. As of the next minor version of Pint (or when the `PINT_ARRAY_PROTOCOL_FALLBACK` environment variable is set to 0 prior to importing Pint as done at the beginning of this page), attempting to access these attributes will instead raise an AttributeError." From ba09ff7c4762cbecd629c0dcd18b54f3bc297641 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 29 Jan 2020 18:27:59 +0100 Subject: [PATCH 338/612] replace the note with a installation section --- docs/pint-pandas.ipynb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index d56f26687..e213aff24 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -12,16 +12,20 @@ "\n", "\n", "\n", - "It is convenient to use the Pandas package when dealing with numerical data, so Pint provides PintArray. A PintArray is a Pandas Extension Array, which allows Pandas to recognise the Quantity and store it in Pandas DataFrames and Series.\n", + "It is convenient to use the Pandas package when dealing with numerical data, so Pint provides PintArray. A PintArray is a Pandas Extension Array, which allows Pandas to recognise the Quantity and store it in Pandas DataFrames and Series." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Installation\n", "\n", - "
\n", "\n", - "**Note:** Pandas support is provided by `pint-pandas`. However, it is not available on PyPI yet, install it with\n", + "Pandas support is provided by `pint-pandas`. It is not available on PyPI yet, to install it use\n", "```\n", "python -m pip install git+https://github.com/hgrecco/pint-pandas.git\n", - "```\n", - "\n", - "
\n" + "```" ] }, { From ae9f3c1022493cdf09eb6a36f833adcb0cb74add Mon Sep 17 00:00:00 2001 From: 5igno Date: Tue, 4 Feb 2020 15:21:46 +0100 Subject: [PATCH 339/612] Change default numpy datatype to np.ptype("int") The test function test_result_type_numpy_func checks the datatype for [[1, 2], [3, 4]] * self.ureg.m to be integer. The test originally ensured the datatype to be np.ptype("int64"), but that fails in 32 bit environments. Using np.ptype("int") instead ensure that the quantity is of default integer type, making numpy itself decide whether it should be np.ptype("int64") or np.ptype("int32"). - [x] Closes #1006 - [x] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors - [x] The change is fully covered by automated unit tests - [ ] ~~Documented in docs/ as appropriate~~ - [ ] Added an entry to the CHANGES file --- pint/testsuite/test_numpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0efb4763f..b0bc9e3e7 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -885,7 +885,7 @@ def test_trim_zeros_numpy_func(self): @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): From ccea5e1d32f1186ed4b5a23fa1c5740cf4cc9de4 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 4 Feb 2020 22:26:42 -0300 Subject: [PATCH 340/612] Reorganize long_description Remove AUTHORS and CHANGES and replace them by a link to github to improve readability in PyPI Closes #983 --- README.rst | 9 +++++++++ setup.cfg | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6c994665f..f90ea4551 100644 --- a/README.rst +++ b/README.rst @@ -142,6 +142,13 @@ ufuncs are supported including automatic conversion of units. For example quantity will be radian. +Pint is maintained by a community of scientists, programmers and entusiasts around the world. +See AUTHORS_ for a complete list. + +To review an ordered list of notable changes for each version of a project, +see CHANGES_ + + .. _Website: http://www.dimensionalanalysis.org/ .. _`comprehensive list of physical units, prefixes and constants`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt .. _`uncertainties package`: https://pythonhosted.org/uncertainties/ @@ -150,3 +157,5 @@ quantity will be radian. .. _`Babel`: http://babel.pocoo.org/ .. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types .. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb +.. _`AUTHORS`: https://github.com/hgrecco/pint/blob/master/AUTHORS +.. _`CHANGES`: https://github.com/hgrecco/pint/blob/master/CHANGES \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a95b2ca1a..83320c1c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ author = Hernan E. Grecco author_email = hernan.grecco@gmail.com license = BSD description = Physical quantities module -long_description = file: README.rst, AUTHORS, CHANGES +long_description = file: README.rst keywords = physical, quantities, unit, conversion, science url = https://github.com/hgrecco/pint classifiers = From 5b60ba0afb9f875bd08895ee9746dbf7264f5a5c Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 4 Feb 2020 22:28:08 -0300 Subject: [PATCH 341/612] Remove `default_en_0.6.txt` This file is no longer necessary and it can always be fetched from github if needed Closes #985 --- pint/default_en_0.6.txt | 360 ---------------------------------------- 1 file changed, 360 deletions(-) delete mode 100644 pint/default_en_0.6.txt diff --git a/pint/default_en_0.6.txt b/pint/default_en_0.6.txt deleted file mode 100644 index fb722c0c1..000000000 --- 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 From e14d78b18c7e324a1d13b63f596350020671fa07 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 5 Feb 2020 10:14:37 -0300 Subject: [PATCH 342/612] Updated CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 785ad6ce8..32b3a423b 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Remove `default_en_0.6.txt` +- Reorganize long_description. - Moved Pi to defintions files. - Use ints (not floats) a defaults at many points in the codebase as in Python 3 the true division is the default one. From 056810bf0160cb370eb85f4eed0560791f5ad8a9 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 5 Feb 2020 14:39:56 +0100 Subject: [PATCH 343/612] also allow nan as a special value --- pint/numpy_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 1073643f8..10f411479 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -655,7 +655,7 @@ def _recursive_convert(arg, unit): if iterable(arg): return tuple(_recursive_convert(a, unit=unit) for a in arg) elif not _is_quantity(arg): - if arg == 0: + if arg == 0 or np.isnan(arg): arg = unit._REGISTRY.Quantity(arg, unit) else: arg = unit._REGISTRY.Quantity(arg, "dimensionless") From d7af72a11bcf45c47a911a4ec5c3abf2f2d6ac0a Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 5 Feb 2020 14:47:11 +0100 Subject: [PATCH 344/612] test that passing a bare np.nan does not raise --- pint/testsuite/test_numpy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index b03b0a6ee..595aa3f5e 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1067,15 +1067,17 @@ def test_resize(self): def test_pad(self): # Tests reproduced with modification from NumPy documentation a = [1, 2, 3, 4, 5] * self.ureg.m - b = self.Q_([4, 6, 8, 9, -3], "degC") + b = self.Q_([4.0, 6.0, 8.0, 9.0, -3.0], "degC") 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(b, (2, 1), "constant", constant_values=self.Q_(10, "degC")), - self.Q_([10, 10, 4, 6, 8, 9, -3, 10], "degC"), + 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 From 95efc46795c2ec22d78c41cd81394728c0488cd1 Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Thu, 6 Feb 2020 20:07:10 +0000 Subject: [PATCH 345/612] Implement parse_pattern function --- docs/tutorial.rst | 50 ++++++++++++++++++++++----- pint/registry.py | 69 +++++++++++++++++++++++++++++++++++++ pint/testsuite/test_unit.py | 41 ++++++++++++++++++++++ 3 files changed, 151 insertions(+), 9 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 158d2ad8b..969344470 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -251,14 +251,14 @@ or .. note:: Pint´s rule for parsing strings with a mixture of numbers and units is that **units are treated with the same precedence as numbers**. - + For example, the unit of .. doctest:: >>> Q_('3 l / 100 km') - + may be unexpected first but is a consequence of applying this rule. Use brackets to get the expected result: @@ -272,6 +272,38 @@ brackets to get the expected result: exposed to when parsing information from untrusted sources. +Strings containing values can be parsed using the ``ureg.parse_pattern`` function. A ``format``-like string with the units defined in it is used as the pattern: + +.. doctest:: + + >>> input_string = '10 feet 10 inches' + >>> pattern = '{feet} feet {inch} inches' + >>> ureg.parse_pattern(input_string, pattern) + [10.0 , 10.0 ] + +To search for multiple matches, set the ``many`` parameter to ``True``. The following example also demonstrates how the parser is able to find matches in amongst filler characters: + +.. doctest:: + + >>> input_string = '10 feet - 20 feet ! 30 feet.' + >>> pattern = '{feet} feet' + >>> ureg.parse_pattern(input_string, pattern, many=True) + [[10.0 ], [20.0 ], [30.0 ]] + +The full power of regex can also be employed when writing patterns: + +.. doctest:: + + >>> input_string = "10` - 20 feet ! 30 ft." + >>> pattern = r"{feet}(`| feet| ft)" + >>> ureg.parse_pattern(input_string, pattern, many=True) + [[10.0 ], [20.0 ], [30.0 ]] + +*Note that the curly brackets (``{}``) are converted to a float-matching pattern by the parser.* + +This function is useful for tasks such as bulk extraction of units from thousands of uniform strings or even very large texts with units dotted around in no particular pattern. + + .. _sec-string-formatting: String formatting @@ -303,12 +335,12 @@ Pint supports float formatting for numpy arrays as well: >>> # scientific form formatting with unit pretty printing >>> print('The array is {:+.2E~P}'.format(accel)) The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² - + Pint also supports 'f-strings'_ from python>=3.6 : .. doctest:: - >>> accel = 1.3 * ureg['meter/second**2'] + >>> accel = 1.3 * ureg['meter/second**2'] >>> print(f'The str is {accel}') The str is 1.3 meter / second ** 2 >>> print(f'The str is {accel:.3e}') @@ -318,7 +350,7 @@ 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}') - The str is 1.3 m/s² + The str is 1.3 m/s² But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: @@ -349,11 +381,11 @@ If you want to use abbreviated unit names, prefix the specification with `~`: The same is true for latex (`L`) and HTML (`H`) specs. .. note:: - The abbreviated unit is drawn from the unit registry where the 3rd item in the - equivalence chain (ie 1 = 2 = **3**) will be returned when the prefix '~' is + The abbreviated unit is drawn from the unit registry where the 3rd item in the + equivalence chain (ie 1 = 2 = **3**) will be returned when the prefix '~' is used. The 1st item in the chain is the canonical name of the unit. -The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatting +The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatting syntax'_ for custom float representations. For example, scientific notation: ..doctest:: @@ -438,4 +470,4 @@ also define the registry as the application registry:: .. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html .. _`Babel`: http://babel.pocoo.org/ .. _'formatting syntax': https://docs.python.org/3/library/string.html#format-specification-mini-language -.. _'f-strings': https://www.python.org/dev/peps/pep-0498/ \ No newline at end of file +.. _'f-strings': https://www.python.org/dev/peps/pep-0498/ diff --git a/pint/registry.py b/pint/registry.py index 4ff979042..6aca174b5 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -85,6 +85,19 @@ _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. @@ -1086,6 +1099,62 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): 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/test_unit.py b/pint/testsuite/test_unit.py index 0b6c086a8..d7eb64906 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -658,6 +658,47 @@ def test_parse_units(self): 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 From a22c1bbf1db5dc3a7017b9c37ec4bc5eaeb95039 Mon Sep 17 00:00:00 2001 From: Jellby Date: Sat, 8 Feb 2020 18:04:05 +0100 Subject: [PATCH 346/612] Adding pint-convert script (see #1012) --- CHANGES | 5 +- pint/constants_en.txt | 2 +- pint/pint-convert | 115 ++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 3 +- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100755 pint/pint-convert diff --git a/CHANGES b/CHANGES index 32b3a423b..19d02a62b 100644 --- a/CHANGES +++ b/CHANGES @@ -4,9 +4,10 @@ Pint Changelog 0.11 (unreleased) ----------------- -- Remove `default_en_0.6.txt` +- Added pint-convert script. +- Remove `default_en_0.6.txt`. - Reorganize long_description. -- Moved Pi to defintions files. +- Moved Pi to definitions files. - Use ints (not floats) a defaults at many points in the codebase as in Python 3 the true division is the default one. - **BREAKING CHANGE**: diff --git a/pint/constants_en.txt b/pint/constants_en.txt index fa485fadf..c3ecbf1c2 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/pint-convert b/pint/pint-convert new file mode 100755 index 000000000..d2540a307 --- /dev/null +++ b/pint/pint-convert @@ -0,0 +1,115 @@ +#!/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 +from pint import UnitRegistry +import sys +import re + +parser = argparse.ArgumentParser(description='Unit converter.') +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') +args = parser.parse_args() + +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/setup.cfg b/setup.cfg index 83320c1c0..277759975 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ python_requires = >=3.6 install_requires = setuptools setup_requires = setuptools; setuptools_scm test_suite = pint.testsuite.testsuite +scripts = pint/pint-convert [options.extras_require] numpy = numpy >= 1.14 @@ -74,4 +75,4 @@ use_parentheses=True line_length=88 [zest.releaser] -python-file-with-version = version.py \ No newline at end of file +python-file-with-version = version.py From c9fb459cea94b20937105da463cd2f65d3837628 Mon Sep 17 00:00:00 2001 From: Jellby Date: Sun, 9 Feb 2020 21:46:12 +0100 Subject: [PATCH 347/612] Typo fix --- pint/pint-convert | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pint/pint-convert b/pint/pint-convert index d2540a307..ff17dec31 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ - pint_convert + pint-convert ~~~~~~~~~~~~ :copyright: 2020 by Pint Authors, see AUTHORS for more details. @@ -9,9 +9,10 @@ """ import argparse -from pint import UnitRegistry -import sys import re +import sys + +from pint import UnitRegistry parser = argparse.ArgumentParser(description='Unit converter.') parser.add_argument('-s', '--system', metavar='sys', default='SI', help='unit system to convert to (default: SI)') From 5c51692227fe81d595764a35e685a435d91253cf Mon Sep 17 00:00:00 2001 From: Jellby Date: Mon, 10 Feb 2020 20:12:33 +0100 Subject: [PATCH 348/612] Add pint-convert documentation --- README.rst | 10 ++++- docs/index.rst | 1 + docs/pint-convert.rst | 91 +++++++++++++++++++++++++++++++++++++++++++ pint/pint-convert | 8 +++- 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 docs/pint-convert.rst diff --git a/README.rst b/README.rst index f90ea4551..97cda301a 100644 --- a/README.rst +++ b/README.rst @@ -87,12 +87,20 @@ Documentation Full documentation is available at http://pint.readthedocs.org/ + GUI Website ----------- This Website_ wraps Pint's "dimensional analysis" methods to provide a GUI. +Command-line converter +---------------------- + +A command-line script `pint-convert` provides a quick way to convert between +units or get conversion factors. + + Design principles ----------------- @@ -158,4 +166,4 @@ see CHANGES_ .. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types .. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb .. _`AUTHORS`: https://github.com/hgrecco/pint/blob/master/AUTHORS -.. _`CHANGES`: https://github.com/hgrecco/pint/blob/master/CHANGES \ No newline at end of file +.. _`CHANGES`: https://github.com/hgrecco/pint/blob/master/CHANGES diff --git a/docs/index.rst b/docs/index.rst index e72ed87f8..5d22dda15 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -128,6 +128,7 @@ User Guide systems currencies pint-pandas.ipynb + pint-convert More information ---------------- diff --git a/docs/pint-convert.rst b/docs/pint-convert.rst new file mode 100644 index 000000000..8d548f8cf --- /dev/null +++ b/docs/pint-convert.rst @@ -0,0 +1,91 @@ +.. _convert: + +Command-line script +=================== + +The script `pint-convert` allows a quick conversion to a target system or +between arbitrary compatible units. + +By default, `pint-convert` converts to SI units:: + + $ pint-convert 225lb + 225 pound = 102.05828325 kg + +use the `--sys` argument to change it:: + + $ pint-convert --sys US 102kg + 102 kilogram = 224.871507429 lb + +or specify directly the target units:: + + $ pint-convert 102kg lb + 102 kilogram = 224.871507429 lb + +The input quantity can contain expressions:: + + $ pint-convert 7ft+2in + 7.166666666666667 foot = 2.1844 m + +in some cases parentheses and quotes may be needed:: + + $ pint-convert "225lb/(7ft+2in)" + 31.3953488372093 pound / foot = 46.7214261353 kg/m + +If a number is omitted, 1 is assumed:: + + $ pint-convert km mi + 1 kilometer = 0.621371192237 mi + +The default precision is 12 significant figures, it can be changed with `-p`, +but note that the accuracy may be affected by floating-point errors:: + + $ pint-convert -p 3 mi + 1 mile = 1.61e+03 m + + $ pint-convert -p 30 ly km + 1 light_year = 9460730472580.80078125 km + +Some contexts are automatically enabled, allowing conversion between not fully +compatible units:: + + $ pint-convert 540nm + 540 nanometer = 5.4e-07 m + + $ pint-convert kcal/mol + $ 1.0 kilocalorie / mole = 4184 kg·m²/mol/s² + + $ pint-convert 540nm kcal/mol + 540 nanometer = 52.9471025594 kcal/mol + +With the `uncertainties` package, the experimental uncertainty in the physical +constants is considered, and the result is given in compact notation, with the +uncertainty in the last figures in parentheses:: + + $ pint-convert Eh eV + 1 hartree = 27.21138624599(5) eV + +The precision is limited by both the maximum number of significant digits (`-p`) +and the maximum number of uncertainty digits (`-u`, 2 by default):: + + $ pint-convert -p 20 Eh eV + 1 hartree = 27.211386245988(52) eV + + $ pint-convert -p 20 -u 4 Eh eV + 1 hartree = 27.21138624598847(5207) eV + +The uncertainty can be disabled with `-U`):: + + $ pint-convert -p 20 -U Eh eV + 1 hartree = 27.211386245988471444 eV + +Correlations between experimental constants are also known, and taken into +account. Use `-C` to disable it:: + + $ pint-convert --sys atomic m_p + 1 proton_mass = 1836.15267344(11) m_e + + $ pint-convert --sys atomic -C m_p + 1 proton_mass = 1836.15267344(79) m_e + +Again, note that results may differ slightly, usually in the last figure, from +more authoritative sources, mainly due to floating-point errors. diff --git a/pint/pint-convert b/pint/pint-convert index ff17dec31..da7df23a8 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -14,7 +14,7 @@ import sys from pint import UnitRegistry -parser = argparse.ArgumentParser(description='Unit converter.') +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)') @@ -22,7 +22,11 @@ parser.add_argument('-U', '--no-unc', dest='unc', action='store_false', help='ig 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') -args = parser.parse_args() +try: + args = parser.parse_args() +except SystemExit: + parser.print_help() + raise ureg = UnitRegistry() ureg.auto_reduce_dimensions = True From fa86d1af3cbb7904a53f08d4e0c08e4cec25519a Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 11 Feb 2020 00:45:11 -0300 Subject: [PATCH 349/612] Make `__str__` and `__format__` locale aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this commit, all string formatting operations are locale aware >>> ureg.set_fmt_locale('fr_FR') >>> str(accel) '1.3 mètre par seconde²' >>> "%s" % accel '1.3 mètre par seconde²' >>> "{}".format(accel) '1.3 mètre par seconde²' It should not break any code as the default locale value for the Registry is `None` (meaning do not localize). Close #984 --- CHANGES | 1 + docs/tutorial.rst | 19 ++++++++++++++++++- pint/quantity.py | 6 ++++++ pint/registry.py | 5 ++++- pint/testsuite/helpers.py | 4 ++++ pint/testsuite/test_babel.py | 35 +++++++++++++++++++++++++++++++++-- 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 32b3a423b..6273f3666 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Make `__str__` and `__format__` locale aware - Remove `default_en_0.6.txt` - Reorganize long_description. - Moved Pi to defintions files. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 969344470..ac9c2b3de 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -419,10 +419,27 @@ Finally, if Babel_ is installed you can translate unit names to any language >>> accel.format_babel(locale='fr_FR') '1.3 mètre par seconde²' -You can also specify the format locale at u +You can also specify the format locale at the registry level either at creation: >>> ureg = UnitRegistry(fmt_locale='fr_FR') +or later: + +.. doctest:: + + >>> ureg.set_fmt_locale('fr_FR') + +and by doing that, string formatting is now localized: + +.. doctest:: + + >>> str(accel) + '1.3 mètre par seconde²' + >>> "%s" % accel + '1.3 mètre par seconde²' + >>> "{}".format(accel) + '1.3 mètre par seconde²' + Using Pint in your projects --------------------------- diff --git a/pint/quantity.py b/pint/quantity.py index 051fc849d..faf9d3429 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -252,6 +252,9 @@ def __deepcopy__(self, memo): return ret def __str__(self): + if self._REGISTRY.fmt_locale is not None: + return self.format_babel() + return format(self) def __bytes__(self): @@ -270,6 +273,9 @@ def __hash__(self): _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: diff --git a/pint/registry.py b/pint/registry.py index c42729f5c..3d43a70ec 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -172,6 +172,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 = [ @@ -216,7 +219,7 @@ def __init__( 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) #: Map between name (string) and value (string) of defaults stored in the #: definitions file. diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 5a9139146..2bf743c72 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 2aac0cdd8..def7c8319 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -5,6 +5,14 @@ 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 @@ def test_registry_locale(self): 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) From 39e5b49e219a0cb46c0662b6dd1488c7c99ef46e Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Wed, 12 Feb 2020 18:59:27 +0300 Subject: [PATCH 350/612] Use setuptools_scm via pyproject.toml --- CHANGES | 1 + pyproject.toml | 5 +++++ setup.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml diff --git a/CHANGES b/CHANGES index 32b3a423b..2fb9e3a17 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Now we use `pyproject.toml` for providing `setuptools_scm` settings - Remove `default_en_0.6.txt` - Reorganize long_description. - Moved Pi to defintions files. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..771af682d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = ["setuptools>=41", "wheel", "setuptools_scm[toml]>=3.4.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] diff --git a/setup.py b/setup.py index 57e6ccddf..f4f9665a5 100644 --- a/setup.py +++ b/setup.py @@ -2,4 +2,4 @@ from setuptools import setup if __name__ == "__main__": - setup(use_scm_version=True) + setup() From e51da66bf319b8c97b6ec9a13b7e3cbcdf00d230 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:05:37 +0100 Subject: [PATCH 351/612] Defines logaritm and exponential for LogarithmicConverter --- pint/compat.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pint/compat.py b/pint/compat.py index 6fb27ee5a..5b293b367 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -149,6 +149,14 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): except ImportError: HAS_BABEL = False +# Defines Logarithm and Exponential for Logarithmic Converter +if HAS_NUMPY: + from numpy import log + from numpy import exp +else: + from math import log + from math import exp + if not HAS_BABEL: babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 From 260d45f6d7f804507b2e96672c12520be627ccfe Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:12:09 +0100 Subject: [PATCH 352/612] Ignores flake8 questions for log and exp unused in compat --- pint/compat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 5b293b367..a6dd0ef33 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -151,11 +151,11 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): # Defines Logarithm and Exponential for Logarithmic Converter if HAS_NUMPY: - from numpy import log - from numpy import exp + from numpy import log # noqa: F401 + from numpy import exp # noqa: F401 else: - from math import log - from math import exp + from math import log # noqa: F401 + from math import exp # noqa: F401 if not HAS_BABEL: babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 From 216fe5228d812b6e4a549587417d7c35a0314df5 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:19:07 +0100 Subject: [PATCH 353/612] Implements LogarithmicConverter --- pint/converters.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/pint/converters.py b/pint/converters.py index 927335016..b3f54dd5f 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -9,6 +9,9 @@ """ +from .compat import HAS_NUMPY, exp, log # noqa: F401 + + class Converter: """Base class for value converters.""" @@ -74,3 +77,62 @@ def from_reference(self, value, inplace=False): value = (value - self.offset) / self.scale return value + + +class LogarithmicConverter(Converter): + """ Converts between linear units and logarithmic units, such as dB, octave, neper or pH. + Q_log = logfactor * log( Q_lin / scale ) / log(log_base) + Parameters + ---------- + scale : float + unit of reference at denominator for logarithmic unit conversion + logbase : float + base of logarithm used in the logarithmic unit conversion + logfactor : float + factor multupled to logarithm for unit conversion + inplace : bool + controls if computation is done in place + """ + + def __init__(self, scale, logbase, logfactor): + """ + Parameters + ---------- + scale : float + unit of reference at denominator inside logarithm for unit conversion + logbase: float + base of logarithm used in unit conversion + logfactor: float + factor multiplied to logarithm for unit conversion + """ + + if HAS_NUMPY is False: + print( + "'numpy' package is not installed. Will use math.log() " + "for logarithmic units." + ) + + self.scale = scale + self.logbase = logbase + self.logfactor = logfactor + + def to_reference(self, value, inplace=False): + if inplace: + value /= self.scale + value = log(value) + value *= self.logfactor / log(self.logbase) + else: + value = self.logfactor * log(value / self.scale) / log(self.logbase) + + return value + + def from_reference(self, value, inplace=False): + if inplace: + value /= self.logfactor + value *= self.logbase + value = exp(value) + value *= self.scale + else: + value = self.scale * exp(log(self.logbase) * (value / self.logfactor)) + + return value From 542fabe5c2a78aa460ac432ce020ae24215c1fe9 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:23:41 +0100 Subject: [PATCH 354/612] Tests logaritmic converter --- pint/testsuite/test_converters.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 061b05da8..532398730 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -1,7 +1,12 @@ import itertools from pint.compat import np -from pint.converters import Converter, OffsetConverter, ScaleConverter +from pint.converters import ( + Converter, + LogarithmicConverter, + OffsetConverter, + ScaleConverter, +) from pint.testsuite import BaseTestCase, helpers @@ -22,6 +27,18 @@ def test_offset_converter(self): self.assertEqual(c.from_reference(c.to_reference(100)), 100) self.assertEqual(c.to_reference(c.from_reference(100)), 100) + def test_log_converter(self): + c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) + self.assertAlmostEqual(c.from_reference(0), 1) + self.assertAlmostEqual(c.from_reference(1), 10) + self.assertAlmostEqual(c.from_reference(2), 100) + self.assertAlmostEqual(c.to_reference(1), 0) + self.assertAlmostEqual(c.to_reference(10), 1) + self.assertAlmostEqual(c.to_reference(100), 2) + arb_value = 20.0 + self.assertAlmostEqual(c.from_reference(c.to_reference(arb_value)), arb_value) + self.assertAlmostEqual(c.to_reference(c.from_reference(arb_value)), arb_value) + @helpers.requires_numpy() def test_converter_inplace(self): for c in (ScaleConverter(20.0), OffsetConverter(20.0, 2)): From 38d48546591872f394e36c428141b8c65d6cd152 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:29:57 +0100 Subject: [PATCH 355/612] Adds LogUnits to CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 32b3a423b..74cf75223 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,8 @@ Pint Changelog - Allow constants in units by using a leading underscore (Issue #989, Thanks Juan Nunez-Iglesias) - Fixed bug where to_compact handled prefix units incorrectly (Issue #960) +- Implements Logarithmic Units like dBm, dB or decade + (Issue #71, Thanks Giorgio Signorello) 0.10.1 (2020-01-07) ------------------- From 87303dde8ad5598ddb6b34c14a660a3afcb33f25 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:35:48 +0100 Subject: [PATCH 356/612] Defines Euler's number e, for Neper definition --- pint/constants_en.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/constants_en.txt b/pint/constants_en.txt index fa485fadf..9737b7c4b 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -8,11 +8,12 @@ #### 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 wien_u = 2.8214393721220788934031913302944851953458817440731 # solution to (u-3)*exp(u)+3 = 0 => u = W(3/exp(3))+3 +eulers_number = 2.71828182845904523536028747135266249775724709369995 #### DEFINED EXACT CONSTANTS #### From ce9c19bb85e27474d0e6c676f04c6612929736d8 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 18:47:08 +0100 Subject: [PATCH 357/612] Switches from_ and to_ reference in LogConverter Switches the definition of from_ reference and to_reference methods. is_multiplitcative is set to False. Print statement removed. --- pint/converters.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/pint/converters.py b/pint/converters.py index b3f54dd5f..9b04b54a7 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -82,6 +82,7 @@ def from_reference(self, value, inplace=False): class LogarithmicConverter(Converter): """ Converts between linear units and logarithmic units, such as dB, octave, neper or pH. Q_log = logfactor * log( Q_lin / scale ) / log(log_base) + Parameters ---------- scale : float @@ -106,31 +107,45 @@ def __init__(self, scale, logbase, logfactor): factor multiplied to logarithm for unit conversion """ - if HAS_NUMPY is False: - print( - "'numpy' package is not installed. Will use math.log() " - "for logarithmic units." - ) - self.scale = scale self.logbase = logbase self.logfactor = logfactor - def to_reference(self, value, inplace=False): + @property + def is_multiplicative(self): + return False + + def from_reference(self, value, inplace=False): + """Converts value from the reference unit to the logarithmic unit + + dBm <------ mW + y dBm = 10 log10( x / 1mW ) + """ if inplace: value /= self.scale - value = log(value) + if HAS_NUMPY: + log(value, value) + else: + value = log(value) value *= self.logfactor / log(self.logbase) else: value = self.logfactor * log(value / self.scale) / log(self.logbase) return value - def from_reference(self, value, inplace=False): + def to_reference(self, value, inplace=False): + """Converts value to the reference unit from the logarithmic unit + + dBm ------> mW + y dBm = 10 log10( x / 1mW ) + """ if inplace: value /= self.logfactor - value *= self.logbase - value = exp(value) + value *= log(self.logbase) + if HAS_NUMPY: + exp(value, value) + else: + value = exp(value) value *= self.scale else: value = self.scale * exp(log(self.logbase) * (value / self.logfactor)) From 8ea37262156ad8972d0c5ca4204059daafa1deb6 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 19:06:02 +0100 Subject: [PATCH 358/612] Implements Logarithmic Units Logartimic units such as dB, dBm decade and neper are defined. Old units thare are not implemented correctly are removed. --- pint/default_en.txt | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index 8bd413313..ee0cbcd5f 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -108,7 +108,6 @@ gram = [mass] = g mole = [substance] = mol kelvin = [temperature]; offset: 0 = K = degK = °K = degree_Kelvin = degreeK # older names supported for compatibility radian = [] = rad -neper = [] = Np bit = [] count = [] @@ -136,13 +135,13 @@ mil = π / 32000 * radian steradian = radian ** 2 = sr square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg -# Logarithmic ratio -bel = 0.5 * ln10 * neper - # Information -byte = 8 * bit = B = octet baud = bit / second = Bd = bps +byte = 8 * bit = B = octet +# byte = 8 * bit = _ = octet +## NOTE: B (byte) symbol can conflict with Bell + # Length angstrom = 1e-10 * meter = Å = ångström = Å micron = micrometer = µ @@ -174,7 +173,10 @@ week = 7 * day fortnight = 2 * week year = 365.25 * day = a = yr = julian_year month = year / 12 -decade = 10 * year + +# decade = 10 * year +## NOTE: decade [time] can conflict with decade [dimensionless] + century = 100 * year = _ = centuries millennium = 1e3 * year = _ = millennia eon = 1e9 * year @@ -305,6 +307,7 @@ inch_H2O_39F = inch * water_39F * g_0 inch_H2O_60F = inch * water_60F * g_0 foot_H2O = foot * water * g_0 = ftH2O = feet_H2O centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O +sound_pressure_level = 20e-6 * pascal = SPL # Torque [torque] = [force] * [length] @@ -467,6 +470,23 @@ buckingham = debye * angstrom bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N +# Logaritmic Unit Definition +# Unit = scale; logbase; logfactor +# x_dB = [logfactor] * log( x_lin / [scale] ) / log( [logbase] ) + +# Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ] + +decibellmilliwatt = 1e-3 W; logbase: 10; logfactor: 10 = dBm + +decibell = 1 ; logbase: 10; logfactor: 10 = dB +# bell = 1 ; logbase: 10; logfactor: = B +## NOTE: B (Bell) symbol conflicts with byte + +decade = 1 ; logbase: 10; logfactor: 1 +## NOTE: decade [time] can conflict with decade [dimensionless] + +neper = 1 ; logbase: 2.71828182845904523536028747135266249775724709369995; logfactor: 0.5 = Np +# neper = 1 ; logbase: eulers_number; logfactor: 0.5 = Np #### UNIT GROUPS #### # Mostly for length, area, volume, mass, force From 8e8530758ef5f3ef5400846a80ffe8314802b1d2 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 19:14:23 +0100 Subject: [PATCH 359/612] Implements UnitDefinition that uses LogConverter. Definition from string is split with one element, for compatibility with Log Units. Modifiers section is adapted to assing Logaritmic Converter --- pint/definitions.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 8ffa15322..2bfce79cd 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -10,7 +10,7 @@ from collections import namedtuple -from .converters import OffsetConverter, ScaleConverter +from .converters import LogarithmicConverter, OffsetConverter, ScaleConverter from .errors import DefinitionSyntaxError from .util import ParserHelper, UnitsContainer, _is_dim @@ -231,7 +231,7 @@ def from_string(cls, definition): definition = ParsedDefinition.from_string(definition) if ";" in definition.value: - [converter, modifiers] = definition.value.split(";", 2) + [converter, modifiers] = definition.value.split(";", 1) try: modifiers = dict( @@ -261,11 +261,23 @@ def from_string(cls, definition): ) reference = UnitsContainer(converter) - if modifiers.get("offset", 0) != 0: - converter = OffsetConverter(converter.scale, modifiers["offset"]) - else: + if not modifiers: converter = ScaleConverter(converter.scale) + elif "offset" in modifiers: + if modifiers.get("offset", 0.0) != 0.0: + converter = OffsetConverter(converter.scale, modifiers["offset"]) + else: + converter = ScaleConverter(converter.scale) + + elif "logbase" in modifiers and "logfactor" in modifiers: + converter = LogarithmicConverter( + converter.scale, modifiers["logbase"], modifiers["logfactor"] + ) + + else: + raise DefinitionSyntaxError("Unable to assing a converter to the unit") + return cls( definition.name, definition.symbol, From 59d1bde149fab8a59e64508640bb28c421b9d8a9 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 19:19:44 +0100 Subject: [PATCH 360/612] Adapts tests_converter for switch from_ to_ As from_reference and to_reference methods for log converter are switched, tests switch too. --- pint/testsuite/test_converters.py | 33 +++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 532398730..b603bad73 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -29,12 +29,12 @@ def test_offset_converter(self): def test_log_converter(self): c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) - self.assertAlmostEqual(c.from_reference(0), 1) - self.assertAlmostEqual(c.from_reference(1), 10) - self.assertAlmostEqual(c.from_reference(2), 100) - self.assertAlmostEqual(c.to_reference(1), 0) - self.assertAlmostEqual(c.to_reference(10), 1) - self.assertAlmostEqual(c.to_reference(100), 2) + self.assertAlmostEqual(c.to_reference(0), 1) + self.assertAlmostEqual(c.to_reference(1), 10) + self.assertAlmostEqual(c.to_reference(2), 100) + self.assertAlmostEqual(c.from_reference(1), 0) + self.assertAlmostEqual(c.from_reference(10), 1) + self.assertAlmostEqual(c.from_reference(100), 2) arb_value = 20.0 self.assertAlmostEqual(c.from_reference(c.to_reference(arb_value)), arb_value) self.assertAlmostEqual(c.to_reference(c.from_reference(arb_value)), arb_value) @@ -52,3 +52,24 @@ def test_converter_inplace(self): r = fun(a, inplace) np.testing.assert_allclose(r, ac) comp(a, r) + + @helpers.requires_numpy() + def test_log_converter_inplace(self): + arb_value = 3.14 + c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) + + from_to = lambda value, inplace: c.from_reference( + c.to_reference(value, inplace), inplace + ) + + to_from = lambda value, inplace: c.to_reference( + c.from_reference(value, inplace), inplace + ) + + for fun, (inplace, comp) in itertools.product( + (from_to, to_from), ((True, self.assertIs), (False, self.assertIsNot)) + ): + arb_array = arb_value * np.ones((1, 10)) + result = fun(arb_array, inplace) + np.testing.assert_allclose(result, arb_array) + comp(arb_array, result) From c073a1d65c50fcb780c2d85f628a3502f98a9421 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 19:31:04 +0100 Subject: [PATCH 361/612] Implements Test for Logaritmic Unit definitions --- pint/testsuite/test_definitions.py | 70 +++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index ac8e47226..7177dc969 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,4 +1,4 @@ -from pint.converters import OffsetConverter, ScaleConverter +from pint.converters import LogarithmicConverter, OffsetConverter, ScaleConverter from pint.definitions import ( AliasDefinition, Definition, @@ -94,6 +94,74 @@ def test_unit_definition(self): "degF = 9 / 5 * kelvin; offset: 255.372222 bla", ) + def test_log_unit_definition(self): + + x = Definition.from_string("decibellmilliwatt = 1 mW; logbase: 10; logfactor: 10 = dBm") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1e-3) + self.assertEqual(x.converter.logbase, 10) + self.assertEqual(x.converter.logfactor, 10) + self.assertEqual(x.reference, UnitsContainer()) + + x = Definition.from_string("decibellmilliwatt = 1e-3 W; logbase: 10; logfactor: 10 = dBm") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1e-3) + self.assertEqual(x.converter.logbase, 10) + self.assertEqual(x.converter.logfactor, 10) + self.assertEqual(x.reference, UnitsContainer()) + + x = Definition.from_string("decibell = 1 ; logbase: 10; logfactor: 10 = dB") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1) + self.assertEqual(x.converter.logbase, 10) + self.assertEqual(x.converter.logfactor, 10) + self.assertEqual(x.reference, UnitsContainer()) + + x = Definition.from_string("bell = 1 ; logbase: 10; logfactor: 1 = B") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1) + self.assertEqual(x.converter.logbase, 10) + self.assertEqual(x.converter.logfactor, 1) + self.assertEqual(x.reference, UnitsContainer()) + + x = Definition.from_string("decade = 1 ; logbase: 10; logfactor: 1") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1) + self.assertEqual(x.converter.logbase, 10) + self.assertEqual(x.converter.logfactor, 1) + self.assertEqual(x.reference, UnitsContainer()) + + eulersnumber = 2.71828182845904523536028747135266249775724709369995 + x = Definition.from_string( + "neper = 1 ; logbase: %1.50f; logfactor: 0.5 = Np" % eulersnumber + ) + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1) + self.assertEqual(x.converter.logbase, eulersnumber) + self.assertEqual(x.converter.logfactor, 0.5) + self.assertEqual(x.reference, UnitsContainer()) + + x = Definition.from_string("octave = 1 ; logbase: 2; logfactor: 1 = oct") + self.assertIsInstance(x, UnitDefinition) + self.assertFalse(x.is_base) + self.assertIsInstance(x.converter, LogarithmicConverter) + self.assertEqual(x.converter.scale, 1) + self.assertEqual(x.converter.logbase, 2) + self.assertEqual(x.converter.logfactor, 1) + self.assertEqual(x.reference, UnitsContainer()) + def test_dimension_definition(self): x = DimensionDefinition("[time]", "", (), None, is_base=True) self.assertTrue(x.is_base) From 28a1f5408507352f14c0817e3f8d934d72f1289e Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 19:38:06 +0100 Subject: [PATCH 362/612] Implements Test cases for Logarithmic Units Creation of Logarithmic Quantity, of conversion, of mixed Log-regular units are implemented. --- pint/testsuite/test_quantity.py | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fbfd773b3..ec4cd8694 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1622,3 +1622,111 @@ def test_offset_autoconvert_gt_zero(self): self.assertTrue(q1 > 0) self.assertTrue(q2 > 0) self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) + +class TestLogarithmicQuantity(QuantityTestCase): + FORCE_NDARRAY = False + + def test_log_quantity_creation(self): + + # Following Quantity Creation Pattern + for args in ( + (4.2, 'dBm'), + (4.2, UnitsContainer(dBm=1)), + (4.2, self.ureg.dBm), + ('4.2 * dBm',), + ("4.2/meter**(-1)",), + (self.Q_(4.2, 'dBm')), + ): + x = self.Q_(*args) + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(dBm=1)) + + x = self.Q_(4.2, UnitsContainer(dBm=1)) + y = self.Q_(x) + self.assertEqual(x.magnitude, y.magnitude) + self.assertEqual(x.units, y.units) + self.assertIsNot(x, y) + + with self.capture_log() as buffer: + self.assertEqual(4.2 * self.ureg.dBm, self.Q_(4.2, 2 * self.ureg.dBm)) + self.assertEqual(len(buffer), 1) + + def test_log_convert(self): + self.assertQuantityAlmostEqual( + self.Q_("2 inch").to("meter"), self.Q_(2.0 * 0.0254, "meter") + ) + + # ## Test dB + # 0 dB == 1 + self.assertQuantityAlmostEqual(Q_(0.0, "dB").to("dimensionless"), Q_(1.0)) + # -10 dB == 0.1 + self.assertQuantityAlmostEqual(Q_(-10.0, "dB").to("dimensionless"), Q_(0.1)) + # +10 dB == 10 + self.assertQuantityAlmostEqual(Q_(+10.0, "dB").to("dimensionless"), Q_(10.0)) + # 30 dB == 1e3 + self.assertQuantityAlmostEqual(Q_(30.0, "dB").to("dimensionless"), Q_(1e3)) + # 60 dB == 1e6 + self.assertQuantityAlmostEqual(Q_(60.0, "dB").to("dimensionless"), Q_(1e6)) + # # 1 dB = 1/10 * bel + # self.assertQuantityAlmostEqual(Q_(1.0, "dB").to("dimensionless"), Q_(1, "bell") / 10) + # # Uncomment Bell unit in default_en.txt + + # ## Test decade + # 1 decade == 10 + self.assertQuantityAlmostEqual(Q_(1.0, "decade").to("dimensionless"), Q_(10.0)) + # 2 decade == 100 + self.assertQuantityAlmostEqual(Q_(2.0, "decade").to("dimensionless"), Q_(100.0)) + + # ## Test dBm + # 0 dBm = 1 mW + self.assertQuantityAlmostEqual(Q_(0.0, "dBm").to("mW"), Q_(1.0, "mW")) + # 10 dBm = 10 mW + self.assertQuantityAlmostEqual(Q_(10.0, "dBm").to("mW"), Q_(10.0, "mW")) + # 20 dBm = 100 mW + self.assertQuantityAlmostEqual(Q_(20.0, "dBm").to("mW"), Q_(100.0, "mW")) + # -10 dBm = 0.1 mW + self.assertQuantityAlmostEqual(Q_(-10.0, "dBm").to("mW"), Q_(0.1, "mW")) + # -20 dBm = 0.01 mW + self.assertQuantityAlmostEqual(Q_(-20.0, "dBm").to("mW"), Q_(0.01, "mW")) + # 3 dBm ≈ 2 mW + self.assertQuantityAlmostEqual(Q_(3.0, "dBm").to("mW"), Q_(1.9952623149688797, "mW")) + + # ## Test octave + # 1 octave = 2 + self.assertQuantityAlmostEqual(Q_(1.0, "octave").to("dimensionless"), Q_(2.0)) + + def test_mix_regular_log_units(self): + # Test regular-logarithmic mixed definition, such as dB/km or dB/cm + + # Multiplications and divisions with a mix of Logarithmic Units and regular Units is normally not possible. + # The reason is that dB are considereded by pint like offset unit. + # Multiplications and divisions that involve offset units are badly defined, so pint raises an error + with self.assertRaises(OffsetUnitCalculusError): + (-10.0 * ureg.dB) / (1 * ureg.cm) + # + # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base. + # With this flag on multiplications and divisions are now possible: + self.ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) + self.assertQuantityAlmostEqual(-10 * self.ureg.dB / self.ureg.cm, 0.1 / self.ureg.cm) + + # def _test_log_quantity_add_sub_raises_exception(self, unit, func): + # # Warning should be provided when trying to sum log units + # pass + +# class TestLogarithmicQuantityBasicMath(QuantityTestCase): +# FORCE_NDARRAY = False +# +# def _test_log_quantity_add_sub(self, unit, func): +# +# # Pure dB arithmetic +# # 5 dBm + 10 dB = 15 dBm +# self.assertQuantityAlmostEqual(5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm) +# # 100*dBm -10*dB = 90*dB +# self.assertQuantityAlmostEqual(100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB) +# # 100 dBW - 5 dBW = 95 dB +# self.assertQuantityAlmostEqual(100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB) +# # 20 dB + 0 dBW == 20 dBW + +# # 100 Hz + 1 octave = 200 Hz +# self.assertQuantityAlmostEqual(100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz) + From c5a4827ddcca003f08d0f6f18eae40bdd5413503 Mon Sep 17 00:00:00 2001 From: 5igno Date: Thu, 13 Feb 2020 23:25:20 +0100 Subject: [PATCH 363/612] Corrects dBm's definition and its test --- pint/default_en.txt | 2 +- pint/testsuite/test_definitions.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/pint/default_en.txt b/pint/default_en.txt index ee0cbcd5f..44ee3a86d 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -476,7 +476,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N # Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ] -decibellmilliwatt = 1e-3 W; logbase: 10; logfactor: 10 = dBm +decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm decibell = 1 ; logbase: 10; logfactor: 10 = dB # bell = 1 ; logbase: 10; logfactor: = B diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 7177dc969..b9da2f307 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -95,24 +95,15 @@ def test_unit_definition(self): ) def test_log_unit_definition(self): - - x = Definition.from_string("decibellmilliwatt = 1 mW; logbase: 10; logfactor: 10 = dBm") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1e-3) - self.assertEqual(x.converter.logbase, 10) - self.assertEqual(x.converter.logfactor, 10) - self.assertEqual(x.reference, UnitsContainer()) - x = Definition.from_string("decibellmilliwatt = 1e-3 W; logbase: 10; logfactor: 10 = dBm") + x = Definition.from_string("decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm") self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, LogarithmicConverter) self.assertEqual(x.converter.scale, 1e-3) self.assertEqual(x.converter.logbase, 10) self.assertEqual(x.converter.logfactor, 10) - self.assertEqual(x.reference, UnitsContainer()) + self.assertEqual(x.reference, UnitsContainer(watt=1)) x = Definition.from_string("decibell = 1 ; logbase: 10; logfactor: 10 = dB") self.assertIsInstance(x, UnitDefinition) From 235d59f3dbde8c1def86d65bd69601b40c82a19c Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 03:04:32 +0100 Subject: [PATCH 364/612] Converts LogsUnits in NonMultiplicativeRegistry --- pint/registry.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 7a33ac764..8b3583669 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -51,7 +51,7 @@ from . import registry_helpers, systems from .compat import babel_parse, tokenizer from .context import Context, ContextChain -from .converters import ScaleConverter +from .converters import ScaleConverter, LogarithmicConverter from .definitions import ( AliasDefinition, Definition, @@ -1288,12 +1288,22 @@ def _convert(self, value, src, dst, inplace=False): if src_offset_unit: value = self._units[src_offset_unit].converter.to_reference(value, inplace) src = src.remove([src_offset_unit]) + if isinstance(self._units[src_offset_unit].converter, LogarithmicConverter): + # Add src reference unit back, for multiplicative section + src_ref = self._units[src_offset_unit].reference + (dim_key, dim_value) = [(dim_key, dim_value) for dim_key, dim_value in src_ref.items()][0] + src = src.add(dim_key, dim_value) # clean dst units from offset units if dst_offset_unit: dst = dst.remove([dst_offset_unit]) + if isinstance(self._units[dst_offset_unit].converter, LogarithmicConverter): + # Add reference Unit back, for multiplicative section + dst_ref = self._units[dst_offset_unit].reference + (dim_key, dim_value) = [(dim_key, dim_value) for dim_key, dim_value in dst_ref.items()][0] + dst = dst.add(dim_key, dim_value) - # Convert non multiplicative units to the dst. + # Convert multiplicative parts to the dst. value = super()._convert(value, src, dst, inplace, False) # Finally convert to offset units specified in destination From 71fe8ca4239f687665909dd464c67294e95c698e Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 12:16:21 +0100 Subject: [PATCH 365/612] Implements helper to add ref to logunits back --- pint/registry.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 8b3583669..fff622485 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -51,7 +51,7 @@ from . import registry_helpers, systems from .compat import babel_parse, tokenizer from .context import Context, ContextChain -from .converters import ScaleConverter, LogarithmicConverter +from .converters import LogarithmicConverter, ScaleConverter from .definitions import ( AliasDefinition, Definition, @@ -1206,7 +1206,7 @@ def _is_multiplicative(self, u): raise UndefinedUnitError(u) def _validate_and_extract(self, units): - + # u is for unit, e is for exponent nonmult_units = [ (u, e) for u, e in units.items() if not self._is_multiplicative(u) ] @@ -1233,6 +1233,21 @@ def _validate_and_extract(self, units): return None + def _add_ref_of_log_unit(self, offset_unit, all_units): + + slct_unit = self._units[offset_unit] + if isinstance(slct_unit.converter, LogarithmicConverter): + # Extract reference unit + slct_ref = slct_unit.reference + # If reference unit is not dimensionless + if slct_ref != UnitsContainer(): + # Extract reference unit + (u, e) = [(u, e) for u, e in slct_ref.items()].pop() + # Add it back to the unit list + return all_units.add(u, e) + # Otherwise, return the units unmodified + return all_units + def _convert(self, value, src, dst, inplace=False): """Convert value from some source to destination units. @@ -1288,22 +1303,16 @@ def _convert(self, value, src, dst, inplace=False): if src_offset_unit: value = self._units[src_offset_unit].converter.to_reference(value, inplace) src = src.remove([src_offset_unit]) - if isinstance(self._units[src_offset_unit].converter, LogarithmicConverter): - # Add src reference unit back, for multiplicative section - src_ref = self._units[src_offset_unit].reference - (dim_key, dim_value) = [(dim_key, dim_value) for dim_key, dim_value in src_ref.items()][0] - src = src.add(dim_key, dim_value) + # Add reference unit for multiplicative section + src = self._add_ref_of_log_unit(src_offset_unit, src) # clean dst units from offset units if dst_offset_unit: dst = dst.remove([dst_offset_unit]) - if isinstance(self._units[dst_offset_unit].converter, LogarithmicConverter): - # Add reference Unit back, for multiplicative section - dst_ref = self._units[dst_offset_unit].reference - (dim_key, dim_value) = [(dim_key, dim_value) for dim_key, dim_value in dst_ref.items()][0] - dst = dst.add(dim_key, dim_value) + # Add reference unit for multiplicative section + dst = self._add_ref_of_log_unit(dst_offset_unit, dst) - # Convert multiplicative parts to the dst. + # Convert non multiplicative units to the dst. value = super()._convert(value, src, dst, inplace, False) # Finally convert to offset units specified in destination From 48f3a1f11f1354eb7554e20733eb08ffbbddb78c Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 12:18:01 +0100 Subject: [PATCH 366/612] Makes test_log_unit_definition Flake-8 compatible --- pint/testsuite/test_definitions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index b9da2f307..00df68f7f 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -96,7 +96,9 @@ def test_unit_definition(self): def test_log_unit_definition(self): - x = Definition.from_string("decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm") + x = Definition.from_string( + "decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm" + ) self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, LogarithmicConverter) From 1e7e090d520e395346b50eeabd51105999dfa7cd Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 12:21:06 +0100 Subject: [PATCH 367/612] Adds dBu and octave to test Log-to-Log conversion --- pint/default_en.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index 44ee3a86d..8316f78d7 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -477,6 +477,7 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N # Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ] decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm +decibellmicrowatt = 1e-6 watt; logbase: 10; logfactor: 10 = dBu decibell = 1 ; logbase: 10; logfactor: 10 = dB # bell = 1 ; logbase: 10; logfactor: = B @@ -485,6 +486,8 @@ decibell = 1 ; logbase: 10; logfactor: 10 = dB decade = 1 ; logbase: 10; logfactor: 1 ## NOTE: decade [time] can conflict with decade [dimensionless] +octave = 1 ; logbase: 2; logfactor: 1 = oct + neper = 1 ; logbase: 2.71828182845904523536028747135266249775724709369995; logfactor: 0.5 = Np # neper = 1 ; logbase: eulers_number; logfactor: 0.5 = Np From cd8b04c23ed25fa3fc8ecd5e65d1e4b3ba4e22e2 Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 12:23:07 +0100 Subject: [PATCH 368/612] Tests Log-to-Log conversion and Log creation --- pint/testsuite/test_quantity.py | 128 ++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 38 deletions(-) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index ec4cd8694..5563adcdf 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1623,77 +1623,129 @@ def test_offset_autoconvert_gt_zero(self): self.assertTrue(q2 > 0) self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) + class TestLogarithmicQuantity(QuantityTestCase): + FORCE_NDARRAY = False def test_log_quantity_creation(self): # Following Quantity Creation Pattern for args in ( - (4.2, 'dBm'), - (4.2, UnitsContainer(dBm=1)), + (4.2, "dBm"), + (4.2, UnitsContainer(decibellmilliwatt=1)), (4.2, self.ureg.dBm), - ('4.2 * dBm',), - ("4.2/meter**(-1)",), - (self.Q_(4.2, 'dBm')), ): x = self.Q_(*args) self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(dBm=1)) - - x = self.Q_(4.2, UnitsContainer(dBm=1)) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + x = self.Q_(self.Q_(4.2, "dBm")) + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + x = self.Q_(4.2, UnitsContainer(decibellmilliwatt=1)) y = self.Q_(x) self.assertEqual(x.magnitude, y.magnitude) self.assertEqual(x.units, y.units) self.assertIsNot(x, y) - with self.capture_log() as buffer: - self.assertEqual(4.2 * self.ureg.dBm, self.Q_(4.2, 2 * self.ureg.dBm)) - self.assertEqual(len(buffer), 1) + # new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) + # x = new_reg.Q_('4.2 * dBm') + # new_reg.assertEqual(x.magnitude, 4.2) + # new_reg.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + # with self.capture_log() as buffer: + # self.assertEqual(4.2 * self.ureg.dBm, self.Q_(4.2, 2 * self.ureg.dBm)) + # self.assertEqual(len(buffer), 1) def test_log_convert(self): - self.assertQuantityAlmostEqual( - self.Q_("2 inch").to("meter"), self.Q_(2.0 * 0.0254, "meter") - ) - + # ## Test dB # 0 dB == 1 - self.assertQuantityAlmostEqual(Q_(0.0, "dB").to("dimensionless"), Q_(1.0)) + self.assertQuantityAlmostEqual( + self.Q_(0.0, "dB").to("dimensionless"), self.Q_(1.0) + ) # -10 dB == 0.1 - self.assertQuantityAlmostEqual(Q_(-10.0, "dB").to("dimensionless"), Q_(0.1)) + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dB").to("dimensionless"), self.Q_(0.1) + ) # +10 dB == 10 - self.assertQuantityAlmostEqual(Q_(+10.0, "dB").to("dimensionless"), Q_(10.0)) + self.assertQuantityAlmostEqual( + self.Q_(+10.0, "dB").to("dimensionless"), self.Q_(10.0) + ) # 30 dB == 1e3 - self.assertQuantityAlmostEqual(Q_(30.0, "dB").to("dimensionless"), Q_(1e3)) + self.assertQuantityAlmostEqual( + self.Q_(30.0, "dB").to("dimensionless"), self.Q_(1e3) + ) # 60 dB == 1e6 - self.assertQuantityAlmostEqual(Q_(60.0, "dB").to("dimensionless"), Q_(1e6)) + self.assertQuantityAlmostEqual( + self.Q_(60.0, "dB").to("dimensionless"), self.Q_(1e6) + ) # # 1 dB = 1/10 * bel - # self.assertQuantityAlmostEqual(Q_(1.0, "dB").to("dimensionless"), Q_(1, "bell") / 10) + # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) # # Uncomment Bell unit in default_en.txt - + # ## Test decade # 1 decade == 10 - self.assertQuantityAlmostEqual(Q_(1.0, "decade").to("dimensionless"), Q_(10.0)) + self.assertQuantityAlmostEqual( + self.Q_(1.0, "decade").to("dimensionless"), self.Q_(10.0) + ) # 2 decade == 100 - self.assertQuantityAlmostEqual(Q_(2.0, "decade").to("dimensionless"), Q_(100.0)) + self.assertQuantityAlmostEqual( + self.Q_(2.0, "decade").to("dimensionless"), self.Q_(100.0) + ) + + # ## Test octave + # 1 octave = 2 + self.assertQuantityAlmostEqual( + self.Q_(1.0, "octave").to("dimensionless"), self.Q_(2.0) + ) + + # ## Test dB to dB units octave - decade + # 1 decade = log2(10) octave + self.assertQuantityAlmostEqual( + self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave") + ) # ## Test dBm # 0 dBm = 1 mW - self.assertQuantityAlmostEqual(Q_(0.0, "dBm").to("mW"), Q_(1.0, "mW")) + self.assertQuantityAlmostEqual(self.Q_(0.0, "dBm").to("mW"), self.Q_(1.0, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(0.0, "dBm"), self.Q_(1.0, "mW").to("dBm") + ) # 10 dBm = 10 mW - self.assertQuantityAlmostEqual(Q_(10.0, "dBm").to("mW"), Q_(10.0, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(10.0, "dBm").to("mW"), self.Q_(10.0, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(10.0, "dBm"), self.Q_(10.0, "mW").to("dBm") + ) # 20 dBm = 100 mW - self.assertQuantityAlmostEqual(Q_(20.0, "dBm").to("mW"), Q_(100.0, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(20.0, "dBm").to("mW"), self.Q_(100.0, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(20.0, "dBm"), self.Q_(100.0, "mW").to("dBm") + ) # -10 dBm = 0.1 mW - self.assertQuantityAlmostEqual(Q_(-10.0, "dBm").to("mW"), Q_(0.1, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dBm").to("mW"), self.Q_(0.1, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dBm"), self.Q_(0.1, "mW").to("dBm") + ) # -20 dBm = 0.01 mW - self.assertQuantityAlmostEqual(Q_(-20.0, "dBm").to("mW"), Q_(0.01, "mW")) - # 3 dBm ≈ 2 mW - self.assertQuantityAlmostEqual(Q_(3.0, "dBm").to("mW"), Q_(1.9952623149688797, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(-20.0, "dBm").to("mW"), self.Q_(0.01, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(-20.0, "dBm"), self.Q_(0.01, "mW").to("dBm") + ) - # ## Test octave - # 1 octave = 2 - self.assertQuantityAlmostEqual(Q_(1.0, "octave").to("dimensionless"), Q_(2.0)) + # ## Test dB to dB units dBm - dBu + # 0 dBm = 1mW = 1e3 uW = 30 dBu + self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu")) def test_mix_regular_log_units(self): # Test regular-logarithmic mixed definition, such as dB/km or dB/cm @@ -1702,17 +1754,18 @@ def test_mix_regular_log_units(self): # The reason is that dB are considereded by pint like offset unit. # Multiplications and divisions that involve offset units are badly defined, so pint raises an error with self.assertRaises(OffsetUnitCalculusError): - (-10.0 * ureg.dB) / (1 * ureg.cm) + (-10.0 * self.ureg.dB) / (1 * self.ureg.cm) # # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base. # With this flag on multiplications and divisions are now possible: - self.ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) - self.assertQuantityAlmostEqual(-10 * self.ureg.dB / self.ureg.cm, 0.1 / self.ureg.cm) + new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) + self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) # def _test_log_quantity_add_sub_raises_exception(self, unit, func): # # Warning should be provided when trying to sum log units # pass + # class TestLogarithmicQuantityBasicMath(QuantityTestCase): # FORCE_NDARRAY = False # @@ -1729,4 +1782,3 @@ def test_mix_regular_log_units(self): # # 100 Hz + 1 octave = 200 Hz # self.assertQuantityAlmostEqual(100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz) - From 5de88e87bc1c15243481c6bf9fa58fd1f7fbfc55 Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 12:29:10 +0100 Subject: [PATCH 369/612] Adds other implementer's contribution to issue#71 --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3e1106207..ffb92427d 100644 --- a/CHANGES +++ b/CHANGES @@ -20,7 +20,7 @@ Pint Changelog Juan Nunez-Iglesias) - Fixed bug where to_compact handled prefix units incorrectly (Issue #960) - Implements Logarithmic Units like dBm, dB or decade - (Issue #71, Thanks Giorgio Signorello) + (Issue #71, Thanks Dima Pustakhod, Giorgio Signorello, Jonathan Wheeler) 0.10.1 (2020-01-07) ------------------- From 39202f8d02219b99c67e2df612d9d08a93235b8a Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 20:17:53 +0100 Subject: [PATCH 370/612] Implements Error for LogarithmicUnit computation --- pint/errors.py | 20 +++++++++++++++++++- pint/testsuite/test_errors.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pint/errors.py b/pint/errors.py index 4ce1eaf3f..8376c4fc9 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -8,6 +8,9 @@ :license: BSD, see LICENSE for more details. """ +OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html" +LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html" + def _file_prefix(filename=None, lineno=None): if filename and lineno is not None: @@ -111,7 +114,22 @@ def __str__(self): return ( "Ambiguous operation with offset unit (%s)." % ", ".join(str(u) for u in self.args) - + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance." + + " See " + + OFFSET_ERROR_DOCS_HTML + + " for guidance." + ) + + +class LogarithmicUnitCalculusError(PintTypeError): + """Raised on inappropriate operations with logarithmic units.""" + + def __str__(self): + return ( + "Ambiguous operation with logarithmic unit (%s)." + % ", ".join(str(u) for u in self.args) + + " See " + + LOG_ERROR_DOCS_HTML + + " for guidance." ) diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index cb75dd7ca..aea20f9a5 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,8 +1,11 @@ import pickle +from pint.errors import OFFSET_ERROR_DOCS_HTML, LOG_ERROR_DOCS_HTML + from pint import ( DefinitionSyntaxError, DimensionalityError, + LogarithmicUnitCalculusError, OffsetUnitCalculusError, Quantity, RedefinitionError, @@ -79,13 +82,34 @@ def test_offset_unit_calculus_error(self): self.assertEqual( str(ex), "Ambiguous operation with offset unit (kilogram). See " - "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.", + + OFFSET_ERROR_DOCS_HTML + + " for guidance.", ) ex = OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units) self.assertEqual( str(ex), "Ambiguous operation with offset unit (kilogram, second). See " - "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.", + + OFFSET_ERROR_DOCS_HTML + + " for guidance.", + ) + + def test_logarithmic_unit_calculus_error(self): + Quantity = UnitRegistry(autoconvert_offset_to_baseunit=True).Quantity + ex = LogarithmicUnitCalculusError(Quantity("1 dB")._units) + self.assertEqual( + str(ex), + "Ambiguous operation with logarithmic unit (decibell). See " + + LOG_ERROR_DOCS_HTML + + " for guidance.", + ) + ex = LogarithmicUnitCalculusError( + Quantity("1 dB")._units, Quantity("1 octave")._units + ) + self.assertEqual( + str(ex), + "Ambiguous operation with logarithmic unit (decibell, octave). See " + + LOG_ERROR_DOCS_HTML + + " for guidance.", ) def test_pickle_definition_syntax_error(self): From 9e132d3a142268f652d53003d3769aba9fa3317f Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 20:27:08 +0100 Subject: [PATCH 371/612] Defines Tests for dB computation or Errors if any A list of operations involving logarithmic units is provided. When still not implementedin pint but executed by the user, pint should rise an exception. The scheleton of a test function to check this user interaction is provided. --- pint/__init__.py | 3 +- pint/testsuite/test_quantity.py | 61 +++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index a6f59c1ec..f69370769 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -16,9 +16,10 @@ import pkg_resources from .context import Context -from .errors import ( +from .errors import ( # noqa: F401 DefinitionSyntaxError, DimensionalityError, + LogarithmicUnitCalculusError, OffsetUnitCalculusError, RedefinitionError, UndefinedUnitError, diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 5563adcdf..5d6892991 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -2,10 +2,16 @@ import datetime import math import operator as op +import unittest import warnings from unittest.mock import patch -from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry +from pint import ( + DimensionalityError, + LogarithmicUnitCalculusError, + OffsetUnitCalculusError, + UnitRegistry, +) from pint.compat import BehaviorChangeWarning, np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase @@ -1761,24 +1767,35 @@ def test_mix_regular_log_units(self): new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) - # def _test_log_quantity_add_sub_raises_exception(self, unit, func): - # # Warning should be provided when trying to sum log units - # pass - - -# class TestLogarithmicQuantityBasicMath(QuantityTestCase): -# FORCE_NDARRAY = False -# -# def _test_log_quantity_add_sub(self, unit, func): -# -# # Pure dB arithmetic -# # 5 dBm + 10 dB = 15 dBm -# self.assertQuantityAlmostEqual(5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm) -# # 100*dBm -10*dB = 90*dB -# self.assertQuantityAlmostEqual(100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB) -# # 100 dBW - 5 dBW = 95 dB -# self.assertQuantityAlmostEqual(100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB) -# # 20 dB + 0 dBW == 20 dBW - -# # 100 Hz + 1 octave = 200 Hz -# self.assertQuantityAlmostEqual(100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz) + +class TestLogarithmicQuantityBasicMath(QuantityTestCase): + + FORCE_NDARRAY = False + + @unittest.expectedFailure + def _test_log_quantity_add_sub_raises_exception(self, unit, func): + # Warning should be provided when trying to .... + self.assertRaises(LogarithmicUnitCalculusError) + + @unittest.expectedFailure + def _test_log_quantity_add_sub(self, unit, func): + + # Pure dB arithmetic + # 5 dBm + 10 dB = 15 dBm + self.assertQuantityAlmostEqual( + 5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm + ) + # 100*dBm -10*dB = 90*dB + self.assertQuantityAlmostEqual( + 100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB + ) + # 100 dBW - 5 dBW = 95 dB + self.assertQuantityAlmostEqual( + 100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB + ) + # 20 dB + 0 dBW == 20 dBW + + # 100 Hz + 1 octave = 200 Hz + self.assertQuantityAlmostEqual( + 100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz + ) From 8352736a14ce688815217865d5a04b0a303974ee Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 20:28:42 +0100 Subject: [PATCH 372/612] Gets test_errors compliant with isort --- pint/testsuite/test_errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index aea20f9a5..57f6633ad 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,7 +1,5 @@ import pickle -from pint.errors import OFFSET_ERROR_DOCS_HTML, LOG_ERROR_DOCS_HTML - from pint import ( DefinitionSyntaxError, DimensionalityError, @@ -12,6 +10,7 @@ UndefinedUnitError, UnitRegistry, ) +from pint.errors import LOG_ERROR_DOCS_HTML, OFFSET_ERROR_DOCS_HTML from pint.testsuite import BaseTestCase From 83422b11fcce06da86130b4d06a771b0bab3ae2d Mon Sep 17 00:00:00 2001 From: 5igno Date: Fri, 14 Feb 2020 20:32:51 +0100 Subject: [PATCH 373/612] Adds 5igno to the list of contributors --- pip-wheel-metadata/Pint.dist-info/AUTHORS | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 pip-wheel-metadata/Pint.dist-info/AUTHORS diff --git a/pip-wheel-metadata/Pint.dist-info/AUTHORS b/pip-wheel-metadata/Pint.dist-info/AUTHORS new file mode 100644 index 000000000..b09c14c24 --- /dev/null +++ b/pip-wheel-metadata/Pint.dist-info/AUTHORS @@ -0,0 +1,51 @@ +Pint is written and maintained by Hernan E. Grecco . + +Other contributors, listed alphabetically, are: + +* Aaron Coleman +* Alexander Böhn +* Ana Krivokapic +* Andrea Zonca +* Andrew Savage +* Brend Wanders +* choloepus +* coutinho +* Clément Pit-Claudel +* Daniel Sokolowski +* Dave Brooks +* David Linke +* Ed Schofield +* Eduard Bopp +* Eli +* Felix Hummel +* Francisco Couzo +* Giel van Schijndel +* Giorgio Signorello +* Guido Imperiale +* Ignacio Fdez. Galván +* James Rowe +* Jim Turner +* Joel B. Mohler +* John David Reaver +* Jonas Olson +* Jules Chéron +* Kaido Kert +* Kenneth D. Mankoff +* Kevin Davies +* Luke Campbell +* Matthieu Dartiailh +* Nate Bogdanowicz +* Peter Grayson +* Richard Barnes +* Ryan Dwyer +* Ryan Kingsbury +* Ryan May +* Sigvald Marholm +* Sundar Raman +* Tiago Coutinho +* Thomas Kluyver +* Tom Ritchford +* Virgil Dupras +* Zebedee Nicholls + +(If you think that your name belongs here, please let the maintainer know) From 4f114ff94aa65ae5a5ce4ea1a2b15c2a7e0fe2c2 Mon Sep 17 00:00:00 2001 From: 5igno Date: Sun, 16 Feb 2020 13:49:40 +0100 Subject: [PATCH 374/612] Adds is_logarithmic as converter property The properties like is_multiplicative or is_logarithmic allow to check if a converter's type without having to compare the instance to its class. Corresponding test cases are added for is_multiplicative and is_logarithmic. --- pint/converters.py | 14 +++++++++++--- pint/testsuite/test_converters.py | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pint/converters.py b/pint/converters.py index 9b04b54a7..4f63ead9d 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -15,7 +15,13 @@ class Converter: """Base class for value converters.""" - is_multiplicative = True + @property + def is_multiplicative(self): + return True + + @property + def is_logarithmic(self): + return False def to_reference(self, value, inplace=False): return value @@ -27,8 +33,6 @@ def from_reference(self, value, inplace=False): class ScaleConverter(Converter): """A linear transformation.""" - is_multiplicative = True - def __init__(self, scale): self.scale = scale @@ -115,6 +119,10 @@ def __init__(self, scale, logbase, logfactor): def is_multiplicative(self): return False + @property + def is_logarithmic(self): + return True + def from_reference(self, value, inplace=False): """Converts value from the reference unit to the logarithmic unit diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index b603bad73..460b3e5c2 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -14,21 +14,28 @@ class TestConverter(BaseTestCase): def test_converter(self): c = Converter() self.assertTrue(c.is_multiplicative) + self.assertFalse(c.is_logarithmic) self.assertTrue(c.to_reference(8)) self.assertTrue(c.from_reference(8)) def test_multiplicative_converter(self): c = ScaleConverter(20.0) + self.assertTrue(c.is_multiplicative) + self.assertFalse(c.is_logarithmic) self.assertEqual(c.from_reference(c.to_reference(100)), 100) self.assertEqual(c.to_reference(c.from_reference(100)), 100) def test_offset_converter(self): c = OffsetConverter(20.0, 2) + self.assertFalse(c.is_multiplicative) + self.assertFalse(c.is_logarithmic) self.assertEqual(c.from_reference(c.to_reference(100)), 100) self.assertEqual(c.to_reference(c.from_reference(100)), 100) def test_log_converter(self): c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) + self.assertFalse(c.is_multiplicative) + self.assertTrue(c.is_logarithmic) self.assertAlmostEqual(c.to_reference(0), 1) self.assertAlmostEqual(c.to_reference(1), 10) self.assertAlmostEqual(c.to_reference(2), 100) From a53f89ca180418f0685a1c248b3ffe2eec67e2eb Mon Sep 17 00:00:00 2001 From: 5igno Date: Sun, 16 Feb 2020 22:45:06 +0100 Subject: [PATCH 375/612] Exposes Definition's property is logaritmic A definition is logarithmic if its associated converter is logarithmic. --- pint/definitions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pint/definitions.py b/pint/definitions.py index 2bfce79cd..54aa741d0 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -119,6 +119,10 @@ def __init__(self, name, symbol, aliases, converter): def is_multiplicative(self): return self._converter.is_multiplicative + @property + def is_logarithmic(self): + return self._converter.is_logarithmic + @classmethod def from_string(cls, definition): """Parse a definition. From 10ffebfdb2f44095eb291874abbc6d6eb8b85334 Mon Sep 17 00:00:00 2001 From: 5igno Date: Sun, 16 Feb 2020 22:48:55 +0100 Subject: [PATCH 376/612] Tests quantity dB defined through multiplication --- pint/testsuite/test_quantity.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 5d6892991..17dba327c 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1656,14 +1656,15 @@ def test_log_quantity_creation(self): self.assertEqual(x.units, y.units) self.assertIsNot(x, y) - # new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) - # x = new_reg.Q_('4.2 * dBm') - # new_reg.assertEqual(x.magnitude, 4.2) - # new_reg.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) - - # with self.capture_log() as buffer: - # self.assertEqual(4.2 * self.ureg.dBm, self.Q_(4.2, 2 * self.ureg.dBm)) - # self.assertEqual(len(buffer), 1) + # Using multiplications for dB units requires autoconversion to baseunits + new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) + x = new_reg.Quantity("4.2 * dBm") + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + with self.capture_log() as buffer: + self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm)) + self.assertEqual(len(buffer), 1) def test_log_convert(self): From aa73ab79651c9757263463383f95f62c1b383199 Mon Sep 17 00:00:00 2001 From: 5igno Date: Sun, 16 Feb 2020 22:53:07 +0100 Subject: [PATCH 377/612] Renames offset_units as non_mul_units Since log units are also non-multiplicative, variable is renamed. --- pint/quantity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 051fc849d..efb58c624 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1790,12 +1790,12 @@ def _is_multiplicative(self): def _get_non_multiplicative_units(self): """Return a list of the of non-multiplicative units of the Quantity object.""" - offset_units = [ + non_mul_units = [ unit for unit in self._units.keys() if not self._REGISTRY._units[unit].is_multiplicative ] - return offset_units + return non_mul_units def _get_delta_units(self): """Return list of delta units ot the Quantity object.""" From 41bcb6358621c1684a560620d81b50277743953c Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 18 Feb 2020 12:30:45 -0600 Subject: [PATCH 378/612] Remove the array interface protocol fallback --- CHANGES | 3 +++ docs/numpy.ipynb | 9 ++------- pint/compat.py | 4 ---- pint/quantity.py | 30 +----------------------------- pint/testsuite/test_numpy.py | 13 ------------- 5 files changed, 6 insertions(+), 53 deletions(-) diff --git a/CHANGES b/CHANGES index 2fb9e3a17..a4f5f4ab1 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Pint Changelog 0.11 (unreleased) ----------------- +- **BREAKING CHANGE**: + The array protocol fallback deprecated in version 0.10 has been removed. + (Issue #1029, Thanks Jon Thielen) - Now we use `pyproject.toml` for providing `setuptools_scm` settings - Remove `default_en_0.6.txt` - Reorganize long_description. diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 4ed462b25..41b649144 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -24,10 +24,6 @@ "# Import NumPy\n", "import numpy as np\n", "\n", - "# Disable Pint's old fallback behavior (must come before importing Pint)\n", - "import os\n", - "os.environ['PINT_ARRAY_PROTOCOL_FALLBACK'] = \"0\"\n", - "\n", "# Import Pint\n", "import pint\n", "ureg = pint.UnitRegistry()\n", @@ -780,8 +776,7 @@ "\n", "This behaviour introduces some performance penalties and increased memory usage. Quantities that must be converted to other units require additional memory and CPU cycles. Therefore, for numerically intensive code, you might want to convert the objects first and then use directly the magnitude, such as by using Pint's `wraps` utility (see [wrapping](wrapping.rst)).\n", "\n", - "Array interface protocol attributes (such as `__array_struct__` and\n", - "`__array_interface__`) are available on Pint Quantities by deferring to the corresponding `__array_*` attribute on the magnitude as casted to an ndarray. This has been found to be potentially incorrect and to cause unexpected behavior, and has therefore been deprecated. As of the next minor version of Pint (or when the `PINT_ARRAY_PROTOCOL_FALLBACK` environment variable is set to 0 prior to importing Pint as done at the beginning of this page), attempting to access these attributes will instead raise an AttributeError." + "Attempting to access array interface protocol attributes (such as `__array_struct__` and `__array_interface__`) on Pint Quantities will raise an AttributeError, since a Quantity is meant to behave as a \"duck array,\" and not a pure ndarray." ] } ], @@ -801,7 +796,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.6" } }, "nbformat": 4, diff --git a/pint/compat.py b/pint/compat.py index 6fb27ee5a..e6037b2af 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 @@ -97,8 +96,6 @@ def __array_function__(self, *args, **kwargs): NP_NO_VALUE = np._NoValue - ARRAY_FALLBACK = bool(int(os.environ.get("PINT_ARRAY_PROTOCOL_FALLBACK", 1))) - except ImportError: np = None @@ -112,7 +109,6 @@ class ndarray: 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/quantity.py b/pint/quantity.py index 051fc849d..59b97d2b1 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -22,7 +22,6 @@ from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 from .compat import ( - ARRAY_FALLBACK, NUMPY_VER, BehaviorChangeWarning, _to_magnitude, @@ -1658,34 +1657,7 @@ def __len__(self): 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/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 595aa3f5e..05aea96d1 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1031,28 +1031,15 @@ def test_insert(self): 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) From e82b9b059ddf30739b362b6a233e832c3d3eddb5 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 18 Feb 2020 13:57:39 -0600 Subject: [PATCH 379/612] Remove array function warning on first creation --- CHANGES | 3 +++ pint/compat.py | 23 ----------------------- pint/quantity.py | 11 ----------- pint/testsuite/test_numpy.py | 1 - pint/testsuite/test_quantity.py | 9 +++------ 5 files changed, 6 insertions(+), 41 deletions(-) diff --git a/CHANGES b/CHANGES index a4f5f4ab1..933afc471 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Pint Changelog 0.11 (unreleased) ----------------- +- Quantities wrapping NumPy arrays will no longer warning for the changed + array function behavior introduced in 0.10. + (Issue #1029, Thanks Jon Thielen) - **BREAKING CHANGE**: The array protocol fallback deprecated in version 0.10 has been removed. (Issue #1029, Thanks Jon Thielen) diff --git a/pint/compat.py b/pint/compat.py index e6037b2af..e8e1a1b1e 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -36,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 @@ -92,7 +71,6 @@ def __array_function__(self, *args, **kwargs): 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 @@ -107,7 +85,6 @@ class ndarray: NUMPY_VER = "0" NUMERIC_TYPES = (Number, Decimal) HAS_NUMPY_ARRAY_FUNCTION = False - SKIP_ARRAY_FUNCTION_CHANGE_WARNING = True NP_NO_VALUE = None def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): diff --git a/pint/quantity.py b/pint/quantity.py index 59b97d2b1..91eee2a52 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -20,12 +20,9 @@ from pkg_resources.extern.packaging import version -from .compat import SKIP_ARRAY_FUNCTION_CHANGE_WARNING # noqa: F401 from .compat import ( NUMPY_VER, - BehaviorChangeWarning, _to_magnitude, - array_function_change_msg, babel_parse, eq, is_duck_array_type, @@ -160,8 +157,6 @@ def __reduce__(self): 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: @@ -214,12 +209,6 @@ def __new__(cls, value, units=None): 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 diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 05aea96d1..3eeab6cf6 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 diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fbfd773b3..51faf1a36 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -6,7 +6,7 @@ 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 @@ def test_notiter(self): 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_([]) From 0b3e73694a4cc19184173589750d8184df766e81 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 19 Feb 2020 13:52:53 -0300 Subject: [PATCH 380/612] Rename `ParsedDefinition` to `PreprocessedDefinition` Close #1010 --- pint/definitions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 8ffa15322..5dfc0ea18 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -15,8 +15,8 @@ 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. @@ -125,7 +125,7 @@ def from_string(cls, definition): Parameters ---------- - definition : str or ParsedDefinition + definition : str or PreprocessedDefinition Returns ------- @@ -133,7 +133,7 @@ def from_string(cls, 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) @@ -184,7 +184,7 @@ class PrefixDefinition(Definition): @classmethod def from_string(cls, definition): 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: @@ -228,7 +228,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal @classmethod def from_string(cls, definition): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) if ";" in definition.value: [converter, modifiers] = definition.value.split(";", 2) @@ -294,7 +294,7 @@ def __init__(self, name, symbol, aliases, converter, reference=None, is_base=Fal @classmethod def from_string(cls, definition): if isinstance(definition, str): - definition = ParsedDefinition.from_string(definition) + definition = PreprocessedDefinition.from_string(definition) converter = ParserHelper.from_string(definition.value) @@ -336,7 +336,7 @@ def __init__(self, name, aliases): def from_string(cls, definition): 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)) From 80c412610ff9b587a9fd28d554fa9bbba52e4940 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 19 Feb 2020 19:41:25 -0300 Subject: [PATCH 381/612] Include .coveragerc --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0cf68650a..cd897ada1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt readthedocs.yml +include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt readthedocs.yml .coveragerc recursive-include pint * recursive-include docs * recursive-include bench * From 3d713cf920c1f164c1c3c2fcfab96956467b0d64 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 19 Feb 2020 19:41:53 -0300 Subject: [PATCH 382/612] Preparing release 0.11 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index c823d74d3..3b4df2a29 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.11 (unreleased) +0.11 (2020-02-19) ----------------- - Added pint-convert script. diff --git a/version.py b/version.py index 17a86c71f..b9d3d6063 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.11.dev0' +__version__ = '0.11' # fmt: on From a0465dc5eebb64f13897294860f4b56bbade1321 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 19 Feb 2020 19:42:20 -0300 Subject: [PATCH 383/612] Back to development: 0.12 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3b4df2a29..8e9c7155f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.12 (unreleased) +----------------- + +- Nothing changed yet. + + 0.11 (2020-02-19) ----------------- diff --git a/version.py b/version.py index b9d3d6063..16c455fd0 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.11' +__version__ = '0.12.dev0' # fmt: on From 5b7d1d27660c04c268e454d3ff530d170e0a2048 Mon Sep 17 00:00:00 2001 From: mark-boer Date: Fri, 21 Feb 2020 16:24:53 +0100 Subject: [PATCH 384/612] fix for key error in numpy.pad --- CHANGES | 4 +++- pint/numpy_func.py | 7 +++---- pint/testsuite/test_numpy.py | 7 +++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 2fb9e3a17..65fd3bcae 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,8 @@ Pint Changelog - Allow constants in units by using a leading underscore (Issue #989, Thanks Juan Nunez-Iglesias) - Fixed bug where to_compact handled prefix units incorrectly (Issue #960) +- Fixed bug where numpy.pad didn't work without specifying constant_values or + end_values (Issue #1026) 0.10.1 (2020-01-07) ------------------- @@ -235,7 +237,7 @@ Pint Changelog - Added several new units and Systems (Issues #749, #737, ) - Started experimental pandas support - (Issue #746 and others. Thanks andrewgsavage, znicholls and others) + (Issue #746 and others. Thanks andrewgsavage, znicholls and others) - wraps and checks now supports kwargs and defaults. (Issue #660, thanks jondoesntgit) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 10f411479..c03887bcb 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -667,10 +667,9 @@ def _recursive_convert(arg, unit): # 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/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 595aa3f5e..6775e7582 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1069,6 +1069,9 @@ def test_pad(self): 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, @@ -1085,6 +1088,10 @@ def test_pad(self): 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, From 27fddf7eec7b9440f07a4de44d5e4b760c7b5091 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 28 Feb 2020 09:53:06 +0000 Subject: [PATCH 385/612] Tests cleanup --- .travis.yml | 2 +- pint/testsuite/helpers.py | 14 +++----------- pint/testsuite/test_issues.py | 11 ----------- pint/testsuite/test_numpy.py | 10 ---------- 4 files changed, 4 insertions(+), 33 deletions(-) diff --git a/.travis.yml b/.travis.yml index ec164c35c..a5afcf492 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ env: - PKGS="python=3.6 numpy uncertainties" - PKGS="python=3.7 numpy uncertainties" - PKGS="python=3.8 numpy uncertainties" - - PKGS="python xarray netCDF4" + - PKGS="python=3.7 numpy uncertainties sparse xarray netCDF4" # TODO: pandas tests # - PKGS="python=3.7 numpy pandas uncertainties pandas" diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 2bf743c72..eae76500d 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -29,14 +29,6 @@ def requires_not_array_function_protocol(): ) -def requires_numpy18(): - if not HAS_NUMPY: - return unittest.skip("Requires NumPy") - return unittest.skipUnless( - StrictVersion(NUMPY_VER) >= StrictVersion("1.8"), "Requires NumPy >= 1.8" - ) - - def requires_numpy_previous_than(version): if not HAS_NUMPY: return unittest.skip("Requires NumPy") @@ -60,7 +52,7 @@ def requires_numpy(): def requires_not_numpy(): - return unittest.skipIf(HAS_NUMPY, "Requires NumPy is not installed.") + return unittest.skipIf(HAS_NUMPY, "Requires NumPy not to be installed.") def requires_babel(): @@ -68,7 +60,7 @@ def requires_babel(): def requires_not_babel(): - return unittest.skipIf(HAS_BABEL, "Requires Babel is not installed") + return unittest.skipIf(HAS_BABEL, "Requires Babel not to be installed") def requires_uncertainties(): @@ -77,7 +69,7 @@ def requires_uncertainties(): def requires_not_uncertainties(): return unittest.skipIf( - HAS_UNCERTAINTIES, "Requires Uncertainties is not installed." + HAS_UNCERTAINTIES, "Requires Uncertainties not to be installed." ) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 5f4271701..60153ddf3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -325,16 +325,6 @@ def test_issue93(self): self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) - @helpers.requires_numpy_previous_than("1.10") - def test_issue94(self): - v1 = np.array([5, 5]) * ureg.meter - v2 = 0.1 * ureg.meter - v3 = np.array([5, 5]) * ureg.meter - v3 += v2 - - np.testing.assert_array_equal((v1 + v2).magnitude, np.array([5.1, 5.1])) - np.testing.assert_array_equal(v3.magnitude, np.array([5, 5])) - def test_issue104(self): x = [ureg("1 meter"), ureg("1 meter"), ureg("1 meter")] @@ -376,7 +366,6 @@ def test_issue121(self): self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - @helpers.requires_numpy18() def test_issue121b(self): sh = (2, 1) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 8e3d11630..56f562921 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -765,16 +765,6 @@ def test_nanstd_numpy_func(self): np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5 ) - @helpers.requires_numpy_previous_than("1.10") - def test_integer_div(self): - a = [1] * self.ureg.m - b = [2] * self.ureg.m - c = a / b # Should be float division - self.assertEqual(c.magnitude[0], 0.5) - - a /= b # Should be integer division - self.assertEqual(a.magnitude[0], 0) - def test_conj(self): self.assertQuantityEqual((self.q * (1 + 1j)).conj(), self.q * (1 - 1j)) self.assertQuantityEqual((self.q * (1 + 1j)).conjugate(), self.q * (1 - 1j)) From 52dcebe63a454bafc489379fa164ac1cd574263e Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 28 Feb 2020 10:57:31 +0000 Subject: [PATCH 386/612] Support pickle protocol 0/1 More tests CHANGES Support pickle protocol 0/1 Polish --- .travis.yml | 2 +- CHANGES | 2 + pint/testsuite/test_application_registry.py | 99 +++++++++++++-------- pint/testsuite/test_errors.py | 40 +++++---- pint/testsuite/test_non_int.py | 21 +++-- pint/testsuite/test_numpy.py | 15 ++-- pint/testsuite/test_quantity.py | 16 ++-- pint/util.py | 15 ++++ 8 files changed, 127 insertions(+), 83 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5afcf492..efea2b18d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ env: - PKGS="python=3.6 numpy uncertainties" - PKGS="python=3.7 numpy uncertainties" - PKGS="python=3.8 numpy uncertainties" - - PKGS="python=3.7 numpy uncertainties sparse xarray netCDF4" + - PKGS="python=3.8 numpy uncertainties sparse xarray netCDF4" # TODO: pandas tests # - PKGS="python=3.7 numpy pandas uncertainties pandas" diff --git a/CHANGES b/CHANGES index b897f29aa..67c173f1f 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ Pint Changelog the registry. - Fixed bug where numpy.pad didn't work without specifying constant_values or end_values (Issue #1026) +- Reinstated support for pickle protocol 0 and 1, which is required by pytables + (Issue #1036, Thanks Guido Imperiale) 0.11 (2020-02-19) diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index 6e2055c09..a06844525 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -14,48 +14,59 @@ from pint.testsuite import BaseTestCase from pint.testsuite.helpers import requires_uncertainties +from .parameterized import ParameterizedTestMixin -class TestDefaultApplicationRegistry(BaseTestCase): - def test_unit(self): +allprotos = ParameterizedTestMixin.parameterize( + ("protocol",), [(p,) for p in range(pickle.HIGHEST_PROTOCOL + 1)] +) + + +class TestDefaultApplicationRegistry(BaseTestCase, ParameterizedTestMixin): + @allprotos + def test_unit(self, protocol): u = Unit("kg") self.assertEqual(str(u), "kilogram") - u = pickle.loads(pickle.dumps(u)) + u = pickle.loads(pickle.dumps(u, protocol)) self.assertEqual(str(u), "kilogram") - def test_quantity_1arg(self): + @allprotos + def test_quantity_1arg(self, protocol): q = Quantity("123 kg") self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) - q = pickle.loads(pickle.dumps(q)) + q = pickle.loads(pickle.dumps(q, protocol)) self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) - def test_quantity_2args(self): + @allprotos + def test_quantity_2args(self, protocol): q = Quantity(123, "kg") self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) - q = pickle.loads(pickle.dumps(q)) + q = pickle.loads(pickle.dumps(q, protocol)) self.assertEqual(str(q.units), "kilogram") self.assertEqual(q.to("t").magnitude, 0.123) @requires_uncertainties() - def test_measurement_2args(self): + @allprotos + def test_measurement_2args(self, protocol): m = Measurement(Quantity(123, "kg"), Quantity(15, "kg")) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 15) self.assertEqual(str(m.units), "kilogram") - m = pickle.loads(pickle.dumps(m)) + m = pickle.loads(pickle.dumps(m, protocol)) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 15) self.assertEqual(str(m.units), "kilogram") @requires_uncertainties() - def test_measurement_3args(self): + @allprotos + def test_measurement_3args(self, protocol): m = Measurement(123, 15, "kg") self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 15) self.assertEqual(str(m.units), "kilogram") - m = pickle.loads(pickle.dumps(m)) + m = pickle.loads(pickle.dumps(m, protocol)) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 15) self.assertEqual(str(m.units), "kilogram") @@ -65,25 +76,27 @@ def test_get_application_registry(self): u = ureg.Unit("kg") self.assertEqual(str(u), "kilogram") - def test_pickle_crash(self): + @allprotos + def test_pickle_crash(self, protocol): ureg = UnitRegistry(None) ureg.define("foo = []") q = ureg.Quantity(123, "foo") - b = pickle.dumps(q) + b = pickle.dumps(q, protocol) self.assertRaises(UndefinedUnitError, pickle.loads, b) - b = pickle.dumps(q.units) + b = pickle.dumps(q.units, protocol) self.assertRaises(UndefinedUnitError, pickle.loads, b) @requires_uncertainties() - def test_pickle_crash_measurement(self): + @allprotos + def test_pickle_crash_measurement(self, protocol): ureg = UnitRegistry(None) ureg.define("foo = []") m = ureg.Quantity(123, "foo").plus_minus(10) - b = pickle.dumps(m) + b = pickle.dumps(m, protocol) self.assertRaises(UndefinedUnitError, pickle.loads, b) -class TestCustomApplicationRegistry(BaseTestCase): +class TestCustomApplicationRegistry(BaseTestCase, ParameterizedTestMixin): def setUp(self): super().setUp() self.ureg_bak = get_application_registry() @@ -97,52 +110,57 @@ def tearDown(self): super().tearDown() set_application_registry(self.ureg_bak) - def test_unit(self): + @allprotos + def test_unit(self, protocol): u = Unit("foo") self.assertEqual(str(u), "foo") - u = pickle.loads(pickle.dumps(u)) + u = pickle.loads(pickle.dumps(u, protocol)) self.assertEqual(str(u), "foo") - def test_quantity_1arg(self): + @allprotos + def test_quantity_1arg(self, protocol): q = Quantity("123 foo") self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) - q = pickle.loads(pickle.dumps(q)) + q = pickle.loads(pickle.dumps(q, protocol)) self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) - def test_quantity_2args(self): + @allprotos + def test_quantity_2args(self, protocol): q = Quantity(123, "foo") self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) - q = pickle.loads(pickle.dumps(q)) + q = pickle.loads(pickle.dumps(q, protocol)) self.assertEqual(str(q.units), "foo") self.assertEqual(q.to("bar").magnitude, 246) @requires_uncertainties() - def test_measurement_2args(self): + @allprotos + def test_measurement_2args(self, protocol): m = Measurement(Quantity(123, "foo"), Quantity(10, "bar")) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") - m = pickle.loads(pickle.dumps(m)) + m = pickle.loads(pickle.dumps(m, protocol)) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") @requires_uncertainties() - def test_measurement_3args(self): + @allprotos + def test_measurement_3args(self, protocol): m = Measurement(123, 5, "foo") self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") - m = pickle.loads(pickle.dumps(m)) + m = pickle.loads(pickle.dumps(m, protocol)) self.assertEqual(m.value.magnitude, 123) self.assertEqual(m.error.magnitude, 5) self.assertEqual(str(m.units), "foo") -class TestSwapApplicationRegistry(BaseTestCase): +class TestSwapApplicationRegistry(BaseTestCase, ParameterizedTestMixin): """Test that the constructors of Quantity, Unit, and Measurement capture the registry that is set as the application registry at creation time @@ -167,12 +185,13 @@ def setUp(self): def tearDown(self): set_application_registry(self.ureg_bak) - def test_quantity_1arg(self): + @allprotos + def test_quantity_1arg(self, protocol): set_application_registry(self.ureg1) q1 = Quantity("1 foo") set_application_registry(self.ureg2) q2 = Quantity("1 foo") - q3 = pickle.loads(pickle.dumps(q1)) + q3 = pickle.loads(pickle.dumps(q1, protocol)) assert q1.dimensionality == {"[dim1]": 1} assert q2.dimensionality == {"[dim2]": 1} assert q3.dimensionality == {"[dim2]": 1} @@ -180,12 +199,13 @@ def test_quantity_1arg(self): assert q2.to("bar").magnitude == 3 assert q3.to("bar").magnitude == 3 - def test_quantity_2args(self): + @allprotos + def test_quantity_2args(self, protocol): set_application_registry(self.ureg1) q1 = Quantity(1, "foo") set_application_registry(self.ureg2) q2 = Quantity(1, "foo") - q3 = pickle.loads(pickle.dumps(q1)) + q3 = pickle.loads(pickle.dumps(q1, protocol)) assert q1.dimensionality == {"[dim1]": 1} assert q2.dimensionality == {"[dim2]": 1} assert q3.dimensionality == {"[dim2]": 1} @@ -193,23 +213,25 @@ def test_quantity_2args(self): assert q2.to("bar").magnitude == 3 assert q3.to("bar").magnitude == 3 - def test_unit(self): + @allprotos + def test_unit(self, protocol): set_application_registry(self.ureg1) u1 = Unit("bar") set_application_registry(self.ureg2) u2 = Unit("bar") - u3 = pickle.loads(pickle.dumps(u1)) + u3 = pickle.loads(pickle.dumps(u1, protocol)) assert u1.dimensionality == {"[dim1]": 1} assert u2.dimensionality == {"[dim2]": 1} assert u3.dimensionality == {"[dim2]": 1} @requires_uncertainties() - def test_measurement_2args(self): + @allprotos + def test_measurement_2args(self, protocol): set_application_registry(self.ureg1) m1 = Measurement(Quantity(10, "foo"), Quantity(1, "foo")) set_application_registry(self.ureg2) m2 = Measurement(Quantity(10, "foo"), Quantity(1, "foo")) - m3 = pickle.loads(pickle.dumps(m1)) + m3 = pickle.loads(pickle.dumps(m1, protocol)) assert m1.dimensionality == {"[dim1]": 1} assert m2.dimensionality == {"[dim2]": 1} @@ -222,12 +244,13 @@ def test_measurement_2args(self): self.assertEqual(m3.to("bar").error.magnitude, 3) @requires_uncertainties() - def test_measurement_3args(self): + @allprotos + def test_measurement_3args(self, protocol): set_application_registry(self.ureg1) m1 = Measurement(10, 1, "foo") set_application_registry(self.ureg2) m2 = Measurement(10, 1, "foo") - m3 = pickle.loads(pickle.dumps(m1)) + m3 = pickle.loads(pickle.dumps(m1, protocol)) assert m1.dimensionality == {"[dim1]": 1} assert m2.dimensionality == {"[dim2]": 1} diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index cb75dd7ca..758ba97d6 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -94,24 +94,28 @@ def test_pickle_definition_syntax_error(self): ureg = UnitRegistry(filename=None) ureg.define("foo = [bar]") ureg.define("bar = 2 foo") - pik = pickle.dumps(ureg.Quantity("1 foo")) - with self.assertRaises(UndefinedUnitError): - pickle.loads(pik) q1 = ureg.Quantity("1 foo") q2 = ureg.Quantity("1 bar") - for ex in [ - DefinitionSyntaxError("foo", filename="a.txt", lineno=123), - RedefinitionError("foo", "bar"), - UndefinedUnitError("meter"), - DimensionalityError("a", "b", "c", "d", extra_msg=": msg"), - OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units), - OffsetUnitCalculusError(q1._units, q2._units), - ]: - with self.subTest(etype=type(ex)): - # assert False, ex.__reduce__() - ex2 = pickle.loads(pickle.dumps(ex)) - assert type(ex) is type(ex2) - self.assertEqual(ex.args, ex2.args) - self.assertEqual(ex.__dict__, ex2.__dict__) - self.assertEqual(str(ex), str(ex2)) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + for ex in [ + DefinitionSyntaxError("foo", filename="a.txt", lineno=123), + RedefinitionError("foo", "bar"), + UndefinedUnitError("meter"), + DimensionalityError("a", "b", "c", "d", extra_msg=": msg"), + OffsetUnitCalculusError( + Quantity("1 kg")._units, Quantity("1 s")._units + ), + OffsetUnitCalculusError(q1._units, q2._units), + ]: + with self.subTest(protocol=protocol, etype=type(ex)): + pik = pickle.dumps(ureg.Quantity("1 foo"), protocol) + with self.assertRaises(UndefinedUnitError): + pickle.loads(pik) + + # assert False, ex.__reduce__() + ex2 = pickle.loads(pickle.dumps(ex, protocol)) + assert type(ex) is type(ex2) + self.assertEqual(ex.args, ex2.args) + self.assertEqual(ex.__dict__, ex2.__dict__) + self.assertEqual(str(ex), str(ex2)) diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py index 47409f2e4..bb5679219 100644 --- a/pint/testsuite/test_non_int.py +++ b/pint/testsuite/test_non_int.py @@ -1,6 +1,7 @@ import copy import math import operator as op +import pickle from decimal import Decimal from fractions import Fraction @@ -351,15 +352,17 @@ def test_offset_delta(self): ) def test_pickle(self): - import pickle - - def pickle_test(q): - self.assertEqual(q, pickle.loads(pickle.dumps(q))) - - pickle_test(self.QP_("32", "")) - pickle_test(self.QP_("2.4", "")) - pickle_test(self.QP_("32", "m/s")) - pickle_test(self.QP_("2.4", "m/s")) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + for magnitude, unit in ( + ("32", ""), + ("2.4", ""), + ("32", "m/s"), + ("2.4", "m/s"), + ): + with self.subTest(protocol=protocol, magnitude=magnitude, unit=unit): + q1 = self.QP_(magnitude, unit) + q2 = pickle.loads(pickle.dumps(q1, protocol)) + self.assertEqual(q1, q2) def test_notiter(self): # Verify that iter() crashes immediately, without needing to draw any diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 56f562921..e057ed9a9 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,5 +1,6 @@ import copy import operator as op +import pickle import unittest from pint import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning @@ -827,14 +828,12 @@ def test_reversible_op(self): self.assertQuantityEqual(x - u, -(u - x)) def test_pickle(self): - import pickle - - def pickle_test(q): - pq = pickle.loads(pickle.dumps(q)) - self.assertNDArrayEqual(q.magnitude, pq.magnitude) - self.assertEqual(q.units, pq.units) - - pickle_test([10, 20] * self.ureg.m) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol): + q1 = [10, 20] * self.ureg.m + q2 = pickle.loads(pickle.dumps(q1, protocol)) + self.assertNDArrayEqual(q1.magnitude, q2.magnitude) + self.assertEqual(q1.units, q2.units) def test_equal(self): x = self.q.magnitude diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 51faf1a36..70cf1574c 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -2,6 +2,7 @@ import datetime import math import operator as op +import pickle import warnings from unittest.mock import patch @@ -483,15 +484,12 @@ def test_offset_delta(self): ) def test_pickle(self): - import pickle - - def pickle_test(q): - self.assertEqual(q, pickle.loads(pickle.dumps(q))) - - pickle_test(self.Q_(32, "")) - pickle_test(self.Q_(2.4, "")) - pickle_test(self.Q_(32, "m/s")) - pickle_test(self.Q_(2.4, "m/s")) + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + for magnitude, unit in ((32, ""), (2.4, ""), (32, "m/s"), (2.4, "m/s")): + with self.subTest(protocol=protocol, magnitude=magnitude, unit=unit): + q1 = self.Q_(magnitude, unit) + q2 = pickle.loads(pickle.dumps(q1, protocol)) + self.assertEqual(q1, q2) @helpers.requires_numpy() def test_from_sequence(self): diff --git a/pint/util.py b/pint/util.py index aeebff1e1..614849361 100644 --- a/pint/util.py +++ b/pint/util.py @@ -412,6 +412,13 @@ def __hash__(self): self._hash = hash(frozenset(self._d.items())) return self._hash + # Only needed by pickle protocol 0 and 1 (used by pytables) + def __getstate__(self): + return self._d, self._hash, self._one, self._non_int_type + + def __setstate__(self, state): + self._d, self._hash, self._one, self._non_int_type = state + def __eq__(self, other): if isinstance(other, UnitsContainer): # UnitsContainer.__hash__(self) is not the same as hash(self); see @@ -638,6 +645,14 @@ def __hash__(self): raise ValueError(mess) return super().__hash__() + # Only needed by pickle protocol 0 and 1 (used by pytables) + def __getstate__(self): + return super().__getstate__() + (self.scale,) + + def __setstate__(self, state): + super().__setstate__(state[:-1]) + self.scale = state[-1] + def __eq__(self, other): if isinstance(other, ParserHelper): return self.scale == other.scale and super().__eq__(other) From 81cabd67722c657b3dae3c2d884b48f30313d9bd Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 28 Feb 2020 11:30:39 +0000 Subject: [PATCH 387/612] Fix regression --- pint/testsuite/test_issues.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 60153ddf3..740101878 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -366,6 +366,7 @@ def test_issue121(self): self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) + @helpers.requires_numpy() def test_issue121b(self): sh = (2, 1) From c9c5823a4edf40187af55d25ef348c40ef053d6f Mon Sep 17 00:00:00 2001 From: Abdurrahmaan Iqbal Date: Mon, 2 Mar 2020 01:00:45 +0000 Subject: [PATCH 388/612] Return expected empty type for parse_pattern function --- pint/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/registry.py b/pint/registry.py index 99873edeb..980fab597 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1183,7 +1183,7 @@ def parse_pattern( """ if not input_string: - return self.Quantity(1) + return [] if many else None # Parse string pattern = pattern_to_regex(pattern) From 86878174f558e9660598f6f65b0d15fb06f45757 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Thu, 5 Mar 2020 10:39:16 -0600 Subject: [PATCH 389/612] Update expected initial release date for pint-xarray --- docs/numpy.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 41b649144..268250f2d 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -754,7 +754,7 @@ "To aid in integration between various array types and Pint (such as by providing convenience methods), the following compatibility packages are available:\n", "\n", "- [pint-pandas](https://github.com/hgrecco/pint-pandas)\n", - "- pint-xarray ([in development](https://github.com/hgrecco/pint/issues/849), initial alpha release planned for January 2020)\n", + "- pint-xarray ([in development](https://github.com/hgrecco/pint/issues/849), initial alpha release planned for April-May 2020)\n", "\n", "(Note: if you have developed a compatibility package for Pint, please submit a pull request to add it to this list!)" ] From 7a905ad6bcdd724e8f279be11c73e0303d4543eb Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 8 Mar 2020 22:33:18 +0100 Subject: [PATCH 390/612] don't require rtol / atol to be quantities --- pint/numpy_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index c03887bcb..19df2e78b 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -763,7 +763,7 @@ def implementation(*args, **kwargs): ("amax", ["a", "initial"], True), ("amin", ["a", "initial"], True), ("searchsorted", ["a", "v"], False), - ("isclose", ["a", "b", "rtol", "atol"], False), + ("isclose", ["a", "b"], False), ("nan_to_num", ["x", "nan", "posinf", "neginf"], True), ("clip", ["a", "a_min", "a_max"], True), ("append", ["arr", "values"], True), From 0eb5d758b479d82f4e0c5ad69779f60e0a5bdc34 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 8 Mar 2020 22:38:10 +0100 Subject: [PATCH 391/612] test that isclose allows python scalars as atol / rtol --- pint/testsuite/test_numpy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index e057ed9a9..587beedfa 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -896,6 +896,10 @@ def test_isclose_numpy_func(self): self.assertNDArrayEqual( np.isclose(self.q, q2), np.array([[False, True], [True, False]]) ) + self.assertNDArrayEqual( + np.isclose(self.q, q2, atol=1e-5, rtol=1e-7), + np.array([[False, True], [True, False]]), + ) @helpers.requires_array_function_protocol() def test_interp_numpy_func(self): From 436b585bf7f4b816195df187eee9678abbbf036c Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 8 Mar 2020 22:42:49 +0100 Subject: [PATCH 392/612] black --- pint/registry.py | 6 ++---- pint/testsuite/test_babel.py | 4 +--- pint/testsuite/test_non_int.py | 4 +--- pint/testsuite/test_numpy.py | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 99873edeb..1cc7441d8 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1126,9 +1126,7 @@ def _parse_units(self, input_string, as_delta=True): return ret - def _eval_token( - self, token, case_sensitive=True, use_decimal=False, **values, - ): + def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): # TODO: remove this code when use_decimal is deprecated if use_decimal: @@ -1214,7 +1212,7 @@ def parse_pattern( return results def parse_expression( - self, input_string, case_sensitive=True, use_decimal=False, **values, + self, input_string, case_sensitive=True, use_decimal=False, **values ): """Parse a mathematical expression including units and return a quantity object. diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index def7c8319..b3d530314 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -58,9 +58,7 @@ def test_registry_locale(self): def test_no_registry_locale(self): ureg = UnitRegistry() distance = 24.0 * ureg.meter - self.assertRaises( - Exception, distance.format_babel, - ) + self.assertRaises(Exception, distance.format_babel) @helpers.requires_babel() def test_str(self): diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py index bb5679219..4a377d25f 100644 --- a/pint/testsuite/test_non_int.py +++ b/pint/testsuite/test_non_int.py @@ -220,9 +220,7 @@ def test_both_symbol(self): def test_dimensionless_units(self): twopi = self.NON_INT_TYPE("2") * self.ureg.pi - self.assertAlmostEqual( - self.QP_("360", "degree").to("radian").magnitude, twopi, - ) + self.assertAlmostEqual(self.QP_("360", "degree").to("radian").magnitude, twopi) self.assertAlmostEqual(self.Q_(twopi, "radian"), self.QP_("360", "degree")) self.assertEqual(self.QP_("1", "radian").dimensionality, UnitsContainer()) self.assertTrue(self.QP_("1", "radian").dimensionless) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 587beedfa..244d02813 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1049,7 +1049,7 @@ def test_pad(self): 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, + 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)), From 5191978e9a546c6b0caf47dc743b0eab54d0eb2d Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Thu, 12 Mar 2020 23:32:10 +0000 Subject: [PATCH 393/612] add, iadd, eq, gt, etc. to treat bare NaN like 0 Trivial Bugfix on legacy numpy black --- CHANGES | 2 + pint/compat.py | 86 ++++++++++++-- pint/numpy_func.py | 13 +- pint/quantity.py | 24 ++-- pint/testsuite/__init__.py | 8 ++ pint/testsuite/test_compat.py | 104 ++++++++++++++++ pint/testsuite/test_issues.py | 34 ------ pint/testsuite/test_quantity.py | 202 ++++++++++++++++++++++++-------- 8 files changed, 359 insertions(+), 114 deletions(-) create mode 100644 pint/testsuite/test_compat.py diff --git a/CHANGES b/CHANGES index 67c173f1f..ced766339 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,8 @@ Pint Changelog end_values (Issue #1026) - Reinstated support for pickle protocol 0 and 1, which is required by pytables (Issue #1036, Thanks Guido Imperiale) +- NaN is now treated the same as zero in addition, equality, and disequality + (Issue #1051, Thanks Guido Imperiale) 0.11 (2020-02-19) diff --git a/pint/compat.py b/pint/compat.py index e8e1a1b1e..a671e1983 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -7,6 +7,7 @@ :copyright: 2013 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +import math import tokenize from decimal import Decimal from io import BytesIO @@ -38,7 +39,7 @@ class BehaviorChangeWarning(UserWarning): try: import numpy as np - from numpy import ndarray + from numpy import ndarray, datetime64 as np_datetime64 HAS_NUMPY = True NUMPY_VER = np.__version__ @@ -81,6 +82,9 @@ def __array_function__(self, *args, **kwargs): class ndarray: pass + class np_datetime64: + pass + HAS_NUMPY = False NUMPY_VER = "0" NUMERIC_TYPES = (Number, Decimal) @@ -154,7 +158,7 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): pass -def is_upcast_type(other): +def is_upcast_type(other) -> bool: """Check if the type object is a upcast type using preset list. Parameters @@ -168,29 +172,29 @@ def is_upcast_type(other): return other in upcast_types -def is_duck_array_type(other): +def is_duck_array_type(cls) -> bool: """Check if the type object represents a (non-Quantity) duck array type. Parameters ---------- - other : object + cls : class Returns ------- bool """ # TODO (NEP 30): replace duck array check with hasattr(other, "__duckarray__") - return other is ndarray or ( - not hasattr(other, "_magnitude") - and not hasattr(other, "_units") + return issubclass(cls, ndarray) or ( + not hasattr(cls, "_magnitude") + and not hasattr(cls, "_units") and HAS_NUMPY_ARRAY_FUNCTION - and hasattr(other, "__array_function__") - and hasattr(other, "ndim") - and hasattr(other, "dtype") + and hasattr(cls, "__array_function__") + and hasattr(cls, "ndim") + and hasattr(cls, "dtype") ) -def eq(lhs, rhs, check_all): +def eq(lhs, rhs, check_all: bool): """Comparison of scalars and arrays. Parameters @@ -200,7 +204,8 @@ def eq(lhs, rhs, check_all): rhs : object right-hand side check_all : bool - if True, reduce sequence to single bool. + if True, reduce sequence to single bool; + return True if all the elements are equal. Returns ------- @@ -208,5 +213,60 @@ def eq(lhs, rhs, check_all): """ out = lhs == rhs if check_all and isinstance(out, ndarray): - return np.all(out) + return out.all() + return out + + +def isnan(obj, check_all: bool): + """Test for NaN or NaT + + Parameters + ---------- + obj : object + scalar or vector + check_all : bool + if True, reduce sequence to single bool; + return True if any of the elements are NaN. + + Returns + ------- + bool or array_like of bool. + Always return False for non-numeric types. + """ + if is_duck_array_type(type(obj)): + if obj.dtype.kind in "if": + out = np.isnan(obj) + elif obj.dtype.kind in "Mm": + out = np.isnat(obj) + else: + # Not a numeric or datetime type + out = np.full(obj.shape, False) + return out.any() if check_all else out + if isinstance(obj, np_datetime64): + return np.isnat(obj) + try: + return math.isnan(obj) + except TypeError: + return False + + +def zero_or_nan(obj, check_all: bool): + """Test if obj is zero, NaN, or NaT + + Parameters + ---------- + obj : object + scalar or vector + check_all : bool + if True, reduce sequence to single bool; + return True if all the elements are zero, NaN, or NaT. + + Returns + ------- + bool or array_like of bool. + Always return False for non-numeric types. + """ + out = eq(obj, 0, False) + isnan(obj, False) + if check_all and is_duck_array_type(type(out)): + return out.all() return out diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 19df2e78b..210f967d7 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -10,7 +10,7 @@ from inspect import signature from itertools import chain -from .compat import eq, is_upcast_type, np +from .compat import is_upcast_type, np, zero_or_nan from .errors import DimensionalityError from .util import iterable, sized @@ -485,12 +485,13 @@ def _power(x1, x2): def _add_subtract_handle_non_quantity_zero(x1, x2): - # As in #121/#122, if a value is 0 (but not Quantity 0) do the operation without - # checking units. We do the calculation instead of just returning the same value to - # enforce any shape checking and type casting due to the operation. - if eq(x1, 0, True): + # As in #121, #122, and #1051, if a value is 0 or NaN (but not a Quantity) do the + # operation without checking units. We do the calculation instead of just returning + # the same value to enforce any shape checking and type casting due to the + # operation. + if zero_or_nan(x1, True): (x2,), output_wrap = unwrap_and_wrap_consistent_units(x2) - elif eq(x2, 0, True): + elif zero_or_nan(x2, True): (x1,), output_wrap = unwrap_and_wrap_consistent_units(x1) else: (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) diff --git a/pint/quantity.py b/pint/quantity.py index d434a0fb7..7ce8019ae 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -29,6 +29,7 @@ is_upcast_type, ndarray, np, + zero_or_nan, ) from .definitions import UnitDefinition from .errors import ( @@ -99,8 +100,7 @@ def wrapped(self, *args, **kwargs): # and expects Quantity * array[Quantity] should return NotImplemented elif isinstance(other, list) and other and isinstance(other[0], type(self)): return NotImplemented - result = f(self, *args, **kwargs) - return result + return f(self, *args, **kwargs) return wrapped @@ -786,7 +786,7 @@ def _iadd_sub(self, other, op): raise except TypeError: return NotImplemented - if eq(other, 0, True): + if zero_or_nan(other, True): # If the other value is 0 (but not Quantity 0) # do the operation without checking units. # We do the calculation instead of just returning the same @@ -887,13 +887,11 @@ def _add_sub(self, other, op): object to be added to / subtracted from self op : function operator function (e.g. operator.add, operator.isub) - - """ if not self._check(other): # other not from same Registry or not a Quantity - if eq(other, 0, True): - # If the other value is 0 (but not Quantity 0) + if zero_or_nan(other, True): + # If the other value is 0 or NaN (but not a Quantity) # do the operation without checking units. # We do the calculation instead of just returning the same # value to enforce any shape checking and type casting due to @@ -1446,9 +1444,9 @@ def __eq__(self, other): # We compare to the base class of Quantity because # each Quantity class is unique. if not isinstance(other, Quantity): - if eq(other, 0, True): - # Handle the special case in which we compare to zero - # (or an array of zeros) + if zero_or_nan(other, True): + # Handle the special case in which we compare to zero or NaN + # (or an array of zeros or NaNs) if self._is_multiplicative: # compare magnitude return eq(self._magnitude, other, False) @@ -1493,9 +1491,9 @@ def compare(self, other, op): return op( self._convert_magnitude_not_inplace(self.UnitsContainer()), other ) - elif eq(other, 0, True): - # Handle the special case in which we compare to zero - # (or an array of zeros) + elif zero_or_nan(other, True): + # Handle the special case in which we compare to zero or NaN + # (or an array of zeros or NaNs) if self._is_multiplicative: # compare magnitude return op(self._magnitude, other) diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 4aa3e324a..1fbc233d4 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -101,6 +101,10 @@ def assertQuantityEqual(self, first, second, msg=None): if isinstance(m1, ndarray) or isinstance(m2, ndarray): np.testing.assert_array_equal(m1, m2, err_msg=msg) + elif math.isnan(m1): + self.assertTrue(math.isnan(m2), msg) + elif math.isnan(m2): + self.assertTrue(math.isnan(m1), msg) else: self.assertEqual(m1, m2, msg) @@ -118,6 +122,10 @@ def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None) if isinstance(m1, ndarray) or isinstance(m2, ndarray): np.testing.assert_allclose(m1, m2, rtol=rtol, atol=atol, err_msg=msg) + elif math.isnan(m1): + self.assertTrue(math.isnan(m2), msg) + elif math.isnan(m2): + self.assertTrue(math.isnan(m1), msg) else: self.assertLessEqual(abs(m1 - m2), atol + rtol * abs(m2), msg=msg) diff --git a/pint/testsuite/test_compat.py b/pint/testsuite/test_compat.py new file mode 100644 index 000000000..926a39291 --- /dev/null +++ b/pint/testsuite/test_compat.py @@ -0,0 +1,104 @@ +import math +from datetime import datetime, timedelta + +import pytest + +from pint.compat import eq, isnan, zero_or_nan + +from .helpers import requires_numpy + + +@pytest.mark.parametrize("check_all", [False, True]) +def test_eq(check_all): + assert eq(0, 0, check_all) + assert not eq(0, 1, check_all) + + +@requires_numpy() +def test_eq_numpy(): + import numpy as np + + assert eq(np.array([1, 2]), np.array([1, 2]), True) + assert not eq(np.array([1, 2]), np.array([1, 3]), True) + np.testing.assert_equal( + eq(np.array([1, 2]), np.array([1, 2]), False), np.array([True, True]) + ) + np.testing.assert_equal( + eq(np.array([1, 2]), np.array([1, 3]), False), np.array([True, False]) + ) + + # Mixed numpy/scalar + assert eq(1, np.array([1, 1]), True) + assert eq(np.array([1, 1]), 1, True) + assert not eq(1, np.array([1, 2]), True) + assert not eq(np.array([1, 2]), 1, True) + + +@pytest.mark.parametrize("check_all", [False, True]) +def test_isnan(check_all): + assert not isnan(0, check_all) + assert not isnan(0.0, check_all) + assert isnan(math.nan, check_all) + assert not isnan(datetime(2000, 1, 1), check_all) + assert not isnan(timedelta(seconds=1), check_all) + assert not isnan("foo", check_all) + + +@requires_numpy() +def test_isnan_numpy(): + import numpy as np + + assert isnan(np.nan, True) + assert isnan(np.nan, False) + assert not isnan(np.array([0, 0]), True) + assert isnan(np.array([0, np.nan]), True) + assert not isnan(np.array(["A", "B"]), True) + np.testing.assert_equal( + isnan(np.array([1, np.nan]), False), np.array([False, True]) + ) + np.testing.assert_equal( + isnan(np.array(["A", "B"]), False), np.array([False, False]) + ) + + +@requires_numpy() +def test_isnan_nat(): + import numpy as np + + a = np.array(["2000-01-01", "NaT"], dtype="M8") + b = np.array(["2000-01-01", "2000-01-02"], dtype="M8") + assert isnan(a, True) + assert not isnan(b, True) + np.testing.assert_equal(isnan(a, False), np.array([False, True])) + np.testing.assert_equal(isnan(b, False), np.array([False, False])) + + # Scalar numpy.datetime64 + assert not isnan(a[0], True) + assert not isnan(a[0], False) + assert isnan(a[1], True) + assert isnan(a[1], False) + + +@pytest.mark.parametrize("check_all", [False, True]) +def test_zero_or_nan(check_all): + assert zero_or_nan(0, check_all) + assert zero_or_nan(math.nan, check_all) + assert not zero_or_nan(1, check_all) + assert not zero_or_nan(datetime(2000, 1, 1), check_all) + assert not zero_or_nan(timedelta(seconds=1), check_all) + assert not zero_or_nan("foo", check_all) + + +@requires_numpy() +def test_zero_or_nan_numpy(): + import numpy as np + + assert zero_or_nan(np.nan, True) + assert zero_or_nan(np.nan, False) + assert zero_or_nan(np.array([0, np.nan]), True) + assert not zero_or_nan(np.array([1, np.nan]), True) + assert not zero_or_nan(np.array([0, 1]), True) + assert not zero_or_nan(np.array(["A", "B"]), True) + np.testing.assert_equal( + zero_or_nan(np.array([0, 1, np.nan]), False), np.array([True, False, True]) + ) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 740101878..230b7975a 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -357,40 +357,6 @@ def test_issue105(self): func("METER") self.assertEqual(val, func("METER", False)) - def test_issue121(self): - z, v = 0, 2.0 - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - - @helpers.requires_numpy() - def test_issue121b(self): - sh = (2, 1) - - z, v = 0, 2.0 - self.assertEqual(z + v * ureg.meter, v * ureg.meter) - self.assertEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertEqual(v * ureg.meter + z, v * ureg.meter) - self.assertEqual(v * ureg.meter - z, v * ureg.meter) - - self.assertEqual(sum([v * ureg.meter, v * ureg.meter]), 2 * v * ureg.meter) - - z, v = np.zeros(sh), 2.0 * np.ones(sh) - self.assertQuantityEqual(z + v * ureg.meter, v * ureg.meter) - self.assertQuantityEqual(z - v * ureg.meter, -v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter + z, v * ureg.meter) - self.assertQuantityEqual(v * ureg.meter - z, v * ureg.meter) - - z, v = np.zeros((3, 1)), 2.0 * np.ones(sh) - for x, y in ((z, v), (z, v * ureg.meter), (v * ureg.meter, z)): - with self.assertRaises(ValueError): - x + y - with self.assertRaises(ValueError): - x - y - @helpers.requires_numpy() def test_issue127(self): q = [1.0, 2.0, 3.0, 4.0] * self.ureg.meter diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 70cf1574c..d4fa77853 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -843,6 +843,106 @@ def test_quantity_float_complex(self): self.assertRaises(DimensionalityError, fun, z) +class TestQuantityNeutralAdd(QuantityTestCase): + """Addition to zero or NaN is allowed between a Quantity and a non-Quantity + """ + + FORCE_NDARRAY = False + + def test_bare_zero(self): + v = self.Q_(2.0, "m") + self.assertEqual(v + 0, v) + self.assertEqual(v - 0, v) + self.assertEqual(0 + v, v) + self.assertEqual(0 - v, -v) + + def test_bare_zero_inplace(self): + v = self.Q_(2.0, "m") + v2 = self.Q_(2.0, "m") + v2 += 0 + self.assertEqual(v2, v) + v2 = self.Q_(2.0, "m") + v2 -= 0 + self.assertEqual(v2, v) + v2 = 0 + v2 += v + self.assertEqual(v2, v) + v2 = 0 + v2 -= v + self.assertEqual(v2, -v) + + def test_bare_nan(self): + v = self.Q_(2.0, "m") + self.assertQuantityEqual(v + math.nan, self.Q_(math.nan, v.units)) + self.assertQuantityEqual(v - math.nan, self.Q_(math.nan, v.units)) + self.assertQuantityEqual(math.nan + v, self.Q_(math.nan, v.units)) + self.assertQuantityEqual(math.nan - v, self.Q_(math.nan, v.units)) + + def test_bare_nan_inplace(self): + v = self.Q_(2.0, "m") + v2 = self.Q_(2.0, "m") + v2 += math.nan + self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + v2 = self.Q_(2.0, "m") + v2 -= math.nan + self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + v2 = math.nan + v2 += v + self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + v2 = math.nan + v2 -= v + self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + + @helpers.requires_numpy() + def test_bare_zero_or_nan_numpy(self): + z = np.array([0.0, np.nan]) + v = self.Q_([1.0, 2.0], "m") + e = self.Q_([1.0, np.nan], "m") + self.assertQuantityEqual(z + v, e) + self.assertQuantityEqual(z - v, -e) + self.assertQuantityEqual(v + z, e) + self.assertQuantityEqual(v - z, e) + + # If any element is non-zero and non-NaN, raise DimensionalityError + nz = np.array([0.0, 1.0]) + with self.assertRaises(DimensionalityError): + nz + v + with self.assertRaises(DimensionalityError): + nz - v + with self.assertRaises(DimensionalityError): + v + nz + with self.assertRaises(DimensionalityError): + v - nz + + # Mismatched shape + z = np.array([0.0, np.nan, 0.0]) + v = self.Q_([1.0, 2.0], "m") + for x, y in ((z, v), (v, z)): + with self.assertRaises(ValueError): + x + y + with self.assertRaises(ValueError): + x - y + + @helpers.requires_numpy() + def test_bare_zero_or_nan_numpy_inplace(self): + z = np.array([0.0, np.nan]) + v = self.Q_([1.0, 2.0], "m") + e = self.Q_([1.0, np.nan], "m") + v += z + self.assertQuantityEqual(v, e) + v = self.Q_([1.0, 2.0], "m") + v -= z + self.assertQuantityEqual(v, e) + v = self.Q_([1.0, 2.0], "m") + z = np.array([0.0, np.nan]) + z += v + self.assertQuantityEqual(z, e) + v = self.Q_([1.0, 2.0], "m") + z = np.array([0.0, np.nan]) + z -= v + self.assertQuantityEqual(z, -e) + + class TestDimensions(QuantityTestCase): FORCE_NDARRAY = False @@ -1506,38 +1606,36 @@ def test_iadd_isub(self): after -= d -class TestCompareZero(QuantityTestCase): - """This test case checks the special treatment that the zero value - receives in the comparisons: pint>=0.9 supports comparisons against zero - even for non-dimensionless quantities - - Parameters - ---------- - - Returns - ------- - +class TestCompareNeutral(QuantityTestCase): + """Test comparisons against non-Quantity zero or NaN values for for + non-dimensionless quantities """ def test_equal_zero(self): - ureg = self.ureg - ureg.autoconvert_offset_to_baseunit = False - self.assertTrue(ureg.Quantity(0, ureg.J) == 0) - self.assertFalse(ureg.Quantity(0, ureg.J) == ureg.Quantity(0, "")) - self.assertFalse(ureg.Quantity(5, ureg.J) == 0) + self.ureg.autoconvert_offset_to_baseunit = False + self.assertTrue(self.Q_(0, "J") == 0) + self.assertFalse(self.Q_(0, "J") == self.Q_(0, "")) + self.assertFalse(self.Q_(5, "J") == 0) + + def test_equal_nan(self): + # nan == nan returns False + self.ureg.autoconvert_offset_to_baseunit = False + self.assertFalse(self.Q_(math.nan, "J") == 0) + self.assertFalse(self.Q_(math.nan, "J") == math.nan) + self.assertFalse(self.Q_(math.nan, "J") == self.Q_(math.nan, "")) + self.assertFalse(self.Q_(5, "J") == math.nan) @helpers.requires_numpy() - def test_equal_zero_NP(self): - ureg = self.ureg - ureg.autoconvert_offset_to_baseunit = False + def test_equal_zero_nan_NP(self): + self.ureg.autoconvert_offset_to_baseunit = False aeq = np.testing.assert_array_equal - aeq(ureg.Quantity(0, ureg.J) == np.zeros(3), np.asarray([True, True, True])) - aeq(ureg.Quantity(5, ureg.J) == np.zeros(3), np.asarray([False, False, False])) + aeq(self.Q_(0, "J") == np.array([0, np.nan]), np.array([True, False])) + aeq(self.Q_(5, "J") == np.array([0, np.nan]), np.array([False, False])) aeq( - ureg.Quantity(np.arange(3), ureg.J) == np.zeros(3), + self.Q_([0, 1, 2], "J") == np.array([0, 0, np.nan]), np.asarray([True, False, False]), ) - self.assertFalse(ureg.Quantity(np.arange(4), ureg.J) == np.zeros(3)) + self.assertFalse(self.Q_(np.arange(4), "J") == np.zeros(3)) def test_offset_equal_zero(self): ureg = self.ureg @@ -1562,39 +1660,47 @@ def test_offset_autoconvert_equal_zero(self): self.assertFalse(q0 == ureg.Quantity(0, "")) def test_gt_zero(self): - ureg = self.ureg - ureg.autoconvert_offset_to_baseunit = False - q0 = ureg.Quantity(0, "J") - q0m = ureg.Quantity(0, "m") - q0less = ureg.Quantity(0, "") - qpos = ureg.Quantity(5, "J") - qneg = ureg.Quantity(-5, "J") + self.ureg.autoconvert_offset_to_baseunit = False + q0 = self.Q_(0, "J") + q0m = self.Q_(0, "m") + q0less = self.Q_(0, "") + qpos = self.Q_(5, "J") + qneg = self.Q_(-5, "J") self.assertTrue(qpos > q0) self.assertTrue(qpos > 0) self.assertFalse(qneg > 0) - self.assertRaises(DimensionalityError, qpos.__gt__, q0less) - self.assertRaises(DimensionalityError, qpos.__gt__, q0m) + with self.assertRaises(DimensionalityError): + qpos > q0less + with self.assertRaises(DimensionalityError): + qpos > q0m + + def test_gt_nan(self): + self.ureg.autoconvert_offset_to_baseunit = False + qn = self.Q_(math.nan, "J") + qnm = self.Q_(math.nan, "m") + qnless = self.Q_(math.nan, "") + qpos = self.Q_(5, "J") + self.assertFalse(qpos > qn) + self.assertFalse(qpos > math.nan) + with self.assertRaises(DimensionalityError): + qpos > qnless + with self.assertRaises(DimensionalityError): + qpos > qnm @helpers.requires_numpy() - def test_gt_zero_NP(self): - ureg = self.ureg - ureg.autoconvert_offset_to_baseunit = False - qpos = ureg.Quantity(5, "J") - qneg = ureg.Quantity(-5, "J") + def test_gt_zero_nan_NP(self): + self.ureg.autoconvert_offset_to_baseunit = False + qpos = self.Q_(5, "J") + qneg = self.Q_(-5, "J") aeq = np.testing.assert_array_equal - aeq(qpos > np.zeros(3), np.asarray([True, True, True])) - aeq(qneg > np.zeros(3), np.asarray([False, False, False])) - aeq( - ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), - np.asarray([False, False, True]), - ) + aeq(qpos > np.array([0, np.nan]), np.asarray([True, False])) + aeq(qneg > np.array([0, np.nan]), np.asarray([False, False])) aeq( - ureg.Quantity(np.arange(-1, 2), ureg.J) > np.zeros(3), - np.asarray([False, False, True]), - ) - self.assertRaises( - ValueError, ureg.Quantity(np.arange(-1, 2), ureg.J).__gt__, np.zeros(4) + self.Q_(np.arange(-2, 3), "J") > np.array([np.nan, 0, 0, 0, np.nan]), + np.asarray([False, False, False, True, False]), ) + with self.assertRaises(ValueError): + self.Q_(np.arange(-1, 2), "J") > np.zeros(4) def test_offset_gt_zero(self): ureg = self.ureg From 8857eb7bd8ae1ef50789af86743a4fd46907a196 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 13 Mar 2020 11:50:15 +0000 Subject: [PATCH 394/612] Treat bare zeros/NaNs as acceptable in all numpy functions Negative tests C --- CHANGES | 8 ++- pint/numpy_func.py | 77 ++++++++---------------- pint/testsuite/test_numpy.py | 97 ++++++++++++++++++------------- pint/testsuite/test_numpy_func.py | 3 +- 4 files changed, 89 insertions(+), 96 deletions(-) diff --git a/CHANGES b/CHANGES index ced766339..a49059dc3 100644 --- a/CHANGES +++ b/CHANGES @@ -12,8 +12,12 @@ Pint Changelog end_values (Issue #1026) - Reinstated support for pickle protocol 0 and 1, which is required by pytables (Issue #1036, Thanks Guido Imperiale) -- NaN is now treated the same as zero in addition, equality, and disequality - (Issue #1051, Thanks Guido Imperiale) +- Bare zeros and NaNs (not wrapped by Quantity) are now gracefully accepted by all numpy + operations; e.g. np.stack([Quantity([1, 2], "m"), [0, np.nan]) is now valid, whereas + np.stack([Quantity([1, 2], "m"), [3, 4]) will continue raising DimensionalityError. + (Issue #1050, Thanks Guido Imperiale) +- NaN is now treated the same as zero in addition, subtraction, equality, and + disequality (Issue #1051, Thanks Guido Imperiale) 0.11 (2020-02-19) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 210f967d7..c2ab62321 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -48,14 +48,13 @@ def _is_sequence_with_quantity_elements(obj): Returns ------- - bool + True if obj is a sequence and at least one element is a Quantity; False otherwise """ return ( iterable(obj) and sized(obj) and not isinstance(obj, str) - and len(obj) > 0 - and all(_is_quantity(item) for item in obj) + and any(_is_quantity(item) for item in obj) ) @@ -67,42 +66,43 @@ def _get_first_input_units(args, kwargs=None): if _is_quantity(arg): return arg.units elif _is_sequence_with_quantity_elements(arg): - return arg[0].units + return next(arg_i.units for arg_i in arg if _is_quantity(arg_i)) + raise TypeError("Expected at least one Quantity; found none") def convert_arg(arg, pre_calc_units): """Convert quantities and sequences of quantities to pre_calc_units and strip units. - Helper function for convert_to_consistent_units. pre_calc_units must be given as a pint - Unit or None. - + Helper function for convert_to_consistent_units. pre_calc_units must be given as a + pint Unit or None. """ if pre_calc_units is not None: if _is_quantity(arg): return arg.m_as(pre_calc_units) elif _is_sequence_with_quantity_elements(arg): - return [item.m_as(pre_calc_units) for item in arg] + return [convert_arg(item, pre_calc_units) for item in arg] elif arg is not None: if pre_calc_units.dimensionless: return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) + elif not _is_quantity(arg) and zero_or_nan(arg, True): + return arg else: raise DimensionalityError("dimensionless", pre_calc_units) - else: - if _is_quantity(arg): - return arg.m - elif _is_sequence_with_quantity_elements(arg): - return [item.m for item in arg] + elif _is_quantity(arg): + return arg.m + elif _is_sequence_with_quantity_elements(arg): + return [convert_arg(item, pre_calc_units) for item in arg] return arg def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): """Prepare args and kwargs for wrapping by unit conversion and stripping. - If pre_calc_units is not None, takes the args and kwargs for a NumPy function and converts - any Quantity or Sequence of Quantities into the units of the first Quantiy/Sequence of - Quantities and returns the magnitudes. Other args/kwargs are treated as dimensionless - Quantities. If pre_calc_units is None, units are simply stripped. - + If pre_calc_units is not None, takes the args and kwargs for a NumPy function and + converts any Quantity or Sequence of Quantities into the units of the first + Quantity/Sequence of Quantities and returns the magnitudes. Other args/kwargs are + treated as dimensionless Quantities. If pre_calc_units is None, units are simply + stripped. """ return ( tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), @@ -484,29 +484,15 @@ def _power(x1, x2): return x2.__rpow__(x1) -def _add_subtract_handle_non_quantity_zero(x1, x2): - # As in #121, #122, and #1051, if a value is 0 or NaN (but not a Quantity) do the - # operation without checking units. We do the calculation instead of just returning - # the same value to enforce any shape checking and type casting due to the - # operation. - if zero_or_nan(x1, True): - (x2,), output_wrap = unwrap_and_wrap_consistent_units(x2) - elif zero_or_nan(x2, True): - (x1,), output_wrap = unwrap_and_wrap_consistent_units(x1) - else: - (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) - return x1, x2, output_wrap - - @implements("add", "ufunc") def _add(x1, x2, *args, **kwargs): - x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) + (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) return output_wrap(np.add(x1, x2, *args, **kwargs)) @implements("subtract", "ufunc") def _subtract(x1, x2, *args, **kwargs): - x1, x2, output_wrap = _add_subtract_handle_non_quantity_zero(x1, x2) + (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) return output_wrap(np.subtract(x1, x2, *args, **kwargs)) @@ -550,26 +536,7 @@ def _interp(x, xp, fp, left=None, right=None, period=None): @implements("where", "function") def _where(condition, *args): - if ( - len(args) == 2 - and not _is_quantity(args[1]) - and not iterable(args[1]) - and (args[1] == 0 or np.isnan(args[1])) - ): - # Special case for y being bare zero or nan - (x,), output_wrap = unwrap_and_wrap_consistent_units(args[0]) - args = x, args[1] - elif ( - len(args) == 2 - and not _is_quantity(args[0]) - and not iterable(args[0]) - and (args[0] == 0 or np.isnan(args[0])) - ): - # Special case for x being bare zero or nan - (y,), output_wrap = unwrap_and_wrap_consistent_units(args[1]) - args = args[0], y - else: - args, output_wrap = unwrap_and_wrap_consistent_units(*args) + args, output_wrap = unwrap_and_wrap_consistent_units(*args) return output_wrap(np.where(condition, *args)) @@ -631,6 +598,8 @@ def _isin(element, test_elements, assume_unique=False, invert=False): elif _is_sequence_with_quantity_elements(test_elements): compatible_test_elements = [] for test_element in test_elements: + if not _is_quantity(test_element): + pass try: compatible_test_elements.append(test_element.m_as(element.units)) except DimensionalityError: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 244d02813..4e8917279 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -29,6 +29,10 @@ def q(self): def q_nan(self): return [[1, 2], [3, np.nan]] * self.ureg.m + @property + def q_zero_or_nan(self): + return [[0, 0], [0, np.nan]] * self.ureg.m + @property def q_temperature(self): return self.Q_([[1, 2], [3, 4]], self.ureg.degC) @@ -182,45 +186,60 @@ def test_squeeze(self): # Changing number of dimensions # Joining arrays @helpers.requires_array_function_protocol() - def test_concatentate(self): - self.assertQuantityEqual( - np.concatenate([self.q] * 2), - self.Q_(np.concatenate([self.q.m] * 2), self.ureg.m), - ) - - @helpers.requires_array_function_protocol() - def test_stack(self): - self.assertQuantityEqual( - np.stack([self.q] * 2), self.Q_(np.stack([self.q.m] * 2), self.ureg.m) - ) - - @helpers.requires_array_function_protocol() - def test_column_stack(self): - self.assertQuantityEqual(np.column_stack([self.q[:, 0], self.q[:, 1]]), self.q) - - @helpers.requires_array_function_protocol() - def test_dstack(self): - self.assertQuantityEqual( - np.dstack([self.q] * 2), self.Q_(np.dstack([self.q.m] * 2), self.ureg.m) - ) - - @helpers.requires_array_function_protocol() - def test_hstack(self): - self.assertQuantityEqual( - np.hstack([self.q] * 2), self.Q_(np.hstack([self.q.m] * 2), self.ureg.m) - ) - - @helpers.requires_array_function_protocol() - def test_vstack(self): - self.assertQuantityEqual( - np.vstack([self.q] * 2), self.Q_(np.vstack([self.q.m] * 2), self.ureg.m) - ) - - @helpers.requires_array_function_protocol() - def test_block(self): - self.assertQuantityEqual( - np.block([self.q[0, :], self.q[1, :]]), self.Q_([1, 2, 3, 4], self.ureg.m) - ) + def test_concat_stack(self): + for func in (np.concatenate, np.stack, np.hstack, np.vstack, np.dstack): + with self.subTest(func=func): + self.assertQuantityEqual( + func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m), + ) + # One or more of the args is a bare array full of zeros or NaNs + self.assertQuantityEqual( + func([self.q_zero_or_nan.m, self.q]), + self.Q_(func([self.q_zero_or_nan.m, self.q.m]), self.ureg.m), + ) + # One or more of the args is a bare array with at least one non-zero, + # non-NaN element + nz = self.q_zero_or_nan + nz.m[0, 0] = 1 + with self.assertRaises(DimensionalityError): + func([nz.m, self.q]) + + @helpers.requires_array_function_protocol() + def test_block_column_stack(self): + for func in (np.block, np.column_stack): + with self.subTest(func=func): + + self.assertQuantityEqual( + func([self.q[:, 0], self.q[:, 1]]), + self.Q_(func([self.q[:, 0].m, self.q[:, 1].m]), self.ureg.m), + ) + + # One or more of the args is a bare array full of zeros or NaNs + self.assertQuantityEqual( + func( + [ + self.q_zero_or_nan[:, 0].m, + self.q[:, 0], + self.q_zero_or_nan[:, 1].m, + ] + ), + self.Q_( + func( + [ + self.q_zero_or_nan[:, 0].m, + self.q[:, 0].m, + self.q_zero_or_nan[:, 1].m, + ] + ), + self.ureg.m, + ), + ) + # One or more of the args is a bare array with at least one non-zero, + # non-NaN element + nz = self.q_zero_or_nan + nz.m[0, 0] = 1 + with self.assertRaises(DimensionalityError): + func([nz[:, 0].m, self.q[:, 0]]) @helpers.requires_array_function_protocol() def test_append(self): diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index fb36c74dc..3a9453d35 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -55,7 +55,8 @@ def test_is_sequence_with_quantity_elements(self): ) ) self.assertTrue(_is_sequence_with_quantity_elements(np.arange(4) * self.ureg.m)) - self.assertFalse(_is_sequence_with_quantity_elements((self.Q_(0), True))) + self.assertTrue(_is_sequence_with_quantity_elements((self.Q_(0), 0))) + self.assertTrue(_is_sequence_with_quantity_elements((0, self.Q_(0)))) self.assertFalse(_is_sequence_with_quantity_elements([1, 3, 5])) self.assertFalse(_is_sequence_with_quantity_elements(9 * self.ureg.m)) self.assertFalse(_is_sequence_with_quantity_elements(np.arange(4))) From 06988093a8e09d69dada4eca860b7c2568675cd8 Mon Sep 17 00:00:00 2001 From: Ben Livingston Date: Fri, 13 Mar 2020 15:55:28 -0700 Subject: [PATCH 395/612] Fixing bug when multiplying quantity by dict --- CHANGES | 2 +- pint/quantity.py | 16 +++++++++++----- pint/testsuite/test_issues.py | 13 +++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index ced766339..066371899 100644 --- a/CHANGES +++ b/CHANGES @@ -14,7 +14,7 @@ Pint Changelog (Issue #1036, Thanks Guido Imperiale) - NaN is now treated the same as zero in addition, equality, and disequality (Issue #1051, Thanks Guido Imperiale) - +- Fixed bug with multiplication of Quantity by dict (Issue #1032) 0.11 (2020-02-19) ----------------- diff --git a/pint/quantity.py b/pint/quantity.py index 7ce8019ae..2cfef145e 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -73,9 +73,12 @@ def __init__(self, internal): def reduce_dimensions(f): def wrapped(self, *args, **kwargs): result = f(self, *args, **kwargs) - if result._REGISTRY.auto_reduce_dimensions: - return result.to_reduced_units() - else: + try: + if result._REGISTRY.auto_reduce_dimensions: + return result.to_reduced_units() + else: + return result + except AttributeError: return result return wrapped @@ -84,8 +87,11 @@ def wrapped(self, *args, **kwargs): def ireduce_dimensions(f): def wrapped(self, *args, **kwargs): result = f(self, *args, **kwargs) - if result._REGISTRY.auto_reduce_dimensions: - result.ito_reduced_units() + try: + if result._REGISTRY.auto_reduce_dimensions: + result.ito_reduced_units() + except AttributeError: + pass return result return wrapped diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 230b7975a..945c7a8fe 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -671,6 +671,19 @@ def test_issue960(self): assert q.units == ureg.nanometer assert q.magnitude == 1 + def test_issue1032(self): + class MultiplicativeDictionary(dict): + def __rmul__(self, other): + return self.__class__( + {key: value * other for key, value in self.items()} + ) + + q = 3 * ureg.s + d = MultiplicativeDictionary({4: 5, 6: 7}) + assert q * d == MultiplicativeDictionary({4: 15 * ureg.s, 6: 21 * ureg.s}) + with self.assertRaises(TypeError): + d * q + try: From 3d7ab3c30bed1a5c49c87b4433b3516c85424473 Mon Sep 17 00:00:00 2001 From: Giulio Malventi Date: Sat, 14 Mar 2020 12:43:28 +0100 Subject: [PATCH 396/612] Changed to relative import --- pint/babel_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/babel_names.py b/pint/babel_names.py index 45601bd1b..c263107f5 100644 --- a/pint/babel_names.py +++ b/pint/babel_names.py @@ -7,7 +7,7 @@ :license: BSD, see LICENSE for more details. """ -from pint.compat import HAS_PROPER_BABEL +from .compat import HAS_PROPER_BABEL _babel_units = dict( standard_gravity='acceleration-g-force', From 2d0357423350e66c5442e074b98118a86d12db69 Mon Sep 17 00:00:00 2001 From: Giulio Malventi Date: Sat, 14 Mar 2020 12:43:59 +0100 Subject: [PATCH 397/612] Changed to relative import --- pint/systems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/systems.py b/pint/systems.py index 47b1ed1f1..7020a1bed 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -17,7 +17,7 @@ from .errors import DefinitionSyntaxError, RedefinitionError from .util import to_units_container, SharedRegistryObject, SourceIterator, logger from .babel_names import _babel_systems -from pint.compat import Loc +from .compat import Loc class _Group(SharedRegistryObject): From d20f82ab42c136dd35c90d0aa80214dd56e4354a Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 19 Mar 2020 19:52:58 +0100 Subject: [PATCH 398/612] implement quantile / nanquantile --- pint/numpy_func.py | 2 ++ pint/testsuite/test_numpy.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index c2ab62321..eb77374ff 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -726,6 +726,8 @@ def implementation(*args, **kwargs): ("nanmax", "a", True), ("percentile", "a", True), ("nanpercentile", "a", True), + ("quantile", "a", True), + ("nanquantile", "a", True), ("flip", "m", True), ("fix", "x", True), ("trim_zeros", ["filt"], True), diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 4e8917279..710d2a800 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1012,6 +1012,15 @@ def test_percentile(self): def test_nanpercentile(self): self.assertQuantityEqual(np.nanpercentile(self.q_nan, 25), self.Q_(1.5, "m")) + @helpers.requires_array_function_protocol() + def test_quantile(self): + self.assertQuantityEqual(np.quantile(self.q, 0.25), self.Q_(1.75, "m")) + + @helpers.requires_array_function_protocol() + def test_nanquantile(self): + self.assertQuantityEqual(np.nanquantile(self.q_nan, 0.25), self.Q_(1.5, "m")) + + @helpers.requires_array_function_protocol() def test_copyto(self): a = self.q.m From 03dd0ce85ab10478492444988663671dcc384e4b Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 19 Mar 2020 19:56:55 +0100 Subject: [PATCH 399/612] black --- pint/testsuite/test_numpy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 710d2a800..4271b9f5f 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -190,7 +190,7 @@ def test_concat_stack(self): for func in (np.concatenate, np.stack, np.hstack, np.vstack, np.dstack): with self.subTest(func=func): self.assertQuantityEqual( - func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m), + func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m) ) # One or more of the args is a bare array full of zeros or NaNs self.assertQuantityEqual( @@ -1020,7 +1020,6 @@ def test_quantile(self): def test_nanquantile(self): self.assertQuantityEqual(np.nanquantile(self.q_nan, 0.25), self.Q_(1.5, "m")) - @helpers.requires_array_function_protocol() def test_copyto(self): a = self.q.m From 9aedbd2139d2c8d39810163430e67ba4bff919c4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 19 Mar 2020 20:12:42 +0100 Subject: [PATCH 400/612] don't try to implement a function that does not exist --- pint/numpy_func.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index eb77374ff..378cad12b 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -260,7 +260,10 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): # Handle functions in submodules func_str_split = func_str.split(".") - func = getattr(np, func_str_split[0]) + func = getattr(np, func_str_split[0], None) + # If the function is not available, do not attempt to implement it + if func is None: + return for func_str_piece in func_str_split[1:]: func = getattr(func, func_str_piece) From 2f03f3dfe62ba09edbe56034b8e0bc7098623878 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 19 Mar 2020 20:37:18 +0100 Subject: [PATCH 401/612] getattr guard the correct function --- pint/numpy_func.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 378cad12b..906104236 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -675,7 +675,10 @@ def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output if np is None: return - func = getattr(np, func_str) + func = getattr(np, func_str, None) + # if NumPy does not implement it, do not implement it either + if func is None: + return @implements(func_str, "function") def implementation(*args, **kwargs): From 18d2a218ba338b49467a64842367061eb53e829a Mon Sep 17 00:00:00 2001 From: FranchuFranchu Date: Thu, 19 Mar 2020 18:40:03 -0300 Subject: [PATCH 402/612] Fixed bug with to_compact --- CHANGES | 2 ++ docs/tutorial.rst | 1 + pint/quantity.py | 7 ++++++- pint/testsuite/test_numpy.py | 2 +- pint/testsuite/test_quantity.py | 6 ++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index a49059dc3..39f82484a 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,8 @@ Pint Changelog (Issue #1050, Thanks Guido Imperiale) - NaN is now treated the same as zero in addition, subtraction, equality, and disequality (Issue #1051, Thanks Guido Imperiale) +- Fixed issue where quantities with a very large magnitude would throw an IndexError + when using to_compact() 0.11 (2020-02-19) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ac9c2b3de..cc8d0e754 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -95,6 +95,7 @@ Sometimes, the magnitude of the quantity will be very large or very small. The method 'to_compact' can adjust the units to make the quantity more human-readable. + .. doctest:: >>> wavelength = 1550 * ureg.nm diff --git a/pint/quantity.py b/pint/quantity.py index 7ce8019ae..3c5bb860d 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -741,7 +741,12 @@ def to_compact(self, unit=None): else: power = int(math.ceil(math.log10(abs(magnitude)) / unit_power / 3)) * 3 - prefix = SI_bases[bisect.bisect_left(SI_powers, power)] + index = bisect.bisect_left(SI_powers, power) + + if index >= len(SI_bases): + index = -1 + + prefix = SI_bases[index] new_unit_str = prefix + unit_str new_unit_container = q_base._units.rename(unit_str, new_unit_str) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 4e8917279..55de9bf05 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -190,7 +190,7 @@ def test_concat_stack(self): for func in (np.concatenate, np.stack, np.hstack, np.vstack, np.dstack): with self.subTest(func=func): self.assertQuantityEqual( - func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m), + func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m) ) # One or more of the args is a bare array full of zeros or NaNs self.assertQuantityEqual( diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index d4fa77853..b58e22762 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -608,6 +608,12 @@ def test_nonnumeric_magnitudes(self): with self.assertWarns(RuntimeWarning): self.compareQuantity_compact(x, x) + def test_very_large_to_compact(self): + # This should not raise an IndexError + self.compareQuantity_compact( + self.Q_(10000, "yottameter"), self.Q_(10 ** 28, "meter").to_compact() + ) + class TestQuantityBasicMath(QuantityTestCase): From 4fb419e6da9185f4b1e39ddb0b19c92c00fc7a9f Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 21 Mar 2020 23:07:18 +0100 Subject: [PATCH 403/612] allow passing mixed args to interp --- pint/numpy_func.py | 3 +++ pint/testsuite/test_numpy.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index c2ab62321..380a4edc6 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -120,6 +120,9 @@ def unwrap_and_wrap_consistent_units(*args): first arg with units, along with a wrapper to restore that unit to the output. """ + if all(not _is_quantity(arg) for arg in args): + return args, lambda x: x + first_input_units = _get_first_input_units(args) args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) return ( diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 4e8917279..2c42d9594 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -929,6 +929,17 @@ def test_interp_numpy_func(self): np.interp(x, xp, fp), self.Q_([6.66667, 20.0], self.ureg.degC), rtol=1e-5 ) + x_ = np.array([1, 4]) + xp_ = np.linspace(0, 3, 5) + fp_ = [0, 5, 10, 15, 20] + + self.assertQuantityAlmostEqual( + np.interp(x_, xp_, fp), self.Q_([6.6667, 20.0], self.ureg.degC), rtol=1e-5 + ) + self.assertQuantityAlmostEqual( + np.interp(x, xp, fp_), [6.6667, 20.0], rtol=1e-5 + ) + def test_comparisons(self): self.assertNDArrayEqual( self.q > 2 * self.ureg.m, np.array([[False, False], [True, True]]) From c8c83bb2b44dff24febb8dddc01b5bc569d74ea1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 21 Mar 2020 23:40:35 +0100 Subject: [PATCH 404/612] black --- pint/testsuite/test_numpy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 2c42d9594..bb3df18a4 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -936,9 +936,7 @@ def test_interp_numpy_func(self): self.assertQuantityAlmostEqual( np.interp(x_, xp_, fp), self.Q_([6.6667, 20.0], self.ureg.degC), rtol=1e-5 ) - self.assertQuantityAlmostEqual( - np.interp(x, xp, fp_), [6.6667, 20.0], rtol=1e-5 - ) + self.assertQuantityAlmostEqual(np.interp(x, xp, fp_), [6.6667, 20.0], rtol=1e-5) def test_comparisons(self): self.assertNDArrayEqual( From 98058d5ad516c69b22401aa8b7e828285edcefe1 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2020 10:50:38 +0200 Subject: [PATCH 405/612] ran black -t py36 . && isort -rc . && flake8 --- pint/systems.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/systems.py b/pint/systems.py index c6f2b96e6..b72ce53c0 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -10,9 +10,8 @@ import re -from .compat import babel_parse - from .babel_names import _babel_systems +from .compat import babel_parse from .definitions import Definition, UnitDefinition from .errors import DefinitionSyntaxError, RedefinitionError from .util import ( From 79630e80529fa132883754a68e3295a027a01e67 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Sun, 29 Mar 2020 13:24:10 +0100 Subject: [PATCH 406/612] test_issues housekeeping Revert test change d black --- pint/testsuite/test_issues.py | 52 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 230b7975a..ad8301a06 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -459,6 +459,19 @@ def test_issue483(self): p = (q ** q).m np.testing.assert_array_equal(p, a ** a) + 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_issue523(self): src, dst = UnitsContainer({"meter": 1}), UnitsContainer({"degF": 1}) value = 10.0 @@ -653,28 +666,23 @@ def test_issue932(self): 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 + @helpers.requires_numpy() + def test_issue973(self): + """Verify that an empty array Quantity can be created through multiplication.""" + q0 = np.array([]) * ureg.m # by Unit + q1 = np.array([]) * ureg("m") # by Quantity + assert isinstance(q0, ureg.Quantity) + assert isinstance(q1, ureg.Quantity) + assert len(q0) == len(q1) == 0 -try: - @pytest.mark.skipif(np is None, reason="NumPy is not available") +if np is not None: + @pytest.mark.parametrize( "callable", [ @@ -706,17 +714,3 @@ def test_issue925(callable, q): type_before = type(q._magnitude) callable(q) assert isinstance(q._magnitude, type_before) - - @pytest.mark.skipif(np is None, reason="NumPy is not available") - def test_issue973(): - """Verify that an empty array Quantity can be created through multiplication.""" - q0 = np.array([]) * ureg.m # by Unit - q1 = np.array([]) * ureg("m") # by Quantity - assert isinstance(q0, ureg.Quantity) - assert isinstance(q1, ureg.Quantity) - assert len(q0) == len(q1) == 0 - - -except AttributeError: - # Calling attributes on np will fail if NumPy is not available - pass From 20910f5f7532aa5b2ecb178309c2c8e443b26ec4 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Sun, 29 Mar 2020 12:50:45 +0100 Subject: [PATCH 407/612] Fix #1062 Fix #1062 chja --- .gitignore | 1 + CHANGES | 2 ++ pint/quantity.py | 41 +++++++++++++++++++++-------------- pint/testsuite/test_issues.py | 9 ++++++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index d3e751fa0..d2fddea12 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ MANIFEST *pytest_cache* .eggs +.mypy_cache # WebDAV file system cache files .DAV/ diff --git a/CHANGES b/CHANGES index 39f82484a..b21be59a8 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,8 @@ Pint Changelog disequality (Issue #1051, Thanks Guido Imperiale) - Fixed issue where quantities with a very large magnitude would throw an IndexError when using to_compact() +- Fixed crash when a Unit with prefix is declared for the first time while a Context + containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale) 0.11 (2020-02-19) diff --git a/pint/quantity.py b/pint/quantity.py index 3c5bb860d..454c46241 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -17,6 +17,7 @@ import operator import re import warnings +from typing import List from pkg_resources.extern.packaging import version @@ -1795,38 +1796,46 @@ def plus_minus(self, error, relative=False): return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units) + def _get_unit_definition(self, unit: str) -> UnitDefinition: + try: + return self._REGISTRY._units[unit] + except KeyError: + # pint#1062: The __init__ method of this object added the unit to + # UnitRegistry._units (e.g. units with prefix are added on the fly the + # first time they're used) but the key was later removed, e.g. because + # a Context with unit redefinitions was deactivated. + self._REGISTRY.parse_units(unit) + return self._REGISTRY._units[unit] + # methods/properties that help for math operations with offset units @property - def _is_multiplicative(self): + def _is_multiplicative(self) -> bool: """Check if the Quantity object has only multiplicative units.""" return not self._get_non_multiplicative_units() - def _get_non_multiplicative_units(self): + def _get_non_multiplicative_units(self) -> List[str]: """Return a list of the of non-multiplicative units of the Quantity object.""" - offset_units = [ + return [ unit - for unit in self._units.keys() - if not self._REGISTRY._units[unit].is_multiplicative + for unit in self._units + if not self._get_unit_definition(unit).is_multiplicative ] - return offset_units - def _get_delta_units(self): + def _get_delta_units(self) -> List[str]: """Return list of delta units ot the Quantity object.""" - delta_units = [u for u in self._units.keys() if u.startswith("delta_")] - return delta_units + return [u for u in self._units if u.startswith("delta_")] - def _has_compatible_delta(self, unit): + def _has_compatible_delta(self, unit: str) -> bool: """"Check if Quantity object has a delta_unit that is compatible with unit """ deltas = self._get_delta_units() if "delta_" + unit in deltas: return True - else: # Look for delta units with same dimension as the offset unit - offset_unit_dim = self._REGISTRY._units[unit].reference - for d in deltas: - if self._REGISTRY._units[d].reference == offset_unit_dim: - return True - return False + # Look for delta units with same dimension as the offset unit + offset_unit_dim = self._get_unit_definition(unit).reference + return any( + self._get_unit_definition(d).reference == offset_unit_dim for d in deltas + ) def _ok_for_muldiv(self, no_offset_units=None): """Checks if Quantity object can be multiplied or divided diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index ad8301a06..a3be41fbd 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -680,6 +680,15 @@ def test_issue973(self): assert isinstance(q1, ureg.Quantity) assert len(q0) == len(q1) == 0 + def test_issue1062(self): + # Must not be used by any other tests + assert "nanometer" not in ureg._units + for i in range(2): + ctx = Context.from_lines(["@context _", "cal = 4 J"]) + with ureg.context("sp", ctx): + q = ureg.Quantity(1, "nm") + q.to("J") + if np is not None: From 9f855b7716d0cdd27c95f5f113d6bf51512a426a Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 14:27:02 +0200 Subject: [PATCH 408/612] strip the output from the notebook cells --- docs/numpy.ipynb | 429 ++------------- docs/pint-pandas.ipynb | 1162 ++-------------------------------------- 2 files changed, 97 insertions(+), 1494 deletions(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 268250f2d..7038eef8e 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -45,17 +45,9 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[3.0 4.0] meter\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "legs1 = Q_(np.asarray([3., 4.]), 'meter')\n", "print(legs1)" @@ -63,17 +55,9 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[3.0 4.0] meter\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "legs1 = [3., 4.] * ureg.meter\n", "print(legs1)" @@ -88,51 +72,27 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0.003 0.004] kilometer\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(legs1.to('kilometer'))" ] }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[length]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(legs1.dimensionality)" ] }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cannot convert from 'meter' ([length]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2)\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "try:\n", " legs1.to('joule')\n", @@ -149,17 +109,9 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[400.0 300.0] centimeter\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "legs2 = [400., 300.] * ureg.centimeter\n", "print(legs2)" @@ -174,17 +126,9 @@ }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[5.0 5.0] meter\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "hyps = np.hypot(legs1, legs2)\n", "print(hyps)" @@ -203,17 +147,9 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0.6435011087932843 0.9272952180016123] radian\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "angles = np.arccos(legs2/hyps)\n", "print(angles)" @@ -228,17 +164,9 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[36.86989764584401 53.13010235415599] degree\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(angles.to('degree'))" ] @@ -253,17 +181,9 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cannot convert from 'centimeter' ([length]) to 'dimensionless' (dimensionless)\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "try:\n", " np.arccos(legs2)\n", @@ -290,17 +210,9 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['alen', 'all', 'allclose', 'amax', 'amin', 'any', 'append', 'argmax', 'argmin', 'argsort', 'around', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'block', 'broadcast_to', 'clip', 'column_stack', 'compress', 'concatenate', 'copy', 'copyto', 'count_nonzero', 'cross', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'diff', 'dot', 'dstack', 'ediff1d', 'einsum', 'empty_like', 'expand_dims', 'fix', 'flip', 'full_like', 'gradient', 'hstack', 'insert', 'interp', 'intersect1d', 'isclose', 'iscomplex', 'isin', 'isreal', 'linalg.solve', 'linspace', 'mean', 'median', 'meshgrid', 'moveaxis', 'nan_to_num', 'nanargmax', 'nanargmin', 'nancumprod', 'nancumsum', 'nanmax', 'nanmean', 'nanmedian', 'nanmin', 'nanpercentile', 'nanstd', 'nansum', 'nanvar', 'ndim', 'nonzero', 'ones_like', 'pad', 'percentile', 'ptp', 'ravel', 'reshape', 'resize', 'result_type', 'rollaxis', 'rot90', 'round_', 'searchsorted', 'shape', 'size', 'sort', 'squeeze', 'stack', 'std', 'sum', 'swapaxes', 'tile', 'transpose', 'trapz', 'trim_zeros', 'unwrap', 'var', 'vstack', 'where', 'zeros_like']\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from pint.numpy_func import HANDLED_FUNCTIONS\n", "print(sorted(list(HANDLED_FUNCTIONS)))" @@ -357,175 +269,9 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "%3\n", - "\n", - "\n", - "Dask array\n", - "\n", - "Dask array\n", - "\n", - "\n", - "NumPy ndarray\n", - "\n", - "NumPy ndarray\n", - "\n", - "\n", - "Dask array->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "CuPy ndarray\n", - "\n", - "CuPy ndarray\n", - "\n", - "\n", - "Dask array->CuPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Sparse COO\n", - "\n", - "Sparse COO\n", - "\n", - "\n", - "Dask array->Sparse COO\n", - "\n", - "\n", - "\n", - "\n", - "NumPy masked array\n", - "\n", - "NumPy masked array\n", - "\n", - "\n", - "Dask array->NumPy masked array\n", - "\n", - "\n", - "\n", - "\n", - "CuPy ndarray->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Sparse COO->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "NumPy masked array->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Jax array\n", - "\n", - "Jax array\n", - "\n", - "\n", - "Jax array->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Pint Quantity\n", - "\n", - "Pint Quantity\n", - "\n", - "\n", - "Pint Quantity->Dask array\n", - "\n", - "\n", - "\n", - "\n", - "Pint Quantity->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Pint Quantity->CuPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "Pint Quantity->Sparse COO\n", - "\n", - "\n", - "\n", - "\n", - "Pint Quantity->NumPy masked array\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable\n", - "\n", - "xarray Dataset/DataArray/Variable\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->Dask array\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->NumPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->CuPy ndarray\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->Sparse COO\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->NumPy masked array\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->Jax array\n", - "\n", - "\n", - "\n", - "\n", - "xarray Dataset/DataArray/Variable->Pint Quantity\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from graphviz import Digraph\n", "\n", @@ -564,44 +310,9 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "Coordinates:\n", - " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", - " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", - " time datetime64[ns] 2013-01-01\n", - "Attributes:\n", - " long_name: 4xDaily Air temperature at sigma level 995\n", - " precision: 2\n", - " GRIB_id: 11\n", - " GRIB_name: TMP\n", - " var_desc: Air temperature\n", - " dataset: NMC Reanalysis\n", - " level_desc: Surface\n", - " statistic: Individual Obs\n", - " parent_stat: Other\n", - " actual_range: [185.16 322.1 ]\n", - "\n", - "\n", - "\n", - "Coordinates:\n", - " time datetime64[ns] 2013-01-01\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import xarray as xr\n", "\n", @@ -625,19 +336,9 @@ }, { "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " meter\n", - "\n", - "0.09462606529121113 meter\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from sparse import COO\n", "\n", @@ -663,22 +364,9 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "masked_array(data=[, --, , --],\n", - " mask=[False, True, False, True],\n", - " fill_value='?',\n", - " dtype=object)\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "m = np.ma.masked_array([2, 3, 5, 7], mask=[False, True, False, True])\n", "\n", @@ -701,28 +389,9 @@ }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - ", 'meter')>\n", - "Coordinates:\n", - " * z (z) int64 0 1 2 3 4 5 6 7 8 9 10 ... 90 91 92 93 94 95 96 97 98 99\n", - " * y (y) int64 -50 -49 -48 -47 -46 -45 -44 -43 ... 43 44 45 46 47 48 49\n", - " * x (x) float64 -20.0 -18.5 -17.0 -15.5 ... 124.0 125.5 127.0 128.5\n", - "\n", - "\n", - ", 'meter')>\n", - "Coordinates:\n", - " y int64 -46\n", - " x float64 125.5\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import dask.array as da\n", "\n", diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index e213aff24..c2fa3d5ef 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -46,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -63,72 +63,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
torqueangular_velocity
011
122
222
333
\n", - "
" - ], - "text/plain": [ - " torque angular_velocity\n", - "0 1 1\n", - "1 2 2\n", - "2 2 2\n", - "3 3 3" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = pd.DataFrame({\n", " \"torque\": pd.Series([1, 2, 2, 3], dtype=\"pint[lbf ft]\"),\n", @@ -146,77 +83,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
torqueangular_velocitypower
0111
1224
2224
3339
\n", - "
" - ], - "text/plain": [ - " torque angular_velocity power\n", - "0 1 1 1\n", - "1 2 2 4\n", - "2 2 2 4\n", - "3 3 3 9" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df['power'] = df['torque'] * df['angular_velocity']\n", "df" @@ -231,23 +100,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torque pint[foot * force_pound]\n", - "angular_velocity pint[revolutions_per_minute]\n", - "power pint[foot * force_pound * revolutions_per_minute]\n", - "dtype: object" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.dtypes" ] @@ -261,24 +116,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 1\n", - "1 4\n", - "2 4\n", - "3 9\n", - "Name: power, dtype: pint[foot * force_pound * revolutions_per_minute]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.power" ] @@ -292,24 +132,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PintArray([1 foot * force_pound * revolutions_per_minute,\n", - " 4 foot * force_pound * revolutions_per_minute,\n", - " 4 foot * force_pound * revolutions_per_minute,\n", - " 9 foot * force_pound * revolutions_per_minute],\n", - " dtype='pint[foot * force_pound * revolutions_per_minute]')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.power.values" ] @@ -323,26 +148,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\\[\\begin{pmatrix}1 & 4 & 4 & 9\\end{pmatrix} foot force_pound revolutions_per_minute\\]" - ], - "text/latex": [ - "$\\begin{pmatrix}1 & 4 & 4 & 9\\end{pmatrix}\\ \\mathrm{foot} \\cdot \\mathrm{force_pound} \\cdot \\mathrm{revolutions_per_minute}$" - ], - "text/plain": [ - "array([1, 4, 4, 9]) " - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.power.values.quantity" ] @@ -356,48 +164,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "foot force_pound revolutions_per_minute" - ], - "text/latex": [ - "$\\mathrm{foot} \\cdot \\mathrm{force_pound} \\cdot \\mathrm{revolutions_per_minute}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.power.pint.units" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PintArray([0.00014198092353610376 kilowatt, 0.000567923694144415 kilowatt,\n", - " 0.000567923694144415 kilowatt, 0.0012778283118249339 kilowatt],\n", - " dtype='pint[kilowatt]')" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.power.pint.to(\"kW\").values" ] @@ -413,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -431,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -453,102 +231,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
rpmkWN mbarl/minkW
01000.0NaN10.01000.010.0NaN
11100.0NaN10.0100000000.010.0NaN
21200.0NaN10.01000.010.0NaN
31200.0NaN10.01000.010.0NaN
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate fluid power\n", - " rpm kW N m bar l/min kW\n", - "0 1000.0 NaN 10.0 1000.0 10.0 NaN\n", - "1 1100.0 NaN 10.0 100000000.0 10.0 NaN\n", - "2 1200.0 NaN 10.0 1000.0 10.0 NaN\n", - "3 1200.0 NaN 10.0 1000.0 10.0 NaN" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = pd.read_csv(io.StringIO(test_data), header=[0, 1])\n", "# df = pd.read_csv(\"/path/to/test_data.csv\", header=[0, 1])\n", @@ -564,118 +249,18 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "speed rpm float64\n", - "mech power kW float64\n", - "torque N m float64\n", - "rail pressure bar float64\n", - "fuel flow rate l/min float64\n", - "fluid power kW float64\n", - "dtype: object" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.dtypes" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.0nan10.01000.010.0nan
11100.0nan10.0100000000.010.0nan
21200.0nan10.01000.010.0nan
31200.0nan10.01000.010.0nan
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate fluid power\n", - "0 1000.0 nan 10.0 1000.0 10.0 nan\n", - "1 1100.0 nan 10.0 100000000.0 10.0 nan\n", - "2 1200.0 nan 10.0 1000.0 10.0 nan\n", - "3 1200.0 nan 10.0 1000.0 10.0 nan" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_ = df.pint.quantify(level=-1)\n", "df_" @@ -690,208 +275,27 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 10000.0\n", - "1 11000.0\n", - "2 12000.0\n", - "3 12000.0\n", - "dtype: pint[meter * newton * revolutions_per_minute]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_.speed * df_.torque" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.0nan10.01000.010.0nan
11100.0nan10.0100000000.010.0nan
21200.0nan10.01000.010.0nan
31200.0nan10.01000.010.0nan
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate fluid power\n", - "0 1000.0 nan 10.0 1000.0 10.0 nan\n", - "1 1100.0 nan 10.0 100000000.0 10.0 nan\n", - "2 1200.0 nan 10.0 1000.0 10.0 nan\n", - "3 1200.0 nan 10.0 1000.0 10.0 nan" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
01000.010000.010.01000.010.010000.0
11100.011000.010.0100000000.010.01000000000.0
21200.012000.010.01000.010.010000.0
31200.012000.010.01000.010.010000.0
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate fluid power\n", - "0 1000.0 10000.0 10.0 1000.0 10.0 10000.0\n", - "1 1100.0 11000.0 10.0 100000000.0 10.0 1000000000.0\n", - "2 1200.0 12000.0 10.0 1000.0 10.0 10000.0\n", - "3 1200.0 12000.0 10.0 1000.0 10.0 10000.0" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_['mech power'] = df_.speed * df_.torque\n", "df_['fluid power'] = df_['fuel flow rate'] * df_['rail pressure']\n", @@ -907,109 +311,9 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrevolutions_per_minutemeter * newton * revolutions_per_minutemeter * newtonbarliter / minutebar * liter / minute
01000.010000.010.01000.010.01.000000e+04
11100.011000.010.0100000000.010.01.000000e+09
21200.012000.010.01000.010.01.000000e+04
31200.012000.010.01000.010.01.000000e+04
\n", - "
" - ], - "text/plain": [ - " speed mech power \\\n", - "unit revolutions_per_minute meter * newton * revolutions_per_minute \n", - "0 1000.0 10000.0 \n", - "1 1100.0 11000.0 \n", - "2 1200.0 12000.0 \n", - "3 1200.0 12000.0 \n", - "\n", - " torque rail pressure fuel flow rate fluid power \n", - "unit meter * newton bar liter / minute bar * liter / minute \n", - "0 10.0 1000.0 10.0 1.000000e+04 \n", - "1 10.0 100000000.0 10.0 1.000000e+09 \n", - "2 10.0 1000.0 10.0 1.000000e+04 \n", - "3 10.0 1000.0 10.0 1.000000e+04 " - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_.pint.dequantify()" ] @@ -1023,109 +327,9 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrevolutions_per_minutekilowattmeter * newtonbarliter / minutekilowatt
01000.01.04719810.01000.010.01.666667e+01
11100.01.15191710.0100000000.010.01.666667e+06
21200.01.25663710.01000.010.01.666667e+01
31200.01.25663710.01000.010.01.666667e+01
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure \\\n", - "unit revolutions_per_minute kilowatt meter * newton bar \n", - "0 1000.0 1.047198 10.0 1000.0 \n", - "1 1100.0 1.151917 10.0 100000000.0 \n", - "2 1200.0 1.256637 10.0 1000.0 \n", - "3 1200.0 1.256637 10.0 1000.0 \n", - "\n", - " fuel flow rate fluid power \n", - "unit liter / minute kilowatt \n", - "0 10.0 1.666667e+01 \n", - "1 10.0 1.666667e+06 \n", - "2 10.0 1.666667e+01 \n", - "3 10.0 1.666667e+01 " - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_['fluid power'] = df_['fluid power'].pint.to(\"kW\")\n", "df_['mech power'] = df_['mech power'].pint.to(\"kW\")\n", @@ -1141,102 +345,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrpmkWN·mbarl/minkW
01000.01.04719810.01000.010.01.666667e+01
11100.01.15191710.0100000000.010.01.666667e+06
21200.01.25663710.01000.010.01.666667e+01
31200.01.25663710.01000.010.01.666667e+01
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate fluid power\n", - "unit rpm kW N·m bar l/min kW\n", - "0 1000.0 1.047198 10.0 1000.0 10.0 1.666667e+01\n", - "1 1100.0 1.151917 10.0 100000000.0 10.0 1.666667e+06\n", - "2 1200.0 1.256637 10.0 1000.0 10.0 1.666667e+01\n", - "3 1200.0 1.256637 10.0 1000.0 10.0 1.666667e+01" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "pintpandas.PintType.ureg.default_format = \"~P\"\n", "df_.pint.dequantify()" @@ -1251,109 +362,9 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
speedmech powertorquerail pressurefuel flow ratefluid power
unitrad/skg·m²/s³kg·m²/s²kg/m/s²m³/skg·m²/s³
0104.7197551047.19755110.01.000000e+080.0001671.666667e+04
1115.1917311151.91730610.01.000000e+130.0001671.666667e+09
2125.6637061256.63706110.01.000000e+080.0001671.666667e+04
3125.6637061256.63706110.01.000000e+080.0001671.666667e+04
\n", - "
" - ], - "text/plain": [ - " speed mech power torque rail pressure fuel flow rate \\\n", - "unit rad/s kg·m²/s³ kg·m²/s² kg/m/s² m³/s \n", - "0 104.719755 1047.197551 10.0 1.000000e+08 0.000167 \n", - "1 115.191731 1151.917306 10.0 1.000000e+13 0.000167 \n", - "2 125.663706 1256.637061 10.0 1.000000e+08 0.000167 \n", - "3 125.663706 1256.637061 10.0 1.000000e+08 0.000167 \n", - "\n", - " fluid power \n", - "unit kg·m²/s³ \n", - "0 1.666667e+04 \n", - "1 1.666667e+09 \n", - "2 1.666667e+04 \n", - "3 1.666667e+04 " - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df_.pint.to_base_units().pint.dequantify()" ] @@ -1370,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1387,7 +398,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1403,7 +414,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1420,7 +431,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1438,69 +449,9 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
lengthwidthdistanceheightdepth
012222
123333
\n", - "
" - ], - "text/plain": [ - " length width distance height depth\n", - "0 1 2 2 2 2\n", - "1 2 3 3 3 3" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = pd.DataFrame({\n", " \"length\" : pd.Series([1,2], dtype=\"pint[m]\"),\n", @@ -1514,26 +465,9 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "meter" - ], - "text/latex": [ - "$\\mathrm{meter}$" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df.length.values.units" ] @@ -1556,7 +490,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.7.6" } }, "nbformat": 4, From 87c5b8185b0d05205c25c1764130ccecdd209629 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 14:32:03 +0200 Subject: [PATCH 409/612] update the requirements needed for the docs --- requirements_docs.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index 06e3532e9..08a7c8695 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -3,3 +3,6 @@ matplotlib nbsphinx numpy pytest +pandas +pint-pandas +jupyter_client From 6e4209607e938233b87f449bc34f052cc99bc6c4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 15:19:09 +0200 Subject: [PATCH 410/612] install pint-pandas from github --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 08a7c8695..91045b281 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -4,5 +4,5 @@ nbsphinx numpy pytest pandas -pint-pandas +git+https://github.com/hgrecco/pint-pandas.git jupyter_client From 5a18ae2b6c99e0623924c4656f57fb49d027e766 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 15:50:16 +0200 Subject: [PATCH 411/612] also install ipykernel --- requirements_docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index 91045b281..5c9efef51 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -6,3 +6,4 @@ pytest pandas git+https://github.com/hgrecco/pint-pandas.git jupyter_client +ipykernel From 6e257e49f51ea34b9cc9c4f9d918b9f335c7749b Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 15:52:06 +0200 Subject: [PATCH 412/612] install graphviz --- requirements_docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index 5c9efef51..240f6f468 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -7,3 +7,4 @@ pandas git+https://github.com/hgrecco/pint-pandas.git jupyter_client ipykernel +graphviz From 3357145b25abbdf1595df8c0a6884270d76144a7 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 15:56:42 +0200 Subject: [PATCH 413/612] install xarray, sparse and dask --- requirements_docs.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index 240f6f468..3736fcf6c 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -8,3 +8,6 @@ git+https://github.com/hgrecco/pint-pandas.git jupyter_client ipykernel graphviz +xarray +sparse +dask From 00afb4690291a72dfa55a827f47eadefaa64894e Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 16:00:47 +0200 Subject: [PATCH 414/612] install the full dask package --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 3736fcf6c..c04ad3cdb 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -10,4 +10,4 @@ ipykernel graphviz xarray sparse -dask +dask[complete] From db34296b8b758561b998ff31efc8e22221455dc3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 16:11:08 +0200 Subject: [PATCH 415/612] explicitly require a new setuptools version --- requirements_docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index c04ad3cdb..8bd0205ee 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -11,3 +11,4 @@ graphviz xarray sparse dask[complete] +setuptools>=41.2 From 42820bc615e26f1b851e456d1f5b173211a31700 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 16:15:29 +0200 Subject: [PATCH 416/612] import pintpandas where needed and update the docs --- docs/pint-pandas.ipynb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index c2fa3d5ef..ab61e60bd 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -41,7 +41,7 @@ "source": [ "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", "\n", - "First some imports" + "First some imports (you don't need to import `pintpandas` for this to work)" ] }, { @@ -197,6 +197,7 @@ "source": [ "import pandas as pd \n", "import pint\n", + "import pintpandas\n", "import io" ] }, @@ -386,7 +387,8 @@ "outputs": [], "source": [ "import pandas as pd \n", - "import pint" + "import pint\n", + "import pintpandas" ] }, { From a9bdef28a8d5ca0423b85587c2f27e342afe55af Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 17:12:52 +0200 Subject: [PATCH 417/612] update travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index efea2b18d..91276f5ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas git+https://github.com/hgrecco/pint-pandas.git jupyter_client ipykernel graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" From b1aa53a63a91d0fd4486567ae6021729ec776027 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 17:30:55 +0200 Subject: [PATCH 418/612] install pint-pandas using pip --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91276f5ea..e1961e79b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas git+https://github.com/hgrecco/pint-pandas.git jupyter_client ipykernel graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -73,7 +73,7 @@ script: # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi + - if [[ $DOCS == 1 ]]; then pip install git+https://github.com/hgrecco/pint-pandas.git && PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi after_success: From 6ee9e75d2c9e6ae55474c501d36a50446b381f6d Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 17:32:58 +0200 Subject: [PATCH 419/612] move the installation of pint-pandas to the install section --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e1961e79b..625820834 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,6 +61,7 @@ install: - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi + - if [[ $DOCS == 1 ]]; then pip install git+https://github.com/hgrecco/pint-pandas.git; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list @@ -73,7 +74,7 @@ script: # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - - if [[ $DOCS == 1 ]]; then pip install git+https://github.com/hgrecco/pint-pandas.git && PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi + - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi after_success: From c00cd4dd2d441a17c739003928ded966bc25d1aa Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 2 Apr 2020 17:36:44 +0200 Subject: [PATCH 420/612] also install the graphviz python package in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 625820834..b67c528dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" From 7693609dd5464ec56660ea4cd71ddbab5d5f7722 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 16:24:33 -0800 Subject: [PATCH 421/612] New Implementation of 'Lx' This approach uses the default/Python format from uncertainties and then adds the required fixups to make it compatible with SIunitx input format. I think this is the most reasonable option available now, as it is workable and preserves access to most of the existing type conversion characters implemented by uncertainties package, but not 'S' or 'P'. --- pint/formatting.py | 4 ++++ pint/measurement.py | 29 +++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index 0e84f8289..3af24cdf4 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -89,6 +89,10 @@ def _pretty_fmt_exponent(num): "power_fmt": "{}^[{}]", "parentheses_fmt": r"\left({}\right)", }, + "Lx": { # Latex format. + "siopts": "separate-uncertainty=true", + "pm_fmt": " +- ", + }, "H": { # HTML format. "as_ratio": True, "single_denominator": True, diff --git a/pint/measurement.py b/pint/measurement.py index c6d99f6f1..2b89d4468 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -89,12 +89,29 @@ def __format__(self, spec): if "Lx" in spec: # the LaTeX siunitx code # the uncertainties module supports formatting # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), - # which siunitx actually accepts as input. we just need to give the 'S' - # formatting option for the uncertainties module. - spec = spec.replace("Lx", "S") - # todo: add support for extracting options - opts = "separate-uncertainty=true" - mstr = format(self.magnitude, spec) + # using type code 'S', which siunitx actually accepts as input. + # However, the implimentation is incompatible with siunitx. + # Uncertainties will do 9.1(1.1), which is invalid, should be 9.1(11). + # TODO: add support for extracting options + # + # Get rid of this code, we'll deal with it here + spec = spec.replace("Lx", '') + # The most compatible format from uncertainties is the default format, + # but even this requires fixups. + # For one, SIUnitx does not except some formats that unc does, like 'P', + # and 'S' is broken as stated, so... + spec = spec.replace("S",'').replace("P",'') + # get SIunitx options + # TODO: allow user to set this value, somehow + opts = _FORMATS["Lx"]["siopts"] + # SI requires space between "+-" (or "\pm") and the nominal value + # and uncertainty, and doesn't accept "+/-", so this setting + # selects the desired replacement. + pm_fmt = _FORMATS["Lx"]["pm_fmt"] + mstr = format(self.magnitude, spec).replace(r"+/-",pm_fmt) + # Also, SIunitx doesn't accept parentheses, which uncs uses with + # scientific notation ('e' or 'E' and somtimes 'g' or 'G'). + mstr = mstr.replace('(','').replace(')',' ') ustr = siunitx_format_unit(self.units) return r"\SI[%s]{%s}{%s}" % (opts, mstr, ustr) From 171bd7bc1506ed9329a1e9798de2b1876576e8e6 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 16:26:30 -0800 Subject: [PATCH 422/612] Unit Test Patches for New 'Lx' Implementation These are changes to the unit tests that correspond to the new implementation of 'Lx' format type conversion. --- pint/testsuite/test_measurement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 8f20e9b19..113a72a38 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -50,13 +50,13 @@ def test_format(self): ("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), ("{:H}", r"\[(4.00 ± 0.10)\ second^2\]"), ("{:C}", "(4.00+/-0.10) second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(10)}{\second\squared}"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{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\]"), ("{:.1fC}", "(4.0+/-0.1) second**2"), - ("{:.1fLx}", r"\SI[separate-uncertainty=true]{4.0(1)}{\second\squared}"), + ("{:.1fLx}", r"\SI[separate-uncertainty=true]{4.0 +- 0.1}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -88,9 +88,9 @@ def test_format_u(self): ("{:.3uC}", "(0.2000+/-0.0100) second**2"), ( "{:.3uLx}", - r"\SI[separate-uncertainty=true]{0.2000(100)}{\second\squared}", + r"\SI[separate-uncertainty=true]{0.2000 +- 0.0100}{\second\squared}", ), - ("{:.1uLx}", r"\SI[separate-uncertainty=true]{0.20(1)}{\second\squared}"), + ("{:.1uLx}", r"\SI[separate-uncertainty=true]{0.20 +- 0.01}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -137,7 +137,7 @@ def test_format_exponential_pos(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\]"), ("{:C}", "(4.00+/-0.10)e+20 second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(10)e+20}{\second\squared}"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00 +- 0.10 e+20}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -154,7 +154,7 @@ def test_format_exponential_neg(self): ), ("{:H}", r"\[(4.00 ± 0.10)×10^{-20}\ second^2\]"), ("{:C}", "(4.00+/-0.10)e-20 second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00(10)e-20}{\second\squared}"), + ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00 +- 0.10 e-20}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) From 6335feb03501ac7d159f9a03eea088c09fdda539 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 18:41:54 -0800 Subject: [PATCH 423/612] Updates to Documentation --- CHANGES | 5 +++++ docs/tutorial.rst | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 27a6c38c6..5cd25969a 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,11 @@ Pint Changelog when using to_compact() - Fixed crash when a Unit with prefix is declared for the first time while a Context containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale) +- New implementation of 'Lx' String Format Type Option + The old implementation treated 'Lx' as 'S' as produced by 'uncertainties' + package, but that is not fully compatible with SIunitx. The new code protects + SIunitx by fixing what unceratinties produces. + (Issue #814) 0.11 (2020-02-19) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index cc8d0e754..474fa2a5e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -389,7 +389,7 @@ The same is true for latex (`L`) and HTML (`H`) specs. The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatting syntax'_ for custom float representations. For example, scientific notation: -..doctest:: +.. doctest:: >>> 'Scientific notation: {:.3e~L}'.format(accel) 'Scientific notation: 1.300\\times 10^{0}\\ \\frac{\\mathrm{m}}{\\mathrm{s}^{2}}' @@ -400,7 +400,10 @@ Pint also supports the LaTeX siunitx package: >>> accel = 1.3 * ureg['meter/second**2'] >>> # siunitx Latex print >>> print('The siunitx representation is {:Lx}'.format(accel)) - The siunitx representation is \SI[]{1.3}{\meter\per\second\squared} + The siunitx representation is \SI{1.3}{\meter\per\second\squared} + >>> accel = accel.plus_minus(0.2) + >>> print('The siunitx representation is {:Lx}'.format(accel)) + The siunitx representation is \SI{1.3 +- 0.2}{\meter\per\second\squared} Additionally, you can specify a default format specification: From 3134dcaf5c1828d4908ae1c0e55d357a78f1a981 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 18:42:55 -0800 Subject: [PATCH 424/612] Empty Default SIunitx Option The default option in 'pint' will override SIunitx settings from the user's document. There still should be a way of setting it, but it should probably be empty to start, since it is easier to change the document settings than to change 'pint' output. Also, this conforms to the example in the tutorial. --- pint/formatting.py | 2 +- pint/measurement.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index 3af24cdf4..a0fb70e37 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -90,7 +90,7 @@ def _pretty_fmt_exponent(num): "parentheses_fmt": r"\left({}\right)", }, "Lx": { # Latex format. - "siopts": "separate-uncertainty=true", + "siopts": "", "pm_fmt": " +- ", }, "H": { # HTML format. diff --git a/pint/measurement.py b/pint/measurement.py index 2b89d4468..d5de20f66 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -104,6 +104,8 @@ def __format__(self, spec): # get SIunitx options # TODO: allow user to set this value, somehow opts = _FORMATS["Lx"]["siopts"] + if opts != '' : + opts = r'[' + opts + r']' # SI requires space between "+-" (or "\pm") and the nominal value # and uncertainty, and doesn't accept "+/-", so this setting # selects the desired replacement. @@ -113,7 +115,7 @@ def __format__(self, spec): # scientific notation ('e' or 'E' and somtimes 'g' or 'G'). mstr = mstr.replace('(','').replace(')',' ') ustr = siunitx_format_unit(self.units) - return r"\SI[%s]{%s}{%s}" % (opts, mstr, ustr) + return r"\SI%s{%s}{%s}" % (opts, mstr, ustr) # standard cases if "L" in spec: From e10a46d22aebc1a020f6a089b4c0f20a1a68b987 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 18:48:15 -0800 Subject: [PATCH 425/612] Update Unit Tests Had to update the unit tests again after removing the default SIunitx option. --- pint/testsuite/test_measurement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 113a72a38..712cd9e26 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -50,13 +50,13 @@ def test_format(self): ("{:L}", r"\left(4.00 \pm 0.10\right)\ \mathrm{second}^{2}"), ("{:H}", r"\[(4.00 ± 0.10)\ second^2\]"), ("{:C}", "(4.00+/-0.10) second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00 +- 0.10}{\second\squared}"), + ("{: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\]"), ("{:.1fC}", "(4.0+/-0.1) second**2"), - ("{:.1fLx}", r"\SI[separate-uncertainty=true]{4.0 +- 0.1}{\second\squared}"), + ("{:.1fLx}", r"\SI{4.0 +- 0.1}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -88,9 +88,9 @@ def test_format_u(self): ("{:.3uC}", "(0.2000+/-0.0100) second**2"), ( "{:.3uLx}", - r"\SI[separate-uncertainty=true]{0.2000 +- 0.0100}{\second\squared}", + r"\SI{0.2000 +- 0.0100}{\second\squared}", ), - ("{:.1uLx}", r"\SI[separate-uncertainty=true]{0.20 +- 0.01}{\second\squared}"), + ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -137,7 +137,7 @@ def test_format_exponential_pos(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\]"), ("{:C}", "(4.00+/-0.10)e+20 second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00 +- 0.10 e+20}{\second\squared}"), + ("{:Lx}", r"\SI{4.00 +- 0.10 e+20}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) @@ -154,7 +154,7 @@ def test_format_exponential_neg(self): ), ("{:H}", r"\[(4.00 ± 0.10)×10^{-20}\ second^2\]"), ("{:C}", "(4.00+/-0.10)e-20 second**2"), - ("{:Lx}", r"\SI[separate-uncertainty=true]{4.00 +- 0.10 e-20}{\second\squared}"), + ("{:Lx}", r"\SI{4.00 +- 0.10 e-20}{\second\squared}"), ): with self.subTest(spec): self.assertEqual(spec.format(m), result) From eee4b919a02d1472f4c5795a2d79447a180664e1 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 21:07:14 -0800 Subject: [PATCH 426/612] Black Code Reformatting --- pint/formatting.py | 5 +---- pint/measurement.py | 12 ++++++------ pint/testsuite/test_measurement.py | 5 +---- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index a0fb70e37..079a7c5a4 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -89,10 +89,7 @@ def _pretty_fmt_exponent(num): "power_fmt": "{}^[{}]", "parentheses_fmt": r"\left({}\right)", }, - "Lx": { # Latex format. - "siopts": "", - "pm_fmt": " +- ", - }, + "Lx": {"siopts": "", "pm_fmt": " +- ",}, # Latex format. "H": { # HTML format. "as_ratio": True, "single_denominator": True, diff --git a/pint/measurement.py b/pint/measurement.py index d5de20f66..dd0f592fc 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -95,25 +95,25 @@ def __format__(self, spec): # TODO: add support for extracting options # # Get rid of this code, we'll deal with it here - spec = spec.replace("Lx", '') + spec = spec.replace("Lx", "") # The most compatible format from uncertainties is the default format, # but even this requires fixups. # For one, SIUnitx does not except some formats that unc does, like 'P', # and 'S' is broken as stated, so... - spec = spec.replace("S",'').replace("P",'') + spec = spec.replace("S", "").replace("P", "") # get SIunitx options # TODO: allow user to set this value, somehow opts = _FORMATS["Lx"]["siopts"] - if opts != '' : - opts = r'[' + opts + r']' + if opts != "": + opts = r"[" + opts + r"]" # SI requires space between "+-" (or "\pm") and the nominal value # and uncertainty, and doesn't accept "+/-", so this setting # selects the desired replacement. pm_fmt = _FORMATS["Lx"]["pm_fmt"] - mstr = format(self.magnitude, spec).replace(r"+/-",pm_fmt) + mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt) # Also, SIunitx doesn't accept parentheses, which uncs uses with # scientific notation ('e' or 'E' and somtimes 'g' or 'G'). - mstr = mstr.replace('(','').replace(')',' ') + mstr = mstr.replace("(", "").replace(")", " ") ustr = siunitx_format_unit(self.units) return r"\SI%s{%s}{%s}" % (opts, mstr, ustr) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 712cd9e26..871e880fe 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -86,10 +86,7 @@ def test_format_u(self): ("{:.3uL}", r"\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}"), ("{:.3uH}", r"\[(0.2000 ± 0.0100)\ second^2\]"), ("{:.3uC}", "(0.2000+/-0.0100) second**2"), - ( - "{:.3uLx}", - r"\SI{0.2000 +- 0.0100}{\second\squared}", - ), + ("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}",), ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), ): with self.subTest(spec): From 3006a05caca1672f9becf086159b1553ab4ff95b Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 21 Dec 2019 21:13:13 -0800 Subject: [PATCH 427/612] Flake8 Errors Fixed --- pint/formatting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/formatting.py b/pint/formatting.py index 079a7c5a4..5f38d7fed 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -89,7 +89,7 @@ def _pretty_fmt_exponent(num): "power_fmt": "{}^[{}]", "parentheses_fmt": r"\left({}\right)", }, - "Lx": {"siopts": "", "pm_fmt": " +- ",}, # Latex format. + "Lx": {"siopts": "", "pm_fmt": " +- ", }, # Latex format. "H": { # HTML format. "as_ratio": True, "single_denominator": True, From c1e6f1f07952ed7efb033e4273a218d3d8f5ac28 Mon Sep 17 00:00:00 2001 From: Porcelain Mouse Date: Sat, 4 Apr 2020 14:26:19 -0700 Subject: [PATCH 428/612] Resolve Disagreement Between black and flake8 --- pint/formatting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/formatting.py b/pint/formatting.py index 5f38d7fed..4eae5cd88 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -89,7 +89,7 @@ def _pretty_fmt_exponent(num): "power_fmt": "{}^[{}]", "parentheses_fmt": r"\left({}\right)", }, - "Lx": {"siopts": "", "pm_fmt": " +- ", }, # Latex format. + "Lx": {"siopts": "", "pm_fmt": " +- "}, # Latex format with SIunitx. "H": { # HTML format. "as_ratio": True, "single_denominator": True, From b4bc3659160f66784ac473b4f8a36997939414a1 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 9 Apr 2020 04:42:09 +0100 Subject: [PATCH 429/612] removed _dir, updated, __dir__, and added __iter__ --- pint/registry.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index a622a2d9c..5e1e0fae2 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -177,26 +177,6 @@ class BaseRegistry(metaclass=RegistryMeta): #: 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 = [ - "Quantity", - "Unit", - "UnitsContainer", - "Measurement", - "define", - "load_definitions", - "get_name", - "get_symbol", - "get_dimensionality", - "get_base_units", - "get_root_units", - "parse_unit_name", - "parse_units", - "parse_expression", - "convert", - ] - def __init__( self, filename="", @@ -310,7 +290,18 @@ def __getitem__(self, item): return self.parse_expression(item) def __dir__(self): - return list(self._units.keys()) + self._dir + #: Calling dir(registry) gives all units, methods, and attributes. + #: Also used for autocompletion in IPython. + return list(self._units.keys()) + list(object.__dir__(self)) + + def __iter__(self): + """Allows for listing all units in registry with `list(ureg)`. + + Returns + ------- + Iterator over names of all units in registry, ordered alphabetically. + """ + return iter(sorted(self._units.keys())) def set_fmt_locale(self, loc): """Change the locale used by default by `format_babel`. From 0a5abcfad89110f9f0df25957556caee76f42ad8 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 9 Apr 2020 05:24:37 +0100 Subject: [PATCH 430/612] test for new iter dunder method --- pint/testsuite/test_unit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index d7eb64906..0e41daf9f 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -240,6 +240,10 @@ def test_default_format(self): self.assertNotEqual(s1, s3) self.assertEqual(ureg.default_format, "~") + def test_iterate(self): + ureg = UnitRegistry() + self.assertTrue('meter' in list(ureg)) + def test_parse_number(self): self.assertEqual(self.ureg.parse_expression("pi"), math.pi) self.assertEqual(self.ureg.parse_expression("x", x=2), 2) From aca1f70673d429eaac6763e2c55227051f09a6c1 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 9 Apr 2020 05:30:11 +0100 Subject: [PATCH 431/612] added record of change --- AUTHORS | 1 + CHANGES | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS b/AUTHORS index db3229e6d..2f51baedc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ Other contributors, listed alphabetically, are: * Sundar Raman * Tiago Coutinho * Thomas Kluyver +* Tom Nicholas * Tom Ritchford * Virgil Dupras * Zebedee Nicholls diff --git a/CHANGES b/CHANGES index 27a6c38c6..75e264d4b 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,9 @@ Pint Changelog when using to_compact() - Fixed crash when a Unit with prefix is declared for the first time while a Context containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale) +- Removed outdated `_dir` attribute of `UnitsRegistry`, and added `__iter__` + method so that now `list(ureg)` returns a list of all units in registry. + (Issue #1072, Thanks Tom Nicholas) 0.11 (2020-02-19) From 1e5c1d6b17c37c52807d5b27601c903b4f16572c Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 9 Apr 2020 05:38:06 +0100 Subject: [PATCH 432/612] automatic formatting checks --- pint/testsuite/test_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 0e41daf9f..76082c94a 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -242,7 +242,7 @@ def test_default_format(self): def test_iterate(self): ureg = UnitRegistry() - self.assertTrue('meter' in list(ureg)) + self.assertTrue("meter" in list(ureg)) def test_parse_number(self): self.assertEqual(self.ureg.parse_expression("pi"), math.pi) From caa0c81eb10fa7fd8c701d71da1894d9fda635f1 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 9 Apr 2020 06:06:38 +0100 Subject: [PATCH 433/612] added link to pint-xarray --- CHANGES | 2 ++ docs/numpy.ipynb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 27a6c38c6..b2ba540db 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,8 @@ Pint Changelog when using to_compact() - Fixed crash when a Unit with prefix is declared for the first time while a Context containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale) +- Added link to budding `pint-xarray` interface library to the docs, next to + the link to pint-pandas. (Thanks Tom Nicholas.) 0.11 (2020-02-19) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 7038eef8e..c3e8fc960 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -423,7 +423,7 @@ "To aid in integration between various array types and Pint (such as by providing convenience methods), the following compatibility packages are available:\n", "\n", "- [pint-pandas](https://github.com/hgrecco/pint-pandas)\n", - "- pint-xarray ([in development](https://github.com/hgrecco/pint/issues/849), initial alpha release planned for April-May 2020)\n", + "- [pint-xarray](https://github.com/TomNicholas/pint-xarray/) (in early development as of April 2020, with [extra discussion here](https://github.com/hgrecco/pint/issues/849#issuecomment-579992247) - please come and help!)\n", "\n", "(Note: if you have developed a compatibility package for Pint, please submit a pull request to add it to this list!)" ] From 8241bc0dc18ff331080fb2f78eb6d94258f5a6eb Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 12 Apr 2020 14:16:26 +0200 Subject: [PATCH 434/612] add packaging as a direct dependency We already depended on it indirectly via setuptools, so this doesn't change anything for library users. --- pint/quantity.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index c1ade313b..c087b0cab 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -19,7 +19,7 @@ import warnings from typing import List -from pkg_resources.extern.packaging import version +from packaging import version from .compat import ( NUMPY_VER, diff --git a/setup.cfg b/setup.cfg index 277759975..75781e575 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ packages = pint zip_safe = True include_package_data = True python_requires = >=3.6 -install_requires = setuptools +install_requires = setuptools; packaging setup_requires = setuptools; setuptools_scm test_suite = pint.testsuite.testsuite scripts = pint/pint-convert From 75ba2627b1d861a41615ebfdd021ba5d003c435e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Fri, 17 Apr 2020 15:46:09 +0200 Subject: [PATCH 435/612] replace pkg_resources.version to importlib.metadata.version. - Update pint/__init__.py - Update docs/conf.py - Add importlib-metadata backport to install-requires Closes #1083 --- CHANGES | 1 + docs/conf.py | 8 ++++++-- pint/__init__.py | 10 +++++++--- setup.cfg | 5 ++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index cfb900701..a3617c2a4 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,7 @@ Pint Changelog - Removed outdated `_dir` attribute of `UnitsRegistry`, and added `__iter__` method so that now `list(ureg)` returns a list of all units in registry. (Issue #1072, Thanks Tom Nicholas) +- Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) 0.11 (2020-02-19) diff --git a/docs/conf.py b/docs/conf.py index 1b772ba3c..924b82596 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,11 @@ import datetime -import pkg_resources +try: + from importlib.metadata import version +except ImportError: + # Backport for Python < 3.8 + from importlib_metadata import version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -60,7 +64,7 @@ # built documents. try: # pragma: no cover - version = pkg_resources.get_distribution(project).version + version = version(project) except Exception: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown diff --git a/pint/__init__.py b/pint/__init__.py index a6f59c1ec..3a0bf1f2e 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -13,8 +13,6 @@ import sys -import pkg_resources - from .context import Context from .errors import ( DefinitionSyntaxError, @@ -31,6 +29,12 @@ from .unit import Unit from .util import logger, pi_theorem +try: + from importlib.metadata import version +except ImportError: + # Backport for Python < 3.8 + from importlib_metadata import version + try: from pintpandas import PintArray, PintType @@ -43,7 +47,7 @@ _, _pintpandas_error, _ = sys.exc_info() try: # pragma: no cover - __version__ = pkg_resources.get_distribution("pint").version + __version__ = version("pint") except Exception: # pragma: no cover # we seem to have a local copy not installed without setuptools # so the reported version will be unknown diff --git a/setup.cfg b/setup.cfg index 75781e575..df22fae7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,10 @@ packages = pint zip_safe = True include_package_data = True python_requires = >=3.6 -install_requires = setuptools; packaging +install_requires = + setuptools + packaging + importlib-metadata; python_version < '3.8' setup_requires = setuptools; setuptools_scm test_suite = pint.testsuite.testsuite scripts = pint/pint-convert From e06902c9f6850717b1c4c5ad0ffbf57785bb4959 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 23 Apr 2020 16:30:03 +0200 Subject: [PATCH 436/612] add tests for prod --- pint/testsuite/test_numpy.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index fd12bf531..594bde051 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -286,6 +286,16 @@ def test_fix(self): def test_prod(self): self.assertEqual(self.q.prod(), 24 * self.ureg.m ** 4) + @helpers.requires_array_function_protocol() + def test_prod_numpy_func(self): + axis = 0 + where = [[True, False], [True, True]] + + self.assertQuantityEqual(np.prod(self.q, axis=axis), [3, 8] * self.ureg.m ** 2) + self.assertQuantityEqual(np.prod(self.q, where=where), 12 * self.ureg.m ** 3) + + self.assertRaises(ValueError, np.prod, self.q, axis=axis, where=where) + def test_sum(self): self.assertEqual(self.q.sum(), 10 * self.ureg.m) self.assertQuantityEqual(self.q.sum(0), [4, 6] * self.ureg.m) From c12aac68f28c1d229b1f90336c17ec2fbf086ebc Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 23 Apr 2020 16:30:53 +0200 Subject: [PATCH 437/612] implement numpy.prod --- pint/numpy_func.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 8501a7e9a..718807119 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -670,6 +670,28 @@ def _all(a, *args, **kwargs): raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") +@implements("prod", "function") +def _prod(a, *args, **kwargs): + arg_names = ("axis", "dtype", "out", "keepdims", "initial", "where") + all_kwargs = dict(**dict(zip(arg_names, args)), **kwargs) + axis = all_kwargs.get("axis", None) + where = all_kwargs.get("where", None) + + if axis is not None and where is not None: + raise ValueError("passing axis and where is not supported") + + result = np.prod(a._magnitude, *args, **kwargs) + + if axis is not None: + exponent = a.size // result.size + units = a.units ** exponent + elif where is not None: + exponent = np.asarray(where, dtype=np.bool_).sum() + units = a.units ** exponent + + return units._REGISTRY.Quantity(result, units) + + # Implement simple matching-unit or stripped-unit functions based on signature From b1d1606082d66a6ea744db492d03b183f8e8f0a2 Mon Sep 17 00:00:00 2001 From: Dima Pustakhod Date: Sat, 2 May 2020 21:34:26 +0200 Subject: [PATCH 438/612] Add None to the example --- docs/wrapping.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 7b56dccd6..8a0be35d9 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -156,7 +156,7 @@ arguments: .. doctest:: - >>> @ureg.wraps(ureg.second, (ureg.meters, ureg.meters/ureg.second**2)) + >>> @ureg.wraps(ureg.second, (ureg.meters, ureg.meters/ureg.second**2, None)) ... def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2'), verbose=False): ... """Calculate time to fall from a height h. ... From a9b3e4eeb68954e9df5222fbb67dcce4b075a563 Mon Sep 17 00:00:00 2001 From: Dima Pustakhod Date: Sat, 2 May 2020 21:37:11 +0200 Subject: [PATCH 439/612] Update CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index a3617c2a4..3babceb28 100644 --- a/CHANGES +++ b/CHANGES @@ -34,7 +34,7 @@ Pint Changelog method so that now `list(ureg)` returns a list of all units in registry. (Issue #1072, Thanks Tom Nicholas) - Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) - +- Fix typo in docs for wraps example with optional arguments. (Issue #1088) 0.11 (2020-02-19) ----------------- From 2be68656065db0a6838ba657ec778f481cc31f99 Mon Sep 17 00:00:00 2001 From: Sam Tygier Date: Fri, 8 May 2020 15:07:17 +0100 Subject: [PATCH 440/612] Add momentum as dimension Allows for example dimension checking for momentum. x.check("[momentum]") --- CHANGES | 1 + pint/default_en.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 3babceb28..d30426344 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,7 @@ Pint Changelog (Issue #1072, Thanks Tom Nicholas) - Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) - Fix typo in docs for wraps example with optional arguments. (Issue #1088) +- Add momentum as a dimension 0.11 (2020-02-19) ----------------- diff --git a/pint/default_en.txt b/pint/default_en.txt index 8bd413313..916e924c7 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -280,6 +280,9 @@ refrigeration_ton = 12e3 * Btu / hour = _ = ton_of_refrigeration # approximate, standard_liter_per_minute = atmosphere * liter / minute = slpm = slm conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 +# Momentum +[momentum] = [length] * [mass] / [time] + # Density (as auxiliary for pressure) [density] = [mass] / [volume] mercury = 13.5951 * kilogram / liter = Hg = Hg_0C = Hg_32F = conventional_mercury From 12d456cde10a445dfaaf7c3ce2a1428e47c9873b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 13 May 2020 10:54:42 +0100 Subject: [PATCH 441/612] Tweak #1062/#1064 A hit on ureg._cache.parse_unit is no guarantee of a hit on ureg._units c --- CHANGES | 6 +++++- pint/registry.py | 10 +++++----- pint/testsuite/test_issues.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index d30426344..f2229b0bc 100644 --- a/CHANGES +++ b/CHANGES @@ -22,7 +22,8 @@ Pint Changelog - Fixed issue where quantities with a very large magnitude would throw an IndexError when using to_compact() - Fixed crash when a Unit with prefix is declared for the first time while a Context - containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale) + containing unit redefinitions is active + (Issues #1062 and #1097, Thanks Guido Imperiale) - New implementation of 'Lx' String Format Type Option The old implementation treated 'Lx' as 'S' as produced by 'uncertainties' package, but that is not fully compatible with SIunitx. The new code protects @@ -37,6 +38,7 @@ Pint Changelog - Fix typo in docs for wraps example with optional arguments. (Issue #1088) - Add momentum as a dimension + 0.11 (2020-02-19) ----------------- @@ -77,6 +79,7 @@ Pint Changelog (Issue #881, Thanks Guido Imperiale) - Fixed __array__ signature to match numpy docs (Issue #974, Thanks Ryan May) + 0.10 (2020-01-05) ----------------- @@ -354,6 +357,7 @@ Pint Changelog - Allow changing shape for Quantities with numpy arrays. (Issue #344, thanks tecki) + 0.7.2 (2016-03-02) ------------------ - Fixed backward incompatibility problem when parsing dimensionless units. diff --git a/pint/registry.py b/pint/registry.py index 5e1e0fae2..84544e1bf 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1081,11 +1081,11 @@ def _parse_units(self, input_string, as_delta=True): """ cache = self._cache.parse_unit - if as_delta: - try: - return cache[input_string] - except KeyError: - pass + # Issue #1097: it is possible, when a unit was defined while a different context + # was active, that the unit is in self._cache.parse_unit but not in self._units. + # If this is the case, force self._units to be repopulated. + if as_delta and input_string in cache and input_string in self._units: + return cache[input_string] if not input_string: return self.UnitsContainer() diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 29c790505..6d1e5d110 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -693,10 +693,10 @@ def test_issue973(self): assert isinstance(q1, ureg.Quantity) assert len(q0) == len(q1) == 0 - def test_issue1062(self): + def test_issue1062_issue1097(self): # Must not be used by any other tests assert "nanometer" not in ureg._units - for i in range(2): + for i in range(5): ctx = Context.from_lines(["@context _", "cal = 4 J"]) with ureg.context("sp", ctx): q = ureg.Quantity(1, "nm") From ac79c0606ef57bf07dac30c276d00219b36ee604 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Wed, 13 May 2020 11:08:39 +0100 Subject: [PATCH 442/612] flake8 --- pint/measurement.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pint/measurement.py b/pint/measurement.py index dd0f592fc..826253fa5 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -49,9 +49,7 @@ def __new__(cls, value, error, units=MISSING): if error is MISSING: mag = value elif error < 0: - raise ValueError( - "The magnitude of the error cannot be negative".format(value, error) - ) + raise ValueError("The magnitude of the error cannot be negative") else: mag = ufloat(value, error) From eed85601c8bdf0829ed256cdf2af9982bc9ed5a2 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 29 May 2020 12:43:04 -0300 Subject: [PATCH 443/612] Preparing release 0.12 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index b897f29aa..da6815216 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.12 (unreleased) +0.12 (2020-05-29) ----------------- - Add full support for Decimal and Fraction at the registry level. diff --git a/version.py b/version.py index 16c455fd0..8d8a079ad 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.12.dev0' +__version__ = '0.12' # fmt: on From dc0f563c60b8fd73693b19a89e7d2711fc07b296 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 29 May 2020 12:43:40 -0300 Subject: [PATCH 444/612] Back to development: 0.13 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index da6815216..921848a94 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.13 (unreleased) +----------------- + +- Nothing changed yet. + + 0.12 (2020-05-29) ----------------- diff --git a/version.py b/version.py index 8d8a079ad..4b893a2fd 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.12' +__version__ = '0.13.dev0' # fmt: on From 6aea23aa303b6f4c470eb1c923e91da7b7c09581 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Fri, 5 Jun 2020 17:54:30 -0700 Subject: [PATCH 445/612] fix superscripting in HTML and pretty print HTML formatting only superscripted the first character of the exponent without brackets around it this commit brackets the base and exponent to fix this rendering issue this commit also improves pretty printing of exponents by using the unicode dot operator instead of a period tests updated to expect the brackets, black and flake8 run --- CHANGES | 1 + pint/formatting.py | 6 +++--- pint/testsuite/test_measurement.py | 16 ++++++++-------- pint/testsuite/test_quantity.py | 12 ++++++------ pint/testsuite/test_unit.py | 12 ++++++------ 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CHANGES b/CHANGES index f2229b0bc..0cfd3d4bc 100644 --- a/CHANGES +++ b/CHANGES @@ -37,6 +37,7 @@ Pint Changelog - Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) - Fix typo in docs for wraps example with optional arguments. (Issue #1088) - Add momentum as a dimension +- Fixed a bug where unit exponents were only partially superscripted in HTML format 0.11 (2020-02-19) diff --git a/pint/formatting.py b/pint/formatting.py index 4eae5cd88..495dfec8c 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -62,8 +62,8 @@ def _pretty_fmt_exponent(num): str """ - # TODO: Will not work for decimals - ret = f"{num:n}".replace("-", "⁻") + # unicode dot operator (U+22C5) looks like a superscript decimal + ret = f"{num:n}".replace("-", "⁻").replace(".", "\u22C5") for n in range(10): ret = ret.replace(str(n), _PRETTY_EXPONENTS[n]) return ret @@ -95,7 +95,7 @@ def _pretty_fmt_exponent(num): "single_denominator": True, "product_fmt": r" ", "division_fmt": r"{}/{}", - "power_fmt": "{}^{}", + "power_fmt": "{{{}}}^{{{}}}", # braces superscript whole exponent "parentheses_fmt": r"({})", }, "": { # Default format. diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 871e880fe..78d48a73b 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}", r"\[(4.00 ± 0.10)\ {second}^{2}\]"), ("{: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}", r"\[(4.0 ± 0.1)\ {second}^{2}\]"), ("{:.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}", r"\[0.2000(100)\ {second}^{2}\]"), ("{:.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}", r"\[(0.2000 ± 0.0100)\ {second}^{2}\]"), ("{:.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}", r"\[(20 ± 1)%\ {second}^{2}\]"), ("{:.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}", r"\[(2.0 ± 0.1)×10^{-1}\ {second}^{2}\]"), ("{:.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}", r"\[(4.00 ± 0.10)×10^{20}\ {second}^{2}\]"), ("{: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}", r"\[(4.00 ± 0.10)×10^{-20}\ {second}^{2}\]"), ("{: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 b58e22762..fa18fe8fc 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}", r"\[4.12345678\ kilogram\ {meter}^{2}/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~}", r"\[4.12345678\ kg\ {m}^{2}/s\]"), ("{:C~}", "4.12345678 kg*m**2/s"), ("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"), ): @@ -209,12 +209,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", r"\[4.12345678\ kilogram\ {meter}^{2}/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~", r"\[4.12345678\ kg\ {m}^{2}/s\]"), ("C~", "4.12345678 kg*m**2/s"), ): with self.subTest(spec): @@ -250,7 +250,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_(), r"\[3.5\ kilogram\ {meter}^{2}/second\]") self.assertEqual( x._repr_latex_(), r"$3.5\ \frac{\mathrm{kilogram} \cdot " @@ -259,7 +259,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_(), r"\[3.5\ kg\ {m}^{2}/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 76082c94a..74b21a670 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}", r"\[kilogram\ {meter}^{2}/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~}", r"\[kg\ {m}^{2}/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", r"\[kilogram\ {meter}^{2}/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~", r"\[kg\ {m}^{2}/s\]"), ("C~", "kg*m**2/s"), ): 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_(), r"\[kilogram\ {meter}^{2}/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_(), r"\[kg\ {m}^{2}/s\]") self.assertEqual( x._repr_latex_(), r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" ) From b71f17d3435f8037ab7df0bd40fadf96a72fb5be Mon Sep 17 00:00:00 2001 From: crusaderky Date: Mon, 15 Jun 2020 10:09:55 +0100 Subject: [PATCH 446/612] Fix CHANGES --- CHANGES | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index f2229b0bc..7925a9bd7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,15 +1,9 @@ Pint Changelog ============== -0.12 (unreleased) +0.13 (unreleased) ----------------- -- Add full support for Decimal and Fraction at the registry level. - **BREAKING CHANGE**: - `use_decimal` is deprecated. Use `non_int_type=Decimal` when instantiating - the registry. -- Fixed bug where numpy.pad didn't work without specifying constant_values or - end_values (Issue #1026) - Reinstated support for pickle protocol 0 and 1, which is required by pytables (Issue #1036, Thanks Guido Imperiale) - Fixed bug with multiplication of Quantity by dict (Issue #1032) @@ -19,7 +13,7 @@ Pint Changelog (Issue #1050, Thanks Guido Imperiale) - NaN is now treated the same as zero in addition, subtraction, equality, and disequality (Issue #1051, Thanks Guido Imperiale) -- Fixed issue where quantities with a very large magnitude would throw an IndexError +- Fixed issue where quantities with a very large magnitude would throw an IndexError when using to_compact() - Fixed crash when a Unit with prefix is declared for the first time while a Context containing unit redefinitions is active @@ -39,6 +33,17 @@ Pint Changelog - Add momentum as a dimension +0.12 (2020-05-29) +----------------- + +- Add full support for Decimal and Fraction at the registry level. + **BREAKING CHANGE**: + `use_decimal` is deprecated. Use `non_int_type=Decimal` when instantiating + the registry. +- Fixed bug where numpy.pad didn't work without specifying constant_values or + end_values (Issue #1026) + + 0.11 (2020-02-19) ----------------- From 9830472d2dcc391d588aaee747246ea67d35df46 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Mon, 15 Jun 2020 10:11:47 +0100 Subject: [PATCH 447/612] Set as development --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index 8d8a079ad..4b893a2fd 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.12' +__version__ = '0.13.dev0' # fmt: on From 80e9c197515ed5de7b3a5155474647f436fda2f6 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Mon, 15 Jun 2020 14:12:43 +0100 Subject: [PATCH 448/612] Align to setuptools_scm --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index 4b893a2fd..16c455fd0 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.13.dev0' +__version__ = '0.12.dev0' # fmt: on From 71139380d747ea65ba8560fc968f1f45ebae4862 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Mon, 15 Jun 2020 14:26:46 +0100 Subject: [PATCH 449/612] Fix precedence order in context redefinitions --- CHANGES | 2 ++ pint/registry.py | 2 +- pint/testsuite/test_contexts.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7925a9bd7..74a226e4f 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,8 @@ Pint Changelog - Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) - Fix typo in docs for wraps example with optional arguments. (Issue #1088) - Add momentum as a dimension +- Multiple contexts containing the same redefinition can now be stacked + (Issue #1108, Thanks Guido Imperiale) 0.12 (2020-05-29) diff --git a/pint/registry.py b/pint/registry.py index 84544e1bf..ed6bb1df1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1546,7 +1546,7 @@ def _switch_context_cache_and_units(self) -> None: on_redefinition_backup = self._on_redefinition self._on_redefinition = "ignore" try: - for ctx in self._active_ctx.contexts: + for ctx in reversed(self._active_ctx.contexts): for definition in ctx.redefinitions: self._redefine(definition) finally: diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index bc575ed4f..b57a4df14 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -835,6 +835,38 @@ def test_non_multiplicative(self): with ureg.context("mult_to_nonmult"): self.assertAlmostEqual(k.to("bogodegrees").magnitude, (100 - 123) / 5) + def test_stack_contexts(self): + ureg = UnitRegistry( + """ + a = [dim1] + b = 1/2 a + c = 1/3 a + d = [dim2] + + @context c1 + b = 1/4 a + c = 1/6 a + [dim1]->[dim2]: value * 2 d/a + @end + @context c2 + b = 1/5 a + [dim1]->[dim2]: value * 3 d/a + @end + """.splitlines() + ) + q = ureg.Quantity(1, "a") + assert q.to("b").magnitude == 2 + assert q.to("c").magnitude == 3 + assert q.to("b", "c1").magnitude == 4 + assert q.to("c", "c1").magnitude == 6 + assert q.to("d", "c1").magnitude == 2 + assert q.to("b", "c2").magnitude == 5 + assert q.to("c", "c2").magnitude == 3 + assert q.to("d", "c2").magnitude == 3 + assert q.to("b", "c1", "c2").magnitude == 5 # c2 takes precedence + assert q.to("c", "c1", "c2").magnitude == 6 # c2 doesn't change it, so use c1 + assert q.to("d", "c1", "c2").magnitude == 3 # c2 takes precedence + def test_err_to_base_unit(self): with self.assertRaises(DefinitionSyntaxError) as e: Context.from_lines(["@context c", "x = [d]"]) From 0e58cbc0754d2f11b9abf100808f0f15f52f849f Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 16 Jun 2020 09:35:22 +0100 Subject: [PATCH 450/612] Tweak CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3f1457701..6e67ea02f 100644 --- a/CHANGES +++ b/CHANGES @@ -31,9 +31,9 @@ Pint Changelog - Replace pkg_resources.version to importlib.metadata.version. (Issue #1083) - Fix typo in docs for wraps example with optional arguments. (Issue #1088) - Add momentum as a dimension +- Fixed a bug where unit exponents were only partially superscripted in HTML format - Multiple contexts containing the same redefinition can now be stacked (Issue #1108, Thanks Guido Imperiale) -- Fixed a bug where unit exponents were only partially superscripted in HTML format 0.12 (2020-05-29) From 47ea60d3e78d6052e121f896cb2b339968d53ac7 Mon Sep 17 00:00:00 2001 From: crusaderky Date: Tue, 16 Jun 2020 18:10:59 +0100 Subject: [PATCH 451/612] Stacked contexts cause RuntimeError: generator raised StopIteration commit 1b105987940a95891ad8223ded1e45a95621969d Author: crusaderky Date: Tue Jun 16 18:10:06 2020 +0100 merge commit d99b2407349f807f3f789be24b774c4ae0221dfa Merge: e899082 583b297 Author: crusaderky Date: Tue Jun 16 18:08:53 2020 +0100 Merge remote-tracking branch 'upstream/master' into issue1112 commit e899082d4e504737d11c1562c41baaddfbc1fb8b Merge: d3c65b6 af723fa Author: crusaderky Date: Tue Jun 16 09:36:04 2020 +0100 Merge branch 'master' into issue1112 commit d3c65b62d225cbe344b877687ccc0e80c2c8af2c Author: crusaderky Date: Mon Jun 15 14:57:50 2020 +0100 _active_ctx may contain empty maps commit d5df37560dd68a59fa42ced632a41debaaf6924b Author: crusaderky Date: Mon Jun 15 14:57:33 2020 +0100 Fix commit ffc64940db28693c1836365b026d51f1a66fecad Author: crusaderky Date: Mon Jun 15 14:36:35 2020 +0100 tweak test commit 106993c4476c789636aed34d7c405034c6797cc8 Merge: ca9adac 80e9c19 Author: crusaderky Date: Mon Jun 15 14:12:54 2020 +0100 Merge branch 'v013' into issue1112 commit ca9adac7279da05e0390f91b2da7f24bc157f001 Author: crusaderky Date: Mon Jun 15 14:10:51 2020 +0100 Test --- CHANGES | 2 ++ pint/context.py | 4 ++-- pint/testsuite/test_issues.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 6e67ea02f..eabdfd034 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,8 @@ Pint Changelog - Fixed a bug where unit exponents were only partially superscripted in HTML format - Multiple contexts containing the same redefinition can now be stacked (Issue #1108, Thanks Guido Imperiale) +- Fixed crash when some specific combinations of contexts were enabled + (Issue #1112, Thanks Guido Imperiale) 0.12 (2020-05-29) diff --git a/pint/context.py b/pint/context.py index e1405d5fc..6a54301c9 100644 --- a/pint/context.py +++ b/pint/context.py @@ -327,8 +327,8 @@ def remove_contexts(self, n: int = None): @property def defaults(self): - if self: - return next(iter(self.maps[0].values())).defaults + for ctx in self.values(): + return ctx.defaults return {} @property diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6d1e5d110..855a6b90f 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -702,6 +702,31 @@ def test_issue1062_issue1097(self): q = ureg.Quantity(1, "nm") q.to("J") + def test_issue1112(self): + ureg = UnitRegistry( + """ + m = [length] + g = [mass] + s = [time] + + ft = 0.305 m + lb = 454 g + + @context c1 + [time]->[length] : value * 10 m/s + @end + @context c2 + ft = 0.3 m + @end + @context c3 + lb = 500 g + @end + """.splitlines() + ) + ureg.enable_contexts("c1") + ureg.enable_contexts("c2") + ureg.enable_contexts("c3") + if np is not None: From 521e63a9ea914a6936000bc02dfb57d488c5a383 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 12:43:01 -0700 Subject: [PATCH 452/612] allow checking if a prefixed unit is 'in' a registry resolves #1086 flake --- pint/registry.py | 9 +++++++++ pint/testsuite/test_issues.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/pint/registry.py b/pint/registry.py index ed6bb1df1..bc946e45b 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -289,6 +289,15 @@ def __getitem__(self, item): ) return self.parse_expression(item) + def __contains__(self, item): + """Support checking prefixed units with the `in` operator + """ + try: + self.__getattr__(item) + return True + except UndefinedUnitError: + return False + def __dir__(self): #: Calling dir(registry) gives all units, methods, and attributes. #: Also used for autocompletion in IPython. diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 855a6b90f..f71ce1aa0 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -702,6 +702,17 @@ def test_issue1062_issue1097(self): q = ureg.Quantity(1, "nm") q.to("J") + def test_issue1086(self): + # units with prefixes should correctly test as 'in' the registry + assert "bits" in ureg + assert "gigabits" in ureg + assert "meters" in ureg + assert "kilometers" in ureg + # unknown or incorrect units should test as 'not in' the registry + assert "magicbits" not in ureg + assert "unknownmeters" not in ureg + assert "gigatrees" not in ureg + def test_issue1112(self): ureg = UnitRegistry( """ From 82fdd3c591e21c222902e467360e9ca218e5ca9f Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 13:09:47 -0700 Subject: [PATCH 453/612] document new ureg.__contains__ feature --- CHANGES | 3 ++- docs/defining.rst | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index eabdfd034..61af9bdce 100644 --- a/CHANGES +++ b/CHANGES @@ -36,13 +36,14 @@ Pint Changelog (Issue #1108, Thanks Guido Imperiale) - Fixed crash when some specific combinations of contexts were enabled (Issue #1112, Thanks Guido Imperiale) +- Added support for checking prefixed units using `in` keyword (Issue #1086) 0.12 (2020-05-29) ----------------- - Add full support for Decimal and Fraction at the registry level. - **BREAKING CHANGE**: + **BREAKING CHANGE**: `use_decimal` is deprecated. Use `non_int_type=Decimal` when instantiating the registry. - Fixed bug where numpy.pad didn't work without specifying constant_values or diff --git a/docs/defining.rst b/docs/defining.rst index 152893a86..fd5c2d456 100644 --- a/docs/defining.rst +++ b/docs/defining.rst @@ -152,3 +152,17 @@ leading underscore: >>> ureg.define('mpg = 1 * mile / gallon') >>> fuel_ec_europe = 5 * ureg.L / ureg._100km >>> fuel_ec_us = (1 / fuel_ec_europe).to(ureg.mpg) + + +Checking if a unit is already defined +------------------------------------- + +The python ``in`` keyword works as expected with unit registries. Check if +a unit has been defined with the following: + +.. doctest:: + + >>> 'MHz' in ureg + True + >>> 'gigatrees' in ureg + False From fef7c1726ffe890abb66881e43b0839129e93535 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 21:50:20 -0700 Subject: [PATCH 454/612] add information on running tests to contributing.rst --- docs/contributing.rst | 32 ++++++++++++++++++++++++++++++++ docs/developers_reference.rst | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index a7c7e41c0..d5c870330 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -32,6 +32,38 @@ Pint uses `bors-ng` as a merge bot and therefore every PR is tested before mergi In any case, feel free to use the `issue tracker`_ to discuss ideas for new features or improvements. +Setting up your environment +--------------------------- + +If you're contributing to this project for the fist time, you can set up your +environment on Linux or OSX with the following commands:: + + $ git clone git@github.com:hgrecco/pint.git + $ cd pint + $ python -m virtualenv venv + $ source venv/bin/activate + $ pip install -e . + $ pip install -r requirements_docs.txt + +Running tests and building documentation +---------------------------------------- + +To run the test suite, invoke pytest from the ``pint`` directory:: + + $ cd pint + $ pytest + +To run the doctests, invoke Sphinx's doctest module from the ``docs`` directory:: + + $ cd docs + $ make doctest + +To build the documentation, invoke Sphinx from the ``docs`` directory:: + + $ cd docs + $ make html + + .. _github: http://github.com/hgrecco/pint .. _`issue tracker`: https://github.com/hgrecco/pint/issues .. _`bors-ng`: https://github.com/bors-ng/bors-ng diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst index 157732805..b4a48891f 100644 --- a/docs/developers_reference.rst +++ b/docs/developers_reference.rst @@ -111,4 +111,4 @@ Pint :members: .. automodule:: pint.testsuite.test_util - :members: \ No newline at end of file + :members: From 11019982aecf453cc136c5018ceb75da7fa6229c Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 22:47:07 -0700 Subject: [PATCH 455/612] restrict Quanity.__repr__() to 9 digit float magnitudes rounding after 9 digits will make doctests more stable and less prone to subtle floating point errors whenever they reference a magnitude's repr() repr() for ints and numpy array magnitudes unchanged --- pint/quantity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index c087b0cab..1f1816648 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -260,7 +260,10 @@ def __bytes__(self): return str(self).encode(locale.getpreferredencoding()) def __repr__(self): - return f"" + if isinstance(self._magnitude, float): + return f"" + else: + return f"" def __hash__(self): self_base = self.to_base_units() From 4522c4df0c538669559570c7b3a39372e71f347d Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 18:59:52 -0700 Subject: [PATCH 456/612] fix doctest errors in most .rst docs doctests updated for new 9-digit Quantity().__repr__() added dependency on Serialize to requirements_docs.txt in systems.rst: three errors remain, one may be a regression, the others I'm not qualified to comment on in nonmult.rst: some issues around floating point estimation are made more robust by using print() seems like Quantity().__repr__() changed since these tests were written, and could benefit from some floating-point truncation in pitheorem.rst the only error was dictionary ordering, which is now deterministic in python3.7, but may still cause issues down the line no doctests in getting.rst, marked all as code blocks --- docs/contexts.rst | 28 ++++++++++++++----- docs/defining.rst | 4 +-- docs/getting.rst | 10 ++++--- docs/measurement.rst | 2 +- docs/nonmult.rst | 62 ++++++++++++++++++++---------------------- docs/pitheorem.rst | 2 +- docs/serialization.rst | 15 +++++----- docs/systems.rst | 26 +++++++++++++++--- docs/tutorial.rst | 61 +++++++++++++++++++++++------------------ docs/wrapping.rst | 46 +++++++++++++++++-------------- requirements_docs.txt | 1 + 11 files changed, 150 insertions(+), 107 deletions(-) diff --git a/docs/contexts.rst b/docs/contexts.rst index 27cb6abc2..9f75c9fdb 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -55,11 +55,15 @@ Contexts can be also enabled for blocks of code using the `with` statement: If you need a particular context in all your code, you can enable it for all -operations with the registry:: +operations with the registry + +.. doctest:: >>> ureg.enable_contexts('sp') -To disable the context, just call:: +To disable the context, just call + +.. doctest:: >>> ureg.disable_contexts() @@ -69,17 +73,23 @@ Enabling multiple contexts You can enable multiple contexts: +.. doctest:: + >>> q.to('Hz', 'sp', 'boltzmann') This works also using the `with` statement: +.. doctest:: + >>> with ureg.context('sp', 'boltzmann'): ... q.to('Hz') or in the registry: +.. doctest:: + >>> ureg.enable_contexts('sp', 'boltzmann') >>> q.to('Hz') @@ -88,6 +98,8 @@ If a conversion rule between two dimensions appears in more than one context, the one in the last context has precedence. This is easy to remember if you think that the previous syntax is equivalent to nest contexts: +.. doctest:: + >>> with ureg.context('sp'): ... with ureg.context('boltzmann') : ... q.to('Hz') @@ -106,7 +118,7 @@ calculate, for example, the wavelength in water of a laser which on air is 530 n >>> wl = 530. * ureg.nm >>> f = wl.to('Hz', 'sp') >>> f.to('nm', 'sp', n=1.33) - + Contexts can also accept Pint Quantity objects as parameters. For example, the 'chemistry' context accepts the molecular weight of a substance (as a Quantity @@ -135,7 +147,7 @@ context and the parameters that you wish to set. ... def f(wl): ... return wl.to('Hz').magnitude >>> f(wl) - 398.496240602 + 425297855014895.6 This decorator can be combined with **wraps** or **check** decorators described in @@ -194,14 +206,16 @@ functions. For example: ... lambda ureg, x: x * ureg.speed_of_light) >>> ureg.add_context(c) >>> ureg("1 s").to("km", "ab") - 299792.458 kilometer + It is also possible to create anonymous contexts without invoking add_context: +.. doctest:: + >>> c = pint.Context() - ... + >>> c.add_transformation('[time]', '[length]', lambda ureg, x: x * ureg.speed_of_light) >>> ureg("1 s").to("km", c) - 299792.458 kilometer + Using contexts for unit redefinition ------------------------------------ diff --git a/docs/defining.rst b/docs/defining.rst index fd5c2d456..43344bcaf 100644 --- a/docs/defining.rst +++ b/docs/defining.rst @@ -112,7 +112,7 @@ Let's add a dog_year (sometimes written as dy) equivalent to 52 (human) days: # We create a quantity based on that unit and we convert to years. >>> lassie_lifespan = Q_(10, 'year') >>> print(lassie_lifespan.to('dog_years')) - 70.23888438100961 dog_year + 70.240384... dog_year Note that we have used the name `dog_years` even though we have not defined the plural form as an alias. Pint takes care of that, so you don't have to. @@ -132,7 +132,7 @@ Same for aliases and derived dimensions: .. doctest:: >>> ureg.define('@alias meter = metro = metr') - >>> ureg.define('[hypervolume] = [length ** 4]') + >>> ureg.define('[hypervolume] = [length] ** 4') .. warning:: diff --git a/docs/getting.rst b/docs/getting.rst index db3b2ebf9..a883175cb 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -11,10 +11,10 @@ You can install it (or upgrade to the latest version) using pip_:: That's all! You can check that Pint is correctly installed by starting up python, and importing pint: -.. testcode:: +.. code-block:: - >>> import pint # doctest: +SKIP - >>> pint.__version__ # doctest: +SKIP + >>> import pint + >>> pint.__version__ .. note:: If you have an old system installation of Python and you don't want to mess with it, you can try `Anaconda CE`_. It is a free Python distribution by @@ -25,7 +25,9 @@ That's all! You can check that Pint is correctly installed by starting up python You can check the installation with the following command: - >>> pint.test() # doctest: +SKIP +.. code-block:: + + >>> pint.test() On Arch Linux, you can alternatively install Pint from the Arch User Repository diff --git a/docs/measurement.rst b/docs/measurement.rst index 12facfb20..78e80083d 100644 --- a/docs/measurement.rst +++ b/docs/measurement.rst @@ -54,7 +54,7 @@ Mathematical operations with Measurements, return new measurements following the .. doctest:: >>> print(2 * book_length) - (40.0 +/- 4.0) centimeter + (40 +/- 4) centimeter >>> width = (10 * ureg.centimeter).plus_minus(1) >>> print('{:.02f}'.format(book_length + width)) (30.00 +/- 2.24) centimeter diff --git a/docs/nonmult.rst b/docs/nonmult.rst index 4d792c1f4..a649d2ad1 100644 --- a/docs/nonmult.rst +++ b/docs/nonmult.rst @@ -14,30 +14,24 @@ kelvin and rankine abbreviated as degF, degC, degK, and degR. For example, to convert from celsius to fahrenheit: -.. testsetup:: - - from pint import UnitRegistry - ureg = UnitRegistry() - ureg.default_format = '.3f' - Q_ = ureg.Quantity - .. doctest:: - >>> from pint import UnitRegistry - >>> ureg = UnitRegistry() - >>> Q_ = ureg.Quantity - >>> home = Q_(25.4, ureg.degC) - >>> print(home.to('degF')) - 77.7200004 degF + >>> from pint import UnitRegistry + >>> ureg = UnitRegistry() + >>> ureg.default_format = '.3f' + >>> Q_ = ureg.Quantity + >>> home = Q_(25.4, ureg.degC) + >>> print(home.to('degF')) + 77.720 degree_Fahrenheit or to other kelvin or rankine: .. doctest:: >>> print(home.to('kelvin')) - 298.55 kelvin + 298.550 kelvin >>> print(home.to('degR')) - 537.39 degR + 537.390 degree_Rankine Additionally, for every non-multiplicative temperature unit in the registry, there is also a *delta* counterpart to specify @@ -48,18 +42,18 @@ is different). .. doctest:: - >>> increase = 12.3 * ureg.delta_degC - >>> print(increase.to(ureg.kelvin)) - 12.3 kelvin - >>> print(increase.to(ureg.delta_degF)) - 22.14 delta_degF + >>> increase = 12.3 * ureg.delta_degC + >>> print(increase.to(ureg.kelvin)) + 12.300 kelvin + >>> print(increase.to(ureg.delta_degF)) + 22.140 delta_degree_Fahrenheit Subtraction of two temperatures given in offset units yields a *delta* unit: .. doctest:: >>> Q_(25.4, ureg.degC) - Q_(10., ureg.degC) - + You can add or subtract a quantity with *delta* unit and a quantity with offset unit: @@ -67,9 +61,9 @@ offset unit: .. doctest:: >>> Q_(25.4, ureg.degC) + Q_(10., ureg.delta_degC) - + >>> Q_(25.4, ureg.degC) - Q_(10., ureg.delta_degC) - + If you want to add a quantity with absolute unit to one with offset unit, like here @@ -94,7 +88,7 @@ or convert the absolute unit to a *delta* unit: .. doctest:: >>> Q_(10., ureg.degC) + heating_rate.to('delta_degC/min') * Q_(30, ureg.min) - + In contrast to subtraction, the addition of quantities with offset units is ambiguous, e.g. for *10 degC + 100 degC* two different result are reasonable @@ -108,7 +102,7 @@ Quantities with *delta* units are multiplicative: >>> speed = 60. * ureg.delta_degC / ureg.min >>> print(speed.to('delta_degC/second')) - 1.0 delta_degC / second + 1.000 delta_degree_Celsius / second However, multiplication, division and exponentiation of quantities with offset units is problematic just like addition. Pint (since version 0.6) @@ -125,7 +119,7 @@ to be explicitly created: ... OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). >>> Q_(25.4, ureg.degC) - + As an alternative to raising an error, pint can be configured to work more relaxed via setting the UnitRegistry parameter *autoconvert_offset_to_baseunit* @@ -139,20 +133,24 @@ to true. In this mode, pint behaves differently: >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit = True) >>> T = 25.4 * ureg.degC >>> T - + * Before all other multiplications, all divisions and in case of exponentiation [#f1]_ involving quantities with offset-units, pint will convert the quantities with offset units automatically to the corresponding base unit before performing the operation. +.. doctest:: + >>> 1/T - + >>> T * 10 * ureg.meter You can change the behaviour at any time: +.. doctest:: + >>> ureg.autoconvert_offset_to_baseunit = False >>> 1/T Traceback (most recent call last): @@ -165,28 +163,28 @@ is found in a multiplicative context. For example, here: .. doctest:: >>> print(ureg.parse_units('degC/meter')) - delta_degC / meter + delta_degree_Celsius / meter but not here: .. doctest:: >>> print(ureg.parse_units('degC')) - degC + degree_Celsius You can override this behaviour: .. doctest:: >>> print(ureg.parse_units('degC/meter', as_delta=False)) - degC / meter + degree_Celsius / meter Note that the magnitude is left unchanged: .. doctest:: >>> Q_(10, 'degC/meter') - + To define a new temperature, you need to specify the offset. For example, this is the definition of the celsius and fahrenheit:: diff --git a/docs/pitheorem.rst b/docs/pitheorem.rst index a2513d506..cd3716528 100644 --- a/docs/pitheorem.rst +++ b/docs/pitheorem.rst @@ -73,7 +73,7 @@ There are 3 fundamental physical units in this equation: time, mass, and length, ... 'M': '[mass]', ... 'L': '[length]', ... 'g': '[acceleration]'}) - [{'T': 2.0, 'g': 1.0, 'L': -1.0}] + [{'T': 2.0, 'L': -1.0, 'g': 1.0}] which means that the dimensionless quantity is: diff --git a/docs/serialization.rst b/docs/serialization.rst index 1ebd24552..aa4872836 100644 --- a/docs/serialization.rst +++ b/docs/serialization.rst @@ -10,10 +10,6 @@ deserialize the object. The easiest way to do this is by converting the quantity to a string: -.. testsetup:: * - - import pint - .. doctest:: >>> import pint @@ -74,15 +70,19 @@ with the magnitude and the units: >>> to_serialize = duration.to_tuple() >>> print(to_serialize) - (24.2, (('year', 1.0),)) + (24.2, (('year', 1),)) And then you can just pickle that: +.. doctest:: + >>> import pickle >>> serialized = pickle.dumps(to_serialize, -1) To unpickle, just +.. doctest:: + >>> loaded = pickle.loads(serialized) >>> ureg.Quantity.from_tuple(loaded) @@ -97,6 +97,8 @@ numerical type such as `numpy.ndarray`). Using the serialize_ package you can load and read from multiple formats: +.. doctest:: + >>> from serialize import dump, load, register_class >>> register_class(ureg.Quantity, ureg.Quantity.to_tuple, ureg.Quantity.from_tuple) >>> dump(duration, 'output.yaml') @@ -113,6 +115,3 @@ Using the serialize_ package you can load and read from multiple formats: .. _hdf5: http://www.h5py.org/ .. _PyTables: http://www.pytables.org .. _dill: https://pypi.python.org/pypi/dill - - - diff --git a/docs/systems.rst b/docs/systems.rst index 7c095c5d3..f209ab5da 100644 --- a/docs/systems.rst +++ b/docs/systems.rst @@ -5,6 +5,8 @@ Different Unit Systems (and default units) Pint Unit Registry has the concept of system, which is a group of units +.. doctest:: + >>> import pint >>> ureg = pint.UnitRegistry(system='mks') >>> ureg.default_system @@ -12,18 +14,24 @@ Pint Unit Registry has the concept of system, which is a group of units This has an effect in the base units. For example: +.. doctest:: + >>> q = 3600. * ureg.meter / ureg.hour >>> q.to_base_units() But if you change to cgs: +.. doctest:: + >>> ureg.default_system = 'cgs' >>> q.to_base_units() or more drastically to: +.. doctest:: + >>> ureg.default_system = 'imperial' >>> '{:.3f}'.format(q.to_base_units()) '1.094 yard / second' @@ -36,12 +44,16 @@ or more drastically to: You can also use system to narrow down the list of compatible units: +.. doctest:: + >>> ureg.default_system = 'mks' >>> ureg.get_compatible_units('meter') frozenset({, }) or for imperial units: +.. doctest:: + >>> ureg.default_system = 'imperial' >>> ureg.get_compatible_units('meter') frozenset({, , , , , , }) @@ -49,19 +61,25 @@ or for imperial units: You can check which unit systems are available: +.. doctest:: + >>> dir(ureg.sys) - ['US', 'cgs', 'imperial', 'mks'] + ['Planck', 'SI', 'US', 'atomic', 'cgs', 'imperial', 'mks'] Or which units are available within a particular system: +.. doctest:: + >>> dir(ureg.sys.imperial) ['UK_hundredweight', 'UK_ton', 'acre_foot', 'cubic_foot', 'cubic_inch', 'cubic_yard', 'drachm', 'foot', 'grain', 'imperial_barrel', 'imperial_bushel', 'imperial_cup', 'imperial_fluid_drachm', 'imperial_fluid_ounce', 'imperial_gallon', 'imperial_gill', 'imperial_peck', 'imperial_pint', 'imperial_quart', 'inch', 'long_hunderweight', 'long_ton', 'mile', 'ounce', 'pound', 'quarter', 'short_hunderdweight', 'short_ton', 'square_foot', 'square_inch', 'square_mile', 'square_yard', 'stone', 'yard'] Notice that this give you the opportunity to choose within units with colliding names: +.. doctest:: + >>> (1 * ureg.sys.imperial.pint).to('liter') - + >>> (1 * ureg.sys.US.pint).to('liter') - + >>> (1 * ureg.sys.US.pint).to(ureg.sys.imperial.pint) - + diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 474fa2a5e..b49c56414 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -10,16 +10,11 @@ Converting Quantities Pint has the concept of Unit Registry, an object within which units are defined and handled. You start by creating your registry: +.. doctest:: + >>> from pint import UnitRegistry >>> ureg = UnitRegistry() -.. testsetup:: * - - from pint import UnitRegistry - ureg = UnitRegistry() - Q_ = ureg.Quantity - - If no parameter is given to the constructor, the unit registry is populated with the default list of units and prefixes. You can now simply use the registry in the following way: @@ -62,7 +57,7 @@ convert quantities to the unit of choice: .. doctest:: >>> speed.to(ureg.inch / ureg.minute ) - + This method returns a new object leaving the original intact as can be seen by: @@ -78,9 +73,9 @@ use the `ito` method: >>> speed.ito(ureg.inch / ureg.minute ) >>> speed - + >>> print(speed) - 7086.614173228345 inch / minute + 7086.6141... inch / minute If you ask Pint to perform an invalid conversion: @@ -103,7 +98,7 @@ human-readable. >>> print(frequency) 193414489032258.03 hertz >>> print(frequency.to_compact()) - 193.41448903225802 terahertz + 193.414489032... terahertz There are also methods 'to_base_units' and 'ito_base_units' which automatically convert to the reference units with the correct dimensionality: @@ -114,12 +109,12 @@ convert to the reference units with the correct dimensionality: >>> print(height) 5.75 foot >>> print(height.to_base_units()) - 1.7526 meter + 1.752... meter >>> print(height) 5.75 foot >>> height.ito_base_units() >>> print(height) - 1.7526 meter + 1.752... meter There are also methods 'to_reduced_units' and 'ito_reduced_units' which perform a simplified dimensional reduction, combining units with the same dimensionality @@ -131,11 +126,11 @@ but otherwise keeping your unit definitions intact. >>> volume = 10*ureg.cc >>> mass = density*volume >>> print(mass) - 14.0 cc * gram / centimeter ** 3 + 14.0 cubic_centimeter * gram / centimeter ** 3 >>> print(mass.to_reduced_units()) 14.0 gram >>> print(mass) - 14.0 cc * gram / centimeter ** 3 + 14.0 cubic_centimeter * gram / centimeter ** 3 >>> mass.ito_reduced_units() >>> print(mass) 14.0 gram @@ -280,7 +275,7 @@ Strings containing values can be parsed using the ``ureg.parse_pattern`` functio >>> input_string = '10 feet 10 inches' >>> pattern = '{feet} feet {inch} inches' >>> ureg.parse_pattern(input_string, pattern) - [10.0 , 10.0 ] + [, ] To search for multiple matches, set the ``many`` parameter to ``True``. The following example also demonstrates how the parser is able to find matches in amongst filler characters: @@ -289,7 +284,7 @@ To search for multiple matches, set the ``many`` parameter to ``True``. The foll >>> input_string = '10 feet - 20 feet ! 30 feet.' >>> pattern = '{feet} feet' >>> ureg.parse_pattern(input_string, pattern, many=True) - [[10.0 ], [20.0 ], [30.0 ]] + [[], [], []] The full power of regex can also be employed when writing patterns: @@ -298,7 +293,7 @@ The full power of regex can also be employed when writing patterns: >>> input_string = "10` - 20 feet ! 30 ft." >>> pattern = r"{feet}(`| feet| ft)" >>> ureg.parse_pattern(input_string, pattern, many=True) - [[10.0 ], [20.0 ], [30.0 ]] + [[], [], []] *Note that the curly brackets (``{}``) are converted to a float-matching pattern by the parser.* @@ -329,6 +324,7 @@ Pint supports float formatting for numpy arrays as well: .. doctest:: + >>> import numpy as np >>> accel = np.array([-1.1, 1e-6, 1.2505, 1.3]) * ureg['meter/second**2'] >>> # float formatting numpy arrays >>> print('The array is {:.2f}'.format(accel)) @@ -350,8 +346,8 @@ Pint also supports 'f-strings'_ from python>=3.6 : The str is 1.3 m / s ** 2 >>> print(f'The str is {accel:~.3e}') The str is 1.300e+00 m / s ** 2 - >>> print(f'The str is {accel:~H}') - The str is 1.3 m/s² + >>> print(f'The str is {accel:~H}') # HTML format (displays well in Jupyter) + The str is \[1.3\ m/{s}^{2}\] But Pint also extends the standard formatting capabilities for unicode and LaTeX representations: @@ -365,9 +361,9 @@ LaTeX representations: >>> # Latex print >>> 'The latex representation is {:L}'.format(accel) 'The latex representation is 1.3\\ \\frac{\\mathrm{meter}}{\\mathrm{second}^{2}}' - >>> # HTML print + >>> # HTML print - good for Jupyter notebooks >>> 'The HTML representation is {:H}'.format(accel) - 'The HTML representation is 1.3 meter/second2' + 'The HTML representation is \\[1.3\\ meter/{second}^{2}\\]' If you want to use abbreviated unit names, prefix the specification with `~`: @@ -390,6 +386,7 @@ The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatti syntax'_ for custom float representations. For example, scientific notation: .. doctest:: + >>> 'Scientific notation: {:.3e~L}'.format(accel) 'Scientific notation: 1.300\\times 10^{0}\\ \\frac{\\mathrm{m}}{\\mathrm{s}^{2}}' @@ -400,15 +397,16 @@ Pint also supports the LaTeX siunitx package: >>> accel = 1.3 * ureg['meter/second**2'] >>> # siunitx Latex print >>> print('The siunitx representation is {:Lx}'.format(accel)) - The siunitx representation is \SI{1.3}{\meter\per\second\squared} + The siunitx representation is \SI[]{1.3}{\meter\per\second\squared} >>> accel = accel.plus_minus(0.2) >>> print('The siunitx representation is {:Lx}'.format(accel)) - The siunitx representation is \SI{1.3 +- 0.2}{\meter\per\second\squared} + The siunitx representation is \SI{1.30 +- 0.20}{\meter\per\second\squared} Additionally, you can specify a default format specification: .. doctest:: + >>> accel = 1.3 * ureg['meter/second**2'] >>> 'The acceleration is {}'.format(accel) 'The acceleration is 1.3 meter / second ** 2' >>> ureg.default_format = 'P' @@ -425,6 +423,8 @@ Finally, if Babel_ is installed you can translate unit names to any language You can also specify the format locale at the registry level either at creation: +.. doctest:: + >>> ureg = UnitRegistry(fmt_locale='fr_FR') or later: @@ -437,6 +437,7 @@ and by doing that, string formatting is now localized: .. doctest:: + >>> accel = 1.3 * ureg['meter/second**2'] >>> str(accel) '1.3 mètre par seconde²' >>> "%s" % accel @@ -451,14 +452,18 @@ Using Pint in your projects If you use Pint in multiple modules within your Python package, you normally want to avoid creating multiple instances of the unit registry. The best way to do this is by instantiating the registry in a single place. For -example, you can add the following code to your package `__init__.py`:: +example, you can add the following code to your package `__init__.py` + +.. code-block:: from pint import UnitRegistry ureg = UnitRegistry() Q_ = ureg.Quantity -Then in `yourmodule.py` the code would be:: +Then in `yourmodule.py` the code would be + +.. code-block:: from . import ureg, Q_ @@ -466,7 +471,9 @@ Then in `yourmodule.py` the code would be:: my_speed = Q_(20, 'm/s') If you are pickling and unplicking Quantities within your project, you should -also define the registry as the application registry:: +also define the registry as the application registry + +.. code-block:: from pint import UnitRegistry, set_application_registry ureg = UnitRegistry() diff --git a/docs/wrapping.rst b/docs/wrapping.rst index 8a0be35d9..d04b0ba1d 100644 --- a/docs/wrapping.rst +++ b/docs/wrapping.rst @@ -13,19 +13,19 @@ requires you to provide numerical values in certain units: .. testsetup:: * - import math - G = 9.806650 - def pendulum_period(length): - return 2*math.pi*math.sqrt(length/G) + import math + G = 9.806650 + def pendulum_period(length): + return 2*math.pi*math.sqrt(length/G) - def pendulum_period2(length, swing_amplitude): - pass + def pendulum_period2(length, swing_amplitude): + pass - def pendulum_period_maxspeed(length, swing_amplitude): - pass + def pendulum_period_maxspeed(length, swing_amplitude): + pass - def pendulum_period_error(length): - pass + def pendulum_period_error(length): + pass .. doctest:: @@ -47,6 +47,7 @@ You could wrap this function to use Quantities instead: >>> from pint import UnitRegistry >>> ureg = UnitRegistry() + >>> Q_ = ureg.Quantity >>> def mypp_caveman(length): ... return pendulum_period(length.to(ureg.meter).magnitude) * ureg.second @@ -55,7 +56,7 @@ and: .. doctest:: >>> mypp_caveman(100 * ureg.centimeter) - + Pint provides a more convenient way to do this: @@ -71,7 +72,7 @@ Or in the decorator format: ... def mypp(length): ... return pendulum_period(length) >>> mypp(100 * ureg.centimeter) - + `wraps` takes 3 input arguments: @@ -94,7 +95,7 @@ the input arguments assigned to units must be a Quantities. .. doctest:: >>> mypp(1. * ureg.meter) - + >>> mypp(1.) Traceback (most recent call last): ... @@ -106,9 +107,9 @@ To enable using non-Quantity numerical values, set strict to False`. >>> mypp_ns = ureg.wraps(ureg.second, ureg.meter, False)(pendulum_period) >>> mypp_ns(1. * ureg.meter) - + >>> mypp_ns(1.) - + In this mode, the value is assumed to have the correct units. @@ -142,7 +143,9 @@ Or if the function has multiple outputs: If there are more return values than specified units, ``None`` is assumed for the extra outputs. For example, given the NREL SOLPOS calculator that outputs solar zenith, azimuth and air mass, the following wrapper assumes no units for -airmass:: +airmass + +.. doctest:: @ureg.wraps(('deg', 'deg'), ('deg', 'deg', 'millibar', 'degC')) def solar_position(lat, lon, press, tamb, timestamp): @@ -166,18 +169,17 @@ arguments: ... d = .5 * g * t**2 ... t = sqrt(2 * d / g) ... """ - ... t = sqrt(2 * height / gravity) + ... t = math.sqrt(2 * height / gravity) ... if verbose: print(str(t) + " seconds to fall") ... return t ... >>> lunar_module_height = Q_(22, 'feet') + Q_(11, 'inches') >>> calculate_time_to_fall(lunar_module_height, verbose=True) 1.1939473204801092 seconds to fall - - >>> + >>> moon_gravity = Q_(1.625, 'm/s^2') - >>> tcalculate_time_to_fall(lunar_module_height, moon_gravity) - + >>> calculate_time_to_fall(lunar_module_height, gravity=moon_gravity) + Specifying relations between arguments @@ -199,6 +201,8 @@ has the unit of `x` squared (`A**2`) You can use more than one label: +.. doctest:: + >>> @ureg.wraps('=A**2*B', ('=A', '=A*B', '=B')) ... def some_function(x, y, z): ... pass diff --git a/requirements_docs.txt b/requirements_docs.txt index 8bd0205ee..50e54ef8e 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -12,3 +12,4 @@ xarray sparse dask[complete] setuptools>=41.2 +Serialize From 304b87dac78ad8d4ac126f0ae4f9d5e0727e0d0c Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 01:19:14 -0700 Subject: [PATCH 457/612] fix doctest warnings, add some docstring sphinx formatting --- pint/definitions.py | 46 ++++++++++++++++++++++++--------------------- pint/registry.py | 6 +++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/pint/definitions.py b/pint/definitions.py index 338d1fc99..8b7e5817a 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -20,17 +20,17 @@ class PreprocessedDefinition( ): """Splits a definition into the constitutive parts. - A definition is given as a string with equalities in a single line. + A definition is given as a string with equalities in a single line:: ---------------> rhs - a = b = c = d = e - | | | -------> aliases (optional) - | | | - | | -----------> symbol (use "_" to - | | - | ---------------> value - | - -------------------> name + a = b = c = d = e + | | | -------> aliases (optional) + | | | + | | -----------> symbol (use "_" for no symbol) + | | + | ---------------> value + | + -------------------> name Attributes ---------- @@ -175,11 +175,12 @@ def __str__(self): class PrefixDefinition(Definition): - """Definition of a prefix. + """Definition of a prefix:: - - = [= ] [= ] [ = ] [...] + - = [= ] [= ] [ = ] [...] + + Example:: - Example: deca- = 1e+1 = da- = deka- """ @@ -205,11 +206,12 @@ def from_string(cls, definition, non_int_type=float): class UnitDefinition(Definition): - """Definition of a unit. + """Definition of a unit:: + + = [= ] [= ] [ = ] [...] - = [= ] [= ] [ = ] [...] + Example:: - Example: millennium = 1e3 * year = _ = millennia Parameters @@ -279,11 +281,12 @@ def from_string(cls, definition, non_int_type=float): class DimensionDefinition(Definition): - """Definition of a dimension. + """Definition of a dimension:: - [dimension name] = + [dimension name] = + + Example:: - Example: [density] = [mass] / [volume] """ @@ -323,11 +326,12 @@ def from_string(cls, definition, non_int_type=float): class AliasDefinition(Definition): - """Additional alias(es) for an already existing unit. + """Additional alias(es) for an already existing unit:: + + @alias = [ = ] [...] - @alias = [ = ] [...] + Example:: - Example: @alias meter = my_meter """ diff --git a/pint/registry.py b/pint/registry.py index bc946e45b..4d6f0fb9b 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1736,9 +1736,9 @@ def with_context(self, name, **kwargs): Example ------- - >>> @ureg.with_context('sp') - ... def my_cool_fun(wavelenght): - ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz')) + >>> @ureg.with_context('sp') + ... def my_cool_fun(wavelength): + ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz')) """ def decorator(func): From 3316441a443b0a24a1a4da13f01a3aabdf3dd8ee Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 22:44:51 -0700 Subject: [PATCH 458/612] update transform call signature in docstring example now the example runs correctly under doctests --- pint/context.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pint/context.py b/pint/context.py index 6a54301c9..f82bb1397 100644 --- a/pint/context.py +++ b/pint/context.py @@ -63,22 +63,24 @@ class Context: ------- >>> from pint.util import UnitsContainer + >>> from pint import Context, UnitRegistry + >>> ureg = UnitRegistry() >>> timedim = UnitsContainer({'[time]': 1}) >>> spacedim = UnitsContainer({'[length]': 1}) - >>> def f(time): + >>> def time_to_len(ureg, time): ... 'Time to length converter' ... return 3. * time >>> c = Context() - >>> c.add_transformation(timedim, spacedim, f) - >>> c.transform(timedim, spacedim, 2) - 6 - >>> def f(time, n): + >>> c.add_transformation(timedim, spacedim, time_to_len) + >>> c.transform(timedim, spacedim, ureg, 2) + 6.0 + >>> def time_to_len_indexed(ureg, time, n=1): ... 'Time to length converter, n is the index of refraction of the material' ... return 3. * time / n - >>> c = Context(n=3) - >>> c.add_transformation(timedim, spacedim, f) - >>> c.transform(timedim, spacedim, 2) - 2 + >>> c = Context(defaults={'n':3}) + >>> c.add_transformation(timedim, spacedim, time_to_len_indexed) + >>> c.transform(timedim, spacedim, ureg, 2) + 2.0 >>> c.redefine("pound = 0.5 kg") """ From 0957ba2193e59b682206a984ce868d1c4a282b40 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 16 Jun 2020 23:04:15 -0700 Subject: [PATCH 459/612] fix doctests in registry.py --- pint/registry.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 4d6f0fb9b..a05937671 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -1675,12 +1675,16 @@ def context(self, *names, **kwargs): Examples -------- - Context can be called by their name:: + Context can be called by their name: + >>> import pint + >>> ureg = pint.UnitRegistry() + >>> ureg.add_context(pint.Context('one')) + >>> ureg.add_context(pint.Context('two')) >>> with ureg.context('one'): ... pass - If a context has an argument, you can specify its value as a keyword argument:: + If a context has an argument, you can specify its value as a keyword argument: >>> with ureg.context('one', n=1): ... pass @@ -1690,13 +1694,13 @@ def context(self, *names, **kwargs): >>> with ureg.context('one', 'two', n=1): ... pass - Or nested allowing you to give different values to the same keyword argument:: + Or nested allowing you to give different values to the same keyword argument: >>> with ureg.context('one', n=1): ... with ureg.context('two', n=2): ... pass - A nested context inherits the defaults from the containing context:: + A nested context inherits the defaults from the containing context: >>> with ureg.context('one', n=1): ... # Here n takes the value of the outer context From 8e1f1c255c4bdbe339df3b8c977c5a8c8d1aa23f Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 01:50:57 -0700 Subject: [PATCH 460/612] add entry to CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 61af9bdce..888e06117 100644 --- a/CHANGES +++ b/CHANGES @@ -37,6 +37,7 @@ Pint Changelog - Fixed crash when some specific combinations of contexts were enabled (Issue #1112, Thanks Guido Imperiale) - Added support for checking prefixed units using `in` keyword (Issue #1086) +- Updated many examples in the documentation to reflect Pint's current behavior 0.12 (2020-05-29) From 65d4ff3cf38ed0a447181e816dbec75aa3134461 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 17 Jun 2020 15:51:15 +0200 Subject: [PATCH 461/612] make sure the case of calling prod without args works, too --- pint/numpy_func.py | 2 ++ pint/testsuite/test_numpy.py | 1 + 2 files changed, 3 insertions(+) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 718807119..599446184 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -688,6 +688,8 @@ def _prod(a, *args, **kwargs): elif where is not None: exponent = np.asarray(where, dtype=np.bool_).sum() units = a.units ** exponent + else: + units = a.units ** a.size return units._REGISTRY.Quantity(result, units) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 594bde051..9ecdcb1d0 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -291,6 +291,7 @@ def test_prod_numpy_func(self): axis = 0 where = [[True, False], [True, True]] + self.assertQuantityEqual(np.prod(self.q), 24 * self.ureg.m ** 4) self.assertQuantityEqual(np.prod(self.q, axis=axis), [3, 8] * self.ureg.m ** 2) self.assertQuantityEqual(np.prod(self.q, where=where), 12 * self.ureg.m ** 3) From e3e0c9ced6870d7f5805d059c9512bfe76b5b62e Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 15 Jun 2020 16:12:12 -0700 Subject: [PATCH 462/612] start documenting log units --- docs/index.rst | 1 + docs/log_units.rst | 129 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 docs/log_units.rst diff --git a/docs/index.rst b/docs/index.rst index e72ed87f8..fb7015ee4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -117,6 +117,7 @@ User Guide tutorial numpy nonmult + log_units wrapping plotting serialization diff --git a/docs/log_units.rst b/docs/log_units.rst new file mode 100644 index 000000000..1f1132779 --- /dev/null +++ b/docs/log_units.rst @@ -0,0 +1,129 @@ +.. _log_units: + + +Logarithmic Units +================= + +Pint supports some logarithmic units, including dB, dBm, octave, and decade +as well as conversions between them and their base units where applicable. +These units behave much like those described in :ref:`nonmult`, so many of +the recommendations there apply here as well. + +.. note:: + + If you're making heavy use of logarithmic units, you may find it helpful to + pass ``autoconvert_offset_to_baseunit=True`` when initializing your ``UnitRegistry()``, + this will allow you to use syntax like ``10.0 * ureg.dBm`` in lieu of the + explicit ``Quantity()`` constructor. Many examples on this page assume + you've passed this parameter, and will not work otherwise. + +.. testsetup:: + + from pint import UnitRegistry + ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) + ureg.default_format = '.3f' + Q_ = ureg.Quantity + +.. doctest:: + + >>> from pint import UnitRegistry + >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) + >>> Q_ = ureg.Quantity + >>> signal_power = -20.0 * ureg.dBm + >>> print(f"{sp.to('milliwatts'):0.3~P}") + 0.01 mW + +TODO: +[ ] explain delta units (what purpose do they have here?) +[ ] example computing mW to dBm +[ ] example computing dB and (something) + + +Multiplication, division and exponentiation of quantities with +offset units is problematic just like addition. Pint (since version 0.6) +will by default raise an error when a quantity with offset unit is used in +these operations. Due to this quantities with offset units cannot be created +like other quantities by multiplication of magnitude and unit but have +to be explicitly created: + +.. doctest:: + + >>> ureg = UnitRegistry() + >>> home = 25.4 * ureg.degC + Traceback (most recent call last): + ... + OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). + >>> Q_(25.4, ureg.degC) + + +As an alternative to raising an error, pint can be configured to work more +relaxed via setting the UnitRegistry parameter *autoconvert_offset_to_baseunit* +to true. In this mode, pint behaves differently: + +* Multiplication of a quantity with a single offset unit with order +1 by + a number or ndarray yields the quantity in the given unit. + +.. doctest:: + + >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit = True) + >>> T = 25.4 * ureg.degC + >>> T + + +* Before all other multiplications, all divisions and in case of + exponentiation [#f1]_ involving quantities with offset-units, pint + will convert the quantities with offset units automatically to the + corresponding base unit before performing the operation. + + >>> 1/T + + >>> T * 10 * ureg.meter + + +You can change the behaviour at any time: + + >>> ureg.autoconvert_offset_to_baseunit = False + >>> 1/T + Traceback (most recent call last): + ... + OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). + +The parser knows about *delta* units and uses them when a temperature unit +is found in a multiplicative context. For example, here: + +.. doctest:: + + >>> print(ureg.parse_units('degC/meter')) + delta_degC / meter + +but not here: + +.. doctest:: + + >>> print(ureg.parse_units('degC')) + degC + +You can override this behaviour: + +.. doctest:: + + >>> print(ureg.parse_units('degC/meter', as_delta=False)) + degC / meter + +Note that the magnitude is left unchanged: + +.. doctest:: + + >>> Q_(10, 'degC/meter') + + +To define a new temperature, you need to specify the offset. For example, +this is the definition of the celsius and fahrenheit:: + + degC = degK; offset: 273.15 = celsius + degF = 5 / 9 * degK; offset: 255.372222 = fahrenheit + +You do not need to define *delta* units, as they are defined automatically. + +.. [#f1] If the exponent is +1, the quantity will not be converted to base + unit but remains unchanged. From 4e6dbff740292ae57230686ad4417e46d63e171c Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 17 Jun 2020 21:07:34 -0300 Subject: [PATCH 463/612] Preparing release 0.13 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index a3d4e598f..e4d965eaa 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.13 (unreleased) +0.13 (2020-06-17) ----------------- - Reinstated support for pickle protocol 0 and 1, which is required by pytables (Issue #1036, Thanks Guido Imperiale) diff --git a/version.py b/version.py index 4b893a2fd..dc64533ad 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.13.dev0' +__version__ = '0.13' # fmt: on From 23bb974664f624b8f7aa3b9f325a21cb6d049fdc Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 17 Jun 2020 21:08:23 -0300 Subject: [PATCH 464/612] Back to development: 0.14 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e4d965eaa..042e9c527 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.14 (unreleased) +----------------- + +- Nothing changed yet. + + 0.13 (2020-06-17) ----------------- - Reinstated support for pickle protocol 0 and 1, which is required by pytables diff --git a/version.py b/version.py index dc64533ad..d6c54db66 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.13' +__version__ = '0.14.dev0' # fmt: on From 4b7171b2f899e05ca32a85eaafcd256173de303c Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 17:43:08 -0700 Subject: [PATCH 465/612] finish first draft of log docs --- docs/log_units.rst | 132 +++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 84 deletions(-) diff --git a/docs/log_units.rst b/docs/log_units.rst index 1f1132779..e22acb058 100644 --- a/docs/log_units.rst +++ b/docs/log_units.rst @@ -17,113 +17,77 @@ the recommendations there apply here as well. explicit ``Quantity()`` constructor. Many examples on this page assume you've passed this parameter, and will not work otherwise. -.. testsetup:: +Defining log units +------------------ - from pint import UnitRegistry - ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) - ureg.default_format = '.3f' - Q_ = ureg.Quantity - -.. doctest:: - - >>> from pint import UnitRegistry - >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) - >>> Q_ = ureg.Quantity - >>> signal_power = -20.0 * ureg.dBm - >>> print(f"{sp.to('milliwatts'):0.3~P}") - 0.01 mW - -TODO: -[ ] explain delta units (what purpose do they have here?) -[ ] example computing mW to dBm -[ ] example computing dB and (something) +First, set up your ``UnitRegistry`` with the suggested flag. - -Multiplication, division and exponentiation of quantities with -offset units is problematic just like addition. Pint (since version 0.6) -will by default raise an error when a quantity with offset unit is used in -these operations. Due to this quantities with offset units cannot be created -like other quantities by multiplication of magnitude and unit but have -to be explicitly created: +If you do not wish to use the ``autoconvert_offset_to_baseunit`` flag, you +will need to define all logarithmic units using the ``Quanity()`` constructor: .. doctest:: + >>> from pint import UnitRegistry >>> ureg = UnitRegistry() - >>> home = 25.4 * ureg.degC + >>> Q_ = ureg.Quantity + >>> signal_power_dbm = 20 * ureg.dBm # must pass flag for this to work Traceback (most recent call last): ... - OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). - >>> Q_(25.4, ureg.degC) - - -As an alternative to raising an error, pint can be configured to work more -relaxed via setting the UnitRegistry parameter *autoconvert_offset_to_baseunit* -to true. In this mode, pint behaves differently: + OffsetUnitCalculusError: Ambiguous operation with offset unit (decibellmilliwatt, ). + >>> Q_(20, 'dBm') # define like this instead + -* Multiplication of a quantity with a single offset unit with order +1 by - a number or ndarray yields the quantity in the given unit. +You will also be restricted in the kinds of operations you can do without +converting to base units first. .. doctest:: - >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit = True) - >>> T = 25.4 * ureg.degC - >>> T - - -* Before all other multiplications, all divisions and in case of - exponentiation [#f1]_ involving quantities with offset-units, pint - will convert the quantities with offset units automatically to the - corresponding base unit before performing the operation. - - >>> 1/T - - >>> T * 10 * ureg.meter - - -You can change the behaviour at any time: - - >>> ureg.autoconvert_offset_to_baseunit = False - >>> 1/T + >>> Q_(10, 'dBm/Hz') * (100 * ureg.Hz) # not feasible without flag Traceback (most recent call last): ... - OffsetUnitCalculusError: Ambiguous operation with offset unit (degC). - -The parser knows about *delta* units and uses them when a temperature unit -is found in a multiplicative context. For example, here: - -.. doctest:: + UndefinedUnitError: 'delta_decibellmilliwatt' is not defined in the unit registry - >>> print(ureg.parse_units('degC/meter')) - delta_degC / meter - -but not here: +Passing the flag will allow you to use a more natural syntax for defining +logarithmic units: .. doctest:: - >>> print(ureg.parse_units('degC')) - degC + >>> from pint import UnitRegistry + >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) + >>> Q_ = ureg.Quantity + >>> 20.0 * ureg.dBm + -You can override this behaviour: +Converting to and from base units +--------------------------------- -.. doctest:: + >>> signal_power_dbm = 20.0 * ureg.dBm + >>> signal_power_dbm.to('mW') + + >>> signal_power_mw = 100.0 * ureg.mW + >>> signal_power_mw + + >>> signal_power_mw.to('dBm') + - >>> print(ureg.parse_units('degC/meter', as_delta=False)) - degC / meter +Compound log units +------------------ -Note that the magnitude is left unchanged: +Pint also works with mixtures of logarithmic and other units. .. doctest:: - >>> Q_(10, 'degC/meter') - - -To define a new temperature, you need to specify the offset. For example, -this is the definition of the celsius and fahrenheit:: - - degC = degK; offset: 273.15 = celsius - degF = 5 / 9 * degK; offset: 255.372222 = fahrenheit + >>> noise_density = -161.0 * ureg['dBm/Hz'] + >>> bandwidth = 10.0 * ureg.kHz + >>> noise_power = noise_density * bandwidth + >>> noise_power.to('dBm') + + >>> noise_power.to('mW') + -You do not need to define *delta* units, as they are defined automatically. - -.. [#f1] If the exponent is +1, the quantity will not be converted to base - unit but remains unchanged. +Multiplication, division and exponentiation of quantities with +offset units is problematic just like addition. Pint (since version 0.6) +will by default raise an error when a quantity with offset unit is used in +these operations. Due to this quantities with offset units cannot be created +like other quantities by multiplication of magnitude and unit but have +to be explicitly created: From 57f74dff4e55ee59941f550986b0bf0ceb3131cd Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:35:38 +0200 Subject: [PATCH 466/612] wrap numpy.prod to provide the prod method --- pint/quantity.py | 17 +++++++++++++++++ pint/testsuite/test_numpy.py | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 1f1816648..63063695a 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -112,6 +112,15 @@ def wrapped(self, *args, **kwargs): return wrapped +def method_wraps(numpy_func): + def wrapper(func): + func.__wrapped__ = numpy_func + + return func + + return wrapper + + @contextlib.contextmanager def printoptions(*args, **kwargs): """Numpy printoptions context manager released with version 1.15.0 @@ -1696,6 +1705,14 @@ def dot(self, b): return np.dot(self, b) + @method_wraps(np.prod) + def prod(self, *args, **kwargs): + """ Return the product of quantity elements over a given axis + + Wraps np.prod(). + """ + return np.prod(self, *args, **kwargs) + def __ito_if_needed(self, to_units): if self.unitless and to_units == "radian": return diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 9ecdcb1d0..a06a495bb 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -284,7 +284,12 @@ def test_fix(self): # Sums, products, differences def test_prod(self): - self.assertEqual(self.q.prod(), 24 * self.ureg.m ** 4) + axis = 0 + where = [[True, False], [True, True]] + + self.assertQuantityEqual(self.q.prod(), 24 * self.ureg.m ** 4) + self.assertQuantityEqual(self.q.prod(axis=axis), [3, 8] * self.ureg.m ** 2) + self.assertQuantityEqual(self.q.prod(where=where), 12 * self.ureg.m ** 3) @helpers.requires_array_function_protocol() def test_prod_numpy_func(self): From 14c2b0506669d480506fdba793e3f3b1b036abe1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:36:56 +0200 Subject: [PATCH 467/612] remove the obsolete prod entry in op_units_output_ufuncs --- pint/numpy_func.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 599446184..aa03e1853 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -416,7 +416,6 @@ def implementation(*args, **kwargs): copy_units_output_ufuncs = ["ldexp", "fmod", "mod", "remainder"] op_units_output_ufuncs = { "var": "square", - "prod": "size", "multiply": "mul", "true_divide": "div", "divide": "div", From df8004046c9d7fdb605094cc87dcf0c4029fcf79 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:42:36 +0200 Subject: [PATCH 468/612] compute the units for pure axis using the shape --- pint/numpy_func.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 599446184..040fc450d 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -683,8 +683,7 @@ def _prod(a, *args, **kwargs): result = np.prod(a._magnitude, *args, **kwargs) if axis is not None: - exponent = a.size // result.size - units = a.units ** exponent + units = a.units ** a.shape[axis] elif where is not None: exponent = np.asarray(where, dtype=np.bool_).sum() units = a.units ** exponent From c93dabda7783a4160b6e48d2bbb4d67165c6284d Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:43:05 +0200 Subject: [PATCH 469/612] compute the units before the result --- pint/numpy_func.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 040fc450d..65ff021ca 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -680,8 +680,6 @@ def _prod(a, *args, **kwargs): if axis is not None and where is not None: raise ValueError("passing axis and where is not supported") - result = np.prod(a._magnitude, *args, **kwargs) - if axis is not None: units = a.units ** a.shape[axis] elif where is not None: @@ -690,6 +688,8 @@ def _prod(a, *args, **kwargs): else: units = a.units ** a.size + result = np.prod(a._magnitude, *args, **kwargs) + return units._REGISTRY.Quantity(result, units) From 698b41d89d148fe0d5b1aa25bd9991bb91e7eb30 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:47:08 +0200 Subject: [PATCH 470/612] treat the axis and where case the same as all others --- pint/numpy_func.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 65ff021ca..2b60fef72 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -679,8 +679,7 @@ def _prod(a, *args, **kwargs): if axis is not None and where is not None: raise ValueError("passing axis and where is not supported") - - if axis is not None: + elif axis is not None: units = a.units ** a.shape[axis] elif where is not None: exponent = np.asarray(where, dtype=np.bool_).sum() From 13648c076f5e1e975691b627899389a8873397ab Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 13:47:30 +0200 Subject: [PATCH 471/612] use np.sum directly --- pint/numpy_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 2b60fef72..cddf8c404 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -682,7 +682,7 @@ def _prod(a, *args, **kwargs): elif axis is not None: units = a.units ** a.shape[axis] elif where is not None: - exponent = np.asarray(where, dtype=np.bool_).sum() + exponent = np.sum(where) units = a.units ** exponent else: units = a.units ** a.size From 7365efaab214a6a7614e93c753daa12bb1ec0959 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 14:18:09 +0200 Subject: [PATCH 472/612] implement the case of axis *and* where --- pint/numpy_func.py | 12 ++++++++++-- pint/testsuite/test_numpy.py | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index cddf8c404..c6713381f 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -677,8 +677,16 @@ def _prod(a, *args, **kwargs): axis = all_kwargs.get("axis", None) where = all_kwargs.get("where", None) + registry = a.units._REGISTRY + if axis is not None and where is not None: - raise ValueError("passing axis and where is not supported") + _, where_ = np.broadcast_arrays(a._magnitude, where) + exponents = np.unique(np.sum(where_, axis=axis)) + if len(exponents) == 1 or (len(exponents) == 2 and 0 in exponents): + units = a.units ** np.max(exponents) + else: + units = registry.dimensionless + a = a.to(units) elif axis is not None: units = a.units ** a.shape[axis] elif where is not None: @@ -689,7 +697,7 @@ def _prod(a, *args, **kwargs): result = np.prod(a._magnitude, *args, **kwargs) - return units._REGISTRY.Quantity(result, units) + return registry.Quantity(result, units) # Implement simple matching-unit or stripped-unit functions based on signature diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 9ecdcb1d0..ea570ef1d 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -295,7 +295,14 @@ def test_prod_numpy_func(self): self.assertQuantityEqual(np.prod(self.q, axis=axis), [3, 8] * self.ureg.m ** 2) self.assertQuantityEqual(np.prod(self.q, where=where), 12 * self.ureg.m ** 3) - self.assertRaises(ValueError, np.prod, self.q, axis=axis, where=where) + self.assertRaises(DimensionalityError, np.prod, self.q, axis=axis, where=where) + self.assertQuantityEqual( + np.prod(self.q, axis=axis, where=[[True, False], [False, True]]), + [1, 4] * self.ureg.m, + ) + self.assertQuantityEqual( + np.prod(self.q, axis=axis, where=[True, False]), [3, 1] * self.ureg.m ** 2 + ) def test_sum(self): self.assertEqual(self.q.sum(), 10 * self.ureg.m) From 37e40ae953cf76f2fb000743a1362bd89ec4060a Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 14:21:38 +0200 Subject: [PATCH 473/612] use a string to make sure quantity is importable without numpy --- pint/quantity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 63063695a..f1aa08194 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -113,6 +113,9 @@ def wrapped(self, *args, **kwargs): def method_wraps(numpy_func): + if isinstance(numpy_func, str): + numpy_func = getattr(np, numpy_func, None) + def wrapper(func): func.__wrapped__ = numpy_func @@ -1705,7 +1708,7 @@ def dot(self, b): return np.dot(self, b) - @method_wraps(np.prod) + @method_wraps("prod") def prod(self, *args, **kwargs): """ Return the product of quantity elements over a given axis From 7d377bf2ef1084f2190cd982c402fcc1ae0bfd28 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 15:45:07 +0200 Subject: [PATCH 474/612] raise a NotImplementedError for numpy < 1.17 --- pint/quantity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pint/quantity.py b/pint/quantity.py index f1aa08194..b6eee7961 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1714,6 +1714,11 @@ def prod(self, *args, **kwargs): Wraps np.prod(). """ + if version.parse(np.__version__) < version.parse("1.17"): + raise NotImplementedError( + f"prod is only correctly defined for numpy >= 1.17 ({np.__version__} is installed)." + " Please try to update your numpy version." + ) return np.prod(self, *args, **kwargs) def __ito_if_needed(self, to_units): From 18062533d0ec7904f00cd5df4cd180ef8a2cd634 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 16:11:04 +0200 Subject: [PATCH 475/612] require the array function protocol for the prod method tests --- pint/testsuite/test_numpy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index a06a495bb..652f38809 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -283,6 +283,7 @@ def test_fix(self): # Sums, products, differences + @helpers.requires_array_function_protocol() def test_prod(self): axis = 0 where = [[True, False], [True, True]] From 9369fee8678b31c7e65ef205abd158af2ea85cdd Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 16:18:51 +0200 Subject: [PATCH 476/612] remove the duplicated prod test from unclassified --- pint/testsuite/test_numpy.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 652f38809..4a6b7e20a 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -789,9 +789,6 @@ def test_std_numpy_func(self): self.assertQuantityAlmostEqual(np.std(self.q), 1.11803 * self.ureg.m, rtol=1e-5) self.assertRaises(OffsetUnitCalculusError, np.std, self.q_temperature) - def test_prod(self): - self.assertEqual(self.q.prod(), 24 * self.ureg.m ** 4) - def test_cumprod(self): self.assertRaises(DimensionalityError, self.q.cumprod) self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) From 7f790bbdf717858003f707df71f3dfe1c05faf8b Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 16:37:18 +0200 Subject: [PATCH 477/612] check HAS_NUMPY_ARRAY_FUNCTION instead of comparing numpy versions --- pint/quantity.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index b6eee7961..6168d248f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -23,6 +23,7 @@ from .compat import ( NUMPY_VER, + HAS_NUMPY_ARRAY_FUNCTION, _to_magnitude, babel_parse, eq, @@ -1714,10 +1715,14 @@ def prod(self, *args, **kwargs): Wraps np.prod(). """ - if version.parse(np.__version__) < version.parse("1.17"): + # TODO: remove after support for 1.16 has been dropped + if HAS_NUMPY_ARRAY_FUNCTION: raise NotImplementedError( - f"prod is only correctly defined for numpy >= 1.17 ({np.__version__} is installed)." - " Please try to update your numpy version." + "prod is only defined for" + " numpy == 1.16 with NUMPY_ARRAY_FUNCTION_PROTOCOL enabled" + f" or for numpy >= 1.17 ({np.__version__} is installed)." + " Please try setting the NUMPY_ARRAY_FUNCTION_PROTOCOL environment variable" + " or updating your numpy version." ) return np.prod(self, *args, **kwargs) From 798531328fad4130ffdaa737597d6d1856f82b7c Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 16:50:15 +0200 Subject: [PATCH 478/612] invert the condition --- pint/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 6168d248f..793507c4f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1716,7 +1716,7 @@ def prod(self, *args, **kwargs): Wraps np.prod(). """ # TODO: remove after support for 1.16 has been dropped - if HAS_NUMPY_ARRAY_FUNCTION: + if not HAS_NUMPY_ARRAY_FUNCTION: raise NotImplementedError( "prod is only defined for" " numpy == 1.16 with NUMPY_ARRAY_FUNCTION_PROTOCOL enabled" From 4ee450d9e5bbb00e4db02b10597c9b0607b016ae Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 18 Jun 2020 16:59:10 +0200 Subject: [PATCH 479/612] isort --- pint/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 793507c4f..f5453d2d3 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -22,8 +22,8 @@ from packaging import version from .compat import ( - NUMPY_VER, HAS_NUMPY_ARRAY_FUNCTION, + NUMPY_VER, _to_magnitude, babel_parse, eq, From ef0ec9f2e3bbe1c4337683fabda4741cd75fd705 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 20 Jun 2020 02:01:29 +0200 Subject: [PATCH 480/612] use UnitStrippedWarning to warn about stripped units --- pint/numpy_func.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 9b13b77a0..0f220b00e 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -11,7 +11,7 @@ from itertools import chain from .compat import is_upcast_type, np, zero_or_nan -from .errors import DimensionalityError +from .errors import DimensionalityError, UnitStrippedWarning from .util import iterable, sized HANDLED_UFUNCS = {} @@ -575,6 +575,7 @@ def _copyto(dst, src, casting="same_kind", where=True): else: warnings.warn( "The unit of the quantity is stripped when copying to non-quantity", + UnitStrippedWarning, stacklevel=2, ) np.copyto(dst, src.m, casting=casting, where=where) From 1343d0983a3dcd842ce5f4ec226bf48c4753195e Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 21 Jun 2020 13:36:34 +0200 Subject: [PATCH 481/612] make sure equals always returns a duck array if we're using numpy --- pint/quantity.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index f5453d2d3..1c17ed46f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1469,6 +1469,13 @@ def __neg__(self): @check_implemented def __eq__(self, other): + def bool_result(value): + if not is_duck_array_type(type(self._magnitude)): + return value + + shape = np.broadcast(self._magnitude, other).shape + return np.full(shape=shape, fill_value=False) + # We compare to the base class of Quantity because # each Quantity class is unique. if not isinstance(other, Quantity): @@ -1486,12 +1493,13 @@ def __eq__(self, other): else: raise OffsetUnitCalculusError(self._units) - return self.dimensionless and eq( - self._convert_magnitude(self.UnitsContainer()), other, False - ) + if self.dimensionless: + return eq(self._convert_magnitude(self.UnitsContainer()), other, False) + + return bool_result(False) if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): - return self.dimensionality == other.dimensionality + return bool_result(self.dimensionality == other.dimensionality) if self._units == other._units: return eq(self._magnitude, other._magnitude, False) @@ -1503,7 +1511,7 @@ def __eq__(self, other): False, ) except DimensionalityError: - return False + return bool_result(False) @check_implemented def __ne__(self, other): From 6d9f82a7dfe667ecba9a6d19865fcb08e7a80eb6 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 21 Jun 2020 13:51:12 +0200 Subject: [PATCH 482/612] add tests to verify that eq always returns a ndarray --- pint/testsuite/test_numpy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 61a8d8185..0e7248b57 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -878,11 +878,23 @@ def test_pickle(self): def test_equal(self): x = self.q.magnitude u = self.Q_(np.ones(x.shape)) + false = np.zeros_like(x, dtype=np.bool_) self.assertQuantityEqual(u, u) self.assertQuantityEqual(u == u, u.magnitude == u.magnitude) self.assertQuantityEqual(u == 1, u.magnitude == 1) + v = self.Q_(np.zeros(x.shape), "m") + w = self.Q_(np.ones(x.shape), "m") + self.assertNDArrayEqual(v == 1, false) + self.assertNDArrayEqual( + self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), + false, + ) + self.assertNDArrayEqual(v == w, false) + self.assertNDArrayEqual(v == w.to("mm"), false) + self.assertNDArrayEqual(u == v, false) + def test_shape(self): u = self.Q_(np.arange(12)) u.shape = 4, 3 From fe1629cbfc00fbfe8315108ef2caf1d6dac16b3f Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 21 Jun 2020 13:54:11 +0200 Subject: [PATCH 483/612] potentially make this work with duck arrays --- pint/compat.py | 2 +- pint/quantity.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index a671e1983..cbd897d1f 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -212,7 +212,7 @@ def eq(lhs, rhs, check_all: bool): bool or array_like of bool """ out = lhs == rhs - if check_all and isinstance(out, ndarray): + if check_all and is_duck_array_type(type(out)): return out.all() return out diff --git a/pint/quantity.py b/pint/quantity.py index 1c17ed46f..33a9f4753 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1470,11 +1470,16 @@ def __neg__(self): @check_implemented def __eq__(self, other): def bool_result(value): + nonlocal other + if not is_duck_array_type(type(self._magnitude)): return value - shape = np.broadcast(self._magnitude, other).shape - return np.full(shape=shape, fill_value=False) + if isinstance(other, Quantity): + other = other._magnitude + + template, _ = np.broadcast_arrays(self._magnitude, other) + return np.full_like(template, fill_value=False) # We compare to the base class of Quantity because # each Quantity class is unique. @@ -1498,6 +1503,7 @@ def bool_result(value): return bool_result(False) + # TODO: this might be expensive. Do we even need it? if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): return bool_result(self.dimensionality == other.dimensionality) From 2dea043bc923ce0cd01a4b7ea3e82531c1dea6ee Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 21 Jun 2020 14:16:09 +0200 Subject: [PATCH 484/612] explicitly specify the dtype as bool --- pint/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 33a9f4753..afff81513 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1479,7 +1479,7 @@ def bool_result(value): other = other._magnitude template, _ = np.broadcast_arrays(self._magnitude, other) - return np.full_like(template, fill_value=False) + return np.full_like(template, fill_value=False, dtype=np.bool_) # We compare to the base class of Quantity because # each Quantity class is unique. From ca64e6055a7a7cd9289b055eae8328c670abe51f Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 21 Jun 2020 14:19:12 +0200 Subject: [PATCH 485/612] black --- pint/testsuite/test_numpy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0e7248b57..07afd8dbd 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -888,8 +888,7 @@ def test_equal(self): w = self.Q_(np.ones(x.shape), "m") self.assertNDArrayEqual(v == 1, false) self.assertNDArrayEqual( - self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), - false, + self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), false, ) self.assertNDArrayEqual(v == w, false) self.assertNDArrayEqual(v == w.to("mm"), false) From 3bdc5a422c55dee97a23b22205b53da035606ef4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 22 Jun 2020 01:53:17 +0200 Subject: [PATCH 486/612] also test for a known true value which exposed a bug in the bool_result function --- pint/quantity.py | 2 +- pint/testsuite/test_numpy.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index afff81513..3f5e170f2 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1479,7 +1479,7 @@ def bool_result(value): other = other._magnitude template, _ = np.broadcast_arrays(self._magnitude, other) - return np.full_like(template, fill_value=False, dtype=np.bool_) + return np.full_like(template, fill_value=value, dtype=np.bool_) # We compare to the base class of Quantity because # each Quantity class is unique. diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 07afd8dbd..105ef6b92 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -878,6 +878,7 @@ def test_pickle(self): def test_equal(self): x = self.q.magnitude u = self.Q_(np.ones(x.shape)) + true = np.ones_like(x, dtype=np.bool_) false = np.zeros_like(x, dtype=np.bool_) self.assertQuantityEqual(u, u) @@ -890,6 +891,7 @@ def test_equal(self): self.assertNDArrayEqual( self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), false, ) + self.assertNDArrayEqual(v == v, true) self.assertNDArrayEqual(v == w, false) self.assertNDArrayEqual(v == w.to("mm"), false) self.assertNDArrayEqual(u == v, false) From bff1e9252c5fe4f43f896d2c77763b0ce79e54e2 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:08:33 -0300 Subject: [PATCH 487/612] Changed pintpandas to pint_pandas --- docs/pint-pandas.ipynb | 12 ++++++------ pint/__init__.py | 8 ++++---- pint/compat.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index ab61e60bd..0aa75d9f1 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -41,7 +41,7 @@ "source": [ "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", "\n", - "First some imports (you don't need to import `pintpandas` for this to work)" + "First some imports (you don't need to import `pint_pandas` for this to work)" ] }, { @@ -197,7 +197,7 @@ "source": [ "import pandas as pd \n", "import pint\n", - "import pintpandas\n", + "import pint_pandas\n", "import io" ] }, @@ -350,7 +350,7 @@ "metadata": {}, "outputs": [], "source": [ - "pintpandas.PintType.ureg.default_format = \"~P\"\n", + "pint_pandas.PintType.ureg.default_format = \"~P\"\n", "df_.pint.dequantify()" ] }, @@ -388,7 +388,7 @@ "source": [ "import pandas as pd \n", "import pint\n", - "import pintpandas" + "import pint_pandas" ] }, { @@ -404,7 +404,7 @@ "metadata": {}, "outputs": [], "source": [ - "PA_ = pintpandas.PintArray" + "PA_ = pint_pandas.PintArray" ] }, { @@ -437,7 +437,7 @@ "metadata": {}, "outputs": [], "source": [ - "pintpandas.PintType.ureg = ureg" + "pint_pandas.PintType.ureg = ureg" ] }, { diff --git a/pint/__init__.py b/pint/__init__.py index 3a0bf1f2e..83f542041 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -36,15 +36,15 @@ from importlib_metadata import version try: - from pintpandas import PintArray, PintType + from pint_pandas import PintArray, PintType del PintType del PintArray - _HAS_PINTPANDAS = True + _HAS_PINT_PANDAS = True except ImportError: - _HAS_PINTPANDAS = False - _, _pintpandas_error, _ = sys.exc_info() + _HAS_PINT_PANDAS = False + _, _pint_pandas_error, _ = sys.exc_info() try: # pragma: no cover __version__ = version("pint") diff --git a/pint/compat.py b/pint/compat.py index a671e1983..5151bae90 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -135,7 +135,7 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): # pint-pandas (PintArray) try: - from pintpandas import PintArray + from pint_pandas import PintArray upcast_types.append(PintArray) except ImportError: From fb6eaa5189b387a0da0f7d1a5fc6e9819bbf01ec Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:10:20 -0300 Subject: [PATCH 488/612] Update travis to pint-pandas --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b67c528dd..a24b68882 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ install: - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi - - if [[ $DOCS == 1 ]]; then pip install git+https://github.com/hgrecco/pint-pandas.git; fi + - if [[ $DOCS == 1 ]]; then pip install pint-pandas; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list From b608bd5c244c69c181b1f0129c2a90afc34c0d4c Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:30:38 -0300 Subject: [PATCH 489/612] Moved pint-pandas installation to requirements in doc testing --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a24b68882..fde3ddfc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0 pint-pandas" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -61,7 +61,6 @@ install: - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi - - if [[ $DOCS == 1 ]]; then pip install pint-pandas; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list From c639ab6af9cd7652e0435c81f4e8c38bb1ac9b72 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:39:49 -0300 Subject: [PATCH 490/612] Updated doctest modifiers in some docs --- docs/getting.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting.rst b/docs/getting.rst index a883175cb..0cdff4e11 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -14,7 +14,7 @@ That's all! You can check that Pint is correctly installed by starting up python .. code-block:: >>> import pint - >>> pint.__version__ + >>> pint.__version__ # doctest: +SKIP .. note:: If you have an old system installation of Python and you don't want to mess with it, you can try `Anaconda CE`_. It is a free Python distribution by @@ -27,7 +27,7 @@ You can check the installation with the following command: .. code-block:: - >>> pint.test() + >>> pint.test() # doctest: +SKIP On Arch Linux, you can alternatively install Pint from the Arch User Repository From 5152644d029d42531ed24e9de89099a45008e681 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:43:39 -0300 Subject: [PATCH 491/612] Moved pint-pandas to pip install --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fde3ddfc5..a24b68882 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0 pint-pandas" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" @@ -61,6 +61,7 @@ install: - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi + - if [[ $DOCS == 1 ]]; then pip install pint-pandas; fi # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list From 85fe6d6105c0eafe011ff07213951cec5f400b38 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:49:02 -0300 Subject: [PATCH 492/612] Fixed some doctests code block --- docs/getting.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting.rst b/docs/getting.rst index 0cdff4e11..09815fe8a 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -11,7 +11,7 @@ You can install it (or upgrade to the latest version) using pip_:: That's all! You can check that Pint is correctly installed by starting up python, and importing pint: -.. code-block:: +.. code-block:: python >>> import pint >>> pint.__version__ # doctest: +SKIP @@ -25,7 +25,7 @@ That's all! You can check that Pint is correctly installed by starting up python You can check the installation with the following command: -.. code-block:: +.. code-block:: python >>> pint.test() # doctest: +SKIP From 04439ea1ac09829f861907966fddc44b36dde412 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 19:57:39 -0300 Subject: [PATCH 493/612] Support for Pint-Pandas 0.1 --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 042e9c527..749461e5d 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Pint Changelog 0.14 (unreleased) ----------------- -- Nothing changed yet. +- Changes required to support Pint-Pandas 0.1. 0.13 (2020-06-17) From 222a1c89e9ce5c24debc749558cf5eb3b231f89a Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 20:33:33 -0300 Subject: [PATCH 494/612] Preparing release 0.14 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 749461e5d..3b4f3f9f5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.14 (unreleased) +0.14 (2020-07-01) ----------------- - Changes required to support Pint-Pandas 0.1. diff --git a/version.py b/version.py index d6c54db66..8f668ca0e 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.14.dev0' +__version__ = '0.14' # fmt: on From 4ab68ba0a00f96ed68fdf084233b61d68127c461 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 1 Jul 2020 20:34:26 -0300 Subject: [PATCH 495/612] Back to development: 0.15 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3b4f3f9f5..368ebfa90 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.15 (unreleased) +----------------- + +- Nothing changed yet. + + 0.14 (2020-07-01) ----------------- diff --git a/version.py b/version.py index 8f668ca0e..fe18e34ab 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.14' +__version__ = '0.15.dev0' # fmt: on From 6a275b975cf2ebf3e71451d90ff824970829a5c4 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Fri, 3 Jul 2020 11:36:22 -0500 Subject: [PATCH 496/612] Implement Dask collection interface for Dask Array Add `__dask_*__` methods to the Quantity class. Add convenience functions `compute()`, `persist()`, and `visualize()`. Wrap these convenience functions to raise an AttributeError if the wrapped object is not a Dask Array. --- pint/compat.py | 8 ++++ pint/quantity.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/pint/compat.py b/pint/compat.py index 5151bae90..713631728 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -157,6 +157,14 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): except ImportError: pass +try: + from dask.base import compute, persist, visualize + import dask.array as dask_array + +except ImportError: + compute, persist, visualize = None, None, None + dask_array = None + def is_upcast_type(other) -> bool: """Check if the type object is a upcast type using preset list. diff --git a/pint/quantity.py b/pint/quantity.py index f5453d2d3..fc996c864 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -26,11 +26,15 @@ NUMPY_VER, _to_magnitude, babel_parse, + compute, + dask_array, eq, is_duck_array_type, is_upcast_type, ndarray, np, + persist, + visualize, zero_or_nan, ) from .definitions import UnitDefinition @@ -125,6 +129,20 @@ def wrapper(func): return wrapper +def check_dask_array(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if isinstance(self._magnitude, dask_array.Array): + return f(self, *args, **kwargs) + else: + msg = "Method {} only implemented for objects of {}, not {}".format( + f.__name__, dask_array.Array, self._magnitude.__class__ + ) + raise AttributeError(msg) + + return wrapper + + @contextlib.contextmanager def printoptions(*args, **kwargs): """Numpy printoptions context manager released with version 1.15.0 @@ -1900,6 +1918,94 @@ def _ok_for_muldiv(self, no_offset_units=None): def to_timedelta(self): return datetime.timedelta(microseconds=self.to("microseconds").magnitude) + # Dask.array.Array ducking + def __dask_graph__(self): + if isinstance(self._magnitude, dask_array.Array): + return self._magnitude.__dask_graph__() + else: + return None + + def __dask_keys__(self): + return self._magnitude.__dask_keys__() + + @staticmethod + def __dask_optimize__(dsk, keys, **kwargs): + return dask_array.Array.__dask_optimize__(dsk, keys, **kwargs) + + @staticmethod + def __dask_scheduler__(dsk, keys, **kwargs): + return dask_array.Array.__dask_scheduler__(dsk, keys, **kwargs) + + def __dask_postcompute__(self): + func, args = self._magnitude.__dask_postcompute__() + return self._dask_finalize, (func, args, self.units) + + def __dask_postpersist__(self): + func, args = self._magnitude.__dask_postpersist__() + return self._dask_finalize, (func, args, self.units) + + @staticmethod + def _dask_finalize(results, func, args, units): + values = func(results, *args) + return Quantity(values, units) + + @check_dask_array + def compute(self, **kwargs): + """Compute a dask collection wrapped by pint.Quantity. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to the ``dask.base.compute`` function. + + Returns + ------- + pint.Quantity + Returns either the result of calling ``dask.base.compute``, in the case + that dask is enabled, or the object on which the ``compute`` method was + called without any modifications. + """ + (result,) = compute(self, **kwargs) + return result + + @check_dask_array + def persist(self, **kwargs): + """Compute a dask collection, and keep as a dask collection, wrapped by + pint.Quantity. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to the ``dask.base.persist`` function. + + Returns + ------- + pint.Quantity + Returns either the result of calling ``dask.base.persist``, in the case + that dask is enabled, or the object on which the ``persist`` method was + called without any modifications. + """ + (result,) = persist(self, **kwargs) + return result + + @check_dask_array + def visualize(self, **kwargs): + """Produce a visual representation of the graph contained in the wrapped + Dask collection. + + The graphviz and python-graphviz libraries are required. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to the ``dask.base.visualize`` function. + + Returns + ------- + + """ + visualize(self, **kwargs) + _Quantity = Quantity From 2617280a7e10de277f8ee236f1d476e2288e463b Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Fri, 3 Jul 2020 11:43:53 -0500 Subject: [PATCH 497/612] Add/update tests for Dask collection interface Update downcast tests to include Dask Array. Add testing module for Dask collection interface and convenience methods. --- pint/testsuite/test_compat_downcast.py | 9 ++- pint/testsuite/test_dask.py | 108 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 pint/testsuite/test_dask.py diff --git a/pint/testsuite/test_compat_downcast.py b/pint/testsuite/test_compat_downcast.py index e85369a14..a47f16876 100644 --- a/pint/testsuite/test_compat_downcast.py +++ b/pint/testsuite/test_compat_downcast.py @@ -5,6 +5,7 @@ # Conditionally import NumPy and any upcast type libraries np = pytest.importorskip("numpy", reason="NumPy is not available") sparse = pytest.importorskip("sparse", reason="sparse is not available") +da = pytest.importorskip("dask.array", reason="Dask is not available") # Set up unit registry and sample ureg = UnitRegistry(force_ndarray_like=True) @@ -16,7 +17,7 @@ def identity(x): return x -@pytest.fixture(params=["sparse", "masked_array"]) +@pytest.fixture(params=["sparse", "masked_array", "dask_array"]) def array(request): """Generate 5x5 arrays of given type for tests.""" if request.param == "sparse": @@ -30,6 +31,8 @@ def array(request): np.arange(25, dtype=np.float).reshape((5, 5)), mask=np.logical_not(np.triu(np.ones((5, 5)))), ) + elif request.param == "dask_array": + return da.arange(25, chunks=5, dtype=float).reshape((5, 5)) @pytest.mark.parametrize( @@ -57,8 +60,8 @@ def array(request): pytest.param(np.sum, np.sum, identity, id="sum ufunc"), pytest.param(np.sqrt, np.sqrt, lambda u: u ** 0.5, id="sqrt ufunc"), pytest.param( - lambda x: np.reshape(x, 25), - lambda x: np.reshape(x, 25), + lambda x: np.reshape(x, (25,)), + lambda x: np.reshape(x, (25,)), identity, id="reshape function", ), diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py new file mode 100644 index 000000000..8ca6afede --- /dev/null +++ b/pint/testsuite/test_dask.py @@ -0,0 +1,108 @@ +import pytest + +from pint import UnitRegistry + +# Conditionally import NumPy and Dask +np = pytest.importorskip("numpy", reason="NumPy is not available") +da = pytest.importorskip("dask.array", reason="Dask is not available") + +ureg = UnitRegistry(force_ndarray_like=True) +units_ = "kilogram" + + +@pytest.fixture +def dask_array(): + return da.arange(0, 25, chunks=5, dtype=float).reshape((5, 5)) + + +@pytest.fixture +def numpy_array(): + return np.arange(0, 25, dtype=float).reshape((5, 5)) + 5 + + +def test_has_dask_graph(dask_array): + """Test that a pint.Quantity wrapped Dask array has a Dask graph.""" + q = ureg.Quantity(dask_array, units_) + assert q.__dask_graph__() == dask_array.__dask_graph__() + + +def test_has_no_dask_graph(numpy_array): + """Test that a pint.Quantity wrapped NumPy array does not have a Dask graph, + and that attempting to access it returns None. + """ + q = ureg.Quantity(numpy_array, units_) + assert q.__dask_graph__() is None + + +def test_has_dask_keys(dask_array): + """Test that a pint.Quantity wrapped Dask array has Dask keys.""" + q = ureg.Quantity(dask_array, units_) + assert q.__dask_keys__() == dask_array.__dask_keys__() + + +def test_compute(dask_array, numpy_array): + """Test the compute() method on a pint.Quantity wrapped Dask array.""" + q = ureg.Quantity(dask_array, units_) + comps = q + 5 * ureg(units_) + res = comps.compute() + + assert np.all(res.m == numpy_array) + assert res.units == units_ + assert q.magnitude is dask_array + + +def test_persist(dask_array, numpy_array): + """Test the persist() method on a pint.Quantity wrapped Dask array. + + For single machines, persist() is expected to return the computed result(s). + """ + q = ureg.Quantity(dask_array, units_) + comps = q + 5 * ureg(units_) + res = comps.persist() + + assert np.all(res.m == numpy_array) + assert res.units == units_ + assert q.magnitude is dask_array + + +def test_visualize(): + pass + + +def test_compute_exception(numpy_array): + """Test exception handling for calling compute() on a pint.Quantity wrapped object + that is not a dask.array.core.Array object. + """ + q = ureg.Quantity(numpy_array, units_) + comps = q + 5 * ureg(units_) + with pytest.raises(AttributeError) as excinfo: + comps.compute() + + exctruth = "Method compute only implemented for objects of , not " + assert str(excinfo.value) == exctruth + + +def test_persist_exception(numpy_array): + """Test exception handling for calling persist() on a pint.Quantity wrapped object + that is not a dask.array.core.Array object. + """ + q = ureg.Quantity(numpy_array, units_) + comps = q + 5 * ureg(units_) + with pytest.raises(AttributeError) as excinfo: + comps.persist() + + exctruth = "Method persist only implemented for objects of , not " + assert str(excinfo.value) == exctruth + + +def test_visualize_exception(numpy_array): + """Test exception handling for calling visualize() on a pint.Quantity wrapped object + that is not a dask.array.core.Array object. + """ + q = ureg.Quantity(numpy_array, units_) + comps = q + 5 * ureg(units_) + with pytest.raises(AttributeError) as excinfo: + comps.visualize() + + exctruth = "Method visualize only implemented for objects of , not " + assert str(excinfo.value) == exctruth From a26e2d771bc1f1afe9b4abab96dfa84b0de76e7e Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Fri, 3 Jul 2020 11:52:13 -0500 Subject: [PATCH 498/612] Update docs with Pint Quantity wrapped Dask Array --- docs/numpy.ipynb | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index c3e8fc960..65768f63e 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -380,6 +380,31 @@ "print(repr(m * ureg.m))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Pint Quantity wrapping Dask Array**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import dask.array as da\n", + "\n", + "d = da.arange(500, chunks=50)\n", + "\n", + "# Must create using Quantity class, otherwise Dask will wrap Pint Quantity\n", + "q = ureg.Quantity(d, ureg.kelvin)\n", + "\n", + "print(repr(q))\n", + "print()\n", + "print(repr(d * ureg.kelvin))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -465,7 +490,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.8.3" } }, "nbformat": 4, From 230941df17b58c070c619ec3fc615c3ef7659dc2 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Fri, 3 Jul 2020 13:11:29 -0500 Subject: [PATCH 499/612] Update CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 368ebfa90..2b182aecf 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Pint Changelog 0.15 (unreleased) ----------------- -- Nothing changed yet. +- Implement Dask collection interface to support Pint Quantity wrapped Dask arrays. 0.14 (2020-07-01) From 11a351feefb5dd333a193c7e8c18f6d0a9a60360 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Fri, 3 Jul 2020 16:43:28 -0500 Subject: [PATCH 500/612] Change __dask_optimize__ and __dask_scheduler__ to class properties Change these methods to `@property`s and have them redirect to the appropriate Dask Array methods. Tests are included. Add a test utility function for incrementing test quantities. --- pint/quantity.py | 12 ++++++------ pint/testsuite/test_dask.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index fc996c864..22809a7e2 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1928,13 +1928,13 @@ def __dask_graph__(self): def __dask_keys__(self): return self._magnitude.__dask_keys__() - @staticmethod - def __dask_optimize__(dsk, keys, **kwargs): - return dask_array.Array.__dask_optimize__(dsk, keys, **kwargs) + @property + def __dask_optimize__(self): + return dask_array.Array.__dask_optimize__ - @staticmethod - def __dask_scheduler__(dsk, keys, **kwargs): - return dask_array.Array.__dask_scheduler__(dsk, keys, **kwargs) + @property + def __dask_scheduler__(self): + return dask_array.Array.__dask_scheduler__ def __dask_postcompute__(self): func, args = self._magnitude.__dask_postcompute__() diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index 8ca6afede..6d8140d79 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -4,15 +4,19 @@ # Conditionally import NumPy and Dask np = pytest.importorskip("numpy", reason="NumPy is not available") -da = pytest.importorskip("dask.array", reason="Dask is not available") +dask = pytest.importorskip("dask", reason="Dask is not available") ureg = UnitRegistry(force_ndarray_like=True) units_ = "kilogram" +def add_five(q): + return q + 5 * ureg(units_) + + @pytest.fixture def dask_array(): - return da.arange(0, 25, chunks=5, dtype=float).reshape((5, 5)) + return dask.array.arange(0, 25, chunks=5, dtype=float).reshape((5, 5)) @pytest.fixture @@ -40,10 +44,28 @@ def test_has_dask_keys(dask_array): assert q.__dask_keys__() == dask_array.__dask_keys__() +def test_dask_scheduler(dask_array): + """Test that a pint.Quantity wrapped Dask array has the correct default scheduler.""" + q = ureg.Quantity(dask_array, units_) + scheduler = q.__dask_scheduler__ + scheduler_name = f'{scheduler.__module__}.{scheduler.__name__}' + true_name = 'dask.threaded.get' + + assert scheduler == dask.array.Array.__dask_scheduler__ + assert scheduler_name == true_name + + +def test_dask_optimize(dask_array): + """Test that a pint.Quantity wrapped Dask array can be optimized.""" + q = ureg.Quantity(dask_array, units_) + + assert q.__dask_optimize__ == dask.array.Array.__dask_optimize__ + + def test_compute(dask_array, numpy_array): """Test the compute() method on a pint.Quantity wrapped Dask array.""" q = ureg.Quantity(dask_array, units_) - comps = q + 5 * ureg(units_) + comps = add_five(q) res = comps.compute() assert np.all(res.m == numpy_array) @@ -57,7 +79,7 @@ def test_persist(dask_array, numpy_array): For single machines, persist() is expected to return the computed result(s). """ q = ureg.Quantity(dask_array, units_) - comps = q + 5 * ureg(units_) + comps = add_five(q) res = comps.persist() assert np.all(res.m == numpy_array) @@ -74,7 +96,7 @@ def test_compute_exception(numpy_array): that is not a dask.array.core.Array object. """ q = ureg.Quantity(numpy_array, units_) - comps = q + 5 * ureg(units_) + comps = add_five(q) with pytest.raises(AttributeError) as excinfo: comps.compute() @@ -87,7 +109,7 @@ def test_persist_exception(numpy_array): that is not a dask.array.core.Array object. """ q = ureg.Quantity(numpy_array, units_) - comps = q + 5 * ureg(units_) + comps = add_five(q) with pytest.raises(AttributeError) as excinfo: comps.persist() @@ -100,7 +122,7 @@ def test_visualize_exception(numpy_array): that is not a dask.array.core.Array object. """ q = ureg.Quantity(numpy_array, units_) - comps = q + 5 * ureg(units_) + comps = add_five(q) with pytest.raises(AttributeError) as excinfo: comps.visualize() From 583e597f95d60b9f217ae000967fd7c3e8875603 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Mon, 6 Jul 2020 10:53:57 -0500 Subject: [PATCH 501/612] Reduce redundancy in Dask array tests --- pint/testsuite/test_dask.py | 97 +++++++++++++------------------------ 1 file changed, 35 insertions(+), 62 deletions(-) diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index 6d8140d79..7e8f737d4 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -24,32 +24,36 @@ def numpy_array(): return np.arange(0, 25, dtype=float).reshape((5, 5)) + 5 -def test_has_dask_graph(dask_array): - """Test that a pint.Quantity wrapped Dask array has a Dask graph.""" +@pytest.mark.parametrize("component", ["__dask_graph__", "__dask_keys__"]) +def test_has_dask_components(dask_array, component): + """Test that a pint.Quantity wrapped Dask array has a Dask graph and keys""" q = ureg.Quantity(dask_array, units_) - assert q.__dask_graph__() == dask_array.__dask_graph__() + + q_method = getattr(q, component) + component_q = q_method() + + truth_method = getattr(dask_array, component) + component_truth = truth_method() + + assert component_q == component_truth def test_has_no_dask_graph(numpy_array): - """Test that a pint.Quantity wrapped NumPy array does not have a Dask graph, + """Test that a pint.Quantity wrapped NumPy array does not have a Dask graph and that attempting to access it returns None. """ q = ureg.Quantity(numpy_array, units_) assert q.__dask_graph__() is None -def test_has_dask_keys(dask_array): - """Test that a pint.Quantity wrapped Dask array has Dask keys.""" - q = ureg.Quantity(dask_array, units_) - assert q.__dask_keys__() == dask_array.__dask_keys__() - - def test_dask_scheduler(dask_array): """Test that a pint.Quantity wrapped Dask array has the correct default scheduler.""" q = ureg.Quantity(dask_array, units_) + scheduler = q.__dask_scheduler__ - scheduler_name = f'{scheduler.__module__}.{scheduler.__name__}' - true_name = 'dask.threaded.get' + scheduler_name = f"{scheduler.__module__}.{scheduler.__name__}" + + true_name = "dask.threaded.get" assert scheduler == dask.array.Array.__dask_scheduler__ assert scheduler_name == true_name @@ -62,25 +66,16 @@ def test_dask_optimize(dask_array): assert q.__dask_optimize__ == dask.array.Array.__dask_optimize__ -def test_compute(dask_array, numpy_array): - """Test the compute() method on a pint.Quantity wrapped Dask array.""" - q = ureg.Quantity(dask_array, units_) - comps = add_five(q) - res = comps.compute() - - assert np.all(res.m == numpy_array) - assert res.units == units_ - assert q.magnitude is dask_array - - -def test_persist(dask_array, numpy_array): - """Test the persist() method on a pint.Quantity wrapped Dask array. - - For single machines, persist() is expected to return the computed result(s). +@pytest.mark.parametrize("method", ["compute", "persist"]) +def test_convenience_methods(dask_array, numpy_array, method): + """Test convenience methods compute() and persist() on a pint.Quantity + wrapped Dask array. """ q = ureg.Quantity(dask_array, units_) + comps = add_five(q) - res = comps.persist() + obj_method = getattr(comps, method) + res = obj_method() assert np.all(res.m == numpy_array) assert res.units == units_ @@ -91,40 +86,18 @@ def test_visualize(): pass -def test_compute_exception(numpy_array): - """Test exception handling for calling compute() on a pint.Quantity wrapped object - that is not a dask.array.core.Array object. - """ - q = ureg.Quantity(numpy_array, units_) - comps = add_five(q) - with pytest.raises(AttributeError) as excinfo: - comps.compute() - - exctruth = "Method compute only implemented for objects of , not " - assert str(excinfo.value) == exctruth - - -def test_persist_exception(numpy_array): - """Test exception handling for calling persist() on a pint.Quantity wrapped object - that is not a dask.array.core.Array object. +@pytest.mark.parametrize("method", ["compute", "persist", "visualize"]) +def test_exception_method_not_implemented(numpy_array, method): + """Test exception handling for convenience methods on a pint.Quantity wrapped + object that is not a dask.array.Array object. """ q = ureg.Quantity(numpy_array, units_) - comps = add_five(q) - with pytest.raises(AttributeError) as excinfo: - comps.persist() - - exctruth = "Method persist only implemented for objects of , not " - assert str(excinfo.value) == exctruth - - -def test_visualize_exception(numpy_array): - """Test exception handling for calling visualize() on a pint.Quantity wrapped object - that is not a dask.array.core.Array object. - """ - q = ureg.Quantity(numpy_array, units_) - comps = add_five(q) - with pytest.raises(AttributeError) as excinfo: - comps.visualize() - exctruth = "Method visualize only implemented for objects of , not " - assert str(excinfo.value) == exctruth + exctruth = ( + f"Method {method} only implemented for objects of" + " , not" + " " + ) + with pytest.raises(AttributeError, match=exctruth): + obj_method = getattr(q, method) + obj_method() From 2461c385c084b7398419a2cdfec7a94eb61199a7 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Mon, 6 Jul 2020 11:00:55 -0500 Subject: [PATCH 502/612] Add test for visualize(), test equivalency of compute() and persist() --- pint/testsuite/test_dask.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index 7e8f737d4..e48affea4 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -1,3 +1,5 @@ +import os + import pytest from pint import UnitRegistry @@ -82,8 +84,31 @@ def test_convenience_methods(dask_array, numpy_array, method): assert q.magnitude is dask_array -def test_visualize(): - pass +def test_compute_persist_equivalent_single_machine(dask_array, numpy_array): + """Test that compute() and persist() return the same result for calls made + on a single machine. + """ + q = ureg.Quantity(dask_array, units_) + + comps = add_five(q) + res_compute = comps.compute() + res_persist = comps.persist() + + assert np.all(res_compute == res_persist) + assert res_compute.units == res_persist.units == units_ + + +def test_visualize(dask_array): + """Test the visualize() method on a pint.Quantity wrapped Dask array.""" + q = ureg.Quantity(dask_array, units_) + + comps = add_five(q) + res = comps.visualize() + + assert res is None + # These commands only work on Unix and Windows + assert os.path.exists("mydask.png") + os.remove("mydask.png") @pytest.mark.parametrize("method", ["compute", "persist", "visualize"]) From 75e1c03dc594b6592b0918cf86270e879275d95e Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Tue, 7 Jul 2020 17:21:10 -0500 Subject: [PATCH 503/612] Add distributed tests. Separate tests for compute() and persist() --- pint/testsuite/test_dask.py | 128 +++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 32 deletions(-) diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index e48affea4..82d796626 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -4,9 +4,16 @@ from pint import UnitRegistry -# Conditionally import NumPy and Dask +# Conditionally import NumPy, Dask, and Distributed np = pytest.importorskip("numpy", reason="NumPy is not available") dask = pytest.importorskip("dask", reason="Dask is not available") +distributed = pytest.importorskip("distributed", reason="Distributed is not available") + +from dask.distributed import Client # isort:skip +from distributed.client import futures_of # isort:skip +from distributed.utils_test import cluster, gen_cluster, loop # isort:skip + +loop = loop # flake8 ureg = UnitRegistry(force_ndarray_like=True) units_ = "kilogram" @@ -26,26 +33,16 @@ def numpy_array(): return np.arange(0, 25, dtype=float).reshape((5, 5)) + 5 -@pytest.mark.parametrize("component", ["__dask_graph__", "__dask_keys__"]) -def test_has_dask_components(dask_array, component): - """Test that a pint.Quantity wrapped Dask array has a Dask graph and keys""" +def test_is_dask_collection(dask_array): + """Test that a pint.Quantity wrapped Dask array is a Dask collection.""" q = ureg.Quantity(dask_array, units_) + assert dask.is_dask_collection(q) - q_method = getattr(q, component) - component_q = q_method() - - truth_method = getattr(dask_array, component) - component_truth = truth_method() - assert component_q == component_truth - - -def test_has_no_dask_graph(numpy_array): - """Test that a pint.Quantity wrapped NumPy array does not have a Dask graph - and that attempting to access it returns None. - """ +def test_is_not_dask_collection(numpy_array): + """Test that other pint.Quantity wrapped objects are not Dask collections.""" q = ureg.Quantity(numpy_array, units_) - assert q.__dask_graph__() is None + assert not dask.is_dask_collection(q) def test_dask_scheduler(dask_array): @@ -68,34 +65,30 @@ def test_dask_optimize(dask_array): assert q.__dask_optimize__ == dask.array.Array.__dask_optimize__ -@pytest.mark.parametrize("method", ["compute", "persist"]) -def test_convenience_methods(dask_array, numpy_array, method): - """Test convenience methods compute() and persist() on a pint.Quantity - wrapped Dask array. - """ +def test_compute(dask_array, numpy_array): + """Test the compute() method on a pint.Quantity wrapped Dask array.""" q = ureg.Quantity(dask_array, units_) comps = add_five(q) - obj_method = getattr(comps, method) - res = obj_method() + res = comps.compute() assert np.all(res.m == numpy_array) + assert not dask.is_dask_collection(res) assert res.units == units_ assert q.magnitude is dask_array -def test_compute_persist_equivalent_single_machine(dask_array, numpy_array): - """Test that compute() and persist() return the same result for calls made - on a single machine. - """ +def test_persist(dask_array, numpy_array): + """Test the persist() method on a pint.Quantity wrapped Dask array.""" q = ureg.Quantity(dask_array, units_) comps = add_five(q) - res_compute = comps.compute() - res_persist = comps.persist() + res = comps.persist() - assert np.all(res_compute == res_persist) - assert res_compute.units == res_persist.units == units_ + assert np.all(res.m == numpy_array) + assert dask.is_dask_collection(res) + assert res.units == units_ + assert q.magnitude is dask_array def test_visualize(dask_array): @@ -111,6 +104,18 @@ def test_visualize(dask_array): os.remove("mydask.png") +def test_compute_persist_equivalent(dask_array, numpy_array): + """Test that compute() and persist() return the same result.""" + q = ureg.Quantity(dask_array, units_) + + comps = add_five(q) + res_compute = comps.compute() + res_persist = comps.persist() + + assert np.all(res_compute == res_persist) + assert res_compute.units == res_persist.units == units_ + + @pytest.mark.parametrize("method", ["compute", "persist", "visualize"]) def test_exception_method_not_implemented(numpy_array, method): """Test exception handling for convenience methods on a pint.Quantity wrapped @@ -126,3 +131,62 @@ def test_exception_method_not_implemented(numpy_array, method): with pytest.raises(AttributeError, match=exctruth): obj_method = getattr(q, method) obj_method() + + +def test_distributed_compute(loop, dask_array, numpy_array): + """Test compute() for distributed machines.""" + q = ureg.Quantity(dask_array, units_) + + with cluster() as (s, [a, b]): + with Client(s["address"], loop=loop): + comps = add_five(q) + res = comps.compute() + + assert np.all(res.m == numpy_array) + assert not dask.is_dask_collection(res) + assert res.units == units_ + + assert q.magnitude is dask_array + + +def test_distributed_persist(loop, dask_array): + """Test persist() for distributed machines.""" + q = ureg.Quantity(dask_array, units_) + + with cluster() as (s, [a, b]): + with Client(s["address"], loop=loop): + comps = add_five(q) + persisted_q = comps.persist() + + comps_truth = dask_array + 5 + persisted_truth = comps_truth.persist() + + assert np.all(persisted_q.m == persisted_truth) + assert dask.is_dask_collection(persisted_q) + assert persisted_q.units == units_ + + assert q.magnitude is dask_array + + +@gen_cluster(client=True, timeout=None) +async def test_async(c, s, a, b): + """Test asynchronous operations.""" + da = dask.array.arange(0, 25, chunks=5, dtype=float).reshape((5, 5)) + q = ureg.Quantity(da, units_) + + x = q + ureg.Quantity(5, units_) + y = x.persist() + assert str(y) + + assert dask.is_dask_collection(y) + assert len(x.__dask_graph__()) > len(y.__dask_graph__()) + + assert not futures_of(x) + assert futures_of(y) + + future = c.compute(y) + w = await future + assert not dask.is_dask_collection(w) + + truth = np.arange(0, 25, dtype=float).reshape((5, 5)) + 5 + assert np.all(truth == w.m) From e529f34fe09334ab9698ee9fb68ebfa14ba62c07 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Wed, 8 Jul 2020 13:43:11 -0500 Subject: [PATCH 504/612] Clean up docstrings; update docs; skip visualize tests with no graphviz --- docs/numpy.ipynb | 9 +++++++-- pint/compat.py | 5 +++-- pint/pint-convert | 1 + pint/quantity.py | 22 ++++++++-------------- pint/testsuite/test_dask.py | 6 +++++- pint/testsuite/test_unit.py | 1 + 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 65768f63e..7b8a0a329 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -402,7 +402,12 @@ "\n", "print(repr(q))\n", "print()\n", - "print(repr(d * ureg.kelvin))" + "\n", + "# DO NOT create using multiplication on the right until\n", + "# https://github.com/dask/dask/issues/4583 is resolved, as\n", + "# unexpected behavior may result\n", + "print(repr(d * ureg.kelvin))\n", + "print(repr(ureg.kelvin * d))" ] }, { @@ -490,7 +495,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.3" + "version": "3.8.2" } }, "nbformat": 4, diff --git a/pint/compat.py b/pint/compat.py index 713631728..172b65193 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -39,7 +39,8 @@ class BehaviorChangeWarning(UserWarning): try: import numpy as np - from numpy import ndarray, datetime64 as np_datetime64 + from numpy import datetime64 as np_datetime64 + from numpy import ndarray HAS_NUMPY = True NUMPY_VER = np.__version__ @@ -158,8 +159,8 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): pass try: + from dask import array as dask_array from dask.base import compute, persist, visualize - import dask.array as dask_array except ImportError: compute, persist, visualize = None, None, None diff --git a/pint/pint-convert b/pint/pint-convert index da7df23a8..4fdf2592f 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -36,6 +36,7 @@ ureg.default_system = args.system if args.unc: import uncertainties + # Measured constans subject to correlation # R_i: Rydberg constant # g_e: Electron g factor diff --git a/pint/quantity.py b/pint/quantity.py index 22809a7e2..561102c73 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1951,49 +1951,43 @@ def _dask_finalize(results, func, args, units): @check_dask_array def compute(self, **kwargs): - """Compute a dask collection wrapped by pint.Quantity. + """Compute the Dask array wrapped by pint.Quantity. Parameters ---------- **kwargs : dict - Any keyword arguments to pass to the ``dask.base.compute`` function. + Any keyword arguments to pass to ``dask.compute``. Returns ------- pint.Quantity - Returns either the result of calling ``dask.base.compute``, in the case - that dask is enabled, or the object on which the ``compute`` method was - called without any modifications. + A pint.Quantity wrapped numpy array. """ (result,) = compute(self, **kwargs) return result @check_dask_array def persist(self, **kwargs): - """Compute a dask collection, and keep as a dask collection, wrapped by - pint.Quantity. + """Persist the Dask Array wrapped by pint.Quantity. Parameters ---------- **kwargs : dict - Any keyword arguments to pass to the ``dask.base.persist`` function. + Any keyword arguments to pass to ``dask.persist``. Returns ------- pint.Quantity - Returns either the result of calling ``dask.base.persist``, in the case - that dask is enabled, or the object on which the ``persist`` method was - called without any modifications. + A pint.Quantity wrapped Dask array. """ (result,) = persist(self, **kwargs) return result @check_dask_array def visualize(self, **kwargs): - """Produce a visual representation of the graph contained in the wrapped - Dask collection. + """Produce a visual representation of the Dask graph. - The graphviz and python-graphviz libraries are required. + The graphviz library is required. Parameters ---------- diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index 82d796626..346575245 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -1,3 +1,4 @@ +import importlib import os import pytest @@ -91,6 +92,9 @@ def test_persist(dask_array, numpy_array): assert q.magnitude is dask_array +@pytest.mark.skipif( + importlib.util.find_spec("graphviz") is None, reason="GraphViz is not available" +) def test_visualize(dask_array): """Test the visualize() method on a pint.Quantity wrapped Dask array.""" q = ureg.Quantity(dask_array, units_) @@ -105,7 +109,7 @@ def test_visualize(dask_array): def test_compute_persist_equivalent(dask_array, numpy_array): - """Test that compute() and persist() return the same result.""" + """Test that compute() and persist() return the same numeric results.""" q = ureg.Quantity(dask_array, units_) comps = add_five(q) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 74b21a670..2cce7e0d1 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -219,6 +219,7 @@ def test_define(self): def test_load(self): import pkg_resources + from pint import unit data = pkg_resources.resource_filename(unit.__name__, "default_en.txt") From d2db231bdd0eb6e47e3a6e09c57d421aebe15d28 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Tue, 23 Jun 2020 13:30:53 -0700 Subject: [PATCH 505/612] resolve remaining doctests, enable doctest in CI closes #1115 --- .travis.yml | 1 + CHANGES | 2 +- docs/systems.rst | 24 +++--------------------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index a24b68882..7671c7996 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,6 +75,7 @@ script: - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi + - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -a -j auto -b doctest -d build/doctrees docs build/doctest; fi - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi after_success: diff --git a/CHANGES b/CHANGES index 2b182aecf..fee1ba480 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,7 @@ Pint Changelog ----------------- - Implement Dask collection interface to support Pint Quantity wrapped Dask arrays. - +- Started automatically testing examples in the documentation 0.14 (2020-07-01) ----------------- diff --git a/docs/systems.rst b/docs/systems.rst index f209ab5da..d4a175d93 100644 --- a/docs/systems.rst +++ b/docs/systems.rst @@ -36,29 +36,11 @@ or more drastically to: >>> '{:.3f}'.format(q.to_base_units()) '1.094 yard / second' -.. warning:: In versions previous to 0.7 ``to_base_units`` returns quantities in the +.. warning:: In versions previous to 0.7, ``to_base_units()`` returns quantities in the units of the definition files (which are called root units). For the definition file - bundled with pint this is meter/gram/second. To get back this behaviour use ``to_root_units``, + bundled with pint this is meter/gram/second. To get back this behaviour use ``to_root_units()``, set ``ureg.system = None`` - -You can also use system to narrow down the list of compatible units: - -.. doctest:: - - >>> ureg.default_system = 'mks' - >>> ureg.get_compatible_units('meter') - frozenset({, }) - -or for imperial units: - -.. doctest:: - - >>> ureg.default_system = 'imperial' - >>> ureg.get_compatible_units('meter') - frozenset({, , , , , , }) - - You can check which unit systems are available: .. doctest:: @@ -71,7 +53,7 @@ Or which units are available within a particular system: .. doctest:: >>> dir(ureg.sys.imperial) - ['UK_hundredweight', 'UK_ton', 'acre_foot', 'cubic_foot', 'cubic_inch', 'cubic_yard', 'drachm', 'foot', 'grain', 'imperial_barrel', 'imperial_bushel', 'imperial_cup', 'imperial_fluid_drachm', 'imperial_fluid_ounce', 'imperial_gallon', 'imperial_gill', 'imperial_peck', 'imperial_pint', 'imperial_quart', 'inch', 'long_hunderweight', 'long_ton', 'mile', 'ounce', 'pound', 'quarter', 'short_hunderdweight', 'short_ton', 'square_foot', 'square_inch', 'square_mile', 'square_yard', 'stone', 'yard'] + ['UK_force_ton', 'UK_hundredweight', ... 'cubic_foot', 'cubic_inch', ... 'thou', 'ton', 'yard'] Notice that this give you the opportunity to choose within units with colliding names: From c7edd2e3dbda51aae632b8cfd7d0e700cb85ee84 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Sat, 11 Jul 2020 20:51:17 -0700 Subject: [PATCH 506/612] update install instructions for pint-pandas in docs --- docs/pint-pandas.ipynb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index 0aa75d9f1..2052f1fed 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -22,9 +22,13 @@ "## Installation\n", "\n", "\n", - "Pandas support is provided by `pint-pandas`. It is not available on PyPI yet, to install it use\n", + "Pandas support is provided by the `pint-pandas` package. To install it use either:\n", "```\n", - "python -m pip install git+https://github.com/hgrecco/pint-pandas.git\n", + "python -m pip install pint-pandas\n", + "```\n", + "Or:\n", + "```\n", + "conda install -c conda-forge pint-pandas\n", "```" ] }, From 1da80f4bb98f197809e00d142364ebf218dddf8a Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Sat, 11 Jul 2020 21:45:08 -0700 Subject: [PATCH 507/612] skip doctests that rely on missing dependencies some configurations in Travis skip optional dependencies, which causes the doctests that rely on them to fail. This fixes that by checking for those dependencies and skipping the affected doctests with :skipif: --- docs/conf.py | 17 +++++++++++++++++ docs/measurement.rst | 12 +++++------- docs/serialization.rst | 1 + docs/tutorial.rst | 1 + 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 924b82596..6a720be8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -321,3 +321,20 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"http://docs.python.org/": None} + +# -- Doctest configuration ----------------------------------------------------- + +# fill a dictionary with package names that may be missing +# this is checked by :skipif: clauses in certain doctests that rely +# on optional dependencies +doctest_global_setup = """ +from importlib.util import find_spec + +not_installed = { + pkg_name: find_spec(pkg_name) is None + for pkg_name in [ + 'uncertainties', + 'serialize', + ] +} +""" diff --git a/docs/measurement.rst b/docs/measurement.rst index 78e80083d..88596c723 100644 --- a/docs/measurement.rst +++ b/docs/measurement.rst @@ -7,6 +7,7 @@ Using Measurements Measurements are the combination of two quantities: the mean value and the error (or uncertainty). The easiest ways to generate a measurement object is from a quantity using the `plus_minus` operator. .. doctest:: + :skipif: not_installed['uncertainties'] >>> import numpy as np >>> from pint import UnitRegistry @@ -15,16 +16,10 @@ Measurements are the combination of two quantities: the mean value and the error >>> print(book_length) (20.0 +/- 2.0) centimeter -.. testsetup:: * - - import numpy as np - from pint import UnitRegistry - ureg = UnitRegistry() - Q_ = ureg.Quantity - You can inspect the mean value, the absolute error and the relative error: .. doctest:: + :skipif: not_installed['uncertainties'] >>> print(book_length.value) 20.0 centimeter @@ -36,6 +31,7 @@ You can inspect the mean value, the absolute error and the relative error: You can also create a Measurement object giving the relative error: .. doctest:: + :skipif: not_installed['uncertainties'] >>> book_length = (20. * ureg.centimeter).plus_minus(.1, relative=True) >>> print(book_length) @@ -44,6 +40,7 @@ You can also create a Measurement object giving the relative error: Measurements support the same formatting codes as Quantity. For example, to pretty print a measurement with 2 decimal positions: .. doctest:: + :skipif: not_installed['uncertainties'] >>> print('{:.02fP}'.format(book_length)) (20.00 ± 2.00) centimeter @@ -52,6 +49,7 @@ Measurements support the same formatting codes as Quantity. For example, to pret Mathematical operations with Measurements, return new measurements following the `Propagation of uncertainty`_ rules. .. doctest:: + :skipif: not_installed['uncertainties'] >>> print(2 * book_length) (40 +/- 4) centimeter diff --git a/docs/serialization.rst b/docs/serialization.rst index aa4872836..01fed50d8 100644 --- a/docs/serialization.rst +++ b/docs/serialization.rst @@ -98,6 +98,7 @@ numerical type such as `numpy.ndarray`). Using the serialize_ package you can load and read from multiple formats: .. doctest:: + :skipif: not_installed['serialize'] >>> from serialize import dump, load, register_class >>> register_class(ureg.Quantity, ureg.Quantity.to_tuple, ureg.Quantity.from_tuple) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b49c56414..d2d18e9de 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -393,6 +393,7 @@ syntax'_ for custom float representations. For example, scientific notation: Pint also supports the LaTeX siunitx package: .. doctest:: + :skipif: not_installed['uncertainties'] >>> accel = 1.3 * ureg['meter/second**2'] >>> # siunitx Latex print From c3ca9d49b3a8019dec1a804db46c924f1b6de0bf Mon Sep 17 00:00:00 2001 From: Ben Loer Date: Mon, 13 Jul 2020 16:46:15 -0700 Subject: [PATCH 508/612] fix reduce dimensions when previous dimension was zeroed out. Fixes #1058 --- pint/quantity.py | 7 +++++-- pint/testsuite/test_issues.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index ad066f332..ea8c12f70 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -677,8 +677,8 @@ def to_base_units(self): def ito_reduced_units(self): """Return Quantity scaled in place to reduced units, i.e. one unit per - dimension. This will not reduce compound units (intentionally), nor - can it make use of contexts at this time. + dimension. This will not reduce compound units (e.g., 'J/kg' will not + be reduced to m**2/s**2), nor can it make use of contexts at this time. """ # shortcuts in case we're dimensionless or only a single unit @@ -691,6 +691,9 @@ def ito_reduced_units(self): # loop through individual units and compare to each other unit # can we do better than a nested loop here? for unit1, exp in self._units.items(): + # make sure it wasn't already reduced to zero exponent on prior pass + if unit1 not in newunits: + continue for unit2 in newunits: if unit1 != unit2: power = self._REGISTRY._get_dimensionality_ratio(unit1, unit2) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index f71ce1aa0..a592d3a55 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -693,6 +693,13 @@ def test_issue973(self): assert isinstance(q1, ureg.Quantity) assert len(q0) == len(q1) == 0 + def test_issue1058(self): + """ verify that auto-reducing quantities with three or more units + of same base type succeeds """ + q = 1 * ureg.mg / ureg.g / ureg.kg + q.ito_reduced_units() + self.assertIsInstance(q, ureg.Quantity) + def test_issue1062_issue1097(self): # Must not be used by any other tests assert "nanometer" not in ureg._units From c64c49fab46cdae73766cbdbada231c33ff711cb Mon Sep 17 00:00:00 2001 From: Ben Loer Date: Mon, 13 Jul 2020 16:55:41 -0700 Subject: [PATCH 509/612] update CHANGES, remove trailing space --- CHANGES | 2 ++ pint/quantity.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index fee1ba480..02f51e226 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,8 @@ Pint Changelog - Implement Dask collection interface to support Pint Quantity wrapped Dask arrays. - Started automatically testing examples in the documentation +- Fixed an exception generated when reducing dimensions with three or more + units of the same type 0.14 (2020-07-01) ----------------- diff --git a/pint/quantity.py b/pint/quantity.py index ea8c12f70..04419b626 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -677,7 +677,7 @@ def to_base_units(self): def ito_reduced_units(self): """Return Quantity scaled in place to reduced units, i.e. one unit per - dimension. This will not reduce compound units (e.g., 'J/kg' will not + dimension. This will not reduce compound units (e.g., 'J/kg' will not be reduced to m**2/s**2), nor can it make use of contexts at this time. """ From 6670f1122539970fce0c37b7db21f04e703a7a02 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 20:57:32 -0700 Subject: [PATCH 510/612] various tutorial improvements: broken links, grammar giving the tutorial page on the docs a once-over for broken Sphinx links also: - rewrote some sections for grammar - changed (added) some section titles - wrapped some long lines --- docs/conf.py | 1 + docs/tutorial.rst | 140 +++++++++++++++++++++++++++------------------- 2 files changed, 85 insertions(+), 56 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6a720be8b..67e156c11 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,7 @@ # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosectionlabel", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.coverage", diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d2d18e9de..a174a123b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -1,62 +1,82 @@ -.. _tutorial: - Tutorial ======== -Converting Quantities +Follow the steps below to get up and running quickly with Pint. + +Initialize a Registry --------------------- -Pint has the concept of Unit Registry, an object within which units are defined -and handled. You start by creating your registry: +Before using Pint, initialize a :class:`UnitRegistry() ` +object. The ``UnitRegistry`` stores the unit definitions, their relationships, +and handles conversions between units. .. doctest:: >>> from pint import UnitRegistry >>> ureg = UnitRegistry() -If no parameter is given to the constructor, the unit registry is populated -with the default list of units and prefixes. -You can now simply use the registry in the following way: +If no parameters are given to the constructor, the ``UnitRegistry`` is populated +with the `default list of units`_ and prefixes. + +Define a Quantity +----------------- + +Once you've initialized your registry, you can define quantities easily: .. doctest:: >>> distance = 24.0 * ureg.meter + >>> distance + >>> print(distance) 24.0 meter - >>> time = 8.0 * ureg.second - >>> print(time) - 8.0 second - >>> print(repr(time)) - -In this code `distance` and `time` are physical quantity objects (`Quantity`). -Physical quantities can be queried for their magnitude, units, and -dimensionality: +As you can see, ``distance`` here is a :class:`Quantity() ` +object that represents a physical quantity. Quantities can be queried for their +magnitude, units, and dimensionality: .. doctest:: - >>> print(distance.magnitude) + >>> distance.magnitude 24.0 - >>> print(distance.units) - meter + >>> distance.units + >>> print(distance.dimensionality) [length] -and can handle mathematical operations between: +and can handle many mathematical operations, including with other +:class:`Quantity() ` objects: .. doctest:: + >>> time = 8.0 * ureg.second + >>> print(time) + 8.0 second >>> speed = distance / time + >>> speed + >>> print(speed) 3.0 meter / second + >>> print(speed.dimensionality) + [length] / [time] + +See `String parsing`_ for more ways of defining a ``Quantity()`` object. + +Converting to Different Units +----------------------------- -As unit registry knows about the relationship between different units, you can -convert quantities to the unit of choice: +As the underlying ``UnitRegistry`` knows about the relationship between +different units, you can convert quantities to the units of your choice using +the ``to()`` method, which accepts a string or a :class:`Unit() ` object: .. doctest:: - >>> speed.to(ureg.inch / ureg.minute ) + >>> speed.to('inch/minute') + + >>> ureg.inch / ureg.minute + + >>> speed.to(ureg.inch / ureg.minute) This method returns a new object leaving the original intact as can be seen by: @@ -67,11 +87,11 @@ This method returns a new object leaving the original intact as can be seen by: 3.0 meter / second If you want to convert in-place (i.e. without creating another object), you can -use the `ito` method: +use the ``ito()`` method: .. doctest:: - >>> speed.ito(ureg.inch / ureg.minute ) + >>> speed.ito(ureg.inch / ureg.minute) >>> speed >>> print(speed) @@ -87,9 +107,8 @@ If you ask Pint to perform an invalid conversion: DimensionalityError: Cannot convert from 'inch / minute' ([length] / [time]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) Sometimes, the magnitude of the quantity will be very large or very small. -The method 'to_compact' can adjust the units to make the quantity more -human-readable. - +The method ``to_compact()`` can adjust the units to make a quantity more +human-readable: .. doctest:: @@ -100,7 +119,7 @@ human-readable. >>> print(frequency.to_compact()) 193.414489032... terahertz -There are also methods 'to_base_units' and 'ito_base_units' which automatically +There are also methods ``to_base_units()`` and ``ito_base_units()`` which automatically convert to the reference units with the correct dimensionality: .. doctest:: @@ -116,7 +135,7 @@ convert to the reference units with the correct dimensionality: >>> print(height) 1.752... meter -There are also methods 'to_reduced_units' and 'ito_reduced_units' which perform +There are also methods ``to_reduced_units()`` and ``ito_reduced_units()`` which perform a simplified dimensional reduction, combining units with the same dimensionality but otherwise keeping your unit definitions intact. @@ -136,7 +155,7 @@ but otherwise keeping your unit definitions intact. 14.0 gram If you want pint to automatically perform dimensional reduction when producing -new quantities, the UnitRegistry accepts a parameter `auto_reduce_dimensions`. +new quantities, the ``UnitRegistry`` class accepts a parameter ``auto_reduce_dimensions``. Dimensional reduction can be slow, so auto-reducing is disabled by default. In some cases it is useful to define physical quantities objects using the @@ -148,7 +167,7 @@ class constructor: >>> Q_(1.78, ureg.meter) == 1.78 * ureg.meter True -(I tend to abbreviate Quantity as `Q_`) The built-in parser recognizes prefixed +(I tend to abbreviate Quantity as ``Q_``) The built-in parser recognizes prefixed and pluralized units even though they are not in the definition list: .. doctest:: @@ -159,7 +178,7 @@ and pluralized units even though they are not in the definition list: >>> print(distance.to(ureg.meter)) 42000.0 meter -If you try to use a unit which is not in the registry: +Pint will complain if you try to use a unit which is not in the registry: .. doctest:: @@ -168,8 +187,8 @@ If you try to use a unit which is not in the registry: ... UndefinedUnitError: 'snail_speed' is not defined in the unit registry -You can add your own units to the registry or build your own list. More info on -that :ref:`defining` +You can add your own units to the existing registry, or build your own list. +See the page on :ref:`defining` units for more information on that. String parsing @@ -182,21 +201,20 @@ Pint can also handle units provided as strings: >>> 2.54 * ureg.parse_expression('centimeter') -or using the registry as a callable for a short form for `parse_expression`: +or using the registry as a callable for a short form for ``parse_expression()``: .. doctest:: >>> 2.54 * ureg('centimeter') -or using the `Quantity` constructor: +or using the ``Quantity`` constructor: .. doctest:: >>> Q_(2.54, 'centimeter') - Numbers are also parsed, so you can use an expression: .. doctest:: @@ -248,14 +266,14 @@ or .. note:: Pint´s rule for parsing strings with a mixture of numbers and units is that **units are treated with the same precedence as numbers**. -For example, the unit of +For example, the units of .. doctest:: >>> Q_('3 l / 100 km') -may be unexpected first but is a consequence of applying this rule. Use +may be unexpected at first but, are a consequence of applying this rule. Use brackets to get the expected result: .. doctest:: @@ -268,7 +286,8 @@ brackets to get the expected result: exposed to when parsing information from untrusted sources. -Strings containing values can be parsed using the ``ureg.parse_pattern`` function. A ``format``-like string with the units defined in it is used as the pattern: +Strings containing values can be parsed using the ``ureg.parse_pattern()`` function. +A ``format``-like string with the units defined in it is used as the pattern: .. doctest:: @@ -277,7 +296,8 @@ Strings containing values can be parsed using the ``ureg.parse_pattern`` functio >>> ureg.parse_pattern(input_string, pattern) [, ] -To search for multiple matches, set the ``many`` parameter to ``True``. The following example also demonstrates how the parser is able to find matches in amongst filler characters: +To search for multiple matches, set the ``many`` parameter to ``True``. The following +example also demonstrates how the parser is able to find matches in amongst filler characters: .. doctest:: @@ -333,7 +353,7 @@ Pint supports float formatting for numpy arrays as well: >>> print('The array is {:+.2E~P}'.format(accel)) The array is [-1.10E+00 +1.00E-06 +1.25E+00 +1.30E+00] m/s² -Pint also supports 'f-strings'_ from python>=3.6 : +Pint also supports `f-strings`_ from python>=3.6 : .. doctest:: @@ -358,9 +378,9 @@ LaTeX representations: >>> # Pretty print >>> 'The pretty representation is {:P}'.format(accel) 'The pretty representation is 1.3 meter/second²' - >>> # Latex print - >>> 'The latex representation is {:L}'.format(accel) - 'The latex representation is 1.3\\ \\frac{\\mathrm{meter}}{\\mathrm{second}^{2}}' + >>> # LaTeX print + >>> 'The LaTeX representation is {:L}'.format(accel) + '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}\\]' @@ -375,22 +395,23 @@ If you want to use abbreviated unit names, prefix the specification with `~`: 'The pretty representation is 1.3 m/s²' -The same is true for latex (`L`) and HTML (`H`) specs. +The same is true for LaTeX (`L`) and HTML (`H`) specs. .. note:: The abbreviated unit is drawn from the unit registry where the 3rd item in the equivalence chain (ie 1 = 2 = **3**) will be returned when the prefix '~' is used. The 1st item in the chain is the canonical name of the unit. -The formatting specs (ie 'L', 'H', 'P') can be used with Python string 'formatting -syntax'_ for custom float representations. For example, scientific notation: +The formatting specs (ie 'L', 'H', 'P') can be used with Python string +`formatting syntax`_ for custom float representations. For example, scientific +notation: .. doctest:: >>> 'Scientific notation: {:.3e~L}'.format(accel) 'Scientific notation: 1.300\\times 10^{0}\\ \\frac{\\mathrm{m}}{\\mathrm{s}^{2}}' -Pint also supports the LaTeX siunitx package: +Pint also supports the LaTeX `siunitx` package: .. doctest:: :skipif: not_installed['uncertainties'] @@ -415,7 +436,10 @@ Additionally, you can specify a default format specification: 'The acceleration is 1.3 meter/second²' -Finally, if Babel_ is installed you can translate unit names to any language +Localizing +---------- + +If Babel_ is installed you can translate unit names to any language .. doctest:: @@ -453,7 +477,7 @@ Using Pint in your projects If you use Pint in multiple modules within your Python package, you normally want to avoid creating multiple instances of the unit registry. The best way to do this is by instantiating the registry in a single place. For -example, you can add the following code to your package `__init__.py` +example, you can add the following code to your package ``__init__.py`` .. code-block:: @@ -462,7 +486,7 @@ example, you can add the following code to your package `__init__.py` Q_ = ureg.Quantity -Then in `yourmodule.py` the code would be +Then in ``yourmodule.py`` the code would be .. code-block:: @@ -481,7 +505,10 @@ also define the registry as the application registry set_application_registry(ureg) -.. warning:: There are no global units in Pint. All units belong to a registry and you can have multiple registries instantiated at the same time. However, you are not supposed to operate between quantities that belong to different registries. Never do things like this: +.. warning:: There are no global units in Pint. All units belong to a registry and + you can have multiple registries instantiated at the same time. However, you + are not supposed to operate between quantities that belong to different registries. + Never do things like this: .. doctest:: @@ -495,8 +522,9 @@ also define the registry as the application registry False +.. _`default list of units`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt .. _eval: http://docs.python.org/3/library/functions.html#eval .. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html .. _`Babel`: http://babel.pocoo.org/ -.. _'formatting syntax': https://docs.python.org/3/library/string.html#format-specification-mini-language -.. _'f-strings': https://www.python.org/dev/peps/pep-0498/ +.. _`formatting syntax`: https://docs.python.org/3/library/string.html#format-specification-mini-language +.. _`f-strings`: https://www.python.org/dev/peps/pep-0498/ From 76007cf57d468c5bf664857412939d559ecefbde Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 21:07:11 -0700 Subject: [PATCH 511/612] install docs: remove references to (removed) AUR package --- docs/getting.rst | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/getting.rst b/docs/getting.rst index 09815fe8a..84239e26b 100644 --- a/docs/getting.rst +++ b/docs/getting.rst @@ -9,13 +9,19 @@ You can install it (or upgrade to the latest version) using pip_:: $ pip install -U pint -That's all! You can check that Pint is correctly installed by starting up python, and importing pint: +That's all! You can check that Pint is correctly installed by starting up python, and importing Pint: .. code-block:: python >>> import pint >>> pint.__version__ # doctest: +SKIP +Or running the test suite: + +.. code-block:: python + + >>> pint.test() + .. note:: If you have an old system installation of Python and you don't want to mess with it, you can try `Anaconda CE`_. It is a free Python distribution by Continuum Analytics that includes many scientific packages. To install pint @@ -23,18 +29,6 @@ That's all! You can check that Pint is correctly installed by starting up python $ conda install -c conda-forge pint -You can check the installation with the following command: - -.. code-block:: python - - >>> pint.test() # doctest: +SKIP - - -On Arch Linux, you can alternatively install Pint from the Arch User Repository -(AUR). The latest release is available as `python-pint`_, and packages tracking -the master branch of the GitHub repository are available as `python-pint-git`_ -and `python2-pint-git`_. - Getting the code ---------------- @@ -56,13 +50,9 @@ Once you have a copy of the source, you can embed it in your Python package, or $ python setup.py install - .. _easy_install: http://pypi.python.org/pypi/setuptools .. _Python: http://www.python.org/ .. _pip: http://www.pip-installer.org/ .. _`Anaconda CE`: https://store.continuum.io/cshop/anaconda -.. _`python-pint`: https://aur.archlinux.org/packages/python-pint/ -.. _`python-pint-git`: https://aur.archlinux.org/packages/python-pint-git/ -.. _`python2-pint-git`: https://aur.archlinux.org/packages/python2-pint-git/ .. _PyPI: https://pypi.python.org/pypi/Pint/ .. _GitHub: https://github.com/hgrecco/pint From 9339669e6107d654b75961be4c0a6e3e4c817a2f Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 21:07:56 -0700 Subject: [PATCH 512/612] module reference: remove the test suite from the autodocs --- docs/developers_reference.rst | 61 ++--------------------------------- 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst index b4a48891f..f2be04636 100644 --- a/docs/developers_reference.rst +++ b/docs/developers_reference.rst @@ -2,8 +2,8 @@ Developer reference =================== -Pint -==== +All Modules +=========== .. automodule:: pint :members: @@ -55,60 +55,3 @@ Pint .. automodule:: pint.util :members: - -.. automodule:: pint.testsuite.helpers - :members: - -.. automodule:: pint.testsuite.parameterized - :members: - -.. automodule:: pint.testsuite.test_babel - :members: - -.. automodule:: pint.testsuite.test_contexts - :members: - -.. automodule:: pint.testsuite.test_converters - :members: - -.. automodule:: pint.testsuite.test_definitions - :members: - -.. automodule:: pint.testsuite.test_errors - :members: - -.. automodule:: pint.testsuite.test_formatter - :members: - -.. automodule:: pint.testsuite.test_infer_base_unit - :members: - -.. automodule:: pint.testsuite.test_issues - :members: - -.. automodule:: pint.testsuite.test_measurement - :members: - -.. automodule:: pint.testsuite.test_numpy - :members: - -.. automodule:: pint.testsuite.test_pint_eval - :members: - -.. automodule:: pint.testsuite.test_pitheorem - :members: - -.. automodule:: pint.testsuite.test_quantity - :members: - -.. automodule:: pint.testsuite.test_systems - :members: - -.. automodule:: pint.testsuite.test_umath - :members: - -.. automodule:: pint.testsuite.test_unit - :members: - -.. automodule:: pint.testsuite.test_util - :members: From 24faa631ce59215cd1098cdf6d2fc03aadbe79e1 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 21:10:28 -0700 Subject: [PATCH 513/612] docs index: add self links to some sections --- docs/index.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5d22dda15..4d045e37f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,8 +17,7 @@ without changing the source code. It supports a lot of numpy mathematical operations **without monkey patching or wrapping numpy**. It has a complete test coverage. It runs in Python 3.6+ with no other -dependency. It is licensed under BSD. - +dependencies. It is licensed under a `BSD 3-clause style license`_. It is extremely easy and natural to use: @@ -39,6 +38,7 @@ and you can make good use of numpy if you want: >>> np.sum(_) +See the :ref:`Tutorial` for more help getting started. Quick Installation ------------------ @@ -57,6 +57,8 @@ or utilizing conda, with the conda-forge channel: and then simply enjoy it! +(See :ref:`Installation ` for more detail.) + Design principles ----------------- @@ -80,12 +82,12 @@ LaTeX and pretty formatting. Unit name translation is available if Babel_ is installed. **Free to choose the numerical type**: You can use any numerical type -(`fraction`, `float`, `decimal`, `numpy.ndarray`, etc). NumPy_ is not required -but supported. +(``fraction``, ``float``, ``decimal``, ``numpy.ndarray``, etc). NumPy_ is not +required, but is supported. **Awesome NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and ufuncs are supported including automatic conversion of units. For example -`numpy.arccos(q)` will require a dimensionless `q` and the units of the output +``numpy.arccos(q)`` will require a dimensionless ``q`` and the units of the output quantity will be radian. **Uncertainties integration**: transparently handles calculations with @@ -98,13 +100,15 @@ points, like positions on a map or absolute temperature scales. **Dependency free**: it depends only on Python and its standard library. It interacts with other packages like numpy and uncertainties if they are installed -**Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. Operations on DataFrames and between columns are units aware, providing even more convenience for users of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. +**Pandas integration**: Thanks to `Pandas Extension Types`_ it is now possible to use Pint with Pandas. +Operations on DataFrames and between columns are units aware, providing even more convenience for users +of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_. When you choose to use a NumPy_ ndarray, its methods and ufuncs are supported including automatic conversion of units. For example -`numpy.arccos(q)` will require a dimensionless `q` and the units of the output -quantity will be radian. +``numpy.arccos(q)`` will require a dimensionless ``q`` and the units +of the output quantity will be radian. User Guide @@ -160,3 +164,4 @@ One last thing .. _`Babel`: http://babel.pocoo.org/ .. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types .. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb +.. _`BSD 3-clause style license`: https://github.com/hgrecco/pint/blob/master/LICENSE From c2b51dc2ffbf6ced2fb2ecde930a155a54111c0d Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 21:31:54 -0700 Subject: [PATCH 514/612] measurements docs: add note about installing uncertainties --- docs/measurement.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/measurement.rst b/docs/measurement.rst index 88596c723..a49c8212b 100644 --- a/docs/measurement.rst +++ b/docs/measurement.rst @@ -4,7 +4,13 @@ Using Measurements ================== -Measurements are the combination of two quantities: the mean value and the error (or uncertainty). The easiest ways to generate a measurement object is from a quantity using the `plus_minus` operator. +If you have the `Uncertainties package`_ installed, you can use Pint to keep +track of measurements with specified uncertainty, and not just exact physical +quantities. + +Measurements are the combination of two quantities: the mean value and the error +(or uncertainty). The easiest ways to generate a measurement object is from a +quantity using the ``plus_minus()`` method. .. doctest:: :skipif: not_installed['uncertainties'] @@ -37,7 +43,8 @@ You can also create a Measurement object giving the relative error: >>> print(book_length) (20.0 +/- 2.0) centimeter -Measurements support the same formatting codes as Quantity. For example, to pretty print a measurement with 2 decimal positions: +Measurements support the same formatting codes as Quantity. For example, to pretty +print a measurement with 2 decimal positions: .. doctest:: :skipif: not_installed['uncertainties'] @@ -46,7 +53,8 @@ Measurements support the same formatting codes as Quantity. For example, to pret (20.00 ± 2.00) centimeter -Mathematical operations with Measurements, return new measurements following the `Propagation of uncertainty`_ rules. +Mathematical operations with Measurements, return new measurements following +the `Propagation of uncertainty`_ rules. .. doctest:: :skipif: not_installed['uncertainties'] @@ -57,7 +65,8 @@ Mathematical operations with Measurements, return new measurements following the >>> print('{:.02f}'.format(book_length + width)) (30.00 +/- 2.24) centimeter -.. note:: only linear combinations are currently supported. +.. note:: Only linear combinations are currently supported. .. _`Propagation of uncertainty`: http://en.wikipedia.org/wiki/Propagation_of_uncertainty +.. _`Uncertainties package`: https://uncertainties-python-package.readthedocs.io/en/latest/ From 3a1d475c5141e2e0fc3049a72e616da534824054 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Mon, 13 Jul 2020 23:34:06 -0700 Subject: [PATCH 515/612] move some content from tutorial to new page this commit adds a new page to the docs about the various ways of defining quantity objects in Pint, and moves some of the content from the tutorial in there not certain if it's a better arrangement, the theory is that a more concise tutorial with links to more detailed pages might get users up and running faster very open to suggestions though --- docs/contexts.rst | 1 - docs/defining-quantities.rst | 142 ++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/tutorial.rst | 152 ++++++++++++----------------------- 4 files changed, 194 insertions(+), 102 deletions(-) create mode 100644 docs/defining-quantities.rst diff --git a/docs/contexts.rst b/docs/contexts.rst index 9f75c9fdb..4077503fd 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -1,4 +1,3 @@ -.. _contexts: Contexts ======== diff --git a/docs/defining-quantities.rst b/docs/defining-quantities.rst new file mode 100644 index 000000000..b0126f36c --- /dev/null +++ b/docs/defining-quantities.rst @@ -0,0 +1,142 @@ +Defining Quantities +=================== + +A quantity in Pint is the product of a unit and a magnitude. + +Pint supports several different ways of defining physical quantities, including +a powerful string parsing system. These methods are largely interchangeable, +though you may **need** to use the constructor form under certain circumstances +(see :doc:`nonmult` for an example of where the constructor form is required). + +By multiplication +----------------- + +If you've read the :ref:`Tutorial`, you're already familiar with defining a +quantity by multiplying a ``Unit()`` and a scalar: + +.. doctest:: + + >>> from pint import UnitRegistry + >>> ureg = UnitRegistry() + >>> ureg.meter + + >>> 30.0 * ureg.meter + + +This works to build up complex units as well: + +.. doctest:: + + >>> 9.8 * ureg.meter / ureg.second**2 + + + +Using the constructor +--------------------- + +In some cases it is useful to define :class:`Quantity() ` +objects using it's class constructor. Using the constructor allows you to +specify the units and magnitude separately. + +We typically abbreviate that constructor as `Q_` to make it's usage less verbose: + +.. doctest:: + + >>> Q_ = ureg.Quantity + >>> Q_(1.78, ureg.meter) + + +As you can see below, the multiplication and constructor methods should produce +the same results: + +.. doctest:: + + >>> Q_(30.0, ureg.meter) == 30.0 * ureg.meter + True + >>> Q_(9.8, ureg.meter / ureg.second**2) + + + +Using string parsing +-------------------- + +Pint includes a powerful parser for detecting magnitudes and units (with or +without prefixes) in strings. Calling the ``UnitRegistry()`` directly +invokes the parsing function: + +.. doctest:: + + >>> 30.0 * ureg('meter') + + >>> ureg('30.0 meters') + + >>> ureg('3000cm').to('meters') + + +The parsing function is also available to the ``Quantity()`` constructor and +the various ``.to()`` methods: + +.. doctest:: + + >>> Q_('30.0 meters') + + >>> Q_(30.0, 'meter') + + >>> Q_('3000.0cm').to('meter') + + +Or as a standalone method on the ``UnitRegistry``: + +.. doctest:: + + >>> 2.54 * ureg.parse_expression('centimeter') + + +It is fairly good at detecting compound units: + +.. doctest:: + + >>> g = ureg('9.8 meters/second**2') + >>> g + + >>> g.to('furlongs/fortnight**2') + + +And behaves well when given dimensionless quantities, which are parsed into +their appropriate objects: + +.. doctest:: + + >>> ureg('2.54') + 2.54 + >>> type(ureg('2.54')) + + >>> Q_('2.54') + + >>> type(Q_('2.54')) + .Quantity'> + +.. note:: Pint's rule for parsing strings with a mixture of numbers and + units is that **units are treated with the same precedence as numbers**. + +For example, the units of + +.. doctest:: + + >>> Q_('3 l / 100 km') + + +may be unexpected at first but, are a consequence of applying this rule. Use +brackets to get the expected result: + +.. doctest:: + + >>> Q_('3 l / (100 km)') + + +.. note:: Since version 0.7, Pint **does not** use eval_ under the hood. + This change removes the `serious security problems`_ that the system is + exposed to when parsing information from untrusted sources. + +.. _eval: http://docs.python.org/3/library/functions.html#eval +.. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html diff --git a/docs/index.rst b/docs/index.rst index 4d045e37f..5bbbd51df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -119,6 +119,7 @@ User Guide getting tutorial + defining-quantities numpy nonmult wrapping diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a174a123b..e473ac4f5 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -2,10 +2,11 @@ Tutorial ======== -Follow the steps below to get up and running quickly with Pint. +Follow the steps below and learn how to use Pint to track physical quantities +and perform unit conversions in Python. -Initialize a Registry ---------------------- +Initializing a Registry +----------------------- Before using Pint, initialize a :class:`UnitRegistry() ` object. The ``UnitRegistry`` stores the unit definitions, their relationships, @@ -19,10 +20,10 @@ and handles conversions between units. If no parameters are given to the constructor, the ``UnitRegistry`` is populated with the `default list of units`_ and prefixes. -Define a Quantity ------------------ +Defining a Quantity +------------------- -Once you've initialized your registry, you can define quantities easily: +Once you've initialized your ``UnitRegistry``, you can define quantities easily: .. doctest:: @@ -45,7 +46,7 @@ magnitude, units, and dimensionality: >>> print(distance.dimensionality) [length] -and can handle many mathematical operations, including with other +and can correctly handle many mathematical operations, including with other :class:`Quantity() ` objects: .. doctest:: @@ -61,13 +62,40 @@ and can handle many mathematical operations, including with other >>> print(speed.dimensionality) [length] / [time] -See `String parsing`_ for more ways of defining a ``Quantity()`` object. +Notice the built-in parser recognizes prefixed and pluralized units even though +they are not in the definition list: + +.. doctest:: + + >>> distance = 42 * ureg.kilometers + >>> print(distance) + 42 kilometer + >>> print(distance.to(ureg.meter)) + 42000.0 meter + +Pint will complain if you try to use a unit which is not in the registry: + +.. doctest:: + + >>> speed = 23 * ureg.snail_speed + Traceback (most recent call last): + ... + UndefinedUnitError: 'snail_speed' is not defined in the unit registry + +You can add your own units to the existing registry, or build your own list. +See the page on :ref:`defining` for more information on that. + +See `String parsing`_ and :doc:`defining-quantities` for more ways of defining +a ``Quantity()`` object. + +``Quantity()`` objects also work well with NumPy arrays, which you can +read about in the section on :doc:`NumPy support `. Converting to Different Units ----------------------------- -As the underlying ``UnitRegistry`` knows about the relationship between -different units, you can convert quantities to the units of your choice using +As the underlying ``UnitRegistry`` knows the relationships between +different units, you can convert a ``Quantity`` to the units of your choice using the ``to()`` method, which accepts a string or a :class:`Unit() ` object: .. doctest:: @@ -97,7 +125,8 @@ use the ``ito()`` method: >>> print(speed) 7086.6141... inch / minute -If you ask Pint to perform an invalid conversion: +Pint will complain if you ask it to perform a conversion it doesn't know +how to do: .. doctest:: @@ -106,6 +135,12 @@ If you ask Pint to perform an invalid conversion: ... DimensionalityError: Cannot convert from 'inch / minute' ([length] / [time]) to 'joule' ([length] ** 2 * [mass] / [time] ** 2) +See the section on :doc:`contexts` for information about expanding Pint's +automatic conversion capabilities for your application. + +Simplifying units +----------------- + Sometimes, the magnitude of the quantity will be very large or very small. The method ``to_compact()`` can adjust the units to make a quantity more human-readable: @@ -158,50 +193,11 @@ If you want pint to automatically perform dimensional reduction when producing new quantities, the ``UnitRegistry`` class accepts a parameter ``auto_reduce_dimensions``. Dimensional reduction can be slow, so auto-reducing is disabled by default. -In some cases it is useful to define physical quantities objects using the -class constructor: - -.. doctest:: - - >>> Q_ = ureg.Quantity - >>> Q_(1.78, ureg.meter) == 1.78 * ureg.meter - True - -(I tend to abbreviate Quantity as ``Q_``) The built-in parser recognizes prefixed -and pluralized units even though they are not in the definition list: - -.. doctest:: - - >>> distance = 42 * ureg.kilometers - >>> print(distance) - 42 kilometer - >>> print(distance.to(ureg.meter)) - 42000.0 meter - -Pint will complain if you try to use a unit which is not in the registry: - -.. doctest:: - - >>> speed = 23 * ureg.snail_speed - Traceback (most recent call last): - ... - UndefinedUnitError: 'snail_speed' is not defined in the unit registry - -You can add your own units to the existing registry, or build your own list. -See the page on :ref:`defining` units for more information on that. - - String parsing -------------- -Pint can also handle units provided as strings: - -.. doctest:: - - >>> 2.54 * ureg.parse_expression('centimeter') - - -or using the registry as a callable for a short form for ``parse_expression()``: +Pint includes powerful string parsing for identifying magnitudes and units. In +many cases, units can be defined as strings: .. doctest:: @@ -212,6 +208,7 @@ or using the ``Quantity`` constructor: .. doctest:: + >>> Q_ = ureg.Quantity >>> Q_(2.54, 'centimeter') @@ -221,11 +218,6 @@ Numbers are also parsed, so you can use an expression: >>> ureg('2.54 * centimeter') - -or: - -.. doctest:: - >>> Q_('2.54 * centimeter') @@ -245,47 +237,6 @@ This enables you to build a simple unit converter in 3 lines: >>> Q_(src).to(dst) -Dimensionless quantities can also be parsed into an appropriate object: - -.. doctest:: - - >>> ureg('2.54') - 2.54 - >>> type(ureg('2.54')) - - -or - -.. doctest:: - - >>> Q_('2.54') - - >>> type(Q_('2.54')) - .Quantity'> - -.. note:: Pint´s rule for parsing strings with a mixture of numbers and - units is that **units are treated with the same precedence as numbers**. - -For example, the units of - -.. doctest:: - - >>> Q_('3 l / 100 km') - - -may be unexpected at first but, are a consequence of applying this rule. Use -brackets to get the expected result: - -.. doctest:: - - >>> Q_('3 l / (100 km)') - - -.. note:: Since version 0.7, Pint **does not** use eval_ under the hood. - This change removes the `serious security problems`_ that the system is - exposed to when parsing information from untrusted sources. - - Strings containing values can be parsed using the ``ureg.parse_pattern()`` function. A ``format``-like string with the units defined in it is used as the pattern: @@ -317,7 +268,8 @@ The full power of regex can also be employed when writing patterns: *Note that the curly brackets (``{}``) are converted to a float-matching pattern by the parser.* -This function is useful for tasks such as bulk extraction of units from thousands of uniform strings or even very large texts with units dotted around in no particular pattern. +This function is useful for tasks such as bulk extraction of units from thousands +of uniform strings or even very large texts with units dotted around in no particular pattern. .. _sec-string-formatting: @@ -523,8 +475,6 @@ also define the registry as the application registry .. _`default list of units`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt -.. _eval: http://docs.python.org/3/library/functions.html#eval -.. _`serious security problems`: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html .. _`Babel`: http://babel.pocoo.org/ .. _`formatting syntax`: https://docs.python.org/3/library/string.html#format-specification-mini-language .. _`f-strings`: https://www.python.org/dev/peps/pep-0498/ From 298a277c8f79a1043684306f41bbb3252f301c71 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 17:44:21 -0700 Subject: [PATCH 516/612] converters.py: fix doc indent warnings --- pint/converters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/converters.py b/pint/converters.py index 4f63ead9d..1f6c09407 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -126,8 +126,8 @@ def is_logarithmic(self): def from_reference(self, value, inplace=False): """Converts value from the reference unit to the logarithmic unit - dBm <------ mW - y dBm = 10 log10( x / 1mW ) + dBm <------ mW + y dBm = 10 log10( x / 1mW ) """ if inplace: value /= self.scale @@ -144,8 +144,8 @@ def from_reference(self, value, inplace=False): def to_reference(self, value, inplace=False): """Converts value to the reference unit from the logarithmic unit - dBm ------> mW - y dBm = 10 log10( x / 1mW ) + dBm ------> mW + y dBm = 10 log10( x / 1mW ) """ if inplace: value /= self.logfactor From fcc5ea41d039d311f22f4eacbaa0e698e5512e19 Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 17:47:07 -0700 Subject: [PATCH 517/612] move log unit tests to their own module --- pint/testsuite/test_log_units.py | 217 +++++++++++++++++++++++++++++++ pint/testsuite/test_quantity.py | 174 +------------------------ 2 files changed, 218 insertions(+), 173 deletions(-) create mode 100644 pint/testsuite/test_log_units.py diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py new file mode 100644 index 000000000..bafa694f4 --- /dev/null +++ b/pint/testsuite/test_log_units.py @@ -0,0 +1,217 @@ +import math +import unittest + +import pytest + +from pint import LogarithmicUnitCalculusError, OffsetUnitCalculusError, UnitRegistry +from pint.testsuite import QuantityTestCase +from pint.unit import UnitsContainer + + +@pytest.fixture(scope="module") +def auto_ureg(): + return UnitRegistry(autoconvert_offset_to_baseunit=True) + + +@pytest.fixture(scope="module") +def ureg(): + return UnitRegistry() + + +class TestLogarithmicQuantity(QuantityTestCase): + + FORCE_NDARRAY = False + + def test_log_quantity_creation(self): + + # Following Quantity Creation Pattern + for args in ( + (4.2, "dBm"), + (4.2, UnitsContainer(decibellmilliwatt=1)), + (4.2, self.ureg.dBm), + ): + x = self.Q_(*args) + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + x = self.Q_(self.Q_(4.2, "dBm")) + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + x = self.Q_(4.2, UnitsContainer(decibellmilliwatt=1)) + y = self.Q_(x) + self.assertEqual(x.magnitude, y.magnitude) + self.assertEqual(x.units, y.units) + self.assertIsNot(x, y) + + # Using multiplications for dB units requires autoconversion to baseunits + new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) + x = new_reg.Quantity("4.2 * dBm") + self.assertEqual(x.magnitude, 4.2) + self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + + with self.capture_log() as buffer: + self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm)) + self.assertEqual(len(buffer), 1) + + def test_log_convert(self): + + # ## Test dB + # 0 dB == 1 + self.assertQuantityAlmostEqual( + self.Q_(0.0, "dB").to("dimensionless"), self.Q_(1.0) + ) + # -10 dB == 0.1 + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dB").to("dimensionless"), self.Q_(0.1) + ) + # +10 dB == 10 + self.assertQuantityAlmostEqual( + self.Q_(+10.0, "dB").to("dimensionless"), self.Q_(10.0) + ) + # 30 dB == 1e3 + self.assertQuantityAlmostEqual( + self.Q_(30.0, "dB").to("dimensionless"), self.Q_(1e3) + ) + # 60 dB == 1e6 + self.assertQuantityAlmostEqual( + self.Q_(60.0, "dB").to("dimensionless"), self.Q_(1e6) + ) + # # 1 dB = 1/10 * bel + # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) + # # Uncomment Bell unit in default_en.txt + + # ## Test decade + # 1 decade == 10 + self.assertQuantityAlmostEqual( + self.Q_(1.0, "decade").to("dimensionless"), self.Q_(10.0) + ) + # 2 decade == 100 + self.assertQuantityAlmostEqual( + self.Q_(2.0, "decade").to("dimensionless"), self.Q_(100.0) + ) + + # ## Test octave + # 1 octave = 2 + self.assertQuantityAlmostEqual( + self.Q_(1.0, "octave").to("dimensionless"), self.Q_(2.0) + ) + + # ## Test dB to dB units octave - decade + # 1 decade = log2(10) octave + self.assertQuantityAlmostEqual( + self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave") + ) + + # ## Test dBm + # 0 dBm = 1 mW + self.assertQuantityAlmostEqual(self.Q_(0.0, "dBm").to("mW"), self.Q_(1.0, "mW")) + self.assertQuantityAlmostEqual( + self.Q_(0.0, "dBm"), self.Q_(1.0, "mW").to("dBm") + ) + # 10 dBm = 10 mW + self.assertQuantityAlmostEqual( + self.Q_(10.0, "dBm").to("mW"), self.Q_(10.0, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(10.0, "dBm"), self.Q_(10.0, "mW").to("dBm") + ) + # 20 dBm = 100 mW + self.assertQuantityAlmostEqual( + self.Q_(20.0, "dBm").to("mW"), self.Q_(100.0, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(20.0, "dBm"), self.Q_(100.0, "mW").to("dBm") + ) + # -10 dBm = 0.1 mW + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dBm").to("mW"), self.Q_(0.1, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(-10.0, "dBm"), self.Q_(0.1, "mW").to("dBm") + ) + # -20 dBm = 0.01 mW + self.assertQuantityAlmostEqual( + self.Q_(-20.0, "dBm").to("mW"), self.Q_(0.01, "mW") + ) + self.assertQuantityAlmostEqual( + self.Q_(-20.0, "dBm"), self.Q_(0.01, "mW").to("dBm") + ) + + # ## Test dB to dB units dBm - dBu + # 0 dBm = 1mW = 1e3 uW = 30 dBu + self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu")) + + def test_mix_regular_log_units(self): + # Test regular-logarithmic mixed definition, such as dB/km or dB/cm + + # Multiplications and divisions with a mix of Logarithmic Units and regular Units is normally not possible. + # The reason is that dB are considered by pint like offset units. + # Multiplications and divisions that involve offset units are badly defined, so pint raises an error + with self.assertRaises(OffsetUnitCalculusError): + (-10.0 * self.ureg.dB) / (1 * self.ureg.cm) + + # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base. + # With this flag on multiplications and divisions are now possible: + new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) + self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) + + +def test_compound_log_unit_multiply_definition(auto_ureg): + """Check that compound log units can be defined using multiply. + """ + Q_ = auto_ureg.Quantity + canonical_def = Q_(-161, "dBm") / auto_ureg.Hz + mult_def = -161 * auto_ureg["dBm/Hz"] + assert mult_def == canonical_def + + +def test_compound_log_unit_quantity_definition(auto_ureg): + """Check that compound log units can be defined using ``Quantity()``. + """ + Q_ = auto_ureg.Quantity + canonical_def = Q_(-161, "dBm") / auto_ureg.Hz + quantity_def = Q_(-161, "dBm/Hz") + assert quantity_def == canonical_def + + +def test_compound_log_unit_parse_definition(auto_ureg): + """Check that compound log units can be defined using ``parse_expression()``. + """ + Q_ = auto_ureg.Quantity + canonical_def = Q_(-161, "dBm") / auto_ureg.Hz + parse_def = auto_ureg.parse_expression("-161 dBm/Hz") + assert canonical_def == parse_def + + +class TestLogarithmicQuantityBasicMath(QuantityTestCase): + + FORCE_NDARRAY = False + + @unittest.expectedFailure + def _test_log_quantity_add_sub_raises_exception(self, unit, func): + # Warning should be provided when trying to .... + self.assertRaises(LogarithmicUnitCalculusError) + + @unittest.expectedFailure + def _test_log_quantity_add_sub(self, unit, func): + + # Pure dB arithmetic + # 5 dBm + 10 dB = 15 dBm + self.assertQuantityAlmostEqual( + 5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm + ) + # 100*dBm -10*dB = 90*dB + self.assertQuantityAlmostEqual( + 100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB + ) + # 100 dBW - 5 dBW = 95 dB + self.assertQuantityAlmostEqual( + 100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB + ) + # 20 dB + 0 dBW == 20 dBW + + # 100 Hz + 1 octave = 200 Hz + self.assertQuantityAlmostEqual( + 100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz + ) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 71fd46c80..49464faed 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -2,9 +2,9 @@ import datetime import math import operator as op -import pickle import warnings from unittest.mock import patch +import pickle from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry from pint.compat import np @@ -1729,175 +1729,3 @@ def test_offset_autoconvert_gt_zero(self): self.assertTrue(q1 > 0) self.assertTrue(q2 > 0) self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) - - -class TestLogarithmicQuantity(QuantityTestCase): - - FORCE_NDARRAY = False - - def test_log_quantity_creation(self): - - # Following Quantity Creation Pattern - for args in ( - (4.2, "dBm"), - (4.2, UnitsContainer(decibellmilliwatt=1)), - (4.2, self.ureg.dBm), - ): - x = self.Q_(*args) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) - - x = self.Q_(self.Q_(4.2, "dBm")) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) - - x = self.Q_(4.2, UnitsContainer(decibellmilliwatt=1)) - y = self.Q_(x) - self.assertEqual(x.magnitude, y.magnitude) - self.assertEqual(x.units, y.units) - self.assertIsNot(x, y) - - # Using multiplications for dB units requires autoconversion to baseunits - new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) - x = new_reg.Quantity("4.2 * dBm") - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) - - with self.capture_log() as buffer: - self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm)) - self.assertEqual(len(buffer), 1) - - def test_log_convert(self): - - # ## Test dB - # 0 dB == 1 - self.assertQuantityAlmostEqual( - self.Q_(0.0, "dB").to("dimensionless"), self.Q_(1.0) - ) - # -10 dB == 0.1 - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dB").to("dimensionless"), self.Q_(0.1) - ) - # +10 dB == 10 - self.assertQuantityAlmostEqual( - self.Q_(+10.0, "dB").to("dimensionless"), self.Q_(10.0) - ) - # 30 dB == 1e3 - self.assertQuantityAlmostEqual( - self.Q_(30.0, "dB").to("dimensionless"), self.Q_(1e3) - ) - # 60 dB == 1e6 - self.assertQuantityAlmostEqual( - self.Q_(60.0, "dB").to("dimensionless"), self.Q_(1e6) - ) - # # 1 dB = 1/10 * bel - # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) - # # Uncomment Bell unit in default_en.txt - - # ## Test decade - # 1 decade == 10 - self.assertQuantityAlmostEqual( - self.Q_(1.0, "decade").to("dimensionless"), self.Q_(10.0) - ) - # 2 decade == 100 - self.assertQuantityAlmostEqual( - self.Q_(2.0, "decade").to("dimensionless"), self.Q_(100.0) - ) - - # ## Test octave - # 1 octave = 2 - self.assertQuantityAlmostEqual( - self.Q_(1.0, "octave").to("dimensionless"), self.Q_(2.0) - ) - - # ## Test dB to dB units octave - decade - # 1 decade = log2(10) octave - self.assertQuantityAlmostEqual( - self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave") - ) - - # ## Test dBm - # 0 dBm = 1 mW - self.assertQuantityAlmostEqual(self.Q_(0.0, "dBm").to("mW"), self.Q_(1.0, "mW")) - self.assertQuantityAlmostEqual( - self.Q_(0.0, "dBm"), self.Q_(1.0, "mW").to("dBm") - ) - # 10 dBm = 10 mW - self.assertQuantityAlmostEqual( - self.Q_(10.0, "dBm").to("mW"), self.Q_(10.0, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(10.0, "dBm"), self.Q_(10.0, "mW").to("dBm") - ) - # 20 dBm = 100 mW - self.assertQuantityAlmostEqual( - self.Q_(20.0, "dBm").to("mW"), self.Q_(100.0, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(20.0, "dBm"), self.Q_(100.0, "mW").to("dBm") - ) - # -10 dBm = 0.1 mW - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dBm").to("mW"), self.Q_(0.1, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dBm"), self.Q_(0.1, "mW").to("dBm") - ) - # -20 dBm = 0.01 mW - self.assertQuantityAlmostEqual( - self.Q_(-20.0, "dBm").to("mW"), self.Q_(0.01, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(-20.0, "dBm"), self.Q_(0.01, "mW").to("dBm") - ) - - # ## Test dB to dB units dBm - dBu - # 0 dBm = 1mW = 1e3 uW = 30 dBu - self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu")) - - def test_mix_regular_log_units(self): - # Test regular-logarithmic mixed definition, such as dB/km or dB/cm - - # Multiplications and divisions with a mix of Logarithmic Units and regular Units is normally not possible. - # The reason is that dB are considereded by pint like offset unit. - # Multiplications and divisions that involve offset units are badly defined, so pint raises an error - with self.assertRaises(OffsetUnitCalculusError): - (-10.0 * self.ureg.dB) / (1 * self.ureg.cm) - # - # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base. - # With this flag on multiplications and divisions are now possible: - new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) - self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) - - -class TestLogarithmicQuantityBasicMath(QuantityTestCase): - - FORCE_NDARRAY = False - - @unittest.expectedFailure - def _test_log_quantity_add_sub_raises_exception(self, unit, func): - # Warning should be provided when trying to .... - self.assertRaises(LogarithmicUnitCalculusError) - - @unittest.expectedFailure - def _test_log_quantity_add_sub(self, unit, func): - - # Pure dB arithmetic - # 5 dBm + 10 dB = 15 dBm - self.assertQuantityAlmostEqual( - 5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm - ) - # 100*dBm -10*dB = 90*dB - self.assertQuantityAlmostEqual( - 100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB - ) - # 100 dBW - 5 dBW = 95 dB - self.assertQuantityAlmostEqual( - 100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB - ) - # 20 dB + 0 dBW == 20 dBW - - # 100 Hz + 1 octave = 200 Hz - self.assertQuantityAlmostEqual( - 100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz - ) From 58b8258a1820c4a760ab9270489a2c48c934bc9d Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 17 Jun 2020 18:14:02 -0700 Subject: [PATCH 518/612] test_log_units: parametrize to/from conversion tests add more test cases for octave, decade --- pint/testsuite/test_log_units.py | 154 ++++++++++++++++--------------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index bafa694f4..f347240e2 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -55,89 +55,15 @@ def test_log_quantity_creation(self): self.assertEqual(len(buffer), 1) def test_log_convert(self): - - # ## Test dB - # 0 dB == 1 - self.assertQuantityAlmostEqual( - self.Q_(0.0, "dB").to("dimensionless"), self.Q_(1.0) - ) - # -10 dB == 0.1 - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dB").to("dimensionless"), self.Q_(0.1) - ) - # +10 dB == 10 - self.assertQuantityAlmostEqual( - self.Q_(+10.0, "dB").to("dimensionless"), self.Q_(10.0) - ) - # 30 dB == 1e3 - self.assertQuantityAlmostEqual( - self.Q_(30.0, "dB").to("dimensionless"), self.Q_(1e3) - ) - # 60 dB == 1e6 - self.assertQuantityAlmostEqual( - self.Q_(60.0, "dB").to("dimensionless"), self.Q_(1e6) - ) # # 1 dB = 1/10 * bel # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) # # Uncomment Bell unit in default_en.txt - # ## Test decade - # 1 decade == 10 - self.assertQuantityAlmostEqual( - self.Q_(1.0, "decade").to("dimensionless"), self.Q_(10.0) - ) - # 2 decade == 100 - self.assertQuantityAlmostEqual( - self.Q_(2.0, "decade").to("dimensionless"), self.Q_(100.0) - ) - - # ## Test octave - # 1 octave = 2 - self.assertQuantityAlmostEqual( - self.Q_(1.0, "octave").to("dimensionless"), self.Q_(2.0) - ) - # ## Test dB to dB units octave - decade # 1 decade = log2(10) octave self.assertQuantityAlmostEqual( self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave") ) - - # ## Test dBm - # 0 dBm = 1 mW - self.assertQuantityAlmostEqual(self.Q_(0.0, "dBm").to("mW"), self.Q_(1.0, "mW")) - self.assertQuantityAlmostEqual( - self.Q_(0.0, "dBm"), self.Q_(1.0, "mW").to("dBm") - ) - # 10 dBm = 10 mW - self.assertQuantityAlmostEqual( - self.Q_(10.0, "dBm").to("mW"), self.Q_(10.0, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(10.0, "dBm"), self.Q_(10.0, "mW").to("dBm") - ) - # 20 dBm = 100 mW - self.assertQuantityAlmostEqual( - self.Q_(20.0, "dBm").to("mW"), self.Q_(100.0, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(20.0, "dBm"), self.Q_(100.0, "mW").to("dBm") - ) - # -10 dBm = 0.1 mW - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dBm").to("mW"), self.Q_(0.1, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(-10.0, "dBm"), self.Q_(0.1, "mW").to("dBm") - ) - # -20 dBm = 0.01 mW - self.assertQuantityAlmostEqual( - self.Q_(-20.0, "dBm").to("mW"), self.Q_(0.01, "mW") - ) - self.assertQuantityAlmostEqual( - self.Q_(-20.0, "dBm"), self.Q_(0.01, "mW").to("dBm") - ) - # ## Test dB to dB units dBm - dBu # 0 dBm = 1mW = 1e3 uW = 30 dBu self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu")) @@ -157,6 +83,78 @@ def test_mix_regular_log_units(self): self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) +@pytest.mark.parametrize( + "db_value,scalar", + [ + (0.0, 1.0), # 0 dB == 1x + (-10.0, 0.1), # -10 dB == 0.1x + (10.0, 10.0), + (30.0, 1e3), + (60.0, 1e6), + ], +) +def test_db_conversion(ureg, db_value, scalar): + """Test that a dB value can be converted to a scalar and back. + """ + Q_ = ureg.Quantity + assert Q_(db_value, "dB").to("dimensionless").magnitude == pytest.approx(scalar) + assert Q_(scalar, "dimensionless").to("dB").magnitude == pytest.approx(db_value) + + +@pytest.mark.parametrize( + "octave,scalar", + [ + (2.0, 4.0), # 2 octave == 4x + (1.0, 2.0), # 1 octave == 2x + (0.0, 1.0), + (-1.0, 0.5), + (-2.0, 0.25), + ], +) +def test_octave_conversion(ureg, octave, scalar): + """Test that an octave can be converted to a scalar and back. + """ + Q_ = ureg.Quantity + assert Q_(octave, "octave").to("dimensionless").magnitude == pytest.approx(scalar) + assert Q_(scalar, "dimensionless").to("octave").magnitude == pytest.approx(octave) + + +@pytest.mark.parametrize( + "decade,scalar", + [ + (2.0, 100.0), # 2 decades == 100x + (1.0, 10.0), # 1 octave == 2x + (0.0, 1.0), + (-1.0, 0.1), + (-2.0, 0.01), + ], +) +def test_decade_conversion(ureg, decade, scalar): + """Test that a decade can be converted to a scalar and back. + """ + Q_ = ureg.Quantity + assert Q_(decade, "decade").to("dimensionless").magnitude == pytest.approx(scalar) + assert Q_(scalar, "dimensionless").to("decade").magnitude == pytest.approx(decade) + + +@pytest.mark.parametrize( + "dbm_value,mw_value", + [ + (0.0, 1.0), # 0.0 dBm == 1.0 mW + (10.0, 10.0), + (20.0, 100.0), + (-10.0, 0.1), + (-20.0, 0.01), + ], +) +def test_dbm_mw_conversion(ureg, dbm_value, mw_value): + """Test dBm values can convert to mW and back. + """ + Q_ = ureg.Quantity + assert Q_(dbm_value, "dBm").to("mW").magnitude == pytest.approx(mw_value) + assert Q_(mw_value, "mW").to("dBm").magnitude == pytest.approx(dbm_value) + + def test_compound_log_unit_multiply_definition(auto_ureg): """Check that compound log units can be defined using multiply. """ @@ -184,6 +182,14 @@ def test_compound_log_unit_parse_definition(auto_ureg): assert canonical_def == parse_def +@pytest.mark.xfail +def test_dbm_db_addition(auto_ureg): + """Test a dB value can be added to a dBm and the answer is correct. + """ + power = (5 * auto_ureg.dBm) + (10 * auto_ureg.dB) + assert power.to('dBm').magnitude == pytest.approx(15) + + class TestLogarithmicQuantityBasicMath(QuantityTestCase): FORCE_NDARRAY = False From 74bcdfb9fb4336d15450767a364d548a44c2086d Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 15 Jul 2020 12:41:32 -0700 Subject: [PATCH 519/612] update CHANGES entry to reflect later feature release date --- CHANGES | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 6b07aa263..2dc0be327 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,9 @@ Pint Changelog - Implement Dask collection interface to support Pint Quantity wrapped Dask arrays. - Started automatically testing examples in the documentation +- Implements Logarithmic Units like dBm, dB or decade + (Issue #71, Thanks Dima Pustakhod, Giorgio Signorello, Jonathan Wheeler) + 0.14 (2020-07-01) ----------------- @@ -90,8 +93,6 @@ Pint Changelog - Allow constants in units by using a leading underscore (Issue #989, Thanks Juan Nunez-Iglesias) - Fixed bug where to_compact handled prefix units incorrectly (Issue #960) -- Implements Logarithmic Units like dBm, dB or decade - (Issue #71, Thanks Dima Pustakhod, Giorgio Signorello, Jonathan Wheeler) 0.10.1 (2020-01-07) From bc65d556cd898c286fe590ed47f463cd802828fb Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 15 Jul 2020 16:13:33 -0700 Subject: [PATCH 520/612] docs: emphasize beta-ness of log unit support --- docs/log_units.rst | 134 ++++++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 50 deletions(-) diff --git a/docs/log_units.rst b/docs/log_units.rst index e22acb058..d2d4602a7 100644 --- a/docs/log_units.rst +++ b/docs/log_units.rst @@ -4,80 +4,100 @@ Logarithmic Units ================= -Pint supports some logarithmic units, including dB, dBm, octave, and decade -as well as conversions between them and their base units where applicable. -These units behave much like those described in :ref:`nonmult`, so many of -the recommendations there apply here as well. +.. warning:: -.. note:: + Support for logarithmic units in Pint is currently in Beta. Please take + careful note of the information below, particularly around `compound log units`_ + to avoid calculation errors. Bug reports and pull requests are always + welcome, please see :doc:`contributing` for more information on + how you can help improve this feature (and Pint in general). - If you're making heavy use of logarithmic units, you may find it helpful to - pass ``autoconvert_offset_to_baseunit=True`` when initializing your ``UnitRegistry()``, - this will allow you to use syntax like ``10.0 * ureg.dBm`` in lieu of the - explicit ``Quantity()`` constructor. Many examples on this page assume - you've passed this parameter, and will not work otherwise. - -Defining log units ------------------- +Pint supports some logarithmic units, including `dB`, `dBm`, `octave`, and `decade` +as well as some conversions between them and their base units where applicable. +These units behave much like those described in :ref:`nonmult`, so many of +the recommendations there apply here as well. -First, set up your ``UnitRegistry`` with the suggested flag. +Setting up the ``UnitRegistry()`` +--------------------------------- -If you do not wish to use the ``autoconvert_offset_to_baseunit`` flag, you -will need to define all logarithmic units using the ``Quanity()`` constructor: +Many of the examples below will fail without supplying the +``autoconvert_offset_to_baseunit=True`` flag. To use logarithmic units, +intialize your ``UnitRegistry()`` like so: .. doctest:: >>> from pint import UnitRegistry - >>> ureg = UnitRegistry() + >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) >>> Q_ = ureg.Quantity - >>> signal_power_dbm = 20 * ureg.dBm # must pass flag for this to work - Traceback (most recent call last): - ... - OffsetUnitCalculusError: Ambiguous operation with offset unit (decibellmilliwatt, ). - >>> Q_(20, 'dBm') # define like this instead - -You will also be restricted in the kinds of operations you can do without -converting to base units first. +If you can't pass that flag you will need to define all logarithmic units +:ref:`using the Quantity() constructor`, and you will +be restricted in the kinds of operations you can do without explicitly calling +`.to_base_units()` first. -.. doctest:: +Defining log quantities +----------------------- - >>> Q_(10, 'dBm/Hz') * (100 * ureg.Hz) # not feasible without flag - Traceback (most recent call last): - ... - UndefinedUnitError: 'delta_decibellmilliwatt' is not defined in the unit registry - -Passing the flag will allow you to use a more natural syntax for defining -logarithmic units: +After you've set up your ``UnitRegistry()`` with the ``autoconvert...`` flag, +you can define simple logarithmic quantities like most others: .. doctest:: - >>> from pint import UnitRegistry - >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True) - >>> Q_ = ureg.Quantity >>> 20.0 * ureg.dBm + >>> ureg('20.0 dBm') + + >>> ureg('20 dB') + + Converting to and from base units --------------------------------- - >>> signal_power_dbm = 20.0 * ureg.dBm - >>> signal_power_dbm.to('mW') - - >>> signal_power_mw = 100.0 * ureg.mW - >>> signal_power_mw +Get a sense of how logarithmic units are handled by using the `.to()` and +`.to_base_units()` methods: + +.. doctest:: + + >>> ureg('20 dBm').to('mW') - >>> signal_power_mw.to('dBm') + >>> ureg('20 dB').to_base_units() + + +.. note:: + + Notice in the above example how the `dB` unit is defined for + power quantities (10*log(p/p0)) not field (amplitude) quantities + (20*log(v/v0)). Take care that you're only using it to multiply power + levels, and not e.g. Voltages. + +Convert back from a base unit to a logarithmic unit using the `.to()` method: + +.. doctest:: + + >>> (100.0 * ureg('mW')).to('dBm') + >>> shift = Q_(4, '') + >>> shift + + >>> shift.to('octave') + Compound log units ------------------ -Pint also works with mixtures of logarithmic and other units. +.. warning:: + + Support for compound logarithmic units is not comprehensive. The following + examples work, but many others will not. Consider converting the logarithmic + portion to base units before adding more units. + +Pint sometimes works with mixtures of logarithmic and other units. Below is an +example of computing RMS noise from a noise density and a bandwidth: .. doctest:: - >>> noise_density = -161.0 * ureg['dBm/Hz'] + >>> noise_density = -161.0 * ureg.dBm / ureg.Hz >>> bandwidth = 10.0 * ureg.kHz >>> noise_power = noise_density * bandwidth >>> noise_power.to('dBm') @@ -85,9 +105,23 @@ Pint also works with mixtures of logarithmic and other units. >>> noise_power.to('mW') -Multiplication, division and exponentiation of quantities with -offset units is problematic just like addition. Pint (since version 0.6) -will by default raise an error when a quantity with offset unit is used in -these operations. Due to this quantities with offset units cannot be created -like other quantities by multiplication of magnitude and unit but have -to be explicitly created: +There are still issues with parsing compound units, so for now the following +will not work: + +.. doctest:: + + >>> -161.0 * ureg('dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) + False + +But this will: + +.. doctest:: + + >>> ureg('-161.0 dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) + True + >>> Q_(-161.0, 'dBm') / ureg.Hz == (-161.0 * ureg.dBm / ureg.Hz) + True + +To begin using this feature while avoiding problems, define logarithmic units +as single-unit quantities and convert them to their base units as quickly as +possible. From fdcedfe609547684aec7f92c4477e2751ed5a56f Mon Sep 17 00:00:00 2001 From: Clark Willison Date: Wed, 15 Jul 2020 16:14:23 -0700 Subject: [PATCH 521/612] test_log_units: rework for pytest, mark some as xfail --- pint/testsuite/test_log_units.py | 124 ++++++++++++++++++++++--------- pint/testsuite/test_quantity.py | 2 +- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index f347240e2..dbc222eec 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -1,11 +1,10 @@ import math -import unittest import pytest -from pint import LogarithmicUnitCalculusError, OffsetUnitCalculusError, UnitRegistry +from pint import OffsetUnitCalculusError, UnitRegistry from pint.testsuite import QuantityTestCase -from pint.unit import UnitsContainer +from pint.unit import Unit, UnitsContainer @pytest.fixture(scope="module") @@ -83,6 +82,69 @@ def test_mix_regular_log_units(self): self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) +log_unit_names = [ + "decibellmilliwatt", + "dBm", + "decibellmicrowatt", + "dBu", + "decibell", + "dB", + "decade", + "octave", + "oct", +] + + +@pytest.mark.parametrize("unit_name", log_unit_names) +def test_unit_by_attribute(ureg, unit_name): + """Can the logarithmic units be accessed by attribute lookups?""" + unit = getattr(ureg, unit_name) + assert isinstance(unit, Unit) + + +@pytest.mark.parametrize("unit_name", log_unit_names) +def test_unit_parsing(ureg, unit_name): + """Can the logarithmic units be understood by the parser?""" + unit = ureg.parse_units(unit_name) + assert isinstance(unit, Unit) + + +@pytest.mark.parametrize("mag", [1.0, 4.2]) +@pytest.mark.parametrize("unit_name", log_unit_names) +def test_quantity_by_constructor(ureg, unit_name, mag): + """Can Quantity() objects be constructed using logarithmic units?""" + q = ureg.Quantity(mag, unit_name) + assert q.magnitude == pytest.approx(mag) + assert q.units == getattr(ureg, unit_name) + + +@pytest.mark.parametrize("mag", [1.0, 4.2]) +@pytest.mark.parametrize("unit_name", log_unit_names) +def test_quantity_by_multiplication(auto_ureg, unit_name, mag): + """Test that logarithmic units can be defined with multiplication + + Requires setting `autoconvert_offset_to_baseunit` to True + """ + unit = getattr(auto_ureg, unit_name) + q = mag * unit + assert q.magnitude == pytest.approx(mag) + assert q.units == unit + + +@pytest.mark.parametrize( + "unit1,unit2", + [ + ("decibellmilliwatt", "dBm"), + ("decibellmicrowatt", "dBu"), + ("decibell", "dB"), + ("octave", "oct"), + ], +) +def test_unit_equivalence(ureg, unit1, unit2): + """Are certain pairs of units equivalent?""" + assert getattr(ureg, unit1) == getattr(ureg, unit2) + + @pytest.mark.parametrize( "db_value,scalar", [ @@ -155,15 +217,17 @@ def test_dbm_mw_conversion(ureg, dbm_value, mw_value): assert Q_(mw_value, "mW").to("dBm").magnitude == pytest.approx(dbm_value) +@pytest.mark.xfail def test_compound_log_unit_multiply_definition(auto_ureg): """Check that compound log units can be defined using multiply. """ Q_ = auto_ureg.Quantity canonical_def = Q_(-161, "dBm") / auto_ureg.Hz - mult_def = -161 * auto_ureg["dBm/Hz"] + mult_def = -161 * auto_ureg("dBm/Hz") assert mult_def == canonical_def +@pytest.mark.xfail def test_compound_log_unit_quantity_definition(auto_ureg): """Check that compound log units can be defined using ``Quantity()``. """ @@ -174,6 +238,13 @@ def test_compound_log_unit_quantity_definition(auto_ureg): def test_compound_log_unit_parse_definition(auto_ureg): + Q_ = auto_ureg.Quantity + canonical_def = Q_(-161, "dBm") / auto_ureg.Hz + parse_def = auto_ureg("-161 dBm/Hz") + assert parse_def == canonical_def + + +def test_compound_log_unit_parse_expr(auto_ureg): """Check that compound log units can be defined using ``parse_expression()``. """ Q_ = auto_ureg.Quantity @@ -187,37 +258,18 @@ def test_dbm_db_addition(auto_ureg): """Test a dB value can be added to a dBm and the answer is correct. """ power = (5 * auto_ureg.dBm) + (10 * auto_ureg.dB) - assert power.to('dBm').magnitude == pytest.approx(15) + assert power.to("dBm").magnitude == pytest.approx(15) -class TestLogarithmicQuantityBasicMath(QuantityTestCase): - - FORCE_NDARRAY = False - - @unittest.expectedFailure - def _test_log_quantity_add_sub_raises_exception(self, unit, func): - # Warning should be provided when trying to .... - self.assertRaises(LogarithmicUnitCalculusError) - - @unittest.expectedFailure - def _test_log_quantity_add_sub(self, unit, func): - - # Pure dB arithmetic - # 5 dBm + 10 dB = 15 dBm - self.assertQuantityAlmostEqual( - 5 * self.ureg.dBm + 10 * self.ureg.dB, 15 * self.ureg.dBm - ) - # 100*dBm -10*dB = 90*dB - self.assertQuantityAlmostEqual( - 100 * self.ureg.dB - 10 * self.ureg.dB, 90 * self.ureg.dB - ) - # 100 dBW - 5 dBW = 95 dB - self.assertQuantityAlmostEqual( - 100 * self.ureg.dBm - 5 * self.ureg.dBm, 95 * self.ureg.dB - ) - # 20 dB + 0 dBW == 20 dBW - - # 100 Hz + 1 octave = 200 Hz - self.assertQuantityAlmostEqual( - 100 * self.ureg.Hz + 1 * self.ureg.octave, 200 * self.ureg.Hz - ) +@pytest.mark.xfail +@pytest.mark.parametrize( + "freq1,octaves,freq2", [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200),], +) +def test_frequency_octave_addition(auto_ureg, freq1, octaves, freq2): + """Test an Octave can be added to a frequency correctly + """ + freq1 = freq1 * auto_ureg.Hz + shift = octaves * auto_ureg.Octave + new_freq = freq1 + shift + assert new_freq.units == freq1.units + assert new_freq.magnitude == pytest.approx(freq2) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 49464faed..fa18fe8fc 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -2,9 +2,9 @@ import datetime import math import operator as op +import pickle import warnings from unittest.mock import patch -import pickle from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry from pint.compat import np From 26ac328422853cca999a23f7fadb3968ebc26f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Wed, 29 Jul 2020 18:15:38 +0200 Subject: [PATCH 522/612] Support rpow operation on dimensionless array Fix #1136 --- CHANGES | 1 + pint/quantity.py | 3 --- pint/testsuite/test_issues.py | 7 +++++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index fee1ba480..cdd4dc664 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,7 @@ Pint Changelog - 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) 0.14 (2020-07-01) ----------------- diff --git a/pint/quantity.py b/pint/quantity.py index ad066f332..5a9a13f99 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1467,9 +1467,6 @@ def __rpow__(self, other): else: if not self.dimensionless: raise DimensionalityError(self._units, "dimensionless") - if is_duck_array_type(type(self._magnitude)): - if np.size(self._magnitude) > 1: - raise DimensionalityError(self._units, "dimensionless") new_self = self.to_root_units() return other ** new_self._magnitude diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index f71ce1aa0..b4879ee05 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -738,6 +738,13 @@ def test_issue1112(self): ureg.enable_contexts("c2") ureg.enable_contexts("c3") + @helpers.requires_numpy() + def test_issue_1136(self): + assert (2 ** ureg.Quantity([2, 3], "") == 2 ** np.array([2, 3])).all() + + with pytest.raises(DimensionalityError): + 2 ** ureg.Quantity([2, 3], "m") + if np is not None: From 185ec6b81c1c6f57263733571face3ef672c4afd Mon Sep 17 00:00:00 2001 From: Ryan May Date: Wed, 29 Jul 2020 02:47:28 -0600 Subject: [PATCH 523/612] Avoid warning when setting a masked value When the underlying data is a masked array, __setitem__ can be given a sentintel np.ma.masked or masked values. This avoids passing them to math.isnan which causes a warning. --- CHANGES | 1 + pint/quantity.py | 2 +- pint/testsuite/test_numpy.py | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index cdd4dc664..9922eda99 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,7 @@ Pint Changelog - 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) +- Eliminated warning when setting a masked value on an underlying MaskedArray. 0.14 (2020-07-01) ----------------- diff --git a/pint/quantity.py b/pint/quantity.py index 5a9a13f99..66642a3e1 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1808,7 +1808,7 @@ def __getitem__(self, key): def __setitem__(self, key, value): try: - if math.isnan(value): + if np.ma.is_masked(value) or math.isnan(value): self._magnitude[key] = value return except TypeError: diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 105ef6b92..d9cee5769 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -2,6 +2,7 @@ import operator as op import pickle import unittest +import warnings from pint import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning from pint.compat import np @@ -850,6 +851,16 @@ def test_setitem(self): q[0] = 1.0 self.assertQuantityEqual(q, [0.001, 1, 2, 3] * self.ureg.m / self.ureg.mm) + # Check that this properly masks the first item without warning + q = self.ureg.Quantity( + np.ma.array([0.0, 1.0, 2.0, 3.0], mask=[False, True, False, False]), "m" + ) + with warnings.catch_warnings(record=True) as w: + q[0] = np.ma.masked + # Check for no warnings + assert not w + assert q.mask[0] + def test_iterator(self): for q, v in zip(self.q.flatten(), [1, 2, 3, 4]): self.assertEqual(q, v * self.ureg.m) From f510beeb39b5b0b233f973dbb7ba64fcfa3e7765 Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Tue, 14 Jul 2020 16:11:01 -0500 Subject: [PATCH 524/612] Change html repr away from latex to a Sparse/Dask-like repr --- .gitignore | 3 ++ CHANGES | 3 ++ docs/tutorial.rst | 8 +-- pint/formatting.py | 8 +-- pint/measurement.py | 12 ++--- pint/quantity.py | 79 ++++++++++++++++++++---------- pint/testsuite/test_measurement.py | 16 +++--- pint/testsuite/test_quantity.py | 25 +++++++--- pint/testsuite/test_unit.py | 16 +++--- pint/unit.py | 4 -- 10 files changed, 101 insertions(+), 73 deletions(-) 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): From 11272d564153766ea790f9b1b91b0612b0aa3cab Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 16 Aug 2020 12:56:31 -0500 Subject: [PATCH 525/612] Update docs with guidelines on when to create extension packages --- docs/contributing.rst | 28 +++++++++++++++++++++++++++- docs/numpy.ipynb | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index d5c870330..19157f1ea 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -63,8 +63,34 @@ To build the documentation, invoke Sphinx from the ``docs`` directory:: $ cd docs $ make html +Extension Packages +------------------ + +Pint naturally integrates with other libraries in the scientific Python ecosystem, and +a small number of +`extension/compatibility packages`_ have arisen to +aid in compatibility between certain packages. Pint's rule of thumb for integration +features that work best as an extension pacakage versus direct inclusion in Pint is: + +* Extension (separate packages) + + * Duck array types that wrap Pint (come above Pint in + `the type casting hierarchy`_) + + * Uses features independent/on top of the libraries + + * Examples: xarray, Pandas + +* Integration (built in to Pint) + + * Duck array types wrapped by Pint (below Pint in the type casting hierarchy) + + * Intermingling of APIs occurs + + * Examples: Dask + .. _github: http://github.com/hgrecco/pint .. _`issue tracker`: https://github.com/hgrecco/pint/issues .. _`bors-ng`: https://github.com/bors-ng/bors-ng -.. _`github docs`: https://help.github.com/articles/closing-issues-via-commit-messages/ \ No newline at end of file +.. _`github docs`: https://help.github.com/articles/closing-issues-via-commit-messages/ diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 7b8a0a329..fa987b280 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -453,7 +453,7 @@ "To aid in integration between various array types and Pint (such as by providing convenience methods), the following compatibility packages are available:\n", "\n", "- [pint-pandas](https://github.com/hgrecco/pint-pandas)\n", - "- [pint-xarray](https://github.com/TomNicholas/pint-xarray/) (in early development as of April 2020, with [extra discussion here](https://github.com/hgrecco/pint/issues/849#issuecomment-579992247) - please come and help!)\n", + "- [pint-xarray](https://github.com/xarray-contrib/pint-xarray/)\n", "\n", "(Note: if you have developed a compatibility package for Pint, please submit a pull request to add it to this list!)" ] From a7b56b9d6f2503fd6c4aacfd43faf129cbc371da Mon Sep 17 00:00:00 2001 From: Jon Thielen Date: Sun, 16 Aug 2020 10:50:29 -0500 Subject: [PATCH 526/612] Add case_sensitive regsitry setting and modify case_sensitive kwarg to default to None to use regsitry setting --- CHANGES | 2 ++ pint/registry.py | 55 ++++++++++++++++++++++++------------- pint/testsuite/__init__.py | 8 ++++++ pint/testsuite/test_unit.py | 44 ++++++++++++++++++++++++++++- 4 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CHANGES b/CHANGES index 13947daea..076b3fc6d 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,8 @@ Pint Changelog - 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) +- Add `case_sensitive` option to registry for case (in)sensitive handling when parsing + units (Issue #1145) - 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/pint/registry.py b/pint/registry.py index a05937671..2686ee9ed 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -167,6 +167,8 @@ class BaseRegistry(metaclass=RegistryMeta): locale identifier string, used in `format_babel` non_int_type : type numerical type used for non integer values. (Default: float) + case_sensitive : bool, optional + Control default case sensitivity of unit parsing. (Default: True) """ @@ -187,6 +189,7 @@ def __init__( preprocessors=None, fmt_locale=None, non_int_type=float, + case_sensitive=True, ): self._register_parsers() self._init_dynamic_classes() @@ -208,6 +211,9 @@ def __init__( #: Numerical type used for non integer values. self.non_int_type = non_int_type + #: Default unit case sensitivity + self.case_sensitive = case_sensitive + #: Map between name (string) and value (string) of defaults stored in the #: definitions file. self._defaults = {} @@ -619,7 +625,7 @@ def _build_cache(self): except Exception as exc: logger.warning(f"Could not resolve {unit_name}: {exc!r}") - def get_name(self, name_or_alias, case_sensitive=True): + def get_name(self, name_or_alias, case_sensitive=None): """Return the canonical name of a unit. """ @@ -645,7 +651,7 @@ def get_name(self, name_or_alias, case_sensitive=True): if prefix: name = prefix + unit_name - symbol = self.get_symbol(name) + symbol = self.get_symbol(name, case_sensitive) prefix_def = self._prefixes[prefix] self._units[name] = UnitDefinition( name, @@ -658,10 +664,10 @@ def get_name(self, name_or_alias, case_sensitive=True): return unit_name - def get_symbol(self, name_or_alias): + def get_symbol(self, name_or_alias, case_sensitive=None): """Return the preferred alias for a unit. """ - candidates = self.parse_unit_name(name_or_alias) + candidates = self.parse_unit_name(name_or_alias, case_sensitive) if not candidates: raise UndefinedUnitError(name_or_alias) elif len(candidates) == 1: @@ -994,7 +1000,7 @@ def _convert(self, value, src, dst, inplace=False, check_dimensionality=True): return value - def parse_unit_name(self, unit_name, case_sensitive=True): + def parse_unit_name(self, unit_name, case_sensitive=None): """Parse a unit to identify prefix, unit name and suffix by walking the list of prefix and suffix. In case of equivalent combinations (e.g. ('kilo', 'gram', '') and @@ -1004,8 +1010,9 @@ def parse_unit_name(self, unit_name, case_sensitive=True): ---------- unit_name : - case_sensitive : - (Default value = True) + case_sensitive : bool or None + Control if unit lookup is case sensitive. Defaults to None, which uses the + registry's case_sensitive setting Returns ------- @@ -1016,9 +1023,12 @@ def parse_unit_name(self, unit_name, case_sensitive=True): self._parse_unit_name(unit_name, case_sensitive=case_sensitive) ) - def _parse_unit_name(self, unit_name, case_sensitive=True): + def _parse_unit_name(self, unit_name, case_sensitive=None): """Helper of parse_unit_name. """ + case_sensitive = ( + self.case_sensitive if case_sensitive is None else case_sensitive + ) stw = unit_name.startswith edw = unit_name.endswith for suffix, prefix in itertools.product(self._suffixes, self._prefixes): @@ -1062,7 +1072,7 @@ def _dedup_candidates(candidates): candidates.pop(("", cp + cu, ""), None) return tuple(candidates) - def parse_units(self, input_string, as_delta=None): + def parse_units(self, input_string, as_delta=None, case_sensitive=None): """Parse a units expression and returns a UnitContainer with the canonical names. @@ -1074,6 +1084,9 @@ def parse_units(self, input_string, as_delta=None): as_delta : bool or None if the expression has multiple units, the parser will interpret non multiplicative units as their `delta_` counterparts. (Default value = None) + case_sensitive : bool or None + Control if unit parsing is case sensitive. Defaults to None, which uses the + registry's setting. Returns ------- @@ -1081,10 +1094,10 @@ def parse_units(self, input_string, as_delta=None): """ for p in self.preprocessors: input_string = p(input_string) - units = self._parse_units(input_string, as_delta) + units = self._parse_units(input_string, as_delta, case_sensitive) return self.Unit(units) - def _parse_units(self, input_string, as_delta=True): + def _parse_units(self, input_string, as_delta=True, case_sensitive=None): """Parse a units expression and returns a UnitContainer with the canonical names. """ @@ -1109,7 +1122,7 @@ def _parse_units(self, input_string, as_delta=True): ret = {} many = len(units) > 1 for name in units: - cname = self.get_name(name) + cname = self.get_name(name, case_sensitive=case_sensitive) value = units[name] if not cname: continue @@ -1126,7 +1139,7 @@ def _parse_units(self, input_string, as_delta=True): return ret - def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): + def _eval_token(self, token, case_sensitive=None, use_decimal=False, **values): # TODO: remove this code when use_decimal is deprecated if use_decimal: @@ -1156,7 +1169,7 @@ def _eval_token(self, token, case_sensitive=True, use_decimal=False, **values): raise Exception("unknown token type") def parse_pattern( - self, input_string, pattern, case_sensitive=True, use_decimal=False, many=False + self, input_string, pattern, case_sensitive=None, use_decimal=False, many=False ): """Parse a string with a given regex pattern and returns result. @@ -1167,7 +1180,7 @@ def parse_pattern( pattern_string: The regex parse string case_sensitive : - (Default value = True) + (Default value = None, which uses registry setting) use_decimal : (Default value = False) many : @@ -1212,7 +1225,7 @@ def parse_pattern( return results def parse_expression( - self, input_string, case_sensitive=True, use_decimal=False, **values + self, input_string, case_sensitive=None, use_decimal=False, **values ): """Parse a mathematical expression including units and return a quantity object. @@ -1224,7 +1237,7 @@ def parse_expression( input_string : case_sensitive : - (Default value = True) + (Default value = None, which uses registry setting) use_decimal : (Default value = False) **values : @@ -1289,13 +1302,13 @@ def __init__( # base units on multiplication and division. self.autoconvert_offset_to_baseunit = autoconvert_offset_to_baseunit - def _parse_units(self, input_string, as_delta=None): + def _parse_units(self, input_string, as_delta=None, case_sensitive=None): """ """ if as_delta is None: as_delta = self.default_as_delta - return super()._parse_units(input_string, as_delta) + return super()._parse_units(input_string, as_delta, case_sensitive) def _define(self, definition): """Add unit to the registry. @@ -2099,6 +2112,8 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): or unit string fmt_locale : locale identifier string, used in `format_babel`. Default to None + case_sensitive : bool, optional + Control default case sensitivity of unit parsing. (Default: True) """ def __init__( @@ -2114,6 +2129,7 @@ def __init__( preprocessors=None, fmt_locale=None, non_int_type=float, + case_sensitive=True, ): super().__init__( @@ -2128,6 +2144,7 @@ def __init__( preprocessors=preprocessors, fmt_locale=fmt_locale, non_int_type=non_int_type, + case_sensitive=case_sensitive, ) def pi_theorem(self, quantities): diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 1fbc233d4..687722134 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -130,6 +130,14 @@ def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None) self.assertLessEqual(abs(m1 - m2), atol + rtol * abs(m2), msg=msg) +class CaseInsensitveQuantityTestCase(QuantityTestCase): + @classmethod + def setUpClass(cls): + cls.ureg = UnitRegistry(case_sensitive=False) + cls.Q_ = cls.ureg.Quantity + cls.U_ = cls.ureg.Unit + + def testsuite(): """A testsuite that has all the pint tests.""" suite = unittest.TestLoader().discover(os.path.dirname(__file__)) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 749a60f5b..dcb3e97a1 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -11,7 +11,7 @@ ) from pint.compat import np from pint.registry import LazyRegistry, UnitRegistry -from pint.testsuite import QuantityTestCase, helpers +from pint.testsuite import CaseInsensitveQuantityTestCase, QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase from pint.util import ParserHelper, UnitsContainer @@ -704,6 +704,48 @@ def test_parse_pattern_many_results_two_units(self): ], ) + def test_case_sensitivity(self): + ureg = self.ureg + # Default + self.assertRaises(UndefinedUnitError, ureg.parse_units, "Meter") + self.assertRaises(UndefinedUnitError, ureg.parse_units, "j") + # Force True + self.assertRaises( + UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True + ) + self.assertRaises( + UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True + ) + # Force False + self.assertEqual( + ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1) + ) + self.assertEqual( + ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1) + ) + + +class TestCaseInsensitiveRegistry(CaseInsensitveQuantityTestCase): + def test_case_sensitivity(self): + ureg = self.ureg + # Default + self.assertEqual(ureg.parse_units("Meter"), UnitsContainer(meter=1)) + self.assertEqual(ureg.parse_units("j"), UnitsContainer(joule=1)) + # Force True + self.assertRaises( + UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True + ) + self.assertRaises( + UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True + ) + # Force False + self.assertEqual( + ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1) + ) + self.assertEqual( + ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1) + ) + class TestCompatibleUnits(QuantityTestCase): FORCE_NDARRAY = False From 7134d0279af6a8f11c7aee6a8e5452999bf2399c Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Tue, 18 Aug 2020 16:57:20 -0600 Subject: [PATCH 527/612] Add option to disable sorting of formatted units --- pint/formatting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pint/formatting.py b/pint/formatting.py index 3cc7643ec..b91231fb5 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -129,6 +129,7 @@ def formatter( locale=None, babel_length="long", babel_plural_form="one", + sort=True, ): """Format a list of (name, exponent) pairs. @@ -157,6 +158,8 @@ def formatter( the plural form, calculated as defined in babel. (Default value = "one") exp_call : callable (Default value = lambda x: f"{x:n}") + sort : bool, optional + True to sort the formatted units alphabetically (Default value = True) Returns ------- @@ -175,7 +178,9 @@ def formatter( pos_terms, neg_terms = [], [] - for key, value in sorted(items): + if sort: + items = sorted(items) + for key, value in items: if locale and babel_length and babel_plural_form and key in _babel_units: _key = _babel_units[key] locale = babel_parse(locale) From 16fd7dd939fdd3d7f1de6780d52374faf6445a83 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Tue, 18 Aug 2020 17:51:32 -0600 Subject: [PATCH 528/612] Update changelog --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 076b3fc6d..9211891e2 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Pint Changelog - Started automatically testing examples in the documentation - Fixed right operand power for dimensionless Quantity to reflect numpy behavior. (Issue #1136) - Eliminated warning when setting a masked value on an underlying MaskedArray. +- Add `sort` option to `formatting.formatter` to permit disabling sorting of component units in format string 0.14 (2020-07-01) ----------------- From c2a4bc401543a7b49749ea4bc77256d12cbcbd61 Mon Sep 17 00:00:00 2001 From: Russell Manser Date: Tue, 18 Aug 2020 20:53:58 -0500 Subject: [PATCH 529/612] Implement deterministic hashing as part of Dask collection interface --- pint/quantity.py | 5 ++++- pint/testsuite/test_dask.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pint/quantity.py b/pint/quantity.py index 5d6ac46f0..61c857beb 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1964,6 +1964,9 @@ def __dask_graph__(self): def __dask_keys__(self): return self._magnitude.__dask_keys__() + def __dask_tokenize__(self): + return (Quantity, self._magnitude.name, self.units) + @property def __dask_optimize__(self): return dask_array.Array.__dask_optimize__ @@ -2028,7 +2031,7 @@ def visualize(self, **kwargs): Parameters ---------- **kwargs : dict - Any keyword arguments to pass to the ``dask.base.visualize`` function. + Any keyword arguments to pass to ``dask.visualize``. Returns ------- diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py index 346575245..836e2dbcf 100644 --- a/pint/testsuite/test_dask.py +++ b/pint/testsuite/test_dask.py @@ -59,6 +59,15 @@ def test_dask_scheduler(dask_array): assert scheduler_name == true_name +def test_dask_tokenize(dask_array): + """Test that a pint.Quantity wrapped Dask array has a unique token.""" + dask_token = dask.base.tokenize(dask_array) + q = ureg.Quantity(dask_array, units_) + + assert dask.base.tokenize(dask_array) != dask.base.tokenize(q) + assert dask.base.tokenize(dask_array) == dask_token + + def test_dask_optimize(dask_array): """Test that a pint.Quantity wrapped Dask array can be optimized.""" q = ureg.Quantity(dask_array, units_) From 85603e644e9ba004188ef79e499b9a40557b446c Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 22 Aug 2020 16:22:37 -0300 Subject: [PATCH 530/612] Preparing release 0.15 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 294604d1b..983664be4 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.15 (unreleased) +0.15 (2020-08-22) ----------------- - Change `Quantity` and `Unit` HTML (i.e., Jupyter notebook) repr away from LaTeX to a diff --git a/version.py b/version.py index fe18e34ab..17975ca3b 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.15.dev0' +__version__ = '0.15' # fmt: on From 26a3add85eb5d913c12fd1fd5b6c5b68555ac2fd Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 22 Aug 2020 16:22:58 -0300 Subject: [PATCH 531/612] Back to development: 0.16 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 983664be4..8128807a3 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.16 (unreleased) +----------------- + +- Nothing changed yet. + + 0.15 (2020-08-22) ----------------- diff --git a/version.py b/version.py index 17975ca3b..b98433240 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.15' +__version__ = '0.16.dev0' # fmt: on From 37cdad16612f35ffada200a868610f44eb2a5d56 Mon Sep 17 00:00:00 2001 From: Kevin Fuhr Date: Mon, 24 Aug 2020 15:00:25 -0600 Subject: [PATCH 532/612] Fix magnitude conversion (was incorrectly inplace) --- CHANGES | 3 ++- pint/quantity.py | 6 +++--- pint/testsuite/test_issues.py | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 8128807a3..12fa7180f 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,8 @@ Pint Changelog 0.16 (unreleased) ----------------- -- Nothing changed yet. +- Fixed issue where performing an operation of a Quantity with certain units would perform an in-place + unit conversion that modified the operand in addition to the returned value (Issues #1102 & #1144) 0.15 (2020-08-22) diff --git a/pint/quantity.py b/pint/quantity.py index 93b4e332c..e88167c85 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1007,7 +1007,7 @@ def _add_sub(self, other, op): units = self._units # If only self has a delta unit, other determines unit of result. elif self._get_delta_units() and not other._get_delta_units(): - magnitude = op(self._convert_magnitude(other._units), other._magnitude) + magnitude = op(self._convert_magnitude_not_inplace(other._units), other._magnitude) units = other._units else: units = self._units @@ -1055,7 +1055,7 @@ def _add_sub(self, other, op): # Replace offset unit in other by the corresponding delta unit. # This is done to prevent a shift by offset in the to()-call. tu = other._units.rename(other_non_mul_unit, "delta_" + other_non_mul_unit) - magnitude = op(self._convert_magnitude(tu), other._magnitude) + magnitude = op(self._convert_magnitude_not_inplace(tu), other._magnitude) units = other._units else: raise OffsetUnitCalculusError(self._units, other._units) @@ -1542,7 +1542,7 @@ def bool_result(value): raise OffsetUnitCalculusError(self._units) if self.dimensionless: - return eq(self._convert_magnitude(self.UnitsContainer()), other, False) + return eq(self._convert_magnitude_not_inplace(self.UnitsContainer()), other, False) return bool_result(False) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index bc63beab0..315835761 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -745,6 +745,27 @@ def test_issue1112(self): ureg.enable_contexts("c2") ureg.enable_contexts("c3") + def test_issue1144_1102(self): + # Performing operations shouldn't modify the original objects + # Issue 1144 + q1 = ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius") + q2 = ureg.Quantity(70.0, "degree_Fahrenheit") + q3 = q1 - q2 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + assert q2 == ureg.Quantity(70.0, "degree_Fahrenheit") + q3 = q2 - q1 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + assert q2 == ureg.Quantity(70.0, "degree_Fahrenheit") + # Issue 1102 + val = [30., 45., 60.] * ureg.degree + val == 1 + 1 == val + assert all(val == ([30., 45., 60.] * ureg.degree)) + # Test for another bug identified by searching on "_convert_magnitude" + q2 = ureg.Quantity(3,"degree_Kelvin") + q3 = q1 - q2 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + @helpers.requires_numpy() def test_issue_1136(self): assert (2 ** ureg.Quantity([2, 3], "") == 2 ** np.array([2, 3])).all() From f3d9290bbbc510f5b1f7ea09116bbe94b1ec9734 Mon Sep 17 00:00:00 2001 From: Kevin Fuhr Date: Mon, 24 Aug 2020 15:10:56 -0600 Subject: [PATCH 533/612] Linting changes --- pint/quantity.py | 10 ++++++++-- pint/testsuite/test_issues.py | 21 +++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index e88167c85..803b871b7 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1007,7 +1007,9 @@ def _add_sub(self, other, op): units = self._units # If only self has a delta unit, other determines unit of result. elif self._get_delta_units() and not other._get_delta_units(): - magnitude = op(self._convert_magnitude_not_inplace(other._units), other._magnitude) + magnitude = op( + self._convert_magnitude_not_inplace(other._units), other._magnitude + ) units = other._units else: units = self._units @@ -1542,7 +1544,11 @@ def bool_result(value): raise OffsetUnitCalculusError(self._units) if self.dimensionless: - return eq(self._convert_magnitude_not_inplace(self.UnitsContainer()), other, False) + return eq( + self._convert_magnitude_not_inplace(self.UnitsContainer()), + other, + False, + ) return bool_result(False) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 315835761..33bd2aa13 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -748,23 +748,24 @@ def test_issue1112(self): def test_issue1144_1102(self): # Performing operations shouldn't modify the original objects # Issue 1144 - q1 = ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius") + ddc = "delta_degree_Celsius" + q1 = ureg.Quantity([-287.78, -32.24, -1.94], ddc) q2 = ureg.Quantity(70.0, "degree_Fahrenheit") - q3 = q1 - q2 - assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + q1 - q2 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], ddc)) assert q2 == ureg.Quantity(70.0, "degree_Fahrenheit") - q3 = q2 - q1 - assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + q2 - q1 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], ddc)) assert q2 == ureg.Quantity(70.0, "degree_Fahrenheit") # Issue 1102 - val = [30., 45., 60.] * ureg.degree + val = [30.0, 45.0, 60.0] * ureg.degree val == 1 1 == val - assert all(val == ([30., 45., 60.] * ureg.degree)) + assert all(val == ([30.0, 45.0, 60.0] * ureg.degree)) # Test for another bug identified by searching on "_convert_magnitude" - q2 = ureg.Quantity(3,"degree_Kelvin") - q3 = q1 - q2 - assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], "delta_degree_Celsius")) + q2 = ureg.Quantity(3, "degree_Kelvin") + q1 - q2 + assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], ddc)) @helpers.requires_numpy() def test_issue_1136(self): From ee0c13b27f5c49aba4a298b0b29f7600be7fc4d0 Mon Sep 17 00:00:00 2001 From: signor82 Date: Tue, 25 Aug 2020 10:50:56 +0200 Subject: [PATCH 534/612] Runs Flake8 on test_log_units.py --- pint/testsuite/test_log_units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index dbc222eec..cb3a4aa02 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -263,7 +263,7 @@ def test_dbm_db_addition(auto_ureg): @pytest.mark.xfail @pytest.mark.parametrize( - "freq1,octaves,freq2", [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200),], + "freq1,octaves,freq2", [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200)], ) def test_frequency_octave_addition(auto_ureg, freq1, octaves, freq2): """Test an Octave can be added to a frequency correctly From 5ee438a9777a74f486801d72a9e40a8b1de51bd4 Mon Sep 17 00:00:00 2001 From: signor82 Date: Tue, 25 Aug 2020 11:18:32 +0200 Subject: [PATCH 535/612] Solves conflict between black and flake8 in test_log_units --- pint/testsuite/test_log_units.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index cb3a4aa02..f7e353695 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -263,7 +263,8 @@ def test_dbm_db_addition(auto_ureg): @pytest.mark.xfail @pytest.mark.parametrize( - "freq1,octaves,freq2", [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200)], + "freq1,octaves,freq2", + [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200),], # noqa: E231 ) def test_frequency_octave_addition(auto_ureg, freq1, octaves, freq2): """Test an Octave can be added to a frequency correctly From ef28ffcc5fbe8aae9d86d0a16a522b746bff9d0e Mon Sep 17 00:00:00 2001 From: signor82 Date: Tue, 25 Aug 2020 11:58:04 +0200 Subject: [PATCH 536/612] Corrects typo: decibel, not decibell (as pointed out by @terikin) --- docs/log_units.rst | 10 +++++----- pint/default_en.txt | 6 +++--- pint/testsuite/test_definitions.py | 4 ++-- pint/testsuite/test_errors.py | 4 ++-- pint/testsuite/test_log_units.py | 22 +++++++++++----------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/log_units.rst b/docs/log_units.rst index d2d4602a7..03e007914 100644 --- a/docs/log_units.rst +++ b/docs/log_units.rst @@ -44,11 +44,11 @@ you can define simple logarithmic quantities like most others: .. doctest:: >>> 20.0 * ureg.dBm - + >>> ureg('20.0 dBm') - + >>> ureg('20 dB') - + Converting to and from base units @@ -76,7 +76,7 @@ Convert back from a base unit to a logarithmic unit using the `.to()` method: .. doctest:: >>> (100.0 * ureg('mW')).to('dBm') - + >>> shift = Q_(4, '') >>> shift @@ -101,7 +101,7 @@ example of computing RMS noise from a noise density and a bandwidth: >>> bandwidth = 10.0 * ureg.kHz >>> noise_power = noise_density * bandwidth >>> noise_power.to('dBm') - + >>> noise_power.to('mW') diff --git a/pint/default_en.txt b/pint/default_en.txt index 13f29383e..7e8c69071 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -479,10 +479,10 @@ nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N # Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ] -decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm -decibellmicrowatt = 1e-6 watt; logbase: 10; logfactor: 10 = dBu +decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm +decibelmicrowatt = 1e-6 watt; logbase: 10; logfactor: 10 = dBu -decibell = 1 ; logbase: 10; logfactor: 10 = dB +decibel = 1 ; logbase: 10; logfactor: 10 = dB # bell = 1 ; logbase: 10; logfactor: = B ## NOTE: B (Bell) symbol conflicts with byte diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 00df68f7f..21ba40122 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -97,7 +97,7 @@ def test_unit_definition(self): def test_log_unit_definition(self): x = Definition.from_string( - "decibellmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm" + "decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm" ) self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) @@ -107,7 +107,7 @@ def test_log_unit_definition(self): self.assertEqual(x.converter.logfactor, 10) self.assertEqual(x.reference, UnitsContainer(watt=1)) - x = Definition.from_string("decibell = 1 ; logbase: 10; logfactor: 10 = dB") + x = Definition.from_string("decibel = 1 ; logbase: 10; logfactor: 10 = dB") self.assertIsInstance(x, UnitDefinition) self.assertFalse(x.is_base) self.assertIsInstance(x.converter, LogarithmicConverter) diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index c89677e5b..3e65c6135 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -97,7 +97,7 @@ def test_logarithmic_unit_calculus_error(self): ex = LogarithmicUnitCalculusError(Quantity("1 dB")._units) self.assertEqual( str(ex), - "Ambiguous operation with logarithmic unit (decibell). See " + "Ambiguous operation with logarithmic unit (decibel). See " + LOG_ERROR_DOCS_HTML + " for guidance.", ) @@ -106,7 +106,7 @@ def test_logarithmic_unit_calculus_error(self): ) self.assertEqual( str(ex), - "Ambiguous operation with logarithmic unit (decibell, octave). See " + "Ambiguous operation with logarithmic unit (decibel, octave). See " + LOG_ERROR_DOCS_HTML + " for guidance.", ) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index f7e353695..7d0928cf6 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -26,18 +26,18 @@ def test_log_quantity_creation(self): # Following Quantity Creation Pattern for args in ( (4.2, "dBm"), - (4.2, UnitsContainer(decibellmilliwatt=1)), + (4.2, UnitsContainer(decibelmilliwatt=1)), (4.2, self.ureg.dBm), ): x = self.Q_(*args) self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) x = self.Q_(self.Q_(4.2, "dBm")) self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) - x = self.Q_(4.2, UnitsContainer(decibellmilliwatt=1)) + x = self.Q_(4.2, UnitsContainer(decibelmilliwatt=1)) y = self.Q_(x) self.assertEqual(x.magnitude, y.magnitude) self.assertEqual(x.units, y.units) @@ -47,7 +47,7 @@ def test_log_quantity_creation(self): new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) x = new_reg.Quantity("4.2 * dBm") self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibellmilliwatt=1)) + self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) with self.capture_log() as buffer: self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm)) @@ -83,11 +83,11 @@ def test_mix_regular_log_units(self): log_unit_names = [ - "decibellmilliwatt", + "decibelmilliwatt", "dBm", - "decibellmicrowatt", + "decibelmicrowatt", "dBu", - "decibell", + "decibel", "dB", "decade", "octave", @@ -134,9 +134,9 @@ def test_quantity_by_multiplication(auto_ureg, unit_name, mag): @pytest.mark.parametrize( "unit1,unit2", [ - ("decibellmilliwatt", "dBm"), - ("decibellmicrowatt", "dBu"), - ("decibell", "dB"), + ("decibelmilliwatt", "dBm"), + ("decibelmicrowatt", "dBu"), + ("decibel", "dB"), ("octave", "oct"), ], ) From e12ac8b44a453676795d216fc3946f4530420b0a Mon Sep 17 00:00:00 2001 From: 5igno Date: Tue, 25 Aug 2020 14:55:15 +0200 Subject: [PATCH 537/612] Runs isort on compat.py --- pint/compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/compat.py b/pint/compat.py index 95745548b..1209030d7 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -129,11 +129,11 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): # Defines Logarithm and Exponential for Logarithmic Converter if HAS_NUMPY: - from numpy import log # noqa: F401 from numpy import exp # noqa: F401 + from numpy import log # noqa: F401 else: - from math import log # noqa: F401 from math import exp # noqa: F401 + from math import log # noqa: F401 if not HAS_BABEL: babel_parse = babel_units = missing_dependency("Babel") # noqa: F811 From c96cf6e187982f4e1bd417487b5753d0f1fda25a Mon Sep 17 00:00:00 2001 From: 5igno Date: Tue, 25 Aug 2020 15:08:34 +0200 Subject: [PATCH 538/612] Removes print statements for tests --- pint/testsuite/test_issues.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index b4879ee05..b2f2888a2 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -520,7 +520,7 @@ def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): lunar_module_height = Q_(10, "m") t1 = calculate_time_to_fall(lunar_module_height) - print(t1) + # print(t1) self.assertAlmostEqual(t1, Q_(1.4285714285714286, "s")) moon_gravity = Q_(1.625, "m/s^2") @@ -580,7 +580,7 @@ def test_issue655b(self): @ureg.check("[length]", "[length]/[time]^2") def pendulum_period(length, G=Q_(1, "standard_gravity")): - print(length) + # print(length) return (2 * math.pi * (length / G) ** 0.5).to("s") length = Q_(1, ureg.m) From 58c469505f77b2a064cafb141c95d00440970430 Mon Sep 17 00:00:00 2001 From: 5igno Date: Tue, 25 Aug 2020 15:33:10 +0200 Subject: [PATCH 539/612] Moves LogUnits implementation to 0.16 --- CHANGES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 60d62eeb8..68fac8dad 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,8 @@ Pint Changelog 0.16 (unreleased) ----------------- -- Nothing changed yet. +- Implements Logarithmic Units like dBm, dB or decade + (Issue #71, Thanks Dima Pustakhod, Clark Willison, Giorgio Signorello, Steven Casagrande, Jonathan Wheeler) 0.15 (2020-08-22) From 0d40f8e234539f466e13dba5b9e93aa13e13d52f Mon Sep 17 00:00:00 2001 From: Kevin Fuhr Date: Tue, 25 Aug 2020 08:23:03 -0600 Subject: [PATCH 540/612] Add missing numpy requirement decorator --- pint/testsuite/test_issues.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 33bd2aa13..f61a5d916 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -745,6 +745,7 @@ def test_issue1112(self): ureg.enable_contexts("c2") ureg.enable_contexts("c3") + @helpers.requires_numpy() def test_issue1144_1102(self): # Performing operations shouldn't modify the original objects # Issue 1144 @@ -761,7 +762,7 @@ def test_issue1144_1102(self): val = [30.0, 45.0, 60.0] * ureg.degree val == 1 1 == val - assert all(val == ([30.0, 45.0, 60.0] * ureg.degree)) + assert all(val == ureg.Quantity([30.0, 45.0, 60.0], "degree")) # Test for another bug identified by searching on "_convert_magnitude" q2 = ureg.Quantity(3, "degree_Kelvin") q1 - q2 From 2e75b734f189cfb81e5f650aeab684e4572053b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Thu, 27 Aug 2020 00:21:54 +0200 Subject: [PATCH 541/612] Replace pkg_resources to importlib.resources - Drop setuptools dependency on runtime - Add importlib-resources backport for python < 3.7 - Update CHANGES - Install package with dependencies when running tests on CI --- .travis.yml | 1 + CHANGES | 2 ++ pint/registry.py | 11 +++++++---- setup.cfg | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7671c7996..dd78a3c8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,6 +65,7 @@ install: # this is superslow but suck it up until updates to pandas are made # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - conda list + - pip install . script: # if we're doing the pandas tests and hence have pytest available, we can diff --git a/CHANGES b/CHANGES index 68fac8dad..845766b5e 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,8 @@ Pint Changelog - Implements Logarithmic Units like dBm, dB or decade (Issue #71, Thanks Dima Pustakhod, Clark Willison, Giorgio Signorello, Steven Casagrande, Jonathan Wheeler) +- Drop dependency on setuptools pkg_resources to read package resources, using std lib importlib.resources instead. + (Issue #1080) 0.15 (2020-08-22) diff --git a/pint/registry.py b/pint/registry.py index 15786d003..097c581ed 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -40,13 +40,17 @@ import os import re from collections import ChainMap, defaultdict -from contextlib import closing, contextmanager +from contextlib import contextmanager from decimal import Decimal from fractions import Fraction from io import StringIO from tokenize import NAME, NUMBER -import pkg_resources +try: + import importlib.resources as importlib_resources +except ImportError: + # Backport for Python < 3.7 + import importlib_resources from . import registry_helpers, systems from .compat import babel_parse, tokenizer @@ -531,8 +535,7 @@ def load_definitions(self, file, is_resource=False): if isinstance(file, str): try: if is_resource: - with closing(pkg_resources.resource_stream(__name__, file)) as fp: - rbytes = fp.read() + rbytes = importlib_resources.read_binary(__package__, file) return self.load_definitions( StringIO(rbytes.decode("utf-8")), is_resource ) diff --git a/setup.cfg b/setup.cfg index df22fae7d..b97221685 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,9 +28,9 @@ zip_safe = True include_package_data = True python_requires = >=3.6 install_requires = - setuptools packaging importlib-metadata; python_version < '3.8' + importlib-resources; python_version < '3.7' setup_requires = setuptools; setuptools_scm test_suite = pint.testsuite.testsuite scripts = pint/pint-convert From 7ba629001cbcba6aaa83771727355c817f4acba8 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 7 Sep 2020 23:51:01 -0300 Subject: [PATCH 542/612] Add pip-wheel-metadata/Pint.dist-info/AUTHORS to gitignore --- .gitignore | 1 + pip-wheel-metadata/Pint.dist-info/AUTHORS | 51 ----------------------- 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 pip-wheel-metadata/Pint.dist-info/AUTHORS diff --git a/.gitignore b/.gitignore index 610ed68ff..ce943cacd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ MANIFEST *pytest_cache* .eggs .mypy_cache +pip-wheel-metadata # WebDAV file system cache files .DAV/ diff --git a/pip-wheel-metadata/Pint.dist-info/AUTHORS b/pip-wheel-metadata/Pint.dist-info/AUTHORS deleted file mode 100644 index b09c14c24..000000000 --- a/pip-wheel-metadata/Pint.dist-info/AUTHORS +++ /dev/null @@ -1,51 +0,0 @@ -Pint is written and maintained by Hernan E. Grecco . - -Other contributors, listed alphabetically, are: - -* Aaron Coleman -* Alexander Böhn -* Ana Krivokapic -* Andrea Zonca -* Andrew Savage -* Brend Wanders -* choloepus -* coutinho -* Clément Pit-Claudel -* Daniel Sokolowski -* Dave Brooks -* David Linke -* Ed Schofield -* Eduard Bopp -* Eli -* Felix Hummel -* Francisco Couzo -* Giel van Schijndel -* Giorgio Signorello -* Guido Imperiale -* Ignacio Fdez. Galván -* James Rowe -* Jim Turner -* Joel B. Mohler -* John David Reaver -* Jonas Olson -* Jules Chéron -* Kaido Kert -* Kenneth D. Mankoff -* Kevin Davies -* Luke Campbell -* Matthieu Dartiailh -* Nate Bogdanowicz -* Peter Grayson -* Richard Barnes -* Ryan Dwyer -* Ryan Kingsbury -* Ryan May -* Sigvald Marholm -* Sundar Raman -* Tiago Coutinho -* Thomas Kluyver -* Tom Ritchford -* Virgil Dupras -* Zebedee Nicholls - -(If you think that your name belongs here, please let the maintainer know) From faac139df5d767555ced253ab9e99f43efa49960 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 7 Sep 2020 23:58:09 -0300 Subject: [PATCH 543/612] Fix the docs --- readthedocs.yml => .readthedocs.yml | 0 docs/tutorial.rst | 6 +++--- requirements_docs.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename readthedocs.yml => .readthedocs.yml (100%) diff --git a/readthedocs.yml b/.readthedocs.yml similarity index 100% rename from readthedocs.yml rename to .readthedocs.yml diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1c64c4ecf..8d6024e08 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -431,7 +431,7 @@ want to avoid creating multiple instances of the unit registry. The best way to do this is by instantiating the registry in a single place. For example, you can add the following code to your package ``__init__.py`` -.. code-block:: +.. code-block:: python from pint import UnitRegistry ureg = UnitRegistry() @@ -440,7 +440,7 @@ example, you can add the following code to your package ``__init__.py`` Then in ``yourmodule.py`` the code would be -.. code-block:: +.. code-block:: python from . import ureg, Q_ @@ -450,7 +450,7 @@ Then in ``yourmodule.py`` the code would be If you are pickling and unplicking Quantities within your project, you should also define the registry as the application registry -.. code-block:: +.. code-block:: python from pint import UnitRegistry, set_application_registry ureg = UnitRegistry() diff --git a/requirements_docs.txt b/requirements_docs.txt index 50e54ef8e..f941167ff 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -4,7 +4,7 @@ nbsphinx numpy pytest pandas -git+https://github.com/hgrecco/pint-pandas.git +pint-pandas jupyter_client ipykernel graphviz From bb99b3c23331852d10c4c4266f8424da2bbf998e Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 8 Sep 2020 00:05:15 -0300 Subject: [PATCH 544/612] Updated docs python to python 3.8 --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 79392e542..4527ed4f3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,7 +5,7 @@ sphinx: configuration: docs/conf.py fail_on_warning: false python: - version: 3.7 + version: 3.8 install: - requirements: requirements_docs.txt - method: pip From f7e9d25e7393a06665011f0c90d527671cb8ea72 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 8 Sep 2020 00:09:41 -0300 Subject: [PATCH 545/612] Enforce pandas>=1.0.4 in requirements_docs --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index f941167ff..a07399ce8 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -3,7 +3,7 @@ matplotlib nbsphinx numpy pytest -pandas +pandas>=1.0.4 pint-pandas jupyter_client ipykernel From 1af9bdff376cf9a6860eae61c0df6e4d32ad5e71 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 8 Sep 2020 00:28:38 -0300 Subject: [PATCH 546/612] Linters --- pint/registry.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pint/registry.py b/pint/registry.py index 097c581ed..a243f4adf 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -46,12 +46,6 @@ from io import StringIO from tokenize import NAME, NUMBER -try: - import importlib.resources as importlib_resources -except ImportError: - # Backport for Python < 3.7 - import importlib_resources - from . import registry_helpers, systems from .compat import babel_parse, tokenizer from .context import Context, ContextChain @@ -85,6 +79,13 @@ to_units_container, ) +try: + import importlib.resources as importlib_resources +except ImportError: + # Backport for Python < 3.7 + import importlib_resources + + _BLOCK_RE = re.compile(r" |\(") From 2510d5bcbc30698085fe10c9bb126fa9d119f942 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 8 Sep 2020 09:32:21 -0300 Subject: [PATCH 547/612] Pin black to ==19.10b0 as 20.8b1 is failing --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dd78a3c8b..9c1a669f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py # for min/max Python versions supported by uncertainties - - PKGS="python=3.7 flake8 black isort" # Have linters fail first and quickly + - PKGS="python=3.7 flake8 black==19.10b0 isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" From 1fb19dff9fa853f6311cf2a664a229b624b0f2e8 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 13 Sep 2020 20:02:07 -0300 Subject: [PATCH 548/612] Updated MANIFEST.in --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index cd897ada1..8729ce863 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt readthedocs.yml .coveragerc +include AUTHORS CHANGES LICENSE README.rst BADGES.rst version.txt .coveragerc .readthedocs.yml recursive-include pint * recursive-include docs * recursive-include bench * From 9a05e600275c592451bda9287205fe0e99a87bb3 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 13 Sep 2020 20:02:21 -0300 Subject: [PATCH 549/612] Preparing release 0.16 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 99a9d9965..c9ce51117 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.16 (unreleased) +0.16 (2020-09-13) ----------------- - Fixed issue where performing an operation of a Quantity with certain units would perform an in-place diff --git a/version.py b/version.py index b98433240..d173bb627 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.16.dev0' +__version__ = '0.16' # fmt: on From cbaf7135899b0ec5e8fa6eeaafb5037ad26d1750 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 13 Sep 2020 20:03:30 -0300 Subject: [PATCH 550/612] Back to development: 0.17 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c9ce51117..a3faeee8c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.17 (unreleased) +-------------- + +- Nothing changed yet. + + 0.16 (2020-09-13) ----------------- diff --git a/version.py b/version.py index d173bb627..40c1ce586 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.16' +__version__ = '0.17.dev0' # fmt: on From 0613b3dff0dcc270e339045812d37501e1b612a7 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 13 Sep 2020 20:58:33 -0300 Subject: [PATCH 551/612] Go back to building the docs in python 3.7 --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 4527ed4f3..79392e542 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,7 +5,7 @@ sphinx: configuration: docs/conf.py fail_on_warning: false python: - version: 3.8 + version: 3.7 install: - requirements: requirements_docs.txt - method: pip From 37445587062cfd0557aae7c5c26905722150e35f Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 14 Sep 2020 19:50:49 -0300 Subject: [PATCH 552/612] pygments>=2.4 in rtd requirements --- requirements_docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_docs.txt b/requirements_docs.txt index a07399ce8..b5fb8a86a 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -13,3 +13,4 @@ sparse dask[complete] setuptools>=41.2 Serialize +pygments>=2.4 From c84d3f4203ddd5f3c0feb5abc7c0ae6040e3a95d Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 21 Sep 2020 23:30:59 -0300 Subject: [PATCH 553/612] Makes unpickling work properly using the Application Registry Close #1175 --- pint/__init__.py | 18 ++++++++++++++++++ pint/measurement.py | 4 ++-- pint/quantity.py | 4 ++-- pint/testsuite/test_issues.py | 9 ++++++++- pint/unit.py | 4 ++-- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 8bea1953c..2cd9c6033 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -89,6 +89,24 @@ def _unpickle(cls, *args): return cls(*args) +def _unpickle_quantity(cls, *args): + """Rebuild quantity upon unpickling using the application registry. + """ + return _unpickle(_APP_REGISTRY.Quantity, *args) + + +def _unpickle_unit(cls, *args): + """Rebuild unit upon unpickling using the application registry. + """ + return _unpickle(_APP_REGISTRY.Unit, *args) + + +def _unpickle_measurement(cls, *args): + """Rebuild measurement upon unpickling using the application registry. + """ + return _unpickle(_APP_REGISTRY.Measurement, *args) + + def set_application_registry(registry): """Set the application registry, which is used for unpickling operations and when invoking pint.Quantity or pint.Unit directly. diff --git a/pint/measurement.py b/pint/measurement.py index 1da1b6447..a1ca2957f 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -70,9 +70,9 @@ def rel(self): def __reduce__(self): # See notes in Quantity.__reduce__ - from . import _unpickle + from . import _unpickle_measurement - return _unpickle, (Measurement, self.magnitude, self._units) + return _unpickle_measurement, (Measurement, self.magnitude, self._units) def __repr__(self): return "".format( diff --git a/pint/quantity.py b/pint/quantity.py index 803b871b7..0395a6544 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -192,11 +192,11 @@ def __reduce__(self): """Allow pickling quantities. Since UnitRegistries are not pickled, upon unpickling the new object is always attached to the application registry. """ - from . import _unpickle + from . import _unpickle_quantity # Note: type(self) would be a mistake as subclasses built by # build_quantity_class can't be pickled - return _unpickle, (Quantity, self.magnitude, self._units) + return _unpickle_quantity, (Quantity, self.magnitude, self._units) def __new__(cls, value, units=None): if is_upcast_type(type(value)): diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index a1ca05097..a3b09c888 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -5,7 +5,7 @@ import pytest -from pint import Context, DimensionalityError, UnitRegistry +from pint import Context, DimensionalityError, UnitRegistry, get_application_registry from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.unit import UnitsContainer @@ -775,6 +775,13 @@ def test_issue_1136(self): with pytest.raises(DimensionalityError): 2 ** ureg.Quantity([2, 3], "m") + def test_issue1175(self): + import pickle + foo1 = get_application_registry().Quantity(1, 's') + foo2 = pickle.loads(pickle.dumps(foo1)) + self.assertIsInstance(foo1, foo2.__class__) + self.assertIsInstance(foo2, foo1.__class__) + if np is not None: diff --git a/pint/unit.py b/pint/unit.py index f09f39e31..eff3c4bca 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -28,9 +28,9 @@ class Unit(PrettyIPython, SharedRegistryObject): def __reduce__(self): # See notes in Quantity.__reduce__ - from . import _unpickle + from . import _unpickle_unit - return _unpickle, (Unit, self._units) + return _unpickle_unit, (Unit, self._units) def __init__(self, units): super().__init__() From 2fbd3f87a9e9a36da94bb0756b12656c0ddc8291 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 21 Sep 2020 23:35:07 -0300 Subject: [PATCH 554/612] Linters and CHANGES --- CHANGES | 4 ++-- pint/testsuite/test_issues.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index a3faeee8c..72853c20c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,8 +4,8 @@ Pint Changelog 0.17 (unreleased) -------------- -- Nothing changed yet. - +- Fix unpickling, now it is using the APP_REGISTRY as expected. + (Issue #1175) 0.16 (2020-09-13) ----------------- diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index a3b09c888..e39b01a54 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -777,7 +777,8 @@ def test_issue_1136(self): def test_issue1175(self): import pickle - foo1 = get_application_registry().Quantity(1, 's') + + foo1 = get_application_registry().Quantity(1, "s") foo2 = pickle.loads(pickle.dumps(foo1)) self.assertIsInstance(foo1, foo2.__class__) self.assertIsInstance(foo2, foo1.__class__) From 6b0b531d5311ab0f61aefd3e76a8dec9144d6b81 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 22 Sep 2020 00:12:24 -0300 Subject: [PATCH 555/612] Preparing release 0.16.1 --- CHANGES | 4 ++-- version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 72853c20c..823e6c4d2 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,8 @@ Pint Changelog ============== -0.17 (unreleased) --------------- +0.16.1 (2020-09-22) +------------------- - Fix unpickling, now it is using the APP_REGISTRY as expected. (Issue #1175) diff --git a/version.py b/version.py index 40c1ce586..50aa8628e 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.17.dev0' +__version__ = '0.16.1' # fmt: on From 5e59f373ea237a95f22960a2b169c3a756a264c9 Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 22 Sep 2020 00:12:57 -0300 Subject: [PATCH 556/612] Back to development: 0.17 --- CHANGES | 6 ++++++ version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 823e6c4d2..b444cb38e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,12 @@ Pint Changelog ============== +0.17 (unreleased) +----------------- + +- Nothing changed yet. + + 0.16.1 (2020-09-22) ------------------- diff --git a/version.py b/version.py index 50aa8628e..40c1ce586 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.16.1' +__version__ = '0.17.dev0' # fmt: on From 26cf55fc3d29fd3f5c0b593c1baf50a43ce51eb6 Mon Sep 17 00:00:00 2001 From: Ryan Rowe Date: Fri, 25 Sep 2020 19:31:48 -0700 Subject: [PATCH 557/612] Fix error on UnitsContainer equality check --- pint/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pint/util.py b/pint/util.py index 614849361..f2162f456 100644 --- a/pint/util.py +++ b/pint/util.py @@ -431,7 +431,11 @@ def __eq__(self, other): other = other._d elif isinstance(other, str): - other = ParserHelper.from_string(other, self._non_int_type) + try: + other = ParserHelper.from_string(other, self._non_int_type) + except DefinitionSyntaxError: + return False + other = other._d return dict.__eq__(self._d, other) From cf773bdd24c37a8181f5fd412acb14318afa8aba Mon Sep 17 00:00:00 2001 From: Kevin Fuhr Date: Thu, 8 Oct 2020 13:54:38 -0400 Subject: [PATCH 558/612] Fixes #1185 - Power for pseudo-dimensionless units --- CHANGES | 3 ++- pint/quantity.py | 21 ++++++++++++++++----- pint/testsuite/test_issues.py | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index b444cb38e..7d426ae60 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,8 @@ Pint Changelog 0.17 (unreleased) ----------------- -- Nothing changed yet. +- Fix issue with reducable dimensionless units when using power (Quantity**ndarray) + (Issue #1185) 0.16.1 (2020-09-22) diff --git a/pint/quantity.py b/pint/quantity.py index 0395a6544..4a3fce39d 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1386,15 +1386,19 @@ def __ipow__(self, other): if is_duck_array_type(type(getattr(other, "_magnitude", other))): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units - # unless the base is dimensionless. + # unless the base is dimensionless. Ensure dimensionless + # units are reduced to "dimensionless". + # Note: this will strip Units of degrees or radians from Quantity if self.dimensionless: if getattr(other, "dimensionless", False): - self._magnitude **= other.m_as("") + self._magnitude = self.m_as("") ** other.m_as("") + self._units = self.UnitsContainer() return self elif not getattr(other, "dimensionless", True): raise DimensionalityError(other._units, "dimensionless") else: - self._magnitude **= other + self._magnitude = self.m_as("") ** other + self._units = self.UnitsContainer() return self elif np.size(other) > 1: raise DimensionalityError( @@ -1444,13 +1448,20 @@ def __pow__(self, other): # arrays are refused as exponent, because they would create # len(array) quantities of len(set(array)) different units # unless the base is dimensionless. + # Note: this will strip Units of degrees or radians from Quantity if self.dimensionless: if getattr(other, "dimensionless", False): - return self.__class__(self.m ** other.m_as("")) + return self.__class__( + self._convert_magnitude_not_inplace(self.UnitsContainer()) + ** other.m_as("") + ) elif not getattr(other, "dimensionless", True): raise DimensionalityError(other._units, "dimensionless") else: - return self.__class__(self.m ** other) + return self.__class__( + self._convert_magnitude_not_inplace(self.UnitsContainer()) + ** other + ) elif np.size(other) > 1: raise DimensionalityError( self._units, diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index e39b01a54..d6f329113 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -783,6 +783,26 @@ def test_issue1175(self): self.assertIsInstance(foo1, foo2.__class__) self.assertIsInstance(foo2, foo1.__class__) + @helpers.requires_numpy() + def test_issue_1185(self): + # Test __pow__ + foo = ureg.Quantity((3, 3), "mm / cm") + assert np.allclose(foo ** ureg.Quantity([2, 3], ""), 0.3 ** np.array([2, 3])) + assert np.allclose(foo ** np.array([2, 3]), 0.3 ** np.array([2, 3])) + assert np.allclose(np.array([2, 3]) ** foo, np.array([2, 3]) ** 0.3) + # Test __ipow__ + foo **= np.array([2, 3]) + assert np.allclose(foo, 0.3 ** np.array([2, 3])) + # Test __rpow__ + assert np.allclose( + np.array((1, 1)).__rpow__(ureg.Quantity((2, 3), "mm / cm")), + np.array((0.2, 0.3)), + ) + assert np.allclose( + ureg.Quantity((20, 20), "mm / cm").__rpow__(np.array((0.2, 0.3))), + np.array((0.04, 0.09)), + ) + if np is not None: @@ -817,3 +837,9 @@ def test_issue925(callable, q): type_before = type(q._magnitude) callable(q) assert isinstance(q._magnitude, type_before) + + +if __name__ == "__main__": + obj = TestIssues() + obj.setUp() + obj.test_issue_1185() From fc4c74febb522d2c32dce4220371a967e34047dc Mon Sep 17 00:00:00 2001 From: Kevin Fuhr Date: Thu, 8 Oct 2020 13:58:02 -0400 Subject: [PATCH 559/612] Remove testing code --- pint/testsuite/test_issues.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index d6f329113..583c8fb44 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -837,9 +837,3 @@ def test_issue925(callable, q): type_before = type(q._magnitude) callable(q) assert isinstance(q._magnitude, type_before) - - -if __name__ == "__main__": - obj = TestIssues() - obj.setUp() - obj.test_issue_1185() From b6b22f38dccac119920acb6a7b5895da7b310f33 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 5 Oct 2020 14:44:10 -0300 Subject: [PATCH 560/612] Implements a first benchmark suite. The benchmark is based on airspeed velocity (asv). Benchmarks are organized with double digits numbers to create a nice, sorted 00-09: Benchmarks that do not require a registry created during setup 10-19: Benchmarks calling functions directly from the registry 20-29: Benchmarks involving Units and Quantity objects (with scalar values) 30-39: Benchmarks involving Quantity objects (with array values) Briefly, you can run the the benchmark calling: $ asv run or if you want to compare certain commits/branches: $ asv run master..mybranch For convenience, we include a list of hashes of all pint versions: $ asv run HASHFILE:benchmarks/hashes.txt -- After running the benchmark, run: $ asv publish $ asv preview To get a nice readable output. More information on: https://asv.readthedocs.io/en/stable/ --- .gitignore | 3 + CHANGES | 2 +- asv.conf.json | 160 +++++++++++++++++++++++++++++ bench/bench.py | 146 -------------------------- bench/bench_base.yaml | 42 -------- bench/bench_numpy.yaml | 23 ----- benchmarks/00_common.py | 2 + benchmarks/01_registry_creation.py | 10 ++ benchmarks/10_registry.py | 101 ++++++++++++++++++ benchmarks/20_quantity.py | 56 ++++++++++ benchmarks/30_numpy.py | 97 +++++++++++++++++ benchmarks/__init__.py | 0 benchmarks/hashes.txt | 32 ++++++ benchmarks/util.py | 38 +++++++ 14 files changed, 500 insertions(+), 212 deletions(-) create mode 100644 asv.conf.json delete mode 100644 bench/bench.py delete mode 100644 bench/bench_base.yaml delete mode 100644 bench/bench_numpy.yaml create mode 100644 benchmarks/00_common.py create mode 100644 benchmarks/01_registry_creation.py create mode 100644 benchmarks/10_registry.py create mode 100644 benchmarks/20_quantity.py create mode 100644 benchmarks/30_numpy.py create mode 100644 benchmarks/__init__.py create mode 100644 benchmarks/hashes.txt create mode 100644 benchmarks/util.py diff --git a/.gitignore b/.gitignore index ce943cacd..e1ec26a30 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ notebooks/pandas_test.csv # dask stuff dask-worker-space + +# airspeed velocity bechmark +.asv/ diff --git a/CHANGES b/CHANGES index b444cb38e..14fe9092d 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Pint Changelog 0.17 (unreleased) ----------------- -- Nothing changed yet. +- Implemented benchmarks based on airspeed velocity. 0.16.1 (2020-09-22) diff --git a/asv.conf.json b/asv.conf.json new file mode 100644 index 000000000..c8c82b595 --- /dev/null +++ b/asv.conf.json @@ -0,0 +1,160 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "pint", + + // The project's homepage + "project_url": "https://github.com/hgrecco/pint", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": ".", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + // "repo_subdir": "", + + // Customizable commands for building, installing, and + // uninstalling the project. See asv.conf.json documentation. + // + // "install_command": ["in-dir={env_dir} python -mpip install {wheel_file}"], + // "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], + // "build_command": [ + // "python setup.py build", + // "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" + // ], + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "default" (for mercurial). + // "branches": ["master"], // for git + // "branches": ["default"], // for mercurial + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "conda", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + "show_commit_url": "http://github.com/hgrecco/pint/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + "pythons": ["3.7"], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + // "conda_channels": ["conda-forge", "defaults"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + + "matrix": { + "numpy": ["1.19"], + // "six": ["", null], // test with and without six installed + // "pip+emcee": [""], // emcee is only available for install with pip. + }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "six": null}, // don't run without six on conda + // ], + // + // "include": [ + // // additional env for python2.7 + // {"python": "2.7", "numpy": "1.8"}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + // "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": ".asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": ".asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": ".asv/html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + // "build_cache_size": 2, + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // }, + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // }, +} diff --git a/bench/bench.py b/bench/bench.py deleted file mode 100644 index c3c9323b2..000000000 --- a/bench/bench.py +++ /dev/null @@ -1,146 +0,0 @@ -import copy -import fnmatch -import os -from timeit import Timer - -import yaml - - -def time_stmt(stmt="pass", setup="pass", number=0, repeat=3): - """Timer function with the same behaviour as running `python -m timeit ` - in the command line. - - Parameters - ---------- - stmt : str - (Default value = "pass") - setup : str - (Default value = "pass") - number : int - (Default value = 0) - repeat : int - (Default value = 3) - - Returns - ------- - float - elapsed time in seconds or NaN if the command failed. - - """ - - t = Timer(stmt, setup) - - if not number: - # determine number so that 0.2 <= total time < 2.0 - for i in range(1, 10): - number = 10 ** i - - try: - x = t.timeit(number) - except Exception: - print(t.print_exc()) - return float("NaN") - - if x >= 0.2: - break - - try: - r = t.repeat(repeat, number) - except Exception: - print(t.print_exc()) - return float("NaN") - - best = min(r) - - return best / number - - -def build_task(task, name="", setup="", number=0, repeat=3): - nt = copy.copy(task) - - nt["name"] = (name + " " + task.get("name", "")).strip() - nt["setup"] = (setup + "\n" + task.get("setup", "")).strip("\n") - nt["stmt"] = task.get("stmt", "") - nt["number"] = task.get("number", number) - nt["repeat"] = task.get("repeat", repeat) - - return nt - - -def time_task(name, stmt="pass", setup="pass", number=0, repeat=3, stmts="", base=""): - - if base: - nvalue = time_stmt(stmt=base, setup=setup, number=number, repeat=repeat) - yield name + " (base)", nvalue - suffix = " (normalized)" - else: - nvalue = 1.0 - suffix = "" - - if stmt: - value = time_stmt(stmt=stmt, setup=setup, number=number, repeat=repeat) - yield name, value / nvalue - - for task in stmts: - new_task = build_task(task, name, setup, number, repeat) - for task_name, value in time_task(**new_task): - yield task_name + suffix, value / nvalue - - -def time_file(filename, name="", setup="", number=0, repeat=3): - """Open a yaml benchmark file an time each statement, - - yields a tuple with filename, task name, time in seconds. - - Parameters - ---------- - filename : - - name : - (Default value = "") - setup : - (Default value = "") - number : - (Default value = 0) - repeat : - (Default value = 3) - - Returns - ------- - - """ - with open(filename, "r") as fp: - tasks = yaml.load(fp) - - for task in tasks: - new_task = build_task(task, name, setup, number, repeat) - for task_name, value in time_task(**new_task): - yield task_name, value - - -def recursive_glob(rootdir=".", pattern="*"): - return [ - os.path.join(looproot, filename) - for looproot, _, filenames in os.walk(rootdir) - for filename in filenames - if fnmatch.fnmatch(filename, pattern) - ] - - -def main(filenames=None): - if not filenames: - filenames = recursive_glob(".", "bench_*.yaml") - elif isinstance(filenames, str): - filenames = [filenames] - - for filename in filenames: - print(filename) - print("-" * len(filename)) - print() - for task_name, value in time_file(filename): - print(f"{value:.2e} {task_name}") - print() - - -if __name__ == "__main__": - main() diff --git a/bench/bench_base.yaml b/bench/bench_base.yaml deleted file mode 100644 index f767a041a..000000000 --- a/bench/bench_base.yaml +++ /dev/null @@ -1,42 +0,0 @@ - - -- name: importing - stmt: import pint - -- name: empty registry - setup: import pint - stmt: ureg = pint.UnitRegistry(None) - -- name: default registry - setup: import pint - stmt: ureg = pint.UnitRegistry() - -- name: finding meter - setup: | - import pint - ureg = pint.UnitRegistry() - stmts: - - name: (attr) - stmt: q = ureg.meter - - name: (item) - stmt: q = ureg['meter'] - -- name: base units - setup: | - import pint - ureg = pint.UnitRegistry() - stmts: - - name: meter - stmt: ureg.get_base_units('meter') - - name: yard - stmt: ureg.get_base_units('yard') - - name: meter / second - stmt: ureg.get_base_units('meter / second') - - name: yard / minute - stmt: ureg.get_base_units('yard / minute') - -- name: build cache - setup: | - import pint - ureg = pint.UnitRegistry() - stmt: ureg._build_cache() diff --git a/bench/bench_numpy.yaml b/bench/bench_numpy.yaml deleted file mode 100644 index 8609bc025..000000000 --- a/bench/bench_numpy.yaml +++ /dev/null @@ -1,23 +0,0 @@ - - -- name: NumPy - setup: | - import numpy as np - import pint - ureg = pint.UnitRegistry() - stmts: - - name: cosine - setup: | - d = np.arange(0, 90, 10) - r = np.deg2rad(d) - base: np.cos(r) - stmts: - - name: radian - setup: x = r * ureg.radian - stmt: np.cos(x) - - name: dimensionless - setup: x = r * ureg.dimensionless - stmt: np.cos(x) - - name: degree - setup: x = d * ureg.degree - stmt: np.cos(x) diff --git a/benchmarks/00_common.py b/benchmarks/00_common.py new file mode 100644 index 000000000..4444d5d37 --- /dev/null +++ b/benchmarks/00_common.py @@ -0,0 +1,2 @@ +def time_import(): + import pint diff --git a/benchmarks/01_registry_creation.py b/benchmarks/01_registry_creation.py new file mode 100644 index 000000000..e5555859a --- /dev/null +++ b/benchmarks/01_registry_creation.py @@ -0,0 +1,10 @@ +import pint + +from . import util + + +def time_create_registry(args): + pint.UnitRegistry(*args) + + +time_create_registry.params = [[(None,), tuple(), (util.get_tiny_def(),)]] diff --git a/benchmarks/10_registry.py b/benchmarks/10_registry.py new file mode 100644 index 000000000..d95a53fce --- /dev/null +++ b/benchmarks/10_registry.py @@ -0,0 +1,101 @@ +import pint + +from . import util + +units = ("meter", "kilometer", "second", "minute", "angstrom") + +other_units = ("meter", "angstrom", "kilometer/second", "angstrom/minute") + +all_values = ("int", "float", "complex") + +ureg = None +data = {} + + +def setup(*args): + + global ureg, data + + data["int"] = 1 + data["float"] = 1.0 + data["complex"] = complex(1, 2) + + ureg = pint.UnitRegistry(util.get_tiny_def()) + + +def my_setup(*args): + global data + setup(*args) + for unit in units + other_units: + data["uc_%s" % unit] = pint.registry.to_units_container(unit, ureg) + + +def time_build_cache(): + ureg._build_cache() + + +def time_getattr(key): + getattr(ureg, key) + + +time_getattr.params = units + + +def time_getitem(key): + ureg[key] + + +time_getitem.params = units + + +def time_parse_unit_name(key): + ureg.parse_unit_name(key) + + +time_parse_unit_name.params = units + + +def time_parse_units(key): + ureg.parse_units(key) + + +time_parse_units.params = units + + +def time_parse_expression(key): + ureg.parse_expression("1.0 " + key) + + +time_parse_expression.params = units + + +def time_base_units(unit): + ureg.get_base_units(unit) + + +time_base_units.params = other_units + + +def time_to_units_container_registry(unit): + pint.registry.to_units_container(unit, ureg) + + +time_to_units_container_registry.params = other_units + + +def time_to_units_container_detached(unit): + pint.registry.to_units_container(unit, ureg) + + +time_to_units_container_detached.params = other_units + + +def time_convert_from_uc(key): + src, dst = key + ureg._convert(1.0, data[src], data[dst]) + + +time_convert_from_uc.setup = my_setup +time_convert_from_uc.params = [ + (("uc_meter", "uc_kilometer"), ("uc_kilometer/second", "uc_angstrom/minute")) +] diff --git a/benchmarks/20_quantity.py b/benchmarks/20_quantity.py new file mode 100644 index 000000000..5f6dd418a --- /dev/null +++ b/benchmarks/20_quantity.py @@ -0,0 +1,56 @@ +import itertools as it +import operator + +import pint + +from . import util + +units = ("meter", "kilometer", "second", "minute", "angstrom") +all_values = ("int", "float", "complex") +all_values_q = tuple( + "%s_%s" % (a, b) for a, b in it.product(all_values, ("meter", "kilometer")) +) + +op1 = (operator.neg, operator.truth) +op2_cmp = (operator.eq,) # operator.lt) +op2_math = (operator.add, operator.sub, operator.mul, operator.truediv) + +ureg = None +data = {} + + +def setup(*args): + + global ureg, data + + data["int"] = 1 + data["float"] = 1.0 + data["complex"] = complex(1, 2) + + ureg = pint.UnitRegistry(util.get_tiny_def()) + + for key in all_values: + data[key + "_meter"] = data[key] * ureg.meter + data[key + "_kilometer"] = data[key] * ureg.kilometer + + +def time_build_by_mul(key): + data[key] * ureg.meter + + +time_build_by_mul.params = all_values + + +def time_op1(key, op): + op(data[key]) + + +time_op1.params = [all_values_q, op1] + + +def time_op2(keys, op): + key1, key2 = keys + op(data[key1], data[key2]) + + +time_op2.params = [tuple(it.product(all_values_q, all_values_q)), op2_math + op2_cmp] diff --git a/benchmarks/30_numpy.py b/benchmarks/30_numpy.py new file mode 100644 index 000000000..ec838335f --- /dev/null +++ b/benchmarks/30_numpy.py @@ -0,0 +1,97 @@ +import itertools as it +import operator + +import numpy as np + +import pint + +from . import util + +lengths = ("short", "mid") +all_values = tuple( + "%s_%s" % (a, b) for a, b in it.product(lengths, ("list", "tuple", "array")) +) +all_arrays = ("short_array", "mid_array") +units = ("meter", "kilometer") +all_arrays_q = tuple("%s_%s" % (a, b) for a, b in it.product(all_arrays, units)) + +ureg = None +data = {} +op1 = (operator.neg,) # operator.truth, +op2_cmp = (operator.eq, operator.lt) +op2_math = (operator.add, operator.sub, operator.mul, operator.truediv) +numpy_op2_cmp = (np.equal, np.less) +numpy_op2_math = (np.add, np.subtract, np.multiply, np.true_divide) + + +def float_range(n): + return (float(x) for x in range(1, n + 1)) + + +def setup(*args): + + global ureg, data + short = list(float_range(3)) + mid = list(float_range(1_000)) + + data["short_list"] = short + data["short_tuple"] = tuple(short) + data["short_array"] = np.asarray(short) + data["mid_list"] = mid + data["mid_tuple"] = tuple(mid) + data["mid_array"] = np.asarray(mid) + + ureg = pint.UnitRegistry(util.get_tiny_def()) + + for key in all_arrays: + data[key + "_meter"] = data[key] * ureg.meter + data[key + "_kilometer"] = data[key] * ureg.kilometer + + +def time_finding_meter_getattr(): + ureg.meter + + +def time_finding_meter_getitem(): + ureg["meter"] + + +def time_base_units(unit): + ureg.get_base_units(unit) + + +time_base_units.params = ["meter", "angstrom", "meter/second", "angstrom/minute"] + + +def time_build_by_mul(key): + data[key] * ureg.meter + + +time_build_by_mul.params = all_arrays + + +def time_op1(key, op): + op(data[key]) + + +time_op1.params = [all_arrays_q, op1 + (np.sqrt, np.square)] + + +def time_op2(keys, op): + key1, key2 = keys + op(data[key1], data[key2]) + + +time_op2.params = [ + ( + ("short_array_meter", "short_array_meter"), + ("short_array_meter", "short_array_kilometer"), + ("short_array_kilometer", "short_array_meter"), + ("short_array_kilometer", "short_array_kilometer"), + ("mid_array_meter", "mid_array_meter"), + ("mid_array_meter", "mid_array_kilometer"), + ("mid_array_kilometer", "mid_array_meter"), + ("mid_array_kilometer", "mid_array_kilometer"), + ), + op2_math + op2_cmp + numpy_op2_math + numpy_op2_cmp, +] diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/benchmarks/hashes.txt b/benchmarks/hashes.txt new file mode 100644 index 000000000..473130bad --- /dev/null +++ b/benchmarks/hashes.txt @@ -0,0 +1,32 @@ +04468ad216555cab2b50dbae62b57f56bf694cc3 +a964aebfb904d5ac4cb2645478f38189b27ad97a +7f77dabbfcd1be8b5943cbb6ee3b049c66d337f6 +d78cc94a9538aeb91b30494bdcde1348cab0e2f6 +040b01283470b95d23a244eb1a791dfd189502c6 +5ca84aca6002ec1550417bf791b3f8cc9e551818 +2dc133ef61baf3f6b2b54814cbeb3299282ee794 +1d3f26bfcabab7751bd324d1fd48825667c2e949 +126e00f691007ff71e6a2c327b19f92d8a478adb +2c0c4ee67f732b0d1b7530e8363b15c57b936b97 +619b95ee13f82560c3cd596095bcd606ba0277ae +6e18639b3db4121322e0fc7009df463b8dc7ee9f +fe0a64b270dbde0946fdcb0819aad178d42f77e6 +6aea7d4aaf424e2a514c004f99b271ecfb446237 +c48c441ec6b8919f104edd6affb57b69aa275a38 +7d9837ead056af2e36009a0b0ed8d1a9e9bf3947 +e7e7de5ca2e1c19963be8a918369fb19186f9a73 +f65bebb6930fdd5ec891a905c30489a52add05f6 +a171cdfbab64903ae67d7461b0904f350191ee29 +7196ae6794a5f7b3ab0a48e880f1797a218e59de +5ee3ff950709ab356d7349d6b56818b0c5fe624b +697834d17d6fb5bb5d0adf945a5d5f5d187506a1 +2b296e79f0260b8d547af8d078178f079d90a3d9 +a91762071e3cecc8d995c993a3175f84ed9ec804 +82b2c2f97b3f929568a07850de4f75b0d16f3c56 +3d713cf920c1f164c1c3c2fcfab96956467b0d64 +eed85601c8bdf0829ed256cdf2af9982bc9ed5a2 +4e6dbff740292ae57230686ad4417e46d63e171c +222a1c89e9ce5c24debc749558cf5eb3b231f89a +85603e644e9ba004188ef79e499b9a40557b446c +9a05e600275c592451bda9287205fe0e99a87bb3 +6b0b531d5311ab0f61aefd3e76a8dec9144d6b81 \ No newline at end of file diff --git a/benchmarks/util.py b/benchmarks/util.py new file mode 100644 index 000000000..4e72048c5 --- /dev/null +++ b/benchmarks/util.py @@ -0,0 +1,38 @@ +import io + +SMALL_VEC_LEN = 3 +MID_VEC_LEN = 1_000 +LARGE_VEC_LEN = 1_000_000 + +TINY_DEF = """ +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- = deka- +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- + +meter = [length] = m = metre +second = [time] = s = sec + +angstrom = 1e-10 * meter = Å = ångström = Å +minute = 60 * second = min +""" + + +def get_tiny_def(): + return io.StringIO(TINY_DEF) From 3b6198b78f804ce0479682729509631f5c308d5a Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 9 Oct 2020 18:20:08 -0300 Subject: [PATCH 561/612] import in benchmark should be ignored by linter --- benchmarks/00_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/00_common.py b/benchmarks/00_common.py index 4444d5d37..261cea932 100644 --- a/benchmarks/00_common.py +++ b/benchmarks/00_common.py @@ -1,2 +1,2 @@ def time_import(): - import pint + import pint # noqa: F401 From 124764af76b4c37b97bd0022314347997d487e76 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Wed, 14 Oct 2020 22:58:40 +0800 Subject: [PATCH 562/612] Correct a typo in systems.py --- pint/systems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/systems.py b/pint/systems.py index b72ce53c0..277865b48 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -264,7 +264,7 @@ class System(SharedRegistryObject): - old_unit_name: a root unit part which is going to be removed from the system. - new_unit_name: a non root unit which is going to replace the old_unit. - If the new_unit_name and the old_unit_name, the later and the colon can be ommited. + If the new_unit_name and the old_unit_name, the later and the colon can be omitted. """ #: Regex to match the header parts of a context. From 1d114004d864503b0cffbc4e569b0f09353bc9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Wed, 28 Oct 2020 23:12:39 +0100 Subject: [PATCH 563/612] Fix scalar ndarray tolist function --- CHANGES | 2 +- pint/quantity.py | 22 ++++++++++++++++------ pint/testsuite/test_numpy.py | 8 ++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index b444cb38e..e7b85d248 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Pint Changelog 0.17 (unreleased) ----------------- -- Nothing changed yet. +- Fix tolist function with scalar ndarray. (Issue #1195) 0.16.1 (2020-09-22) diff --git a/pint/quantity.py b/pint/quantity.py index 0395a6544..6c59f7ce8 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1879,12 +1879,22 @@ def __setitem__(self, key, value): def tolist(self): units = self._units - return [ - self.__class__(value, units).tolist() - if isinstance(value, list) - else self.__class__(value, units) - for value in self._magnitude.tolist() - ] + + try: + values = self._magnitude.tolist() + if not isinstance(values, list): + return self.__class__(values, units) + + return [ + self.__class__(value, units).tolist() + if isinstance(value, list) + else self.__class__(value, units) + for value in self._magnitude.tolist() + ] + except AttributeError: + raise AttributeError( + f"Magnitude '{type(self._magnitude).__name__}' does not support tolist." + ) # Measurement support def plus_minus(self, error, relative=False): diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index d9cee5769..3be74f8c4 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -26,6 +26,10 @@ def setUpClass(cls): def q(self): return [[1, 2], [3, 4]] * self.ureg.m + @property + def q_scalar(self): + return np.array(5) * self.ureg.m + @property def q_nan(self): return [[1, 2], [3, np.nan]] * self.ureg.m @@ -497,6 +501,10 @@ def test_tolist(self): self.q.tolist(), [[1 * self.ureg.m, 2 * self.ureg.m], [3 * self.ureg.m, 4 * self.ureg.m]], ) + self.assertEqual(self.q_scalar.tolist(), 5 * self.ureg.m) + + with self.assertRaises(AttributeError): + (5 * self.ureg.m).tolist() def test_fill(self): tmp = self.q From 0fedfcee89100781e4168acf0ef7cfcdbcb01a8a Mon Sep 17 00:00:00 2001 From: Robin Tesse Date: Sat, 31 Oct 2020 10:29:33 +0100 Subject: [PATCH 564/612] Add sign to numpy_func.py --- docs/numpy.ipynb | 4 ++-- pint/numpy_func.py | 2 +- pint/testsuite/test_issues.py | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index fa987b280..5edf125bf 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -203,7 +203,7 @@ "- **Math operations**: `add`, `subtract`, `multiply`, `divide`, `logaddexp`, `logaddexp2`, `true_divide`, `floor_divide`, `negative`, `remainder`, `mod`, `fmod`, `absolute`, `rint`, `sign`, `conj`, `exp`, `exp2`, `log`, `log2`, `log10`, `expm1`, `log1p`, `sqrt`, `square`, `cbrt`, `reciprocal`\n", "- **Trigonometric functions**: `sin`, `cos`, `tan`, `arcsin`, `arccos`, `arctan`, `arctan2`, `hypot`, `sinh`, `cosh`, `tanh`, `arcsinh`, `arccosh`, `arctanh`\n", "- **Comparison functions**: `greater`, `greater_equal`, `less`, `less_equal`, `not_equal`, `equal`\n", - "- **Floating functions**: `isreal`, `iscomplex`, `isfinite`, `isinf`, `isnan`, `signbit`, `copysign`, `nextafter`, `modf`, `ldexp`, `frexp`, `fmod`, `floor`, `ceil`, `trunc`\n", + "- **Floating functions**: `isreal`, `iscomplex`, `isfinite`, `isinf`, `isnan`, `signbit`, `sign`, `copysign`, `nextafter`, `modf`, `ldexp`, `frexp`, `fmod`, `floor`, `ceil`, `trunc`\n", "\n", "And the following NumPy functions:" ] @@ -500,4 +500,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 0f220b00e..bcf406acb 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -339,7 +339,7 @@ def implementation(*args, **kwargs): - `op_units_output_ufuncs`: determine output unit from input unit as determined by operation (see `get_op_output_unit`) """ -strip_unit_input_output_ufuncs = ["isnan", "isinf", "isfinite", "signbit"] +strip_unit_input_output_ufuncs = ["isnan", "isinf", "isfinite", "signbit", "sign"] matching_input_bare_output_ufuncs = [ "equal", "greater", diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index e39b01a54..0bb7caef4 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -783,6 +783,12 @@ def test_issue1175(self): self.assertIsInstance(foo1, foo2.__class__) self.assertIsInstance(foo2, foo1.__class__) + @helpers.requires_numpy() + def test_issue1174(self): + q = [1.0, -2.0, 3.0, -4.0] * self.ureg.meter + self.assertTrue(np.sign(q[0].magnitude)) + self.assertTrue(np.sign(q[1].magnitude)) + if np is not None: From 79dc97e652d67f4a9eae4b8be6590374a32edd17 Mon Sep 17 00:00:00 2001 From: "Lewis A. Marshall" Date: Sat, 31 Oct 2020 11:00:28 -0700 Subject: [PATCH 565/612] Quantity comparisons ensure other is Quantity. --- pint/quantity.py | 10 +++++++++- pint/testsuite/test_measurement.py | 8 +++++++- pint/testsuite/test_quantity.py | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pint/quantity.py b/pint/quantity.py index 0395a6544..92cb81b6e 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1577,7 +1577,7 @@ def __ne__(self, other): @check_implemented def compare(self, other, op): - if not isinstance(other, self.__class__): + if not isinstance(other, Quantity): if self.dimensionless: return op( self._convert_magnitude_not_inplace(self.UnitsContainer()), other @@ -1598,6 +1598,14 @@ def compare(self, other, op): else: raise ValueError("Cannot compare Quantity and {}".format(type(other))) + + # Registry equality check based on util.SharedRegistryObject + if self._REGISTRY is not other._REGISTRY: + mess = "Cannot operate with {} and {} of different registries." + raise ValueError( + mess.format(self.__class__.__name__, other.__class__.__name__) + ) + if self._units == other._units: return op(self._magnitude, other._magnitude) if self.dimensionality != other.dimensionality: diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index b5acff0e3..5ebb33b82 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -86,7 +86,7 @@ def test_format_u(self): ("{:.3uL}", r"\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}"), ("{:.3uH}", "(0.2000 ± 0.0100) second2"), ("{:.3uC}", "(0.2000+/-0.0100) second**2"), - ("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}",), + ("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}"), ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), ): with self.subTest(spec): @@ -236,3 +236,9 @@ def test_propagate_product(self): r.value.magnitude, ml.value.magnitude / mr.value.magnitude ) self.assertEqual(r.value.units, ml.value.units / mr.value.units) + + def test_measurement_comparison(self): + x = self.Q_(4.2, "meter") + y = self.Q_(5.0, "meter").plus_minus(0.1) + self.assertTrue(x<=y) + self.assertFalse(x>=y) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index b5780e568..fe86801c0 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 import DimensionalityError, OffsetUnitCalculusError, UnitRegistry, Quantity, get_application_registry from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase @@ -63,6 +63,8 @@ def test_quantity_comparison(self): y = self.Q_(4.2, "meter") z = self.Q_(5, "meter") j = self.Q_(5, "meter*meter") + k = 5 * get_application_registry().meter # Include a comparison to the application registry + l = Quantity(5, "meter") # Include a comparison to a directly created Quantity # identity for single object self.assertTrue(x == x) @@ -81,6 +83,11 @@ def test_quantity_comparison(self): self.assertTrue(x != z) self.assertTrue(x < z) + # Compare with items to the separate application registry + self.assertTrue(k >= l) # These should both be from application registry + with self.assertRaises(ValueError): + z>l # One from local registry, one from application registry + self.assertTrue(z != j) self.assertNotEqual(z, j) From 8a93b6f0a58868a72b666359976c45f29e6cc809 Mon Sep 17 00:00:00 2001 From: "Lewis A. Marshall" Date: Sat, 31 Oct 2020 11:11:17 -0700 Subject: [PATCH 566/612] Formatting and changelog. --- CHANGES | 3 ++- pint/quantity.py | 3 +-- pint/testsuite/test_measurement.py | 4 ++-- pint/testsuite/test_quantity.py | 18 +++++++++++++----- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index b444cb38e..83498df9e 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,8 @@ Pint Changelog 0.17 (unreleased) ----------------- -- Nothing changed yet. +- Fix comparisons between Quantities and Measurements. + (Issue #1134) 0.16.1 (2020-09-22) diff --git a/pint/quantity.py b/pint/quantity.py index 92cb81b6e..9b8c0db83 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -1598,13 +1598,12 @@ def compare(self, other, op): else: raise ValueError("Cannot compare Quantity and {}".format(type(other))) - # Registry equality check based on util.SharedRegistryObject if self._REGISTRY is not other._REGISTRY: mess = "Cannot operate with {} and {} of different registries." raise ValueError( mess.format(self.__class__.__name__, other.__class__.__name__) - ) + ) if self._units == other._units: return op(self._magnitude, other._magnitude) diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 5ebb33b82..4263d0735 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -240,5 +240,5 @@ def test_propagate_product(self): def test_measurement_comparison(self): x = self.Q_(4.2, "meter") y = self.Q_(5.0, "meter").plus_minus(0.1) - self.assertTrue(x<=y) - self.assertFalse(x>=y) + self.assertTrue(x <= y) + self.assertFalse(x >= y) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fe86801c0..c3e455c28 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -6,7 +6,13 @@ import warnings from unittest.mock import patch -from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry, Quantity, get_application_registry +from pint import ( + DimensionalityError, + OffsetUnitCalculusError, + Quantity, + UnitRegistry, + get_application_registry, +) from pint.compat import np from pint.testsuite import QuantityTestCase, helpers from pint.testsuite.parameterized import ParameterizedTestCase @@ -63,8 +69,10 @@ def test_quantity_comparison(self): y = self.Q_(4.2, "meter") z = self.Q_(5, "meter") j = self.Q_(5, "meter*meter") - k = 5 * get_application_registry().meter # Include a comparison to the application registry - l = Quantity(5, "meter") # Include a comparison to a directly created Quantity + + # Include a comparison to the application registry + k = 5 * get_application_registry().meter + l = Quantity(5, "meter") # Include a comparison to a directly created Quantity # identity for single object self.assertTrue(x == x) @@ -84,9 +92,9 @@ def test_quantity_comparison(self): self.assertTrue(x < z) # Compare with items to the separate application registry - self.assertTrue(k >= l) # These should both be from application registry + self.assertTrue(k >= l) # These should both be from application registry with self.assertRaises(ValueError): - z>l # One from local registry, one from application registry + z > l # One from local registry, one from application registry self.assertTrue(z != j) From 0b5ff93744de4053f8f315d526539033dafa47b1 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 6 Jan 2021 19:24:12 -0300 Subject: [PATCH 567/612] Updated to 3.9 --- asv.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asv.conf.json b/asv.conf.json index c8c82b595..dcc485746 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -55,7 +55,7 @@ // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. - "pythons": ["3.7"], + "pythons": ["3.9"], // The list of conda channel names to be searched for benchmark // dependency packages in the specified order From 8d5f4b96952a05be9b03b749b943c1e93684f2be Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 6 Jan 2021 20:14:12 -0300 Subject: [PATCH 568/612] Pin pandas version for docs to 1.1.3 (1.2 is failing right now) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9c1a669f0..d0ffc736f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black==19.10b0 isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas==1.1.3 jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" From dc366e7af04c8c15db4b1c31ac461e3d9faad49f Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 6 Jan 2021 20:19:19 -0300 Subject: [PATCH 569/612] Ignore some benchmark related files --- .gitignore | 1 + benchmarks/hashes.txt | 32 -------------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 benchmarks/hashes.txt diff --git a/.gitignore b/.gitignore index e1ec26a30..2d9bf377d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ dask-worker-space # airspeed velocity bechmark .asv/ +benchmarks/hashes.txt diff --git a/benchmarks/hashes.txt b/benchmarks/hashes.txt deleted file mode 100644 index 473130bad..000000000 --- a/benchmarks/hashes.txt +++ /dev/null @@ -1,32 +0,0 @@ -04468ad216555cab2b50dbae62b57f56bf694cc3 -a964aebfb904d5ac4cb2645478f38189b27ad97a -7f77dabbfcd1be8b5943cbb6ee3b049c66d337f6 -d78cc94a9538aeb91b30494bdcde1348cab0e2f6 -040b01283470b95d23a244eb1a791dfd189502c6 -5ca84aca6002ec1550417bf791b3f8cc9e551818 -2dc133ef61baf3f6b2b54814cbeb3299282ee794 -1d3f26bfcabab7751bd324d1fd48825667c2e949 -126e00f691007ff71e6a2c327b19f92d8a478adb -2c0c4ee67f732b0d1b7530e8363b15c57b936b97 -619b95ee13f82560c3cd596095bcd606ba0277ae -6e18639b3db4121322e0fc7009df463b8dc7ee9f -fe0a64b270dbde0946fdcb0819aad178d42f77e6 -6aea7d4aaf424e2a514c004f99b271ecfb446237 -c48c441ec6b8919f104edd6affb57b69aa275a38 -7d9837ead056af2e36009a0b0ed8d1a9e9bf3947 -e7e7de5ca2e1c19963be8a918369fb19186f9a73 -f65bebb6930fdd5ec891a905c30489a52add05f6 -a171cdfbab64903ae67d7461b0904f350191ee29 -7196ae6794a5f7b3ab0a48e880f1797a218e59de -5ee3ff950709ab356d7349d6b56818b0c5fe624b -697834d17d6fb5bb5d0adf945a5d5f5d187506a1 -2b296e79f0260b8d547af8d078178f079d90a3d9 -a91762071e3cecc8d995c993a3175f84ed9ec804 -82b2c2f97b3f929568a07850de4f75b0d16f3c56 -3d713cf920c1f164c1c3c2fcfab96956467b0d64 -eed85601c8bdf0829ed256cdf2af9982bc9ed5a2 -4e6dbff740292ae57230686ad4417e46d63e171c -222a1c89e9ce5c24debc749558cf5eb3b231f89a -85603e644e9ba004188ef79e499b9a40557b446c -9a05e600275c592451bda9287205fe0e99a87bb3 -6b0b531d5311ab0f61aefd3e76a8dec9144d6b81 \ No newline at end of file From 3484cdec44ae15f4f3675372c384d9d6caaa3cf9 Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 6 Jan 2021 21:21:57 -0300 Subject: [PATCH 570/612] Pin babel to 2.8 (2.9 breaks travis) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d0ffc736f..e180fd362 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - PKGS="python=3.7 flake8 black==19.10b0 isort" # Have linters fail first and quickly # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas==1.1.3 jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0" + - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas==1.1.3 jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0 babel==2.8" - PKGS="python=3.6" - PKGS="python=3.7" - PKGS="python=3.8" From 44bb57f9fff6e69742f548fa20fe043d7bf8a3b5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 01:08:55 -0300 Subject: [PATCH 571/612] automerge for changes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..8383fff94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGES merge=union From 726e7ef5d5ce9d5ff695f5eddb0a6c944f81f558 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 02:25:16 -0300 Subject: [PATCH 572/612] Updated linter to pre-commit --- .pre-commit-config.yaml | 14 ++++++++++++++ .travis.yml | 6 +++--- pull_request_template.md | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..87c22cc44 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/ambv/black + rev: 20.8b1 + hooks: + - id: black +- repo: https://github.com/pre-commit/mirrors-isort + rev: 'v5.6.4' + hooks: + - id: isort +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + diff --git a/.travis.yml b/.travis.yml index e180fd362..3238d6fab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py # for min/max Python versions supported by uncertainties - - PKGS="python=3.7 flake8 black==19.10b0 isort" # Have linters fail first and quickly + - PKGS="python=3.8 pre-commit" # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas==1.1.3 jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0 babel==2.8" - PKGS="python=3.6" @@ -58,7 +58,7 @@ install: - conda create -n travis $PKGS pytest pytest-cov coveralls - source activate travis - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - - if [[ $PKGS =~ flake8 ]]; then LINT=1; else LINT=0; fi + - if [[ $PKGS =~ pre-commit ]]; then LINT=1; else LINT=0; fi - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi - if [[ $DOCS == 1 ]]; then pip install pint-pandas; fi @@ -74,7 +74,7 @@ script: # test notebooks too if pandas available # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - - if [[ $LINT == 1 ]]; then black -t py36 --check . && isort -rc -c . && flake8; fi + - if [[ $LINT == 1 ]]; then pre-commit run --all-files; fi - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -a -j auto -b doctest -d build/doctrees docs build/doctest; fi - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi diff --git a/pull_request_template.md b/pull_request_template.md index d3fdd1693..8ee5e7574 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,5 +1,5 @@ - [ ] Closes # (insert issue number) -- [ ] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors +- [ ] Executed ``pre-commit run --all-files`` with no errors - [ ] The change is fully covered by automated unit tests - [ ] Documented in docs/ as appropriate - [ ] Added an entry to the CHANGES file From a750205ae10c5c2c0247668279330f10659ed02b Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 02:25:28 -0300 Subject: [PATCH 573/612] Run linter on all files --- pint/__init__.py | 9 +- pint/context.py | 9 +- pint/converters.py | 10 +- pint/definitions.py | 3 +- pint/formatting.py | 3 +- pint/matplotlib.py | 9 +- pint/numpy_func.py | 6 +- pint/pint-convert | 142 +++++++++++++++++++---------- pint/quantity.py | 15 ++- pint/registry.py | 32 +++---- pint/systems.py | 21 ++--- pint/testsuite/test_issues.py | 4 +- pint/testsuite/test_log_units.py | 33 +++---- pint/testsuite/test_measurement.py | 5 +- pint/testsuite/test_numpy.py | 3 +- pint/testsuite/test_quantity.py | 3 +- pint/testsuite/test_umath.py | 130 +++++++++++++------------- pint/unit.py | 5 +- 18 files changed, 228 insertions(+), 214 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 2cd9c6033..067aa317d 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -90,20 +90,17 @@ def _unpickle(cls, *args): def _unpickle_quantity(cls, *args): - """Rebuild quantity upon unpickling using the application registry. - """ + """Rebuild quantity upon unpickling using the application registry.""" return _unpickle(_APP_REGISTRY.Quantity, *args) def _unpickle_unit(cls, *args): - """Rebuild unit upon unpickling using the application registry. - """ + """Rebuild unit upon unpickling using the application registry.""" return _unpickle(_APP_REGISTRY.Unit, *args) def _unpickle_measurement(cls, *args): - """Rebuild measurement upon unpickling using the application registry. - """ + """Rebuild measurement upon unpickling using the application registry.""" return _unpickle(_APP_REGISTRY.Measurement, *args) diff --git a/pint/context.py b/pint/context.py index f82bb1397..6cd440e8a 100644 --- a/pint/context.py +++ b/pint/context.py @@ -224,16 +224,14 @@ def to_num(val): return ctx def add_transformation(self, src, dst, func): - """Add a transformation function to the context. - """ + """Add a transformation function to the context.""" _key = self.__keytransform__(src, dst) self.funcs[_key] = func self.relation_to_context[_key] = self def remove_transformation(self, src, dst): - """Add a transformation function to the context. - """ + """Add a transformation function to the context.""" _key = self.__keytransform__(src, dst) del self.funcs[_key] @@ -244,8 +242,7 @@ def __keytransform__(src, dst): return to_units_container(src), to_units_container(dst) def transform(self, src, dst, registry, value): - """Transform a value. - """ + """Transform a value.""" _key = self.__keytransform__(src, dst) return self.funcs[_key](registry, value, **self.defaults) diff --git a/pint/converters.py b/pint/converters.py index 1f6c09407..3e758bc4e 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -84,7 +84,7 @@ def from_reference(self, value, inplace=False): class LogarithmicConverter(Converter): - """ Converts between linear units and logarithmic units, such as dB, octave, neper or pH. + """Converts between linear units and logarithmic units, such as dB, octave, neper or pH. Q_log = logfactor * log( Q_lin / scale ) / log(log_base) Parameters @@ -126,8 +126,8 @@ def is_logarithmic(self): def from_reference(self, value, inplace=False): """Converts value from the reference unit to the logarithmic unit - dBm <------ mW - y dBm = 10 log10( x / 1mW ) + dBm <------ mW + y dBm = 10 log10( x / 1mW ) """ if inplace: value /= self.scale @@ -144,8 +144,8 @@ def from_reference(self, value, inplace=False): def to_reference(self, value, inplace=False): """Converts value to the reference unit from the logarithmic unit - dBm ------> mW - y dBm = 10 log10( x / 1mW ) + dBm ------> mW + y dBm = 10 log10( x / 1mW ) """ if inplace: value /= self.logfactor diff --git a/pint/definitions.py b/pint/definitions.py index 67adca11f..510238af3 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -58,8 +58,7 @@ def from_string(cls, definition): class _NotNumeric(Exception): - """Internal exception. Do not expose outside Pint - """ + """Internal exception. Do not expose outside Pint""" def __init__(self, value): self.value = value diff --git a/pint/formatting.py b/pint/formatting.py index b91231fb5..825b615ca 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -281,8 +281,7 @@ def format_unit(unit, spec, **kwspec): def siunitx_format_unit(units): - """Returns LaTeX code for the unit that can be put into an siunitx command. - """ + """Returns LaTeX code for the unit that can be put into an siunitx command.""" # NOTE: unit registry is required to identify unit prefixes. registry = units._REGISTRY diff --git a/pint/matplotlib.py b/pint/matplotlib.py index 914c62f3c..c642f549b 100644 --- a/pint/matplotlib.py +++ b/pint/matplotlib.py @@ -30,16 +30,14 @@ def __init__(self, registry): self._reg = registry def convert(self, value, unit, axis): - """Convert :`Quantity` instances for matplotlib to use. - """ + """Convert :`Quantity` instances for matplotlib to use.""" if iterable(value): return [self._convert_value(v, unit, axis) for v in value] else: return self._convert_value(value, unit, axis) def _convert_value(self, value, unit, axis): - """Handle converting using attached unit or falling back to axis units. - """ + """Handle converting using attached unit or falling back to axis units.""" if hasattr(value, "units"): return value.to(unit).magnitude else: @@ -53,8 +51,7 @@ def axisinfo(unit, axis): @staticmethod def default_units(x, axis): - """Get the default unit to use for the given combination of unit and axis. - """ + """Get the default unit to use for the given combination of unit and axis.""" if iterable(x) and sized(x): return getattr(x[0], "units", None) return getattr(x, "units", None) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 0f220b00e..0aaf65ff8 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -59,8 +59,7 @@ def _is_sequence_with_quantity_elements(obj): def _get_first_input_units(args, kwargs=None): - """Obtain the first valid unit from a collection of args and kwargs. - """ + """Obtain the first valid unit from a collection of args and kwargs.""" kwargs = kwargs or {} for arg in chain(args, kwargs.values()): if _is_quantity(arg): @@ -885,8 +884,7 @@ def implementation(a, *args, **kwargs): def numpy_wrap(func_type, func, args, kwargs, types): - """Return the result from a NumPy function/ufunc as wrapped by Pint. - """ + """Return the result from a NumPy function/ufunc as wrapped by Pint.""" if func_type == "function": handled = HANDLED_FUNCTIONS diff --git a/pint/pint-convert b/pint/pint-convert index 4fdf2592f..138e73187 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -10,18 +10,51 @@ 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') +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: @@ -31,7 +64,7 @@ except SystemExit: ureg = UnitRegistry() ureg.auto_reduce_dimensions = True ureg.autoconvert_offset_to_baseunit = True -ureg.enable_contexts('Gau', 'ESU', 'sp', 'energy', 'boltzmann') +ureg.enable_contexts("Gau", "ESU", "sp", "energy", "boltzmann") ureg.default_system = args.system if args.unc: @@ -44,22 +77,26 @@ if args.unc: # 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) + 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) + 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) @@ -67,55 +104,68 @@ if args.unc: 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 + 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._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) + 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): + 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) + if prec_unc > 0: + fmt = ".{}uS".format(prec_unc) else: try: nq = nq.magnitude.n * nq.units - except: + except Exception: pass - fmt = '{:' + fmt + '} {:~P}' - print(('{:} = ' + fmt).format(q, nq.magnitude, nq.units)) + 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: + if isinstance(num, uncertainties.UFloat): + full = ("{:" + fmt + "}").format(num) + unc = re.search(r"\+\/-[0.]*([\d.]*)", full).group(1) + unc = len(unc.replace(".", "")) + except Exception: pass return max(0, min(prec_unc, unc)) + convert(args.fr, args.to) diff --git a/pint/quantity.py b/pint/quantity.py index 0395a6544..f523bf890 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -493,8 +493,7 @@ def dimensionality(self): return self._dimensionality def check(self, dimension): - """Return true if the quantity's dimension matches passed dimension. - """ + """Return true if the quantity's dimension matches passed dimension.""" return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) @classmethod @@ -569,7 +568,7 @@ def compatible_units(self, *contexts): return self._REGISTRY.get_compatible_units(self._units) def is_compatible_with(self, other, *contexts, **ctx_kwargs): - """ check if the other object is compatible + """check if the other object is compatible Parameters ---------- @@ -740,7 +739,7 @@ def to_reduced_units(self): return newq def to_compact(self, unit=None): - """"Return Quantity rescaled to compact, human-readable units. + """ "Return Quantity rescaled to compact, human-readable units. To get output in terms of a different unit, use the unit parameter. @@ -1774,7 +1773,7 @@ def dot(self, b): @method_wraps("prod") def prod(self, *args, **kwargs): - """ Return the product of quantity elements over a given axis + """Return the product of quantity elements over a given axis Wraps np.prod(). """ @@ -1928,8 +1927,7 @@ def _get_delta_units(self) -> List[str]: return [u for u in self._units if u.startswith("delta_")] def _has_compatible_delta(self, unit: str) -> bool: - """"Check if Quantity object has a delta_unit that is compatible with unit - """ + """ "Check if Quantity object has a delta_unit that is compatible with unit""" deltas = self._get_delta_units() if "delta_" + unit in deltas: return True @@ -1940,8 +1938,7 @@ def _has_compatible_delta(self, unit: str) -> bool: ) def _ok_for_muldiv(self, no_offset_units=None): - """Checks if Quantity object can be multiplied or divided - """ + """Checks if Quantity object can be multiplied or divided""" is_ok = True if no_offset_units is None: diff --git a/pint/registry.py b/pint/registry.py index a243f4adf..61386ae80 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -276,8 +276,7 @@ def _register_parsers(self): self._register_parser("@defaults", self._parse_defaults) def _parse_defaults(self, ifile): - """Loader for a @default section. - """ + """Loader for a @default section.""" next(ifile) for lineno, part in ifile.block_iter(): k, v = part.split("=") @@ -301,8 +300,7 @@ def __getitem__(self, item): return self.parse_expression(item) def __contains__(self, item): - """Support checking prefixed units with the `in` operator - """ + """Support checking prefixed units with the `in` operator""" try: self.__getattr__(item) return True @@ -630,8 +628,7 @@ def _build_cache(self): logger.warning(f"Could not resolve {unit_name}: {exc!r}") def get_name(self, name_or_alias, case_sensitive=None): - """Return the canonical name of a unit. - """ + """Return the canonical name of a unit.""" if name_or_alias == "dimensionless": return "" @@ -669,8 +666,7 @@ def get_name(self, name_or_alias, case_sensitive=None): return unit_name def get_symbol(self, name_or_alias, case_sensitive=None): - """Return the preferred alias for a unit. - """ + """Return the preferred alias for a unit.""" candidates = self.parse_unit_name(name_or_alias, case_sensitive) if not candidates: raise UndefinedUnitError(name_or_alias) @@ -700,8 +696,7 @@ def get_dimensionality(self, input_units): return self._get_dimensionality(input_units) def _get_dimensionality(self, input_units): - """Convert a UnitsContainer to base dimensions. - """ + """Convert a UnitsContainer to base dimensions.""" if not input_units: return self.UnitsContainer() @@ -880,8 +875,7 @@ def _get_root_units_recurse(self, ref, exp, accumulators): self._get_root_units_recurse(reg.reference, exp2, accumulators) def get_compatible_units(self, input_units, group_or_system=None): - """ - """ + """""" input_units = to_units_container(input_units) equiv = self._get_compatible_units(input_units, group_or_system) @@ -889,8 +883,7 @@ def get_compatible_units(self, input_units, group_or_system=None): return frozenset(self.Unit(eq) for eq in equiv) def _get_compatible_units(self, input_units, group_or_system): - """ - """ + """""" if not input_units: return frozenset() @@ -898,7 +891,7 @@ def _get_compatible_units(self, input_units, group_or_system): return self._cache.dimensional_equivalents[src_dim] def is_compatible_with(self, obj1, obj2, *contexts, **ctx_kwargs): - """ check if the other object is compatible + """check if the other object is compatible Parameters ---------- @@ -1028,8 +1021,7 @@ def parse_unit_name(self, unit_name, case_sensitive=None): ) def _parse_unit_name(self, unit_name, case_sensitive=None): - """Helper of parse_unit_name. - """ + """Helper of parse_unit_name.""" case_sensitive = ( self.case_sensitive if case_sensitive is None else case_sensitive ) @@ -1307,8 +1299,7 @@ def __init__( self.autoconvert_offset_to_baseunit = autoconvert_offset_to_baseunit def _parse_units(self, input_string, as_delta=None, case_sensitive=None): - """ - """ + """""" if as_delta is None: as_delta = self.default_as_delta @@ -1598,8 +1589,7 @@ def _switch_context_cache_and_units(self) -> None: self._on_redefinition = on_redefinition_backup def _redefine(self, definition: UnitDefinition) -> None: - """Redefine a unit from a context - """ + """Redefine a unit from a context""" # Find original definition in the UnitRegistry candidates = self.parse_unit_name(definition.name) if not candidates: diff --git a/pint/systems.py b/pint/systems.py index b72ce53c0..d78fea2fc 100644 --- a/pint/systems.py +++ b/pint/systems.py @@ -122,8 +122,7 @@ def is_used_group(self, group_name): return False def add_units(self, *unit_names): - """Add units to group. - """ + """Add units to group.""" for unit_name in unit_names: self._unit_names.add(unit_name) @@ -134,16 +133,14 @@ def non_inherited_unit_names(self): return frozenset(self._unit_names) def remove_units(self, *unit_names): - """Remove units from group. - """ + """Remove units from group.""" for unit_name in unit_names: self._unit_names.remove(unit_name) self.invalidate_members() def add_groups(self, *group_names): - """Add groups to group. - """ + """Add groups to group.""" d = self._REGISTRY._groups for group_name in group_names: @@ -161,8 +158,7 @@ def add_groups(self, *group_names): self.invalidate_members() def remove_groups(self, *group_names): - """Remove groups from group. - """ + """Remove groups from group.""" d = self._REGISTRY._groups for group_name in group_names: grp = d[group_name] @@ -333,22 +329,19 @@ def invalidate_members(self): self._computed_members = None def add_groups(self, *group_names): - """Add groups to group. - """ + """Add groups to group.""" self._used_groups |= set(group_names) self.invalidate_members() def remove_groups(self, *group_names): - """Remove groups from group. - """ + """Remove groups from group.""" self._used_groups -= set(group_names) self.invalidate_members() def format_babel(self, locale): - """translate the name of the system. - """ + """translate the name of the system.""" if locale and self.name in _babel_systems: name = _babel_systems[self.name] locale = babel_parse(locale) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index e39b01a54..d12c3abc3 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -694,8 +694,8 @@ def test_issue973(self): assert len(q0) == len(q1) == 0 def test_issue1058(self): - """ verify that auto-reducing quantities with three or more units - of same base type succeeds """ + """verify that auto-reducing quantities with three or more units + of same base type succeeds""" q = 1 * ureg.mg / ureg.g / ureg.kg q.ito_reduced_units() self.assertIsInstance(q, ureg.Quantity) diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index 7d0928cf6..0860dd7a4 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -156,8 +156,7 @@ def test_unit_equivalence(ureg, unit1, unit2): ], ) def test_db_conversion(ureg, db_value, scalar): - """Test that a dB value can be converted to a scalar and back. - """ + """Test that a dB value can be converted to a scalar and back.""" Q_ = ureg.Quantity assert Q_(db_value, "dB").to("dimensionless").magnitude == pytest.approx(scalar) assert Q_(scalar, "dimensionless").to("dB").magnitude == pytest.approx(db_value) @@ -174,8 +173,7 @@ def test_db_conversion(ureg, db_value, scalar): ], ) def test_octave_conversion(ureg, octave, scalar): - """Test that an octave can be converted to a scalar and back. - """ + """Test that an octave can be converted to a scalar and back.""" Q_ = ureg.Quantity assert Q_(octave, "octave").to("dimensionless").magnitude == pytest.approx(scalar) assert Q_(scalar, "dimensionless").to("octave").magnitude == pytest.approx(octave) @@ -192,8 +190,7 @@ def test_octave_conversion(ureg, octave, scalar): ], ) def test_decade_conversion(ureg, decade, scalar): - """Test that a decade can be converted to a scalar and back. - """ + """Test that a decade can be converted to a scalar and back.""" Q_ = ureg.Quantity assert Q_(decade, "decade").to("dimensionless").magnitude == pytest.approx(scalar) assert Q_(scalar, "dimensionless").to("decade").magnitude == pytest.approx(decade) @@ -210,8 +207,7 @@ def test_decade_conversion(ureg, decade, scalar): ], ) def test_dbm_mw_conversion(ureg, dbm_value, mw_value): - """Test dBm values can convert to mW and back. - """ + """Test dBm values can convert to mW and back.""" Q_ = ureg.Quantity assert Q_(dbm_value, "dBm").to("mW").magnitude == pytest.approx(mw_value) assert Q_(mw_value, "mW").to("dBm").magnitude == pytest.approx(dbm_value) @@ -219,8 +215,7 @@ def test_dbm_mw_conversion(ureg, dbm_value, mw_value): @pytest.mark.xfail def test_compound_log_unit_multiply_definition(auto_ureg): - """Check that compound log units can be defined using multiply. - """ + """Check that compound log units can be defined using multiply.""" Q_ = auto_ureg.Quantity canonical_def = Q_(-161, "dBm") / auto_ureg.Hz mult_def = -161 * auto_ureg("dBm/Hz") @@ -229,8 +224,7 @@ def test_compound_log_unit_multiply_definition(auto_ureg): @pytest.mark.xfail def test_compound_log_unit_quantity_definition(auto_ureg): - """Check that compound log units can be defined using ``Quantity()``. - """ + """Check that compound log units can be defined using ``Quantity()``.""" Q_ = auto_ureg.Quantity canonical_def = Q_(-161, "dBm") / auto_ureg.Hz quantity_def = Q_(-161, "dBm/Hz") @@ -245,8 +239,7 @@ def test_compound_log_unit_parse_definition(auto_ureg): def test_compound_log_unit_parse_expr(auto_ureg): - """Check that compound log units can be defined using ``parse_expression()``. - """ + """Check that compound log units can be defined using ``parse_expression()``.""" Q_ = auto_ureg.Quantity canonical_def = Q_(-161, "dBm") / auto_ureg.Hz parse_def = auto_ureg.parse_expression("-161 dBm/Hz") @@ -255,8 +248,7 @@ def test_compound_log_unit_parse_expr(auto_ureg): @pytest.mark.xfail def test_dbm_db_addition(auto_ureg): - """Test a dB value can be added to a dBm and the answer is correct. - """ + """Test a dB value can be added to a dBm and the answer is correct.""" power = (5 * auto_ureg.dBm) + (10 * auto_ureg.dB) assert power.to("dBm").magnitude == pytest.approx(15) @@ -264,11 +256,14 @@ def test_dbm_db_addition(auto_ureg): @pytest.mark.xfail @pytest.mark.parametrize( "freq1,octaves,freq2", - [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200),], # noqa: E231 + [ + (100, 2.0, 400), + (50, 1.0, 100), + (200, 0.0, 200), + ], # noqa: E231 ) def test_frequency_octave_addition(auto_ureg, freq1, octaves, freq2): - """Test an Octave can be added to a frequency correctly - """ + """Test an Octave can be added to a frequency correctly""" freq1 = freq1 * auto_ureg.Hz shift = octaves * auto_ureg.Octave new_freq = freq1 + shift diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index b5acff0e3..f9f041ab8 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -86,7 +86,10 @@ def test_format_u(self): ("{:.3uL}", r"\left(0.2000 \pm 0.0100\right)\ \mathrm{second}^{2}"), ("{:.3uH}", "(0.2000 ± 0.0100) second2"), ("{:.3uC}", "(0.2000+/-0.0100) second**2"), - ("{:.3uLx}", r"\SI{0.2000 +- 0.0100}{\second\squared}",), + ( + "{:.3uLx}", + r"\SI{0.2000 +- 0.0100}{\second\squared}", + ), ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), ): with self.subTest(spec): diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index d9cee5769..4ab26755b 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -900,7 +900,8 @@ def test_equal(self): w = self.Q_(np.ones(x.shape), "m") self.assertNDArrayEqual(v == 1, false) self.assertNDArrayEqual( - self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), false, + self.Q_(np.zeros_like(x), "m") == self.Q_(np.zeros_like(x), "s"), + false, ) self.assertNDArrayEqual(v == v, true) self.assertNDArrayEqual(v == w, false) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index b5780e568..673955432 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -859,8 +859,7 @@ def test_quantity_float_complex(self): class TestQuantityNeutralAdd(QuantityTestCase): - """Addition to zero or NaN is allowed between a Quantity and a non-Quantity - """ + """Addition to zero or NaN is allowed between a Quantity and a non-Quantity""" FORCE_NDARRAY = False diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index 75b73659f..e11f7d8fb 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -274,34 +274,34 @@ class TestMathUfuncs(TestUFuncs): http://docs.scipy.org/doc/numpy/reference/ufuncs.html#math-operations - add(x1, x2[, out]) Add arguments element-wise. - subtract(x1, x2[, out]) Subtract arguments, element-wise. - multiply(x1, x2[, out]) Multiply arguments element-wise. - divide(x1, x2[, out]) Divide arguments element-wise. - logaddexp(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs. - logaddexp2(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs in base-2. - true_divide(x1, x2[, out]) Returns a true division of the inputs, element-wise. - floor_divide(x1, x2[, out]) Return the largest integer smaller or equal to the division of the inputs. - negative(x[, out]) Returns an array with the negative of each element of the original array. - power(x1, x2[, out]) First array elements raised to powers from second array, element-wise. NOT IMPLEMENTED - remainder(x1, x2[, out]) Return element-wise remainder of division. - mod(x1, x2[, out]) Return element-wise remainder of division. - fmod(x1, x2[, out]) Return the element-wise remainder of division. - absolute(x[, out]) Calculate the absolute value element-wise. - rint(x[, out]) Round elements of the array to the nearest integer. - sign(x[, out]) Returns an element-wise indication of the sign of a number. - conj(x[, out]) Return the complex conjugate, element-wise. - exp(x[, out]) Calculate the exponential of all elements in the input array. - exp2(x[, out]) Calculate 2**p for all p in the input array. - log(x[, out]) Natural logarithm, element-wise. - log2(x[, out]) Base-2 logarithm of x. - log10(x[, out]) Return the base 10 logarithm of the input array, element-wise. - expm1(x[, out]) Calculate exp(x) - 1 for all elements in the array. - log1p(x[, out]) Return the natural logarithm of one plus the input array, element-wise. - sqrt(x[, out]) Return the positive square-root of an array, element-wise. - square(x[, out]) Return the element-wise square of the input. - reciprocal(x[, out]) Return the reciprocal of the argument, element-wise. - ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array. + add(x1, x2[, out]) Add arguments element-wise. + subtract(x1, x2[, out]) Subtract arguments, element-wise. + multiply(x1, x2[, out]) Multiply arguments element-wise. + divide(x1, x2[, out]) Divide arguments element-wise. + logaddexp(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs. + logaddexp2(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs in base-2. + true_divide(x1, x2[, out]) Returns a true division of the inputs, element-wise. + floor_divide(x1, x2[, out]) Return the largest integer smaller or equal to the division of the inputs. + negative(x[, out]) Returns an array with the negative of each element of the original array. + power(x1, x2[, out]) First array elements raised to powers from second array, element-wise. NOT IMPLEMENTED + remainder(x1, x2[, out]) Return element-wise remainder of division. + mod(x1, x2[, out]) Return element-wise remainder of division. + fmod(x1, x2[, out]) Return the element-wise remainder of division. + absolute(x[, out]) Calculate the absolute value element-wise. + rint(x[, out]) Round elements of the array to the nearest integer. + sign(x[, out]) Returns an element-wise indication of the sign of a number. + conj(x[, out]) Return the complex conjugate, element-wise. + exp(x[, out]) Calculate the exponential of all elements in the input array. + exp2(x[, out]) Calculate 2**p for all p in the input array. + log(x[, out]) Natural logarithm, element-wise. + log2(x[, out]) Base-2 logarithm of x. + log10(x[, out]) Return the base 10 logarithm of the input array, element-wise. + expm1(x[, out]) Calculate exp(x) - 1 for all elements in the array. + log1p(x[, out]) Return the natural logarithm of one plus the input array, element-wise. + sqrt(x[, out]) Return the positive square-root of an array, element-wise. + square(x[, out]) Return the element-wise square of the input. + reciprocal(x[, out]) Return the reciprocal of the argument, element-wise. + ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array. Parameters ---------- @@ -422,22 +422,22 @@ class TestTrigUfuncs(TestUFuncs): http://docs.scipy.org/doc/numpy/reference/ufuncs.html#trigonometric-functions - sin(x[, out]) Trigonometric sine, element-wise. - cos(x[, out]) Cosine elementwise. - tan(x[, out]) Compute tangent element-wise. - arcsin(x[, out]) Inverse sine, element-wise. - arccos(x[, out]) Trigonometric inverse cosine, element-wise. - arctan(x[, out]) Trigonometric inverse tangent, element-wise. - arctan2(x1, x2[, out]) Element-wise arc tangent of x1/x2 choosing the quadrant correctly. - hypot(x1, x2[, out]) Given the “legs” of a right triangle, return its hypotenuse. - sinh(x[, out]) Hyperbolic sine, element-wise. - cosh(x[, out]) Hyperbolic cosine, element-wise. - tanh(x[, out]) Compute hyperbolic tangent element-wise. - arcsinh(x[, out]) Inverse hyperbolic sine elementwise. - arccosh(x[, out]) Inverse hyperbolic cosine, elementwise. - arctanh(x[, out]) Inverse hyperbolic tangent elementwise. - deg2rad(x[, out]) Convert angles from degrees to radians. - rad2deg(x[, out]) Convert angles from radians to degrees. + sin(x[, out]) Trigonometric sine, element-wise. + cos(x[, out]) Cosine elementwise. + tan(x[, out]) Compute tangent element-wise. + arcsin(x[, out]) Inverse sine, element-wise. + arccos(x[, out]) Trigonometric inverse cosine, element-wise. + arctan(x[, out]) Trigonometric inverse tangent, element-wise. + arctan2(x1, x2[, out]) Element-wise arc tangent of x1/x2 choosing the quadrant correctly. + hypot(x1, x2[, out]) Given the “legs” of a right triangle, return its hypotenuse. + sinh(x[, out]) Hyperbolic sine, element-wise. + cosh(x[, out]) Hyperbolic cosine, element-wise. + tanh(x[, out]) Compute hyperbolic tangent element-wise. + arcsinh(x[, out]) Inverse hyperbolic sine elementwise. + arccosh(x[, out]) Inverse hyperbolic cosine, elementwise. + arctanh(x[, out]) Inverse hyperbolic tangent elementwise. + deg2rad(x[, out]) Convert angles from degrees to radians. + rad2deg(x[, out]) Convert angles from radians to degrees. Parameters ---------- @@ -679,12 +679,12 @@ class TestComparisonUfuncs(TestUFuncs): http://docs.scipy.org/doc/numpy/reference/ufuncs.html#comparison-functions - greater(x1, x2[, out]) Return the truth value of (x1 > x2) element-wise. - greater_equal(x1, x2[, out]) Return the truth value of (x1 >= x2) element-wise. - less(x1, x2[, out]) Return the truth value of (x1 < x2) element-wise. - less_equal(x1, x2[, out]) Return the truth value of (x1 =< x2) element-wise. - not_equal(x1, x2[, out]) Return (x1 != x2) element-wise. - equal(x1, x2[, out]) Return (x1 == x2) element-wise. + greater(x1, x2[, out]) Return the truth value of (x1 > x2) element-wise. + greater_equal(x1, x2[, out]) Return the truth value of (x1 >= x2) element-wise. + less(x1, x2[, out]) Return the truth value of (x1 < x2) element-wise. + less_equal(x1, x2[, out]) Return the truth value of (x1 =< x2) element-wise. + not_equal(x1, x2[, out]) Return (x1 != x2) element-wise. + equal(x1, x2[, out]) Return (x1 == x2) element-wise. Parameters ---------- @@ -718,21 +718,21 @@ class TestFloatingUfuncs(TestUFuncs): http://docs.scipy.org/doc/numpy/reference/ufuncs.html#floating-functions - isreal(x) Returns a bool array, where True if input element is real. - iscomplex(x) Returns a bool array, where True if input element is complex. - isfinite(x[, out]) Test element-wise for finite-ness (not infinity or not Not a Number). - isinf(x[, out]) Test element-wise for positive or negative infinity. - isnan(x[, out]) Test element-wise for Not a Number (NaN), return result as a bool array. - signbit(x[, out]) Returns element-wise True where signbit is set (less than zero). - copysign(x1, x2[, out]) Change the sign of x1 to that of x2, element-wise. - nextafter(x1, x2[, out]) Return the next representable floating-point value after x1 in the direction of x2 element-wise. - modf(x[, out1, out2]) Return the fractional and integral parts of an array, element-wise. - ldexp(x1, x2[, out]) Compute y = x1 * 2**x2. - frexp(x[, out1, out2]) Split the number, x, into a normalized fraction (y1) and exponent (y2) - fmod(x1, x2[, out]) Return the element-wise remainder of division. - floor(x[, out]) Return the floor of the input, element-wise. - ceil(x[, out]) Return the ceiling of the input, element-wise. - trunc(x[, out]) Return the truncated value of the input, element-wise. + isreal(x) Returns a bool array, where True if input element is real. + iscomplex(x) Returns a bool array, where True if input element is complex. + isfinite(x[, out]) Test element-wise for finite-ness (not infinity or not Not a Number). + isinf(x[, out]) Test element-wise for positive or negative infinity. + isnan(x[, out]) Test element-wise for Not a Number (NaN), return result as a bool array. + signbit(x[, out]) Returns element-wise True where signbit is set (less than zero). + copysign(x1, x2[, out]) Change the sign of x1 to that of x2, element-wise. + nextafter(x1, x2[, out]) Return the next representable floating-point value after x1 in the direction of x2 element-wise. + modf(x[, out1, out2]) Return the fractional and integral parts of an array, element-wise. + ldexp(x1, x2[, out]) Compute y = x1 * 2**x2. + frexp(x[, out1, out2]) Split the number, x, into a normalized fraction (y1) and exponent (y2) + fmod(x1, x2[, out]) Return the element-wise remainder of division. + floor(x[, out]) Return the floor of the input, element-wise. + ceil(x[, out]) Return the ceiling of the input, element-wise. + trunc(x[, out]) Return the truncated value of the input, element-wise. Parameters ---------- diff --git a/pint/unit.py b/pint/unit.py index eff3c4bca..f104c83cd 100644 --- a/pint/unit.py +++ b/pint/unit.py @@ -113,8 +113,7 @@ def format_babel(self, spec="", **kwspec): @property def dimensionless(self): - """Return True if the Unit is dimensionless; False otherwise. - """ + """Return True if the Unit is dimensionless; False otherwise.""" return not bool(self.dimensionality) @property @@ -141,7 +140,7 @@ def compatible_units(self, *contexts): return self._REGISTRY.get_compatible_units(self) def is_compatible_with(self, other, *contexts, **ctx_kwargs): - """ check if the other object is compatible + """check if the other object is compatible Parameters ---------- From b04003ac2464ff068e197c1c2ff9287f3cd0542a Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 12:22:25 -0300 Subject: [PATCH 574/612] Linter --- pint/testsuite/test_quantity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index cf2efdb02..8b6a19ef3 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -72,7 +72,7 @@ def test_quantity_comparison(self): # Include a comparison to the application registry k = 5 * get_application_registry().meter - l = Quantity(5, "meter") # Include a comparison to a directly created Quantity + m = Quantity(5, "meter") # Include a comparison to a directly created Quantity # identity for single object self.assertTrue(x == x) @@ -92,9 +92,9 @@ def test_quantity_comparison(self): self.assertTrue(x < z) # Compare with items to the separate application registry - self.assertTrue(k >= l) # These should both be from application registry + self.assertTrue(k >= m) # These should both be from application registry with self.assertRaises(ValueError): - z > l # One from local registry, one from application registry + z > m # One from local registry, one from application registry self.assertTrue(z != j) From b4ba5fda0684826e83954b0802c045dca18c84cd Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 12:23:33 -0300 Subject: [PATCH 575/612] Updated CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 066a28580..05151b259 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,7 @@ Pint Changelog ----------------- - Fix comparisons between Quantities and Measurements. - (Issue #1134) + (Issue #1134, thanks lewisamarshall) - Implemented benchmarks based on airspeed velocity. From 525da04e5bfa48879eb7d6343e5f391c2c786944 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 12:25:26 -0300 Subject: [PATCH 576/612] Updated CHANGES --- CHANGES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index f21443a44..ba72bee4f 100644 --- a/CHANGES +++ b/CHANGES @@ -7,7 +7,8 @@ Pint Changelog - Fix comparisons between Quantities and Measurements. (Issue #1134, thanks lewisamarshall) - Implemented benchmarks based on airspeed velocity. -- Fix tolist function with scalar ndarray. (Issue #1195) +- Fix tolist function with scalar ndarray. + (Issue #1195, thanks jules-ch) 0.16.1 (2020-09-22) From 23ddaff124ad09445d9097408971d13f2fdb2736 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 7 Jan 2021 12:28:24 -0300 Subject: [PATCH 577/612] Updated CHANGES --- CHANGES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ba72bee4f..5bb632bb3 100644 --- a/CHANGES +++ b/CHANGES @@ -9,7 +9,8 @@ Pint Changelog - Implemented benchmarks based on airspeed velocity. - Fix tolist function with scalar ndarray. (Issue #1195, thanks jules-ch) - +- UnitsContainer returns false if other is str and cannnot be parsed + (Issue #1179, thanks rfrowe) 0.16.1 (2020-09-22) ------------------- From 661824b418183f90c18eef0fda4d434190e56131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 8 Jan 2021 17:04:38 +0100 Subject: [PATCH 578/612] Parse NumPy version with LooseVersion StrictVersion cannot parse rc versions: ValueError: invalid version number '1.20.0rc2' --- pint/testsuite/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index eae76500d..383914cfc 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -1,7 +1,7 @@ import doctest import re import unittest -from distutils.version import StrictVersion +from distutils.version import LooseVersion from ..compat import ( HAS_BABEL, @@ -33,7 +33,7 @@ def requires_numpy_previous_than(version): if not HAS_NUMPY: return unittest.skip("Requires NumPy") return unittest.skipUnless( - StrictVersion(NUMPY_VER) < StrictVersion(version), + LooseVersion(NUMPY_VER) < LooseVersion(version), "Requires NumPy < %s" % version, ) @@ -42,7 +42,7 @@ def requires_numpy_at_least(version): if not HAS_NUMPY: return unittest.skip("Requires NumPy") return unittest.skipUnless( - StrictVersion(NUMPY_VER) >= StrictVersion(version), + LooseVersion(NUMPY_VER) >= LooseVersion(version), "Requires NumPy >= %s" % version, ) From 32b6ef749695181c24540df13c46ed18f781b73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Fri, 8 Jan 2021 21:04:27 +0100 Subject: [PATCH 579/612] Fix typos --- pint/converters.py | 2 +- pint/definitions.py | 2 +- pint/formatting.py | 2 +- pint/measurement.py | 2 +- pint/pint-convert | 2 +- pint/registry.py | 10 +++++----- pint/testsuite/test_non_int.py | 2 +- pint/testsuite/test_systems.py | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pint/converters.py b/pint/converters.py index 3e758bc4e..eae71ad59 100644 --- a/pint/converters.py +++ b/pint/converters.py @@ -94,7 +94,7 @@ class LogarithmicConverter(Converter): logbase : float base of logarithm used in the logarithmic unit conversion logfactor : float - factor multupled to logarithm for unit conversion + factor multiplied to logarithm for unit conversion inplace : bool controls if computation is done in place """ diff --git a/pint/definitions.py b/pint/definitions.py index 510238af3..7e30c8942 100644 --- a/pint/definitions.py +++ b/pint/definitions.py @@ -283,7 +283,7 @@ def from_string(cls, definition, non_int_type=float): ) else: - raise DefinitionSyntaxError("Unable to assing a converter to the unit") + raise DefinitionSyntaxError("Unable to assign a converter to the unit") return cls( definition.name, diff --git a/pint/formatting.py b/pint/formatting.py index 825b615ca..a4ea4f299 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -235,7 +235,7 @@ def formatter( return _join(division_fmt, [pos_ret, neg_ret]) -# Extract just the type from the specification mini-langage: see +# Extract just the type from the specification mini-language: see # http://docs.python.org/2/library/string.html#format-specification-mini-language # We also add uS for uncertainties. _BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS") diff --git a/pint/measurement.py b/pint/measurement.py index a1ca2957f..183e1a4c6 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -110,7 +110,7 @@ def __format__(self, spec): pm_fmt = _FORMATS["Lx"]["pm_fmt"] mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt) # Also, SIunitx doesn't accept parentheses, which uncs uses with - # scientific notation ('e' or 'E' and somtimes 'g' or 'G'). + # scientific notation ('e' or 'E' and sometimes 'g' or 'G'). mstr = mstr.replace("(", "").replace(")", " ") ustr = siunitx_format_unit(self.units) return r"\SI%s{%s}{%s}" % (opts, mstr, ustr) diff --git a/pint/pint-convert b/pint/pint-convert index 138e73187..27e2f7e0b 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -70,7 +70,7 @@ ureg.default_system = args.system if args.unc: import uncertainties - # Measured constans subject to correlation + # Measured constants subject to correlation # R_i: Rydberg constant # g_e: Electron g factor # m_u: Atomic mass constant diff --git a/pint/registry.py b/pint/registry.py index 61386ae80..8f3730158 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -4,7 +4,7 @@ Defines the Registry, a class to contain units and their relations. -The module actually defines 5 registries with different capabilites: +The module actually defines 5 registries with different capabilities: - BaseRegistry: Basic unit definition and querying. Conversion between multiplicative units. @@ -1468,7 +1468,7 @@ def _convert(self, value, src, dst, inplace=False): class ContextRegistry(BaseRegistry): """Handle of Contexts. - Conversion between units with different dimenstions according + Conversion between units with different dimensions according to previously established relations (contexts). (e.g. in the spectroscopy, conversion between frequency and energy is possible) @@ -1940,7 +1940,7 @@ def get_group(self, name, create_if_needed=True): return self._groups[name] if not create_if_needed: - raise ValueError("Unkown group %s" % name) + raise ValueError("Unknown group %s" % name) return self.Group(name) @@ -1983,7 +1983,7 @@ def get_system(self, name, create_if_needed=True): return self._systems[name] if not create_if_needed: - raise ValueError("Unkown system %s" % name) + raise ValueError("Unknown system %s" % name) return self.System(name) @@ -2112,7 +2112,7 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry): In the context of a multiplication of units, interpret non-multiplicative units as their *delta* counterparts. autoconvert_offset_to_baseunit : - If True converts offset units in quantites are + If True converts offset units in quantities are converted to their base units in multiplicative context. If False no conversion happens. on_redefinition : str diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py index 4a377d25f..410e5765b 100644 --- a/pint/testsuite/test_non_int.py +++ b/pint/testsuite/test_non_int.py @@ -971,7 +971,7 @@ def test_division_with_scalar(self, input_tuple, expected): self.assertEqual(op.truediv(in1, in2).units, expected.units) self.assertQuantityAlmostEqual(op.truediv(in1, in2), expected) - exponentiation = [ # resuls without / with autoconvert + exponentiation = [ # results without / with autoconvert ((("10", "degC"), "1"), [("10", "degC"), ("10", "degC")]), # ((('10', "degC"), 0.5), ["error", (283.15 ** '0.5', "kelvin**0.5")]), ((("10", "degC"), "0"), [("1.0", ""), ("1.0", "")]), diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index ffe084dff..9b364427b 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -9,7 +9,7 @@ def _build_empty_reg_root(self): grp.invalidate_members() return ureg, ureg.get_group("root") - def test_units_programatically(self): + def test_units_programmatically(self): ureg, root = self._build_empty_reg_root() d = ureg._groups @@ -31,7 +31,7 @@ def test_cyclic(self): self.assertRaises(ValueError, g3.add_groups, "g2") self.assertRaises(ValueError, g3.add_groups, "root") - def test_groups_programatically(self): + def test_groups_programmatically(self): ureg, root = self._build_empty_reg_root() d = ureg._groups g2 = ureg.Group("g2") From 43753ac79bc43a161555ba8d8259c5bfcbcecc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Fri, 8 Jan 2021 21:07:25 +0100 Subject: [PATCH 580/612] Remove redundant parentheses --- pint/testsuite/test_issues.py | 2 +- pint/testsuite/test_quantity.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6be33db94..d33da6bda 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -436,7 +436,7 @@ def test_issue354_356_370(self): self.assertEqual("{:~}".format(1 * self.ureg("MiB")), "1 MiB") def test_issue468(self): - @ureg.wraps(("kg"), "meter") + @ureg.wraps("kg", "meter") def f(x): return x diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 8b6a19ef3..c1db505b8 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1478,8 +1478,8 @@ def test_division_with_scalar(self, input_tuple, expected): (((10, "degC"), 0), [(1.0, ""), (1.0, "")]), (((10, "degC"), -1), ["error", (1 / (10 + 273.15), "kelvin**-1")]), (((10, "degC"), -2), ["error", (1 / (10 + 273.15) ** 2.0, "kelvin**-2")]), - (((0, "degC"), -2), ["error", (1 / (273.15) ** 2, "kelvin**-2")]), - (((10, "degC"), (2, "")), ["error", ((283.15) ** 2, "kelvin**2")]), + (((0, "degC"), -2), ["error", (1 / 273.15 ** 2, "kelvin**-2")]), + (((10, "degC"), (2, "")), ["error", (283.15 ** 2, "kelvin**2")]), (((10, "degC"), (10, "degK")), ["error", "error"]), (((10, "kelvin"), (2, "")), [(100.0, "kelvin**2"), (100.0, "kelvin**2")]), ((2, (2, "kelvin")), ["error", "error"]), From e9332b1c12170663fd352bac79acea2cc2f81bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Fri, 8 Jan 2021 21:08:14 +0100 Subject: [PATCH 581/612] Unescape overescaped regexes --- pint/formatting.py | 2 +- pint/pint-convert | 2 +- pint/registry.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pint/formatting.py b/pint/formatting.py index a4ea4f299..afc51fe8e 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -13,7 +13,7 @@ from .babel_names import _babel_lengths, _babel_units from .compat import babel_parse -__JOIN_REG_EXP = re.compile(r"\{\d*\}") +__JOIN_REG_EXP = re.compile(r"{\d*}") def _join(fmt, iterable): diff --git a/pint/pint-convert b/pint/pint-convert index 27e2f7e0b..2d8eae097 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -161,7 +161,7 @@ def use_unc(num, fmt, prec_unc): try: if isinstance(num, uncertainties.UFloat): full = ("{:" + fmt + "}").format(num) - unc = re.search(r"\+\/-[0.]*([\d.]*)", full).group(1) + unc = re.search(r"\+/-[0.]*([\d.]*)", full).group(1) unc = len(unc.replace(".", "")) except Exception: pass diff --git a/pint/registry.py b/pint/registry.py index 8f3730158..6b2697f88 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -86,7 +86,7 @@ import importlib_resources -_BLOCK_RE = re.compile(r" |\(") +_BLOCK_RE = re.compile(r"[ (]") @functools.lru_cache() From de0cd290ba9fda37691545c7f122d39677380c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Fri, 8 Jan 2021 20:58:20 +0100 Subject: [PATCH 582/612] Remove Python2 numeric int/float converting relics --- pint/measurement.py | 2 +- pint/quantity.py | 4 ++-- pint/testsuite/test_issues.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/measurement.py b/pint/measurement.py index a1ca2957f..1f63771f5 100644 --- a/pint/measurement.py +++ b/pint/measurement.py @@ -66,7 +66,7 @@ def error(self): @property def rel(self): - return float(abs(self.magnitude.std_dev / self.magnitude.nominal_value)) + return abs(self.magnitude.std_dev / self.magnitude.nominal_value) def __reduce__(self): # See notes in Quantity.__reduce__ diff --git a/pint/quantity.py b/pint/quantity.py index ed2a48070..265e0af5f 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -805,9 +805,9 @@ def to_compact(self, unit=None): unit_str, unit_power = units[0] if unit_power > 0: - power = int(math.floor(math.log10(abs(magnitude)) / unit_power / 3)) * 3 + power = math.floor(math.log10(abs(magnitude)) / unit_power / 3) * 3 else: - power = int(math.ceil(math.log10(abs(magnitude)) / unit_power / 3)) * 3 + power = math.ceil(math.log10(abs(magnitude)) / unit_power / 3) * 3 index = bisect.bisect_left(SI_powers, power) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6be33db94..fb3d08785 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -102,7 +102,7 @@ def test_issue45(self): import math self.assertAlmostEqual(math.sqrt(4 * ureg.m / ureg.cm), math.sqrt(4 * 100)) - self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.0) + self.assertAlmostEqual(ureg.V / ureg.mV, 1000.0) @helpers.requires_numpy() def test_issue45b(self): From 8bd0421965e027940004ea0d0e9c10068b2330c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Fri, 8 Jan 2021 21:23:39 +0100 Subject: [PATCH 583/612] Change deprecated assertEquals to assertEqual in tests --- pint/testsuite/test_contexts.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index b57a4df14..df14b72c9 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -793,9 +793,9 @@ def test_define_nan(self): ) q = ureg.Quantity("10 GBP") - self.assertEquals(q.magnitude, 10) - self.assertEquals(q.units.dimensionality, {"[currency]": 1}) - self.assertEquals(q.to("GBP").magnitude, 10) + self.assertEqual(q.magnitude, 10) + self.assertEqual(q.units.dimensionality, {"[currency]": 1}) + self.assertEqual(q.to("GBP").magnitude, 10) self.assertTrue(math.isnan(q.to("USD").magnitude)) self.assertAlmostEqual(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11) @@ -870,7 +870,7 @@ def test_stack_contexts(self): def test_err_to_base_unit(self): with self.assertRaises(DefinitionSyntaxError) as e: Context.from_lines(["@context c", "x = [d]"]) - self.assertEquals(str(e.exception), "Can't define base units within a context") + self.assertEqual(str(e.exception), "Can't define base units within a context") def test_err_change_base_unit(self): ureg = UnitRegistry( @@ -886,7 +886,7 @@ def test_err_change_base_unit(self): with self.assertRaises(ValueError) as e: ureg.enable_contexts("c") - self.assertEquals( + self.assertEqual( str(e.exception), "Can't redefine a base unit to a derived one" ) @@ -904,7 +904,7 @@ def test_err_change_dimensionality(self): ) with self.assertRaises(ValueError) as e: ureg.enable_contexts("c") - self.assertEquals( + self.assertEqual( str(e.exception), "Can't change dimensionality of baz from [d1] to [d2] in a context", ) @@ -932,14 +932,14 @@ def test_err_cyclic_dependency(self): def test_err_dimension_redefinition(self): with self.assertRaises(DefinitionSyntaxError) as e: Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) - self.assertEquals( + self.assertEqual( str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" ) def test_err_prefix_redefinition(self): with self.assertRaises(DefinitionSyntaxError) as e: Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) - self.assertEquals( + self.assertEqual( str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" ) @@ -948,7 +948,7 @@ def test_err_redefine_alias(self): with self.subTest(s): with self.assertRaises(DefinitionSyntaxError) as e: Context.from_lines(["@context c", s]) - self.assertEquals( + self.assertEqual( str(e.exception), "Can't change a unit's symbol or aliases within a context", ) @@ -967,7 +967,7 @@ def test_err_redefine_with_prefix(self): ) with self.assertRaises(ValueError) as e: ureg.enable_contexts("c") - self.assertEquals( + self.assertEqual( str(e.exception), "Can't redefine a unit with a prefix: kilopound" ) @@ -982,4 +982,4 @@ def test_err_new_unit(self): ) with self.assertRaises(UndefinedUnitError) as e: ureg.enable_contexts("c") - self.assertEquals(str(e.exception), "'bar' is not defined in the unit registry") + self.assertEqual(str(e.exception), "'bar' is not defined in the unit registry") From 659f6304d0054c844c3347af9df6dffa14cb7f9f Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 8 Jan 2021 20:20:14 -0300 Subject: [PATCH 584/612] Migration of all tests to pytest The intento of this large commit is to move away from unittest into pytest. To speed-up migration we made certain choices that might need to be revisted in the future: - use pytest-subtests to replace unittest subtest infrastructure A lot of this might be better using parametrize - While we have removed certain classes and flatten them to functions, some were kept for no particular reason (rewriting the QuantityTestCase base class) - We have created some fixtures but not optimized. Feature optimization (i.e. using global registries for certain tests might speed up testing as it will void constructing the registry over and over) --- .travis.yml | 2 +- pint/testsuite/__init__.py | 133 +-- pint/testsuite/conftest.py | 77 ++ pint/testsuite/helpers.py | 224 +++-- pint/testsuite/parameterized.py | 147 --- pint/testsuite/test_application_registry.py | 239 +++-- pint/testsuite/test_babel.py | 159 ++-- pint/testsuite/test_compat.py | 29 +- pint/testsuite/test_contexts.py | 915 ++++++++++--------- pint/testsuite/test_converters.py | 70 +- pint/testsuite/test_definitions.py | 211 ++--- pint/testsuite/test_errors.py | 89 +- pint/testsuite/test_formatter.py | 63 +- pint/testsuite/test_infer_base_unit.py | 18 +- pint/testsuite/test_issues.py | 356 ++++---- pint/testsuite/test_log_units.py | 48 +- pint/testsuite/test_measurement.py | 109 +-- pint/testsuite/test_non_int.py | 473 +++++----- pint/testsuite/test_numpy.py | 809 ++++++++-------- pint/testsuite/test_numpy_func.py | 157 ++-- pint/testsuite/test_pint_eval.py | 6 +- pint/testsuite/test_pitheorem.py | 31 +- pint/testsuite/test_quantity.py | 961 ++++++++++---------- pint/testsuite/test_systems.py | 139 +-- pint/testsuite/test_umath.py | 49 +- pint/testsuite/test_unit.py | 720 ++++++++------- pint/testsuite/test_util.py | 247 ++--- setup.cfg | 6 +- 28 files changed, 3286 insertions(+), 3201 deletions(-) create mode 100644 pint/testsuite/conftest.py delete mode 100644 pint/testsuite/parameterized.py diff --git a/.travis.yml b/.travis.yml index 3238d6fab..d81d89fab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,7 +55,7 @@ before_install: - export TEST_OPTS="-rfsxEX -s --cov=pint --cov-config=.coveragerc" install: - - conda create -n travis $PKGS pytest pytest-cov coveralls + - conda create -n travis $PKGS pytest pytest-cov coveralls pytest-subtests - source activate travis - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - if [[ $PKGS =~ pre-commit ]]; then LINT=1; else LINT=0; fi diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index 687722134..35b6c8975 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -1,141 +1,26 @@ import doctest -import logging import math import os import unittest -from contextlib import contextmanager -from logging.handlers import BufferingHandler -from pint import Quantity, UnitRegistry, logger -from pint.compat import ndarray, np +from pint import UnitRegistry from pint.testsuite.helpers import PintOutputChecker -class TestHandler(BufferingHandler): - def __init__(self, only_warnings=False): - # BufferingHandler takes a "capacity" argument - # so as to know when to flush. As we're overriding - # shouldFlush anyway, we can set a capacity of zero. - # You can call flush() manually to clear out the - # buffer. - self.only_warnings = only_warnings - BufferingHandler.__init__(self, 0) - - def shouldFlush(self): - return False - - def emit(self, record): - if self.only_warnings and record.level != logging.WARNING: - return - self.buffer.append(record.__dict__) - - -class BaseTestCase(unittest.TestCase): - - CHECK_NO_WARNING = True - - @contextmanager - def capture_log(self, level=logging.DEBUG): - th = TestHandler() - th.setLevel(level) - logger.addHandler(th) - if self._test_handler is not None: - buflen = len(self._test_handler.buffer) - yield th.buffer - if self._test_handler is not None: - self._test_handler.buffer = self._test_handler.buffer[:buflen] - - def setUp(self): - self._test_handler = None - if self.CHECK_NO_WARNING: - self._test_handler = th = TestHandler() - th.setLevel(logging.WARNING) - logger.addHandler(th) - - def tearDown(self): - if self._test_handler is not None: - buf = self._test_handler.buffer - msg = "\n".join(record.get("msg", str(record)) for record in buf) - self.assertEqual(len(buf), 0, msg=f"{len(buf)} warnings raised.\n{msg}") - - -class QuantityTestCase(BaseTestCase): - - FORCE_NDARRAY = False +class QuantityTestCase: + kwargs = {} @classmethod - def setUpClass(cls): - cls.ureg = UnitRegistry(force_ndarray=cls.FORCE_NDARRAY) + def setup_class(cls): + cls.ureg = UnitRegistry(**cls.kwargs) cls.Q_ = cls.ureg.Quantity cls.U_ = cls.ureg.Unit - def _get_comparable_magnitudes(self, first, second, msg): - if isinstance(first, Quantity) and isinstance(second, Quantity): - second = second.to(first) - self.assertEqual( - first.units, second.units, msg=msg + " Units are not equal." - ) - m1, m2 = first.magnitude, second.magnitude - elif isinstance(first, Quantity): - self.assertTrue( - first.dimensionless, msg=msg + " The first is not dimensionless." - ) - first = first.to("") - m1, m2 = first.magnitude, second - elif isinstance(second, Quantity): - self.assertTrue( - second.dimensionless, msg=msg + " The second is not dimensionless." - ) - second = second.to("") - m1, m2 = first, second.magnitude - else: - m1, m2 = first, second - - return m1, m2 - - def assertQuantityEqual(self, first, second, msg=None): - if msg is None: - msg = "Comparing %r and %r. " % (first, second) - - m1, m2 = self._get_comparable_magnitudes(first, second, msg) - - if isinstance(m1, ndarray) or isinstance(m2, ndarray): - np.testing.assert_array_equal(m1, m2, err_msg=msg) - elif math.isnan(m1): - self.assertTrue(math.isnan(m2), msg) - elif math.isnan(m2): - self.assertTrue(math.isnan(m1), msg) - else: - self.assertEqual(m1, m2, msg) - - def assertQuantityAlmostEqual(self, first, second, rtol=1e-07, atol=0, msg=None): - if msg is None: - try: - msg = "Comparing %r and %r. " % (first, second) - except TypeError: - try: - msg = "Comparing %s and %s. " % (first, second) - except Exception: - msg = "Comparing" - - m1, m2 = self._get_comparable_magnitudes(first, second, msg) - - if isinstance(m1, ndarray) or isinstance(m2, ndarray): - np.testing.assert_allclose(m1, m2, rtol=rtol, atol=atol, err_msg=msg) - elif math.isnan(m1): - self.assertTrue(math.isnan(m2), msg) - elif math.isnan(m2): - self.assertTrue(math.isnan(m1), msg) - else: - self.assertLessEqual(abs(m1 - m2), atol + rtol * abs(m2), msg=msg) - - -class CaseInsensitveQuantityTestCase(QuantityTestCase): @classmethod - def setUpClass(cls): - cls.ureg = UnitRegistry(case_sensitive=False) - cls.Q_ = cls.ureg.Quantity - cls.U_ = cls.ureg.Unit + def teardown_class(cls): + cls.ureg = None + cls.Q_ = None + cls.U_ = None def testsuite(): diff --git a/pint/testsuite/conftest.py b/pint/testsuite/conftest.py new file mode 100644 index 000000000..529c33f89 --- /dev/null +++ b/pint/testsuite/conftest.py @@ -0,0 +1,77 @@ +# pytest fixtures + +import io + +import pytest + +import pint + + +@pytest.fixture +def registry_empty(): + return pint.UnitRegistry(None) + + +@pytest.fixture +def registry_tiny(): + return pint.UnitRegistry( + io.StringIO( + """ +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- = deka- +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- + +meter = [length] = m = metre +second = [time] = s = sec + +angstrom = 1e-10 * meter = Å = ångström = Å +minute = 60 * second = min +""" + ) + ) + + +@pytest.fixture +def func_registry(): + return pint.UnitRegistry() + + +@pytest.fixture(scope="class") +def class_registry(): + """Only use for those test that do not modify the registry.""" + return pint.UnitRegistry() + + +@pytest.fixture(scope="session") +def sess_registry(): + """Only use for those test that do not modify the registry.""" + return pint.UnitRegistry() + + +@pytest.fixture(scope="class") +def class_tiny_app_registry(): + ureg_bak = pint.get_application_registry() + ureg = pint.UnitRegistry(None) + ureg.define("foo = []") + ureg.define("bar = foo / 2") + pint.set_application_registry(ureg) + assert pint.get_application_registry() is ureg + yield ureg + pint.set_application_registry(ureg_bak) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index eae76500d..0ec405a44 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -1,7 +1,15 @@ import doctest +import math +import pickle import re -import unittest -from distutils.version import StrictVersion +import warnings +from distutils.version import LooseVersion +from numbers import Number + +import pytest + +from pint import Quantity +from pint.compat import ndarray, np from ..compat import ( HAS_BABEL, @@ -11,68 +19,6 @@ NUMPY_VER, ) - -def requires_array_function_protocol(): - if not HAS_NUMPY: - return unittest.skip("Requires NumPy") - return unittest.skipUnless( - HAS_NUMPY_ARRAY_FUNCTION, "Requires __array_function__ protocol to be enabled" - ) - - -def requires_not_array_function_protocol(): - if not HAS_NUMPY: - return unittest.skip("Requires NumPy") - return unittest.skipIf( - HAS_NUMPY_ARRAY_FUNCTION, - "Requires __array_function__ protocol to be unavailable or disabled", - ) - - -def requires_numpy_previous_than(version): - if not HAS_NUMPY: - return unittest.skip("Requires NumPy") - return unittest.skipUnless( - StrictVersion(NUMPY_VER) < StrictVersion(version), - "Requires NumPy < %s" % version, - ) - - -def requires_numpy_at_least(version): - if not HAS_NUMPY: - return unittest.skip("Requires NumPy") - return unittest.skipUnless( - StrictVersion(NUMPY_VER) >= StrictVersion(version), - "Requires NumPy >= %s" % version, - ) - - -def requires_numpy(): - return unittest.skipUnless(HAS_NUMPY, "Requires NumPy") - - -def requires_not_numpy(): - return unittest.skipIf(HAS_NUMPY, "Requires NumPy not to be installed.") - - -def requires_babel(): - return unittest.skipUnless(HAS_BABEL, "Requires Babel with units support") - - -def requires_not_babel(): - return unittest.skipIf(HAS_BABEL, "Requires Babel not to be installed") - - -def requires_uncertainties(): - return unittest.skipUnless(HAS_UNCERTAINTIES, "Requires Uncertainties") - - -def requires_not_uncertainties(): - return unittest.skipIf( - HAS_UNCERTAINTIES, "Requires Uncertainties not to be installed." - ) - - _number_re = r"([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)" _q_re = re.compile( r"= LooseVersion(version), + reason="Requires NumPy >= %s" % version, + ), + ) + + +requires_babel = pytest.mark.skipif( + not HAS_BABEL, reason="Requires Babel with units support" +) +requires_not_babel = pytest.mark.skipif( + HAS_BABEL, reason="Requires Babel not to be installed" +) +requires_uncertainties = pytest.mark.skipif( + not HAS_UNCERTAINTIES, reason="Requires Uncertainties" +) +requires_not_uncertainties = pytest.mark.skipif( + HAS_UNCERTAINTIES, reason="Requires Uncertainties not to be installed." +) + +# Parametrization + +allprotos = pytest.mark.parametrize( + ("protocol",), [(p,) for p in range(pickle.HIGHEST_PROTOCOL + 1)] +) + +check_all_bool = pytest.mark.parametrize("check_all", [False, True]) diff --git a/pint/testsuite/parameterized.py b/pint/testsuite/parameterized.py deleted file mode 100644 index 8e5df98a0..000000000 --- a/pint/testsuite/parameterized.py +++ /dev/null @@ -1,147 +0,0 @@ -# Adds Parameterized tests for Python's unittest module -# -# Code from: parameterizedtestcase, version: 0.1.0 -# Homepage: https://github.com/msabramo/python_unittest_parameterized_test_case -# Author: Marc Abramowitz, email: marc@marc-abramowitz.com -# License: MIT -# -# Use like this: -# -# from parameterizedtestcase import ParameterizedTestCase -# -# class MyTests(ParameterizedTestCase): -# @ParameterizedTestCase.parameterize( -# ("input", "expected_output"), -# [ -# ("2+4", 6), -# ("3+5", 8), -# ("6*9", 54), -# ] -# ) -# def test_eval(self, input, expected_output): -# self.assertEqual(eval(input), expected_output) - -import unittest -from collections.abc import Callable -from functools import wraps - - -def augment_method_docstring( - method, new_class_dict, classname, param_names, param_values, new_method -): - param_assignments_str = "; ".join( - ["%s = %s" % (k, v) for (k, v) in zip(param_names, param_values)] - ) - extra_doc = "%s (%s.%s) [with %s] " % ( - method.__name__, - new_class_dict.get("__module__", ""), - classname, - param_assignments_str, - ) - - try: - new_method.__doc__ = extra_doc + new_method.__doc__ - except TypeError: # Catches when new_method.__doc__ is None - new_method.__doc__ = extra_doc - - -class ParameterizedTestCaseMetaClass(type): - method_counter = {} - - def __new__(meta, classname, bases, class_dict): - new_class_dict = {} - - for attr_name, attr_value in list(class_dict.items()): - if isinstance(attr_value, Callable) and hasattr(attr_value, "param_names"): - # print("Processing attr_name = %r; attr_value = %r" % ( - # attr_name, attr_value)) - - method = attr_value - param_names = attr_value.param_names - data = attr_value.data - func_name_format = attr_value.func_name_format - - meta.process_method( - classname, - method, - param_names, - data, - new_class_dict, - func_name_format, - ) - else: - new_class_dict[attr_name] = attr_value - - return type.__new__(meta, classname, bases, new_class_dict) - - @classmethod - def process_method( - cls, classname, method, param_names, data, new_class_dict, func_name_format - ): - method_counter = cls.method_counter - - for param_values in data: - new_method = cls.new_method(method, param_values) - method_counter[method.__name__] = method_counter.get(method.__name__, 0) + 1 - case_data = dict(list(zip(param_names, param_values))) - case_data["func_name"] = method.__name__ - case_data["case_num"] = method_counter[method.__name__] - - new_method.__name__ = func_name_format.format(**case_data) - - augment_method_docstring( - method, new_class_dict, classname, param_names, param_values, new_method - ) - new_class_dict[new_method.__name__] = new_method - - @classmethod - def new_method(cls, method, param_values): - @wraps(method) - def new_method(self): - return method(self, *param_values) - - return new_method - - -class ParameterizedTestMixin(metaclass=ParameterizedTestCaseMetaClass): - @classmethod - def parameterize( - cls, param_names, data, func_name_format="{func_name}_{case_num:05d}" - ): - """Decorator for parameterizing a test method - example: - - @ParameterizedTestCase.parameterize( - ("isbn", "expected_title"), [ - ("0262033844", "Introduction to Algorithms"), - ("0321558146", "Campbell Essential Biology")]) - - Parameters - ---------- - param_names : - - data : - - func_name_format : - (Default value = "{func_name}_{case_num:05d}") - - Returns - ------- - - """ - - def decorator(func): - @wraps(func) - def newfunc(*arg, **kwargs): - return func(*arg, **kwargs) - - newfunc.param_names = param_names - newfunc.data = data - newfunc.func_name_format = func_name_format - - return newfunc - - return decorator - - -class ParameterizedTestCase(unittest.TestCase, ParameterizedTestMixin): - pass diff --git a/pint/testsuite/test_application_registry.py b/pint/testsuite/test_application_registry.py index a06844525..59c35211a 100644 --- a/pint/testsuite/test_application_registry.py +++ b/pint/testsuite/test_application_registry.py @@ -2,6 +2,8 @@ """ import pickle +import pytest + from pint import ( Measurement, Quantity, @@ -11,156 +13,152 @@ get_application_registry, set_application_registry, ) -from pint.testsuite import BaseTestCase -from pint.testsuite.helpers import requires_uncertainties - -from .parameterized import ParameterizedTestMixin - -allprotos = ParameterizedTestMixin.parameterize( - ("protocol",), [(p,) for p in range(pickle.HIGHEST_PROTOCOL + 1)] -) +from pint.testsuite import helpers -class TestDefaultApplicationRegistry(BaseTestCase, ParameterizedTestMixin): - @allprotos +class TestDefaultApplicationRegistry: + @helpers.allprotos def test_unit(self, protocol): u = Unit("kg") - self.assertEqual(str(u), "kilogram") + assert str(u) == "kilogram" u = pickle.loads(pickle.dumps(u, protocol)) - self.assertEqual(str(u), "kilogram") + assert str(u) == "kilogram" - @allprotos + @helpers.allprotos def test_quantity_1arg(self, protocol): q = Quantity("123 kg") - self.assertEqual(str(q.units), "kilogram") - self.assertEqual(q.to("t").magnitude, 0.123) + assert str(q.units) == "kilogram" + assert q.to("t").magnitude == 0.123 q = pickle.loads(pickle.dumps(q, protocol)) - self.assertEqual(str(q.units), "kilogram") - self.assertEqual(q.to("t").magnitude, 0.123) + assert str(q.units) == "kilogram" + assert q.to("t").magnitude == 0.123 - @allprotos + @helpers.allprotos def test_quantity_2args(self, protocol): q = Quantity(123, "kg") - self.assertEqual(str(q.units), "kilogram") - self.assertEqual(q.to("t").magnitude, 0.123) + assert str(q.units) == "kilogram" + assert q.to("t").magnitude == 0.123 q = pickle.loads(pickle.dumps(q, protocol)) - self.assertEqual(str(q.units), "kilogram") - self.assertEqual(q.to("t").magnitude, 0.123) + assert str(q.units) == "kilogram" + assert q.to("t").magnitude == 0.123 - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_2args(self, protocol): m = Measurement(Quantity(123, "kg"), Quantity(15, "kg")) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 15) - self.assertEqual(str(m.units), "kilogram") + assert m.value.magnitude == 123 + assert m.error.magnitude == 15 + assert str(m.units) == "kilogram" m = pickle.loads(pickle.dumps(m, protocol)) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 15) - self.assertEqual(str(m.units), "kilogram") + assert m.value.magnitude == 123 + assert m.error.magnitude == 15 + assert str(m.units) == "kilogram" - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_3args(self, protocol): m = Measurement(123, 15, "kg") - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 15) - self.assertEqual(str(m.units), "kilogram") + assert m.value.magnitude == 123 + assert m.error.magnitude == 15 + assert str(m.units) == "kilogram" m = pickle.loads(pickle.dumps(m, protocol)) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 15) - self.assertEqual(str(m.units), "kilogram") + assert m.value.magnitude == 123 + assert m.error.magnitude == 15 + assert str(m.units) == "kilogram" def test_get_application_registry(self): ureg = get_application_registry() u = ureg.Unit("kg") - self.assertEqual(str(u), "kilogram") + assert str(u) == "kilogram" - @allprotos + @helpers.allprotos def test_pickle_crash(self, protocol): ureg = UnitRegistry(None) ureg.define("foo = []") q = ureg.Quantity(123, "foo") b = pickle.dumps(q, protocol) - self.assertRaises(UndefinedUnitError, pickle.loads, b) + with pytest.raises(UndefinedUnitError): + pickle.loads(b) b = pickle.dumps(q.units, protocol) - self.assertRaises(UndefinedUnitError, pickle.loads, b) + with pytest.raises(UndefinedUnitError): + pickle.loads(b) - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_pickle_crash_measurement(self, protocol): ureg = UnitRegistry(None) ureg.define("foo = []") m = ureg.Quantity(123, "foo").plus_minus(10) b = pickle.dumps(m, protocol) - self.assertRaises(UndefinedUnitError, pickle.loads, b) + with pytest.raises(UndefinedUnitError): + pickle.loads(b) -class TestCustomApplicationRegistry(BaseTestCase, ParameterizedTestMixin): - def setUp(self): - super().setUp() - self.ureg_bak = get_application_registry() - self.ureg = UnitRegistry(None) - self.ureg.define("foo = []") - self.ureg.define("bar = foo / 2") - set_application_registry(self.ureg) - assert get_application_registry() is self.ureg +class TestCustomApplicationRegistry: + @classmethod + def setup_class(cls): + cls.ureg_bak = get_application_registry() + cls.ureg = UnitRegistry(None) + cls.ureg.define("foo = []") + cls.ureg.define("bar = foo / 2") + set_application_registry(cls.ureg) + assert get_application_registry() is cls.ureg - def tearDown(self): - super().tearDown() - set_application_registry(self.ureg_bak) + @classmethod + def teardown_class(cls): + set_application_registry(cls.ureg_bak) - @allprotos + @helpers.allprotos def test_unit(self, protocol): u = Unit("foo") - self.assertEqual(str(u), "foo") + assert str(u) == "foo" u = pickle.loads(pickle.dumps(u, protocol)) - self.assertEqual(str(u), "foo") + assert str(u) == "foo" - @allprotos + @helpers.allprotos def test_quantity_1arg(self, protocol): q = Quantity("123 foo") - self.assertEqual(str(q.units), "foo") - self.assertEqual(q.to("bar").magnitude, 246) + assert str(q.units) == "foo" + assert q.to("bar").magnitude == 246 q = pickle.loads(pickle.dumps(q, protocol)) - self.assertEqual(str(q.units), "foo") - self.assertEqual(q.to("bar").magnitude, 246) + assert str(q.units) == "foo" + assert q.to("bar").magnitude == 246 - @allprotos + @helpers.allprotos def test_quantity_2args(self, protocol): q = Quantity(123, "foo") - self.assertEqual(str(q.units), "foo") - self.assertEqual(q.to("bar").magnitude, 246) + assert str(q.units) == "foo" + assert q.to("bar").magnitude == 246 q = pickle.loads(pickle.dumps(q, protocol)) - self.assertEqual(str(q.units), "foo") - self.assertEqual(q.to("bar").magnitude, 246) + assert str(q.units) == "foo" + assert q.to("bar").magnitude == 246 - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_2args(self, protocol): m = Measurement(Quantity(123, "foo"), Quantity(10, "bar")) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 5) - self.assertEqual(str(m.units), "foo") + assert m.value.magnitude == 123 + assert m.error.magnitude == 5 + assert str(m.units) == "foo" m = pickle.loads(pickle.dumps(m, protocol)) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 5) - self.assertEqual(str(m.units), "foo") + assert m.value.magnitude == 123 + assert m.error.magnitude == 5 + assert str(m.units) == "foo" - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_3args(self, protocol): m = Measurement(123, 5, "foo") - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 5) - self.assertEqual(str(m.units), "foo") + assert m.value.magnitude == 123 + assert m.error.magnitude == 5 + assert str(m.units) == "foo" m = pickle.loads(pickle.dumps(m, protocol)) - self.assertEqual(m.value.magnitude, 123) - self.assertEqual(m.error.magnitude, 5) - self.assertEqual(str(m.units), "foo") + assert m.value.magnitude == 123 + assert m.error.magnitude == 5 + assert str(m.units) == "foo" -class TestSwapApplicationRegistry(BaseTestCase, ParameterizedTestMixin): +class TestSwapApplicationRegistry: """Test that the constructors of Quantity, Unit, and Measurement capture the registry that is set as the application registry at creation time @@ -172,20 +170,21 @@ class TestSwapApplicationRegistry(BaseTestCase, ParameterizedTestMixin): """ - def setUp(self): - super().setUp() - self.ureg_bak = get_application_registry() - self.ureg1 = UnitRegistry(None) - self.ureg1.define("foo = [dim1]") - self.ureg1.define("bar = foo / 2") - self.ureg2 = UnitRegistry(None) - self.ureg2.define("foo = [dim2]") - self.ureg2.define("bar = foo / 3") - - def tearDown(self): - set_application_registry(self.ureg_bak) - - @allprotos + @classmethod + def setup_class(cls): + cls.ureg_bak = get_application_registry() + cls.ureg1 = UnitRegistry(None) + cls.ureg1.define("foo = [dim1]") + cls.ureg1.define("bar = foo / 2") + cls.ureg2 = UnitRegistry(None) + cls.ureg2.define("foo = [dim2]") + cls.ureg2.define("bar = foo / 3") + + @classmethod + def teardown_class(cls): + set_application_registry(cls.ureg_bak) + + @helpers.allprotos def test_quantity_1arg(self, protocol): set_application_registry(self.ureg1) q1 = Quantity("1 foo") @@ -199,7 +198,7 @@ def test_quantity_1arg(self, protocol): assert q2.to("bar").magnitude == 3 assert q3.to("bar").magnitude == 3 - @allprotos + @helpers.allprotos def test_quantity_2args(self, protocol): set_application_registry(self.ureg1) q1 = Quantity(1, "foo") @@ -213,7 +212,7 @@ def test_quantity_2args(self, protocol): assert q2.to("bar").magnitude == 3 assert q3.to("bar").magnitude == 3 - @allprotos + @helpers.allprotos def test_unit(self, protocol): set_application_registry(self.ureg1) u1 = Unit("bar") @@ -224,8 +223,8 @@ def test_unit(self, protocol): assert u2.dimensionality == {"[dim2]": 1} assert u3.dimensionality == {"[dim2]": 1} - @requires_uncertainties() - @allprotos + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_2args(self, protocol): set_application_registry(self.ureg1) m1 = Measurement(Quantity(10, "foo"), Quantity(1, "foo")) @@ -236,15 +235,15 @@ def test_measurement_2args(self, protocol): assert m1.dimensionality == {"[dim1]": 1} assert m2.dimensionality == {"[dim2]": 1} assert m3.dimensionality == {"[dim2]": 1} - self.assertEqual(m1.to("bar").value.magnitude, 20) - self.assertEqual(m2.to("bar").value.magnitude, 30) - self.assertEqual(m3.to("bar").value.magnitude, 30) - self.assertEqual(m1.to("bar").error.magnitude, 2) - self.assertEqual(m2.to("bar").error.magnitude, 3) - self.assertEqual(m3.to("bar").error.magnitude, 3) - - @requires_uncertainties() - @allprotos + assert m1.to("bar").value.magnitude == 20 + assert m2.to("bar").value.magnitude == 30 + assert m3.to("bar").value.magnitude == 30 + assert m1.to("bar").error.magnitude == 2 + assert m2.to("bar").error.magnitude == 3 + assert m3.to("bar").error.magnitude == 3 + + @helpers.requires_uncertainties() + @helpers.allprotos def test_measurement_3args(self, protocol): set_application_registry(self.ureg1) m1 = Measurement(10, 1, "foo") @@ -254,9 +253,9 @@ def test_measurement_3args(self, protocol): assert m1.dimensionality == {"[dim1]": 1} assert m2.dimensionality == {"[dim2]": 1} - self.assertEqual(m1.to("bar").value.magnitude, 20) - self.assertEqual(m2.to("bar").value.magnitude, 30) - self.assertEqual(m3.to("bar").value.magnitude, 30) - self.assertEqual(m1.to("bar").error.magnitude, 2) - self.assertEqual(m2.to("bar").error.magnitude, 3) - self.assertEqual(m3.to("bar").error.magnitude, 3) + assert m1.to("bar").value.magnitude == 20 + assert m2.to("bar").value.magnitude == 30 + assert m3.to("bar").value.magnitude == 30 + assert m1.to("bar").error.magnitude == 2 + assert m2.to("bar").error.magnitude == 3 + assert m3.to("bar").error.magnitude == 3 diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index b3d530314..d88990706 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -1,83 +1,82 @@ import os +import pytest + from pint import UnitRegistry -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() - dirname = os.path.dirname(__file__) - ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) - - distance = 24.0 * ureg.meter - self.assertEqual( - distance.format_babel(locale="fr_FR", length="long"), "24.0 mètres" - ) - time = 8.0 * ureg.second - self.assertEqual( - time.format_babel(locale="fr_FR", length="long"), "8.0 secondes" - ) - self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") - acceleration = distance / time ** 2 - self.assertEqual( - acceleration.format_babel(locale="fr_FR", length="long"), - "0.375 mètre par seconde²", - ) - mks = ureg.get_system("mks") - self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") - - @helpers.requires_babel() - def test_registry_locale(self): - ureg = UnitRegistry(fmt_locale="fr_FR") - dirname = os.path.dirname(__file__) - ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) - - distance = 24.0 * ureg.meter - self.assertEqual(distance.format_babel(length="long"), "24.0 mètres") - time = 8.0 * ureg.second - self.assertEqual(time.format_babel(length="long"), "8.0 secondes") - self.assertEqual(time.format_babel(locale="ro", length="short"), "8.0 s") - acceleration = distance / time ** 2 - self.assertEqual( - acceleration.format_babel(length="long"), "0.375 mètre par seconde²" - ) - mks = ureg.get_system("mks") - self.assertEqual(mks.format_babel(locale="fr_FR"), "métrique") - - @helpers.requires_babel() - def test_no_registry_locale(self): - ureg = UnitRegistry() - distance = 24.0 * ureg.meter - self.assertRaises(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) +from pint.testsuite import helpers + + +@helpers.requires_not_babel() +def test_no_babel(sess_registry): + ureg = sess_registry + distance = 24.0 * ureg.meter + with pytest.raises(Exception): + distance.format_babel(locale="fr_FR", length="long") + + +@helpers.requires_babel() +def test_format(sess_registry): + ureg = sess_registry + dirname = os.path.dirname(__file__) + ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) + + distance = 24.0 * ureg.meter + assert distance.format_babel(locale="fr_FR", length="long") == "24.0 mètres" + time = 8.0 * ureg.second + assert time.format_babel(locale="fr_FR", length="long") == "8.0 secondes" + assert time.format_babel(locale="ro", length="short") == "8.0 s" + acceleration = distance / time ** 2 + assert ( + acceleration.format_babel(locale="fr_FR", length="long") + == "0.375 mètre par seconde²" + ) + mks = ureg.get_system("mks") + assert mks.format_babel(locale="fr_FR") == "métrique" + + +@helpers.requires_babel() +def test_registry_locale(): + ureg = UnitRegistry(fmt_locale="fr_FR") + dirname = os.path.dirname(__file__) + ureg.load_definitions(os.path.join(dirname, "../xtranslated.txt")) + + distance = 24.0 * ureg.meter + assert distance.format_babel(length="long") == "24.0 mètres" + time = 8.0 * ureg.second + assert time.format_babel(length="long") == "8.0 secondes" + assert time.format_babel(locale="ro", length="short") == "8.0 s" + acceleration = distance / time ** 2 + assert acceleration.format_babel(length="long") == "0.375 mètre par seconde²" + mks = ureg.get_system("mks") + assert mks.format_babel(locale="fr_FR") == "métrique" + + +@helpers.requires_babel() +def test_no_registry_locale(sess_registry): + ureg = sess_registry + distance = 24.0 * ureg.meter + with pytest.raises(Exception): + distance.format_babel() + + +@helpers.requires_babel() +def test_str(func_registry): + ureg = func_registry + d = 24.0 * ureg.meter + + s = "24.0 meter" + assert str(d) == s + assert "%s" % d == s + assert "{}".format(d) == s + + ureg.set_fmt_locale("fr_FR") + s = "24.0 mètres" + assert str(d) == s + assert "%s" % d == s + assert "{}".format(d) == s + + ureg.set_fmt_locale(None) + s = "24.0 meter" + assert str(d) == s + assert "%s" % d == s + assert "{}".format(d) == s diff --git a/pint/testsuite/test_compat.py b/pint/testsuite/test_compat.py index 926a39291..5f3ba5d00 100644 --- a/pint/testsuite/test_compat.py +++ b/pint/testsuite/test_compat.py @@ -1,23 +1,18 @@ import math from datetime import datetime, timedelta -import pytest +from pint.compat import eq, isnan, np, zero_or_nan +from pint.testsuite import helpers -from pint.compat import eq, isnan, zero_or_nan -from .helpers import requires_numpy - - -@pytest.mark.parametrize("check_all", [False, True]) +@helpers.check_all_bool def test_eq(check_all): assert eq(0, 0, check_all) assert not eq(0, 1, check_all) -@requires_numpy() +@helpers.requires_numpy def test_eq_numpy(): - import numpy as np - assert eq(np.array([1, 2]), np.array([1, 2]), True) assert not eq(np.array([1, 2]), np.array([1, 3]), True) np.testing.assert_equal( @@ -34,7 +29,7 @@ def test_eq_numpy(): assert not eq(np.array([1, 2]), 1, True) -@pytest.mark.parametrize("check_all", [False, True]) +@helpers.check_all_bool def test_isnan(check_all): assert not isnan(0, check_all) assert not isnan(0.0, check_all) @@ -44,10 +39,8 @@ def test_isnan(check_all): assert not isnan("foo", check_all) -@requires_numpy() +@helpers.requires_numpy def test_isnan_numpy(): - import numpy as np - assert isnan(np.nan, True) assert isnan(np.nan, False) assert not isnan(np.array([0, 0]), True) @@ -61,10 +54,8 @@ def test_isnan_numpy(): ) -@requires_numpy() +@helpers.requires_numpy def test_isnan_nat(): - import numpy as np - a = np.array(["2000-01-01", "NaT"], dtype="M8") b = np.array(["2000-01-01", "2000-01-02"], dtype="M8") assert isnan(a, True) @@ -79,7 +70,7 @@ def test_isnan_nat(): assert isnan(a[1], False) -@pytest.mark.parametrize("check_all", [False, True]) +@helpers.check_all_bool def test_zero_or_nan(check_all): assert zero_or_nan(0, check_all) assert zero_or_nan(math.nan, check_all) @@ -89,10 +80,8 @@ def test_zero_or_nan(check_all): assert not zero_or_nan("foo", check_all) -@requires_numpy() +@helpers.requires_numpy def test_zero_or_nan_numpy(): - import numpy as np - assert zero_or_nan(np.nan, True) assert zero_or_nan(np.nan, False) assert zero_or_nan(np.array([0, np.nan]), True) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index b57a4df14..7bdf0ff50 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -1,7 +1,11 @@ import itertools +import logging import math +import re from collections import defaultdict +import pytest + from pint import ( DefinitionSyntaxError, DimensionalityError, @@ -9,7 +13,7 @@ UnitRegistry, ) from pint.context import Context -from pint.testsuite import QuantityTestCase +from pint.testsuite import helpers from pint.util import UnitsContainer @@ -81,44 +85,44 @@ def add_sharedargdef_ctxs(ureg): ureg.add_context(d) -class TestContexts(QuantityTestCase): - def test_known_context(self): +class TestContexts: + def test_known_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) with ureg.context("lc"): - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph with ureg.context("lc", n=1): - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph - def test_known_context_enable(self): + def test_known_context_enable(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) ureg.enable_contexts("lc") - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph ureg.disable_contexts(1) - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph ureg.enable_contexts("lc", n=1) - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph ureg.disable_contexts(1) - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph - def test_graph(self): + def test_graph(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) l = UnitsContainer({"[length]": 1.0}) # noqa: E741 @@ -135,29 +139,29 @@ def test_graph(self): g.update({l: {t, c}, t: {l}, c: {l}}) with ureg.context("lc"): - self.assertEqual(ureg._active_ctx.graph, g_sp) + assert ureg._active_ctx.graph == g_sp with ureg.context("lc", n=1): - self.assertEqual(ureg._active_ctx.graph, g_sp) + assert ureg._active_ctx.graph == g_sp with ureg.context("ab"): - self.assertEqual(ureg._active_ctx.graph, g_ab) + assert ureg._active_ctx.graph == g_ab with ureg.context("lc"): with ureg.context("ab"): - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g with ureg.context("ab"): with ureg.context("lc"): - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g with ureg.context("lc", "ab"): - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g with ureg.context("ab", "lc"): - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g - def test_graph_enable(self): + def test_graph_enable(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) l = UnitsContainer({"[length]": 1.0}) # noqa: E741 @@ -174,84 +178,84 @@ def test_graph_enable(self): g.update({l: {t, c}, t: {l}, c: {l}}) ureg.enable_contexts("lc") - self.assertEqual(ureg._active_ctx.graph, g_sp) + assert ureg._active_ctx.graph == g_sp ureg.disable_contexts(1) ureg.enable_contexts("lc", n=1) - self.assertEqual(ureg._active_ctx.graph, g_sp) + assert ureg._active_ctx.graph == g_sp ureg.disable_contexts(1) ureg.enable_contexts("ab") - self.assertEqual(ureg._active_ctx.graph, g_ab) + assert ureg._active_ctx.graph == g_ab ureg.disable_contexts(1) ureg.enable_contexts("lc") ureg.enable_contexts("ab") - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g ureg.disable_contexts(2) ureg.enable_contexts("ab") ureg.enable_contexts("lc") - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g ureg.disable_contexts(2) ureg.enable_contexts("lc", "ab") - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g ureg.disable_contexts(2) ureg.enable_contexts("ab", "lc") - self.assertEqual(ureg._active_ctx.graph, g) + assert ureg._active_ctx.graph == g ureg.disable_contexts(2) - def test_known_nested_context(self): + def test_known_nested_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) with ureg.context("lc"): x = dict(ureg._active_ctx) y = dict(ureg._active_ctx.graph) - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph with ureg.context("ab"): - self.assertTrue(ureg._active_ctx) - self.assertTrue(ureg._active_ctx.graph) - self.assertNotEqual(x, ureg._active_ctx) - self.assertNotEqual(y, ureg._active_ctx.graph) + assert ureg._active_ctx + assert ureg._active_ctx.graph + assert x != ureg._active_ctx + assert y != ureg._active_ctx.graph - self.assertEqual(x, ureg._active_ctx) - self.assertEqual(y, ureg._active_ctx.graph) + assert x == ureg._active_ctx + assert y == ureg._active_ctx.graph - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph - def test_unknown_context(self): - ureg = UnitRegistry() + def test_unknown_context(self, func_registry): + ureg = func_registry add_ctxs(ureg) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): with ureg.context("la"): pass - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph - def test_unknown_nested_context(self): + def test_unknown_nested_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) with ureg.context("lc"): x = dict(ureg._active_ctx) y = dict(ureg._active_ctx.graph) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): with ureg.context("la"): pass - self.assertEqual(x, ureg._active_ctx) - self.assertEqual(y, ureg._active_ctx.graph) + assert x == ureg._active_ctx + assert y == ureg._active_ctx.graph - self.assertFalse(ureg._active_ctx) - self.assertFalse(ureg._active_ctx.graph) + assert not ureg._active_ctx + assert not ureg._active_ctx.graph - def test_one_context(self): + def test_one_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) @@ -262,14 +266,16 @@ def test_one_context(self): meter_units = ureg.get_compatible_units(ureg.meter) hertz_units = ureg.get_compatible_units(ureg.hertz) - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) - self.assertEqual(ureg.get_compatible_units(q), meter_units | hertz_units) - self.assertRaises(DimensionalityError, q.to, "Hz") - self.assertEqual(ureg.get_compatible_units(q), meter_units) + assert q.to("Hz") == s + assert ureg.get_compatible_units(q) == meter_units | hertz_units + with pytest.raises(DimensionalityError): + q.to("Hz") + assert ureg.get_compatible_units(q) == meter_units - def test_multiple_context(self): + def test_multiple_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) @@ -281,16 +287,18 @@ def test_multiple_context(self): hertz_units = ureg.get_compatible_units(ureg.hertz) ampere_units = ureg.get_compatible_units(ureg.ampere) - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc", "ab"): - self.assertEqual(q.to("Hz"), s) - self.assertEqual( - ureg.get_compatible_units(q), meter_units | hertz_units | ampere_units + assert q.to("Hz") == s + assert ( + ureg.get_compatible_units(q) == meter_units | hertz_units | ampere_units ) - self.assertRaises(DimensionalityError, q.to, "Hz") - self.assertEqual(ureg.get_compatible_units(q), meter_units) + with pytest.raises(DimensionalityError): + q.to("Hz") + assert ureg.get_compatible_units(q) == meter_units - def test_nested_context(self): + def test_nested_context(self, func_registry): ureg = UnitRegistry() add_ctxs(ureg) @@ -298,20 +306,23 @@ def test_nested_context(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s with ureg.context("ab"): - self.assertEqual(q.to("Hz"), s) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s + assert q.to("Hz") == s with ureg.context("ab"): - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) - self.assertRaises(DimensionalityError, q.to, "Hz") + assert q.to("Hz") == s + with pytest.raises(DimensionalityError): + q.to("Hz") - def test_context_with_arg(self): + def test_context_with_arg(self, func_registry): ureg = UnitRegistry() @@ -320,23 +331,27 @@ def test_context_with_arg(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc", n=1): - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s with ureg.context("ab"): - self.assertEqual(q.to("Hz"), s) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s + assert q.to("Hz") == s with ureg.context("ab"): - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc", n=1): - self.assertEqual(q.to("Hz"), s) - self.assertRaises(DimensionalityError, q.to, "Hz") + assert q.to("Hz") == s + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertRaises(TypeError, q.to, "Hz") + with pytest.raises(TypeError): + q.to("Hz") - def test_enable_context_with_arg(self): + def test_enable_context_with_arg(self, func_registry): ureg = UnitRegistry() @@ -345,28 +360,32 @@ def test_enable_context_with_arg(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") ureg.enable_contexts("lc", n=1) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s ureg.enable_contexts("ab") - self.assertEqual(q.to("Hz"), s) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s + assert q.to("Hz") == s ureg.disable_contexts(1) ureg.disable_contexts(1) ureg.enable_contexts("ab") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") ureg.enable_contexts("lc", n=1) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s ureg.disable_contexts(1) - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") ureg.disable_contexts(1) ureg.enable_contexts("lc") - self.assertRaises(TypeError, q.to, "Hz") + with pytest.raises(TypeError): + q.to("Hz") ureg.disable_contexts(1) - def test_context_with_arg_def(self): + def test_context_with_arg_def(self, func_registry): ureg = UnitRegistry() @@ -375,33 +394,39 @@ def test_context_with_arg_def(self): q = 500 * ureg.meter s = (ureg.speed_of_light / q).to("Hz") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s with ureg.context("ab"): - self.assertEqual(q.to("Hz"), s) - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s + assert q.to("Hz") == s with ureg.context("ab"): - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) - self.assertRaises(DimensionalityError, q.to, "Hz") + assert q.to("Hz") == s + with pytest.raises(DimensionalityError): + q.to("Hz") - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc", n=2): - self.assertEqual(q.to("Hz"), s / 2) + assert q.to("Hz") == s / 2 with ureg.context("ab"): - self.assertEqual(q.to("Hz"), s / 2) - self.assertEqual(q.to("Hz"), s / 2) + assert q.to("Hz") == s / 2 + assert q.to("Hz") == s / 2 with ureg.context("ab"): - self.assertRaises(DimensionalityError, q.to, "Hz") + with pytest.raises(DimensionalityError): + q.to("Hz") with ureg.context("lc", n=2): - self.assertEqual(q.to("Hz"), s / 2) - self.assertRaises(DimensionalityError, q.to, "Hz") + assert q.to("Hz") == s / 2 + with pytest.raises(DimensionalityError): + q.to("Hz") - def test_context_with_sharedarg_def(self): + def test_context_with_sharedarg_def(self, func_registry): ureg = UnitRegistry() @@ -412,63 +437,66 @@ def test_context_with_sharedarg_def(self): u = (1 / 500) * ureg.ampere with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s) + assert q.to("Hz") == s with ureg.context("ab"): - self.assertEqual(q.to("ampere"), u) + assert q.to("ampere") == u with ureg.context("ab"): - self.assertEqual(q.to("ampere"), 0 * u) + assert q.to("ampere") == 0 * u with ureg.context("lc"): - self.assertRaises(ZeroDivisionError, ureg.Quantity.to, q, "Hz") + with pytest.raises(ZeroDivisionError): + ureg.Quantity.to(q, "Hz") with ureg.context("lc", n=2): - self.assertEqual(q.to("Hz"), s / 2) + assert q.to("Hz") == s / 2 with ureg.context("ab"): - self.assertEqual(q.to("ampere"), 2 * u) + assert q.to("ampere") == 2 * u with ureg.context("ab", n=3): - self.assertEqual(q.to("ampere"), 3 * u) + assert q.to("ampere") == 3 * u with ureg.context("lc"): - self.assertEqual(q.to("Hz"), s / 3) + assert q.to("Hz") == s / 3 with ureg.context("lc", n=2): - self.assertEqual(q.to("Hz"), s / 2) + assert q.to("Hz") == s / 2 with ureg.context("ab", n=4): - self.assertEqual(q.to("ampere"), 4 * u) + assert q.to("ampere") == 4 * u with ureg.context("ab", n=3): - self.assertEqual(q.to("ampere"), 3 * u) + assert q.to("ampere") == 3 * u with ureg.context("lc", n=6): - self.assertEqual(q.to("Hz"), s / 6) + assert q.to("Hz") == s / 6 - def test_anonymous_context(self): + def test_anonymous_context(self, func_registry): ureg = UnitRegistry() c = Context() c.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("5 cm/s")) - self.assertRaises(ValueError, ureg.add_context, c) + with pytest.raises(ValueError): + ureg.add_context(c) x = ureg("10 cm") expect = ureg("2 s") - self.assertQuantityEqual(x.to("s", c), expect) + helpers.assert_quantity_equal(x.to("s", c), expect) with ureg.context(c): - self.assertQuantityEqual(x.to("s"), expect) + helpers.assert_quantity_equal(x.to("s"), expect) ureg.enable_contexts(c) - self.assertQuantityEqual(x.to("s"), expect) + helpers.assert_quantity_equal(x.to("s"), expect) ureg.disable_contexts(1) - self.assertRaises(DimensionalityError, x.to, "s") + with pytest.raises(DimensionalityError): + x.to("s") # Multiple anonymous contexts c2 = Context() c2.add_transformation("[length]", "[time]", lambda ureg, x: x / ureg("10 cm/s")) c2.add_transformation("[mass]", "[time]", lambda ureg, x: x / ureg("10 kg/s")) with ureg.context(c2, c): - self.assertQuantityEqual(x.to("s"), expect) + helpers.assert_quantity_equal(x.to("s"), expect) # Transformations only in c2 are still working even if c takes priority - self.assertQuantityEqual(ureg("100 kg").to("s"), ureg("10 s")) + helpers.assert_quantity_equal(ureg("100 kg").to("s"), ureg("10 s")) with ureg.context(c, c2): - self.assertQuantityEqual(x.to("s"), ureg("1 s")) + helpers.assert_quantity_equal(x.to("s"), ureg("1 s")) def _test_ctx(self, ctx): ureg = UnitRegistry() @@ -477,30 +505,32 @@ def _test_ctx(self, ctx): nctx = len(ureg._contexts) - self.assertNotIn(ctx.name, ureg._contexts) + assert ctx.name not in ureg._contexts ureg.add_context(ctx) - self.assertIn(ctx.name, ureg._contexts) - self.assertEqual(len(ureg._contexts), nctx + 1 + len(ctx.aliases)) + assert ctx.name in ureg._contexts + assert len(ureg._contexts) == nctx + 1 + len(ctx.aliases) with ureg.context(ctx.name): - self.assertEqual(q.to("Hz"), s) - self.assertEqual(s.to("meter"), q) + assert q.to("Hz") == s + assert s.to("meter") == q ureg.remove_context(ctx.name) - self.assertNotIn(ctx.name, ureg._contexts) - self.assertEqual(len(ureg._contexts), nctx) + assert ctx.name not in ureg._contexts + assert len(ureg._contexts) == nctx - def test_parse_invalid(self): - for badrow in ( + @pytest.mark.parametrize( + "badrow", + ( "[length] = 1 / [time]: c / value", "1 / [time] = [length]: c / value", "[length] <- [time] = c / value", "[length] - [time] = c / value", - ): - with self.subTest(badrow): - with self.assertRaises(DefinitionSyntaxError): - Context.from_lines(["@context c", badrow]) + ), + ) + def test_parse_invalid(self, badrow): + with pytest.raises(DefinitionSyntaxError): + Context.from_lines(["@context c", badrow]) def test_parse_simple(self): @@ -518,19 +548,19 @@ def test_parse_simple(self): ] c = Context.from_lines(s) - self.assertEqual(c.name, "longcontextname") - self.assertEqual(c.aliases, ()) - self.assertEqual(c.defaults, {}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.name == "longcontextname" + assert c.aliases == () + assert c.defaults == {} + assert c.funcs.keys() == {a, b} self._test_ctx(c) s = ["@context longcontextname = lc", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) - self.assertEqual(c.name, "longcontextname") - self.assertEqual(c.aliases, ("lc",)) - self.assertEqual(c.defaults, {}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.name == "longcontextname" + assert c.aliases == ("lc",) + assert c.defaults == {} + assert c.funcs.keys() == {a, b} self._test_ctx(c) s = [ @@ -539,10 +569,10 @@ def test_parse_simple(self): ] c = Context.from_lines(s) - self.assertEqual(c.name, "longcontextname") - self.assertEqual(c.aliases, ("lc", "lcn")) - self.assertEqual(c.defaults, {}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.name == "longcontextname" + assert c.aliases == ("lc", "lcn") + assert c.defaults == {} + assert c.funcs.keys() == {a, b} self._test_ctx(c) def test_parse_auto_inverse(self): @@ -557,8 +587,8 @@ def test_parse_auto_inverse(self): s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) - self.assertEqual(c.defaults, {}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.defaults == {} + assert c.funcs.keys() == {a, b} self._test_ctx(c) def test_parse_define(self): @@ -571,8 +601,8 @@ def test_parse_define(self): s = ["@context longcontextname", "[length] <-> 1 / [time]: c / value"] c = Context.from_lines(s) - self.assertEqual(c.defaults, {}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.defaults == {} + assert c.funcs.keys() == {a, b} self._test_ctx(c) def test_parse_parameterized(self): @@ -586,8 +616,8 @@ def test_parse_parameterized(self): s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: n * c / value"] c = Context.from_lines(s) - self.assertEqual(c.defaults, {"n": 1}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.defaults == {"n": 1} + assert c.funcs.keys() == {a, b} self._test_ctx(c) s = [ @@ -596,36 +626,42 @@ def test_parse_parameterized(self): ] c = Context.from_lines(s) - self.assertEqual(c.defaults, {"n": 1, "bla": 2}) - self.assertEqual(c.funcs.keys(), {a, b}) + assert c.defaults == {"n": 1, "bla": 2} + assert c.funcs.keys() == {a, b} # If the variable is not present in the definition, then raise an error s = ["@context(n=1) longcontextname", "[length] <-> 1 / [time]: c / value"] - self.assertRaises(DefinitionSyntaxError, Context.from_lines, s) + with pytest.raises(DefinitionSyntaxError): + Context.from_lines(s) - def test_warnings(self): + def test_warnings(self, caplog): ureg = UnitRegistry() - with self.capture_log() as buffer: + with caplog.at_level(logging.DEBUG, "pint"): add_ctxs(ureg) d = Context("ab") ureg.add_context(d) - self.assertEqual(len(buffer), 1) - self.assertIn("ab", str(buffer[-1])) + assert len(caplog.records) == 1 + assert "ab" in str(caplog.records[-1].args) d = Context("ab1", aliases=("ab",)) ureg.add_context(d) - self.assertEqual(len(buffer), 2) - self.assertIn("ab", str(buffer[-1])) + assert len(caplog.records) == 2 + assert "ab" in str(caplog.records[-1].args) -class TestDefinedContexts(QuantityTestCase): +class TestDefinedContexts: + @classmethod + def setup_class(cls): + cls.ureg = UnitRegistry() - FORCE_NDARRAY = False + @classmethod + def teardown_class(cls): + cls.ureg = None def test_defined(self): ureg = self.ureg @@ -638,11 +674,11 @@ def test_defined(self): b = Context.__keytransform__( UnitsContainer({"[length]": 1.0}), UnitsContainer({"[time]": -1.0}) ) - self.assertIn(a, ureg._contexts["sp"].funcs) - self.assertIn(b, ureg._contexts["sp"].funcs) + assert a in ureg._contexts["sp"].funcs + assert b in ureg._contexts["sp"].funcs with ureg.context("sp"): - self.assertIn(a, ureg._active_ctx) - self.assertIn(b, ureg._active_ctx) + assert a in ureg._active_ctx + assert b in ureg._active_ctx def test_spectroscopy(self): ureg = self.ureg @@ -659,18 +695,18 @@ def test_spectroscopy(self): a.dimensionality, b.dimensionality ) p = find_shortest_path(ureg._active_ctx.graph, da, db) - self.assertTrue(p) + assert p msg = "{} <-> {}".format(a, b) # assertAlmostEqualRelError converts second to first - self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) + helpers.assert_quantity_almost_equal(b, a, rtol=0.01, msg=msg) for a, b in itertools.product(eq, eq): - self.assertQuantityAlmostEqual(a.to(b.units, "sp"), b, rtol=0.01) + helpers.assert_quantity_almost_equal(a.to(b.units, "sp"), b, rtol=0.01) def test_textile(self): ureg = self.ureg qty_direct = 1.331 * ureg.tex - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): qty_indirect = qty_direct.to("Nm") with ureg.context("textile"): @@ -681,18 +717,23 @@ def test_textile(self): b = qty_indirect.to_base_units() da, db = Context.__keytransform__(a.dimensionality, b.dimensionality) p = find_shortest_path(ureg._active_ctx.graph, da, db) - self.assertTrue(p) + assert p msg = "{} <-> {}".format(a, b) - self.assertQuantityAlmostEqual(b, a, rtol=0.01, msg=msg) + helpers.assert_quantity_almost_equal(b, a, rtol=0.01, msg=msg) # Check RKM <-> cN/tex conversion - self.assertQuantityAlmostEqual(1 * ureg.RKM, 0.980665 * ureg.cN / ureg.tex) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( + 1 * ureg.RKM, 0.980665 * ureg.cN / ureg.tex + ) + helpers.assert_quantity_almost_equal( (1 / 0.980665) * ureg.RKM, 1 * ureg.cN / ureg.tex ) - self.assertAlmostEqual((1 * ureg.RKM).to(ureg.cN / ureg.tex).m, 0.980665) - self.assertAlmostEqual( - (1 * ureg.cN / ureg.tex).to(ureg.RKM).m, 1 / 0.980665 + assert ( + round(abs((1 * ureg.RKM).to(ureg.cN / ureg.tex).m - 0.980665), 7) == 0 + ) + assert ( + round(abs((1 * ureg.cN / ureg.tex).to(ureg.RKM).m - 1 / 0.980665), 7) + == 0 ) def test_decorator(self): @@ -705,13 +746,14 @@ def test_decorator(self): def f(wl): return wl.to("terahertz") - self.assertRaises(DimensionalityError, f, a) + with pytest.raises(DimensionalityError): + f(a) @ureg.with_context("sp") def g(wl): return wl.to("terahertz") - self.assertEqual(b, g(a)) + assert b == g(a) def test_decorator_composition(self): ureg = self.ureg @@ -730,256 +772,257 @@ def f(wl): def g(wl): return wl.to("terahertz") - self.assertEqual(b, f(a)) - self.assertEqual(b, g(a)) - - -class TestContextRedefinitions(QuantityTestCase): - def test_redefine(self): - ureg = UnitRegistry( - """ - foo = [d] = f = foo_alias - bar = 2 foo = b = bar_alias - baz = 3 bar = _ = baz_alias - asd = 4 baz - - @context c - # Note how we're redefining a symbol, not the base name, as a - # function of another name - b = 5 f - """.splitlines() - ) - # Units that are somehow directly or indirectly defined as a function of the - # overridden unit are also affected - foo = ureg.Quantity(1, "foo") - bar = ureg.Quantity(1, "bar") - asd = ureg.Quantity(1, "asd") - - # Test without context before and after, to verify that the cache and units have - # not been polluted - for enable_ctx in (False, True, False): - with self.subTest(enable_ctx): - if enable_ctx: - ureg.enable_contexts("c") - k = 5 - else: - k = 2 - - self.assertEqual(foo.to("b").magnitude, 1 / k) - self.assertEqual(foo.to("bar").magnitude, 1 / k) - self.assertEqual(foo.to("bar_alias").magnitude, 1 / k) - self.assertEqual(foo.to("baz").magnitude, 1 / k / 3) - self.assertEqual(bar.to("foo").magnitude, k) - self.assertEqual(bar.to("baz").magnitude, 1 / 3) - self.assertEqual(asd.to("foo").magnitude, 4 * 3 * k) - self.assertEqual(asd.to("bar").magnitude, 4 * 3) - self.assertEqual(asd.to("baz").magnitude, 4) - - ureg.disable_contexts() - - def test_define_nan(self): - ureg = UnitRegistry( - """ - USD = [currency] - EUR = nan USD - GBP = nan USD - - @context c - EUR = 1.11 USD - # Note that we're changing which unit GBP is defined against - GBP = 1.18 EUR - @end - """.splitlines() - ) + assert b == f(a) + assert b == g(a) + + +def test_redefine(subtests): + ureg = UnitRegistry( + """ + foo = [d] = f = foo_alias + bar = 2 foo = b = bar_alias + baz = 3 bar = _ = baz_alias + asd = 4 baz + + @context c + # Note how we're redefining a symbol, not the base name, as a + # function of another name + b = 5 f + """.splitlines() + ) + # Units that are somehow directly or indirectly defined as a function of the + # overridden unit are also affected + foo = ureg.Quantity(1, "foo") + bar = ureg.Quantity(1, "bar") + asd = ureg.Quantity(1, "asd") + + # Test without context before and after, to verify that the cache and units have + # not been polluted + for enable_ctx in (False, True, False): + with subtests.test(enable_ctx): + if enable_ctx: + ureg.enable_contexts("c") + k = 5 + else: + k = 2 + + assert foo.to("b").magnitude == 1 / k + assert foo.to("bar").magnitude == 1 / k + assert foo.to("bar_alias").magnitude == 1 / k + assert foo.to("baz").magnitude == 1 / k / 3 + assert bar.to("foo").magnitude == k + assert bar.to("baz").magnitude == 1 / 3 + assert asd.to("foo").magnitude == 4 * 3 * k + assert asd.to("bar").magnitude == 4 * 3 + assert asd.to("baz").magnitude == 4 + + ureg.disable_contexts() + + +def test_define_nan(): + ureg = UnitRegistry( + """ + USD = [currency] + EUR = nan USD + GBP = nan USD + + @context c + EUR = 1.11 USD + # Note that we're changing which unit GBP is defined against + GBP = 1.18 EUR + @end + """.splitlines() + ) + + q = ureg.Quantity("10 GBP") + assert q.magnitude == 10 + assert q.units.dimensionality == {"[currency]": 1} + assert q.to("GBP").magnitude == 10 + assert math.isnan(q.to("USD").magnitude) + assert round(abs(q.to("USD", "c").magnitude - 10 * 1.18 * 1.11), 7) == 0 + + +def test_non_multiplicative(subtests): + ureg = UnitRegistry( + """ + kelvin = [temperature] + fahrenheit = 5 / 9 * kelvin; offset: 255 + bogodegrees = 9 * kelvin + + @context nonmult_to_nonmult + fahrenheit = 7 * kelvin; offset: 123 + @end + @context nonmult_to_mult + fahrenheit = 123 * kelvin + @end + @context mult_to_nonmult + bogodegrees = 5 * kelvin; offset: 123 + @end + """.splitlines() + ) + k = ureg.Quantity(100, "kelvin") + + with subtests.test("baseline"): + assert round(abs(k.to("fahrenheit").magnitude - (100 - 255) * 9 / 5), 7) == 0 + assert round(abs(k.to("bogodegrees").magnitude - 100 / 9), 7) == 0 + + with subtests.test("nonmult_to_nonmult"): + with ureg.context("nonmult_to_nonmult"): + assert round(abs(k.to("fahrenheit").magnitude - (100 - 123) / 7), 7) == 0 + + with subtests.test("nonmult_to_mult"): + with ureg.context("nonmult_to_mult"): + assert round(abs(k.to("fahrenheit").magnitude - 100 / 123), 7) == 0 + + with subtests.test("mult_to_nonmult"): + with ureg.context("mult_to_nonmult"): + assert round(abs(k.to("bogodegrees").magnitude - (100 - 123) / 5), 7) == 0 + + +def test_stack_contexts(): + ureg = UnitRegistry( + """ + a = [dim1] + b = 1/2 a + c = 1/3 a + d = [dim2] + + @context c1 + b = 1/4 a + c = 1/6 a + [dim1]->[dim2]: value * 2 d/a + @end + @context c2 + b = 1/5 a + [dim1]->[dim2]: value * 3 d/a + @end + """.splitlines() + ) + q = ureg.Quantity(1, "a") + assert q.to("b").magnitude == 2 + assert q.to("c").magnitude == 3 + assert q.to("b", "c1").magnitude == 4 + assert q.to("c", "c1").magnitude == 6 + assert q.to("d", "c1").magnitude == 2 + assert q.to("b", "c2").magnitude == 5 + assert q.to("c", "c2").magnitude == 3 + assert q.to("d", "c2").magnitude == 3 + assert q.to("b", "c1", "c2").magnitude == 5 # c2 takes precedence + assert q.to("c", "c1", "c2").magnitude == 6 # c2 doesn't change it, so use c1 + assert q.to("d", "c1", "c2").magnitude == 3 # c2 takes precedence + + +def test_err_to_base_unit(): + expected = "Can't define base units within a context" + with pytest.raises(DefinitionSyntaxError, match=expected): + Context.from_lines(["@context c", "x = [d]"]) + + +def test_err_change_base_unit(): + ureg = UnitRegistry( + """ + foo = [d1] + bar = [d2] + + @context c + bar = foo + @end + """.splitlines() + ) - q = ureg.Quantity("10 GBP") - self.assertEquals(q.magnitude, 10) - self.assertEquals(q.units.dimensionality, {"[currency]": 1}) - self.assertEquals(q.to("GBP").magnitude, 10) - self.assertTrue(math.isnan(q.to("USD").magnitude)) - self.assertAlmostEqual(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11) - - def test_non_multiplicative(self): - ureg = UnitRegistry( - """ - kelvin = [temperature] - fahrenheit = 5 / 9 * kelvin; offset: 255 - bogodegrees = 9 * kelvin - - @context nonmult_to_nonmult - fahrenheit = 7 * kelvin; offset: 123 - @end - @context nonmult_to_mult - fahrenheit = 123 * kelvin - @end - @context mult_to_nonmult - bogodegrees = 5 * kelvin; offset: 123 - @end - """.splitlines() - ) - k = ureg.Quantity(100, "kelvin") - - with self.subTest("baseline"): - self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 255) * 9 / 5) - self.assertAlmostEqual(k.to("bogodegrees").magnitude, 100 / 9) - - with self.subTest("nonmult_to_nonmult"): - with ureg.context("nonmult_to_nonmult"): - self.assertAlmostEqual(k.to("fahrenheit").magnitude, (100 - 123) / 7) - - with self.subTest("nonmult_to_mult"): - with ureg.context("nonmult_to_mult"): - self.assertAlmostEqual(k.to("fahrenheit").magnitude, 100 / 123) - - with self.subTest("mult_to_nonmult"): - with ureg.context("mult_to_nonmult"): - self.assertAlmostEqual(k.to("bogodegrees").magnitude, (100 - 123) / 5) - - def test_stack_contexts(self): - ureg = UnitRegistry( - """ - a = [dim1] - b = 1/2 a - c = 1/3 a - d = [dim2] - - @context c1 - b = 1/4 a - c = 1/6 a - [dim1]->[dim2]: value * 2 d/a - @end - @context c2 - b = 1/5 a - [dim1]->[dim2]: value * 3 d/a - @end - """.splitlines() - ) - q = ureg.Quantity(1, "a") - assert q.to("b").magnitude == 2 - assert q.to("c").magnitude == 3 - assert q.to("b", "c1").magnitude == 4 - assert q.to("c", "c1").magnitude == 6 - assert q.to("d", "c1").magnitude == 2 - assert q.to("b", "c2").magnitude == 5 - assert q.to("c", "c2").magnitude == 3 - assert q.to("d", "c2").magnitude == 3 - assert q.to("b", "c1", "c2").magnitude == 5 # c2 takes precedence - assert q.to("c", "c1", "c2").magnitude == 6 # c2 doesn't change it, so use c1 - assert q.to("d", "c1", "c2").magnitude == 3 # c2 takes precedence - - def test_err_to_base_unit(self): - with self.assertRaises(DefinitionSyntaxError) as e: - Context.from_lines(["@context c", "x = [d]"]) - self.assertEquals(str(e.exception), "Can't define base units within a context") - - def test_err_change_base_unit(self): - ureg = UnitRegistry( - """ - foo = [d1] - bar = [d2] - - @context c - bar = foo - @end - """.splitlines() - ) + expected = "Can't redefine a base unit to a derived one" + with pytest.raises(ValueError, match=expected): + ureg.enable_contexts("c") - with self.assertRaises(ValueError) as e: - ureg.enable_contexts("c") - self.assertEquals( - str(e.exception), "Can't redefine a base unit to a derived one" - ) - def test_err_change_dimensionality(self): - ureg = UnitRegistry( - """ - foo = [d1] - bar = [d2] - baz = foo - - @context c - baz = bar - @end - """.splitlines() - ) - with self.assertRaises(ValueError) as e: - ureg.enable_contexts("c") - self.assertEquals( - str(e.exception), - "Can't change dimensionality of baz from [d1] to [d2] in a context", - ) +def test_err_change_dimensionality(): + ureg = UnitRegistry( + """ + foo = [d1] + bar = [d2] + baz = foo - def test_err_cyclic_dependency(self): - ureg = UnitRegistry( - """ - foo = [d] - bar = foo + @context c baz = bar + @end + """.splitlines() + ) + expected = re.escape( + "Can't change dimensionality of baz from [d1] to [d2] in a context" + ) + with pytest.raises(ValueError, match=expected): + ureg.enable_contexts("c") - @context c - bar = baz - @end - """.splitlines() - ) - # TODO align this exception and the one you get when you implement a cyclic - # dependency within the base registry. Ideally this exception should be - # raised by enable_contexts. + +def test_err_cyclic_dependency(): + ureg = UnitRegistry( + """ + foo = [d] + bar = foo + baz = bar + + @context c + bar = baz + @end + """.splitlines() + ) + # TODO align this exception and the one you get when you implement a cyclic + # dependency within the base registry. Ideally this exception should be + # raised by enable_contexts. + ureg.enable_contexts("c") + q = ureg.Quantity("bar") + with pytest.raises(RecursionError): + q.to("foo") + + +def test_err_dimension_redefinition(): + expected = re.escape("Expected = ; got [d1] = [d2] * [d3]") + with pytest.raises(DefinitionSyntaxError, match=expected): + Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) + + +def test_err_prefix_redefinition(): + expected = re.escape("Expected = ; got [d1] = [d2] * [d3]") + with pytest.raises(DefinitionSyntaxError, match=expected): + Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) + + +def test_err_redefine_alias(subtests): + expected = "Can't change a unit's symbol or aliases within a context" + for s in ("foo = bar = f", "foo = bar = _ = baz"): + with subtests.test(s): + with pytest.raises(DefinitionSyntaxError, match=expected): + Context.from_lines(["@context c", s]) + + +def test_err_redefine_with_prefix(): + ureg = UnitRegistry( + """ + kilo- = 1000 + gram = [mass] + pound = 454 gram + + @context c + kilopound = 500000 gram + @end + """.splitlines() + ) + + expected = "Can't redefine a unit with a prefix: kilopound" + with pytest.raises(ValueError, match=expected): ureg.enable_contexts("c") - q = ureg.Quantity("bar") - with self.assertRaises(RecursionError): - q.to("foo") - - def test_err_dimension_redefinition(self): - with self.assertRaises(DefinitionSyntaxError) as e: - Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) - self.assertEquals( - str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" - ) - def test_err_prefix_redefinition(self): - with self.assertRaises(DefinitionSyntaxError) as e: - Context.from_lines(["@context c", "[d1] = [d2] * [d3]"]) - self.assertEquals( - str(e.exception), "Expected = ; got [d1] = [d2] * [d3]" - ) - def test_err_redefine_alias(self): - for s in ("foo = bar = f", "foo = bar = _ = baz"): - with self.subTest(s): - with self.assertRaises(DefinitionSyntaxError) as e: - Context.from_lines(["@context c", s]) - self.assertEquals( - str(e.exception), - "Can't change a unit's symbol or aliases within a context", - ) - - def test_err_redefine_with_prefix(self): - ureg = UnitRegistry( - """ - kilo- = 1000 - gram = [mass] - pound = 454 gram - - @context c - kilopound = 500000 gram - @end - """.splitlines() - ) - with self.assertRaises(ValueError) as e: - ureg.enable_contexts("c") - self.assertEquals( - str(e.exception), "Can't redefine a unit with a prefix: kilopound" - ) +def test_err_new_unit(): + ureg = UnitRegistry( + """ + foo = [d] + @context c + bar = foo + @end + """.splitlines() + ) - def test_err_new_unit(self): - ureg = UnitRegistry( - """ - foo = [d] - @context c - bar = foo - @end - """.splitlines() - ) - with self.assertRaises(UndefinedUnitError) as e: - ureg.enable_contexts("c") - self.assertEquals(str(e.exception), "'bar' is not defined in the unit registry") + expected = "'bar' is not defined in the unit registry" + with pytest.raises(UndefinedUnitError, match=expected): + ureg.enable_contexts("c") diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py index 460b3e5c2..6e8d59a9e 100644 --- a/pint/testsuite/test_converters.py +++ b/pint/testsuite/test_converters.py @@ -7,60 +7,67 @@ OffsetConverter, ScaleConverter, ) -from pint.testsuite import BaseTestCase, helpers +from pint.testsuite import helpers -class TestConverter(BaseTestCase): +class TestConverter: def test_converter(self): c = Converter() - self.assertTrue(c.is_multiplicative) - self.assertFalse(c.is_logarithmic) - self.assertTrue(c.to_reference(8)) - self.assertTrue(c.from_reference(8)) + assert c.is_multiplicative + assert not c.is_logarithmic + assert c.to_reference(8) + assert c.from_reference(8) def test_multiplicative_converter(self): c = ScaleConverter(20.0) - self.assertTrue(c.is_multiplicative) - self.assertFalse(c.is_logarithmic) - self.assertEqual(c.from_reference(c.to_reference(100)), 100) - self.assertEqual(c.to_reference(c.from_reference(100)), 100) + assert c.is_multiplicative + assert not c.is_logarithmic + assert c.from_reference(c.to_reference(100)) == 100 + assert c.to_reference(c.from_reference(100)) == 100 def test_offset_converter(self): c = OffsetConverter(20.0, 2) - self.assertFalse(c.is_multiplicative) - self.assertFalse(c.is_logarithmic) - self.assertEqual(c.from_reference(c.to_reference(100)), 100) - self.assertEqual(c.to_reference(c.from_reference(100)), 100) + assert not c.is_multiplicative + assert not c.is_logarithmic + assert c.from_reference(c.to_reference(100)) == 100 + assert c.to_reference(c.from_reference(100)) == 100 def test_log_converter(self): c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) - self.assertFalse(c.is_multiplicative) - self.assertTrue(c.is_logarithmic) - self.assertAlmostEqual(c.to_reference(0), 1) - self.assertAlmostEqual(c.to_reference(1), 10) - self.assertAlmostEqual(c.to_reference(2), 100) - self.assertAlmostEqual(c.from_reference(1), 0) - self.assertAlmostEqual(c.from_reference(10), 1) - self.assertAlmostEqual(c.from_reference(100), 2) + assert not c.is_multiplicative + assert c.is_logarithmic + assert round(abs(c.to_reference(0) - 1), 7) == 0 + assert round(abs(c.to_reference(1) - 10), 7) == 0 + assert round(abs(c.to_reference(2) - 100), 7) == 0 + assert round(abs(c.from_reference(1) - 0), 7) == 0 + assert round(abs(c.from_reference(10) - 1), 7) == 0 + assert round(abs(c.from_reference(100) - 2), 7) == 0 arb_value = 20.0 - self.assertAlmostEqual(c.from_reference(c.to_reference(arb_value)), arb_value) - self.assertAlmostEqual(c.to_reference(c.from_reference(arb_value)), arb_value) + assert ( + round(abs(c.from_reference(c.to_reference(arb_value)) - arb_value), 7) == 0 + ) + assert ( + round(abs(c.to_reference(c.from_reference(arb_value)) - arb_value), 7) == 0 + ) - @helpers.requires_numpy() + @helpers.requires_numpy def test_converter_inplace(self): for c in (ScaleConverter(20.0), OffsetConverter(20.0, 2)): fun1 = lambda x, y: c.from_reference(c.to_reference(x, y), y) fun2 = lambda x, y: c.to_reference(c.from_reference(x, y), y) for fun, (inplace, comp) in itertools.product( - (fun1, fun2), ((True, self.assertIs), (False, self.assertIsNot)) + (fun1, fun2), ((True, True), (False, False)) ): a = np.ones((1, 10)) ac = np.ones((1, 10)) r = fun(a, inplace) np.testing.assert_allclose(r, ac) - comp(a, r) + if comp: + assert a is r + else: + assert a is not r - @helpers.requires_numpy() + @helpers.requires_numpy def test_log_converter_inplace(self): arb_value = 3.14 c = LogarithmicConverter(scale=1, logbase=10, logfactor=1) @@ -74,9 +81,12 @@ def test_log_converter_inplace(self): ) for fun, (inplace, comp) in itertools.product( - (from_to, to_from), ((True, self.assertIs), (False, self.assertIsNot)) + (from_to, to_from), ((True, True), (False, False)) ): arb_array = arb_value * np.ones((1, 10)) result = fun(arb_array, inplace) np.testing.assert_allclose(result, arb_array) - comp(arb_array, result) + if comp: + assert arb_array is result + else: + assert arb_array is not result diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py index 21ba40122..43cd03c78 100644 --- a/pint/testsuite/test_definitions.py +++ b/pint/testsuite/test_definitions.py @@ -1,3 +1,5 @@ +import pytest + from pint.converters import LogarithmicConverter, OffsetConverter, ScaleConverter from pint.definitions import ( AliasDefinition, @@ -7,165 +9,164 @@ UnitDefinition, ) from pint.errors import DefinitionSyntaxError -from pint.testsuite import BaseTestCase from pint.util import UnitsContainer -class TestDefinition(BaseTestCase): +class TestDefinition: def test_invalid(self): - with self.assertRaises(DefinitionSyntaxError): + with pytest.raises(DefinitionSyntaxError): Definition.from_string("x = [time] * meter") - with self.assertRaises(DefinitionSyntaxError): + with pytest.raises(DefinitionSyntaxError): Definition.from_string("[x] = [time] * meter") def test_prefix_definition(self): - self.assertRaises(ValueError, Definition.from_string, "m- = 1e-3 k") + with pytest.raises(ValueError): + Definition.from_string("m- = 1e-3 k") for definition in ("m- = 1e-3", "m- = 10**-3", "m- = 0.001"): x = Definition.from_string(definition) - self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, "m") - self.assertEqual(x.aliases, ()) - self.assertEqual(x.converter.to_reference(1000), 1) - self.assertEqual(x.converter.from_reference(0.001), 1) - self.assertEqual(str(x), "m") + assert isinstance(x, PrefixDefinition) + assert x.name == "m" + assert x.aliases == () + assert x.converter.to_reference(1000) == 1 + assert x.converter.from_reference(0.001) == 1 + assert str(x) == "m" x = Definition.from_string("kilo- = 1e-3 = k-") - self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, "kilo") - self.assertEqual(x.aliases, ()) - self.assertEqual(x.symbol, "k") - self.assertEqual(x.converter.to_reference(1000), 1) - self.assertEqual(x.converter.from_reference(0.001), 1) + assert isinstance(x, PrefixDefinition) + assert x.name == "kilo" + assert x.aliases == () + assert x.symbol == "k" + assert x.converter.to_reference(1000) == 1 + assert x.converter.from_reference(0.001) == 1 x = Definition.from_string("kilo- = 1e-3 = k- = anotherk-") - self.assertIsInstance(x, PrefixDefinition) - self.assertEqual(x.name, "kilo") - self.assertEqual(x.aliases, ("anotherk",)) - self.assertEqual(x.symbol, "k") - self.assertEqual(x.converter.to_reference(1000), 1) - self.assertEqual(x.converter.from_reference(0.001), 1) + assert isinstance(x, PrefixDefinition) + assert x.name == "kilo" + assert x.aliases == ("anotherk",) + assert x.symbol == "k" + assert x.converter.to_reference(1000) == 1 + assert x.converter.from_reference(0.001) == 1 def test_baseunit_definition(self): x = Definition.from_string("meter = [length]") - self.assertIsInstance(x, UnitDefinition) - self.assertTrue(x.is_base) - self.assertEqual(x.reference, UnitsContainer({"[length]": 1})) + assert isinstance(x, UnitDefinition) + assert x.is_base + assert x.reference == UnitsContainer({"[length]": 1}) def test_unit_definition(self): x = Definition.from_string("coulomb = ampere * second") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, ScaleConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.reference, UnitsContainer(ampere=1, second=1)) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, ScaleConverter) + assert x.converter.scale == 1 + assert x.reference == UnitsContainer(ampere=1, second=1) x = Definition.from_string("faraday = 96485.3399 * coulomb") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, ScaleConverter) - self.assertEqual(x.converter.scale, 96485.3399) - self.assertEqual(x.reference, UnitsContainer(coulomb=1)) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, ScaleConverter) + assert x.converter.scale == 96485.3399 + assert x.reference == UnitsContainer(coulomb=1) x = Definition.from_string("degF = 9 / 5 * kelvin; offset: 255.372222") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, OffsetConverter) - self.assertEqual(x.converter.scale, 9 / 5) - self.assertEqual(x.converter.offset, 255.372222) - self.assertEqual(x.reference, UnitsContainer(kelvin=1)) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, OffsetConverter) + assert x.converter.scale == 9 / 5 + assert x.converter.offset == 255.372222 + assert x.reference == UnitsContainer(kelvin=1) x = Definition.from_string( "turn = 6.28 * radian = _ = revolution = = cycle = _" ) - self.assertIsInstance(x, UnitDefinition) - self.assertEqual(x.name, "turn") - self.assertEqual(x.aliases, ("revolution", "cycle")) - self.assertEqual(x.symbol, "turn") - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, ScaleConverter) - self.assertEqual(x.converter.scale, 6.28) - self.assertEqual(x.reference, UnitsContainer(radian=1)) - - self.assertRaises( - ValueError, - Definition.from_string, - "degF = 9 / 5 * kelvin; offset: 255.372222 bla", - ) + assert isinstance(x, UnitDefinition) + assert x.name == "turn" + assert x.aliases == ("revolution", "cycle") + assert x.symbol == "turn" + assert not x.is_base + assert isinstance(x.converter, ScaleConverter) + assert x.converter.scale == 6.28 + assert x.reference == UnitsContainer(radian=1) + + with pytest.raises(ValueError): + Definition.from_string( + "degF = 9 / 5 * kelvin; offset: 255.372222 bla", + ) def test_log_unit_definition(self): x = Definition.from_string( "decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm" ) - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1e-3) - self.assertEqual(x.converter.logbase, 10) - self.assertEqual(x.converter.logfactor, 10) - self.assertEqual(x.reference, UnitsContainer(watt=1)) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1e-3 + assert x.converter.logbase == 10 + assert x.converter.logfactor == 10 + assert x.reference == UnitsContainer(watt=1) x = Definition.from_string("decibel = 1 ; logbase: 10; logfactor: 10 = dB") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.converter.logbase, 10) - self.assertEqual(x.converter.logfactor, 10) - self.assertEqual(x.reference, UnitsContainer()) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1 + assert x.converter.logbase == 10 + assert x.converter.logfactor == 10 + assert x.reference == UnitsContainer() x = Definition.from_string("bell = 1 ; logbase: 10; logfactor: 1 = B") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.converter.logbase, 10) - self.assertEqual(x.converter.logfactor, 1) - self.assertEqual(x.reference, UnitsContainer()) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1 + assert x.converter.logbase == 10 + assert x.converter.logfactor == 1 + assert x.reference == UnitsContainer() x = Definition.from_string("decade = 1 ; logbase: 10; logfactor: 1") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.converter.logbase, 10) - self.assertEqual(x.converter.logfactor, 1) - self.assertEqual(x.reference, UnitsContainer()) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1 + assert x.converter.logbase == 10 + assert x.converter.logfactor == 1 + assert x.reference == UnitsContainer() eulersnumber = 2.71828182845904523536028747135266249775724709369995 x = Definition.from_string( "neper = 1 ; logbase: %1.50f; logfactor: 0.5 = Np" % eulersnumber ) - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.converter.logbase, eulersnumber) - self.assertEqual(x.converter.logfactor, 0.5) - self.assertEqual(x.reference, UnitsContainer()) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1 + assert x.converter.logbase == eulersnumber + assert x.converter.logfactor == 0.5 + assert x.reference == UnitsContainer() x = Definition.from_string("octave = 1 ; logbase: 2; logfactor: 1 = oct") - self.assertIsInstance(x, UnitDefinition) - self.assertFalse(x.is_base) - self.assertIsInstance(x.converter, LogarithmicConverter) - self.assertEqual(x.converter.scale, 1) - self.assertEqual(x.converter.logbase, 2) - self.assertEqual(x.converter.logfactor, 1) - self.assertEqual(x.reference, UnitsContainer()) + assert isinstance(x, UnitDefinition) + assert not x.is_base + assert isinstance(x.converter, LogarithmicConverter) + assert x.converter.scale == 1 + assert x.converter.logbase == 2 + assert x.converter.logfactor == 1 + assert x.reference == UnitsContainer() def test_dimension_definition(self): x = DimensionDefinition("[time]", "", (), None, is_base=True) - self.assertTrue(x.is_base) - self.assertEqual(x.name, "[time]") + assert x.is_base + assert x.name == "[time]" x = Definition.from_string("[speed] = [length]/[time]") - self.assertIsInstance(x, DimensionDefinition) - self.assertEqual(x.reference, UnitsContainer({"[length]": 1, "[time]": -1})) + assert isinstance(x, DimensionDefinition) + assert x.reference == UnitsContainer({"[length]": 1, "[time]": -1}) def test_alias_definition(self): x = Definition.from_string("@alias meter = metro = metr") - self.assertIsInstance(x, AliasDefinition) - self.assertEqual(x.name, "meter") - self.assertEqual(x.aliases, ("metro", "metr")) + assert isinstance(x, AliasDefinition) + assert x.name == "meter" + assert x.aliases == ("metro", "metr") diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py index 3e65c6135..c6f14fbd7 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -1,5 +1,7 @@ import pickle +import pytest + from pint import ( DefinitionSyntaxError, DimensionalityError, @@ -11,107 +13,106 @@ UnitRegistry, ) from pint.errors import LOG_ERROR_DOCS_HTML, OFFSET_ERROR_DOCS_HTML -from pint.testsuite import BaseTestCase -class TestErrors(BaseTestCase): +class TestErrors: def test_definition_syntax_error(self): ex = DefinitionSyntaxError("foo") - self.assertEqual(str(ex), "foo") + assert str(ex) == "foo" # filename and lineno can be attached after init ex.filename = "a.txt" ex.lineno = 123 - self.assertEqual(str(ex), "While opening a.txt, in line 123: foo") + assert str(ex) == "While opening a.txt, in line 123: foo" ex = DefinitionSyntaxError("foo", lineno=123) - self.assertEqual(str(ex), "In line 123: foo") + assert str(ex) == "In line 123: foo" ex = DefinitionSyntaxError("foo", filename="a.txt") - self.assertEqual(str(ex), "While opening a.txt: foo") + assert str(ex) == "While opening a.txt: foo" ex = DefinitionSyntaxError("foo", filename="a.txt", lineno=123) - self.assertEqual(str(ex), "While opening a.txt, in line 123: foo") + assert str(ex) == "While opening a.txt, in line 123: foo" def test_redefinition_error(self): ex = RedefinitionError("foo", "bar") - self.assertEqual(str(ex), "Cannot redefine 'foo' (bar)") + assert str(ex) == "Cannot redefine 'foo' (bar)" # filename and lineno can be attached after init ex.filename = "a.txt" ex.lineno = 123 - self.assertEqual( - str(ex), "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" + assert ( + str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" ) ex = RedefinitionError("foo", "bar", lineno=123) - self.assertEqual(str(ex), "In line 123: Cannot redefine 'foo' (bar)") + assert str(ex) == "In line 123: Cannot redefine 'foo' (bar)" ex = RedefinitionError("foo", "bar", filename="a.txt") - self.assertEqual(str(ex), "While opening a.txt: Cannot redefine 'foo' (bar)") + assert str(ex) == "While opening a.txt: Cannot redefine 'foo' (bar)" ex = RedefinitionError("foo", "bar", filename="a.txt", lineno=123) - self.assertEqual( - str(ex), "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" + assert ( + str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)" ) def test_undefined_unit_error(self): x = ("meter",) msg = "'meter' is not defined in the unit registry" - self.assertEqual(str(UndefinedUnitError(x)), msg) - self.assertEqual(str(UndefinedUnitError(list(x))), msg) - self.assertEqual(str(UndefinedUnitError(set(x))), msg) + assert str(UndefinedUnitError(x)) == msg + assert str(UndefinedUnitError(list(x))) == msg + assert str(UndefinedUnitError(set(x))) == msg def test_undefined_unit_error_multi(self): x = ("meter", "kg") msg = "('meter', 'kg') are not defined in the unit registry" - self.assertEqual(str(UndefinedUnitError(x)), msg) - self.assertEqual(str(UndefinedUnitError(list(x))), msg) + assert str(UndefinedUnitError(x)) == msg + assert str(UndefinedUnitError(list(x))) == msg def test_dimensionality_error(self): ex = DimensionalityError("a", "b") - self.assertEqual(str(ex), "Cannot convert from 'a' to 'b'") + assert str(ex) == "Cannot convert from 'a' to 'b'" ex = DimensionalityError("a", "b", "c") - self.assertEqual(str(ex), "Cannot convert from 'a' (c) to 'b' ()") + assert str(ex) == "Cannot convert from 'a' (c) to 'b' ()" ex = DimensionalityError("a", "b", "c", "d", extra_msg=": msg") - self.assertEqual(str(ex), "Cannot convert from 'a' (c) to 'b' (d): msg") + assert str(ex) == "Cannot convert from 'a' (c) to 'b' (d): msg" def test_offset_unit_calculus_error(self): ex = OffsetUnitCalculusError(Quantity("1 kg")._units) - self.assertEqual( - str(ex), - "Ambiguous operation with offset unit (kilogram). See " + assert ( + str(ex) + == "Ambiguous operation with offset unit (kilogram). See " + OFFSET_ERROR_DOCS_HTML - + " for guidance.", + + " for guidance." ) ex = OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units) - self.assertEqual( - str(ex), - "Ambiguous operation with offset unit (kilogram, second). See " + assert ( + str(ex) + == "Ambiguous operation with offset unit (kilogram, second). See " + OFFSET_ERROR_DOCS_HTML - + " for guidance.", + + " for guidance." ) def test_logarithmic_unit_calculus_error(self): Quantity = UnitRegistry(autoconvert_offset_to_baseunit=True).Quantity ex = LogarithmicUnitCalculusError(Quantity("1 dB")._units) - self.assertEqual( - str(ex), - "Ambiguous operation with logarithmic unit (decibel). See " + assert ( + str(ex) + == "Ambiguous operation with logarithmic unit (decibel). See " + LOG_ERROR_DOCS_HTML - + " for guidance.", + + " for guidance." ) ex = LogarithmicUnitCalculusError( Quantity("1 dB")._units, Quantity("1 octave")._units ) - self.assertEqual( - str(ex), - "Ambiguous operation with logarithmic unit (decibel, octave). See " + assert ( + str(ex) + == "Ambiguous operation with logarithmic unit (decibel, octave). See " + LOG_ERROR_DOCS_HTML - + " for guidance.", + + " for guidance." ) - def test_pickle_definition_syntax_error(self): + def test_pickle_definition_syntax_error(self, subtests): # OffsetUnitCalculusError raised from a custom ureg must be pickleable even if # the ureg is not registered as the application ureg ureg = UnitRegistry(filename=None) @@ -131,14 +132,14 @@ def test_pickle_definition_syntax_error(self): ), OffsetUnitCalculusError(q1._units, q2._units), ]: - with self.subTest(protocol=protocol, etype=type(ex)): + with subtests.test(protocol=protocol, etype=type(ex)): pik = pickle.dumps(ureg.Quantity("1 foo"), protocol) - with self.assertRaises(UndefinedUnitError): + with pytest.raises(UndefinedUnitError): pickle.loads(pik) # assert False, ex.__reduce__() ex2 = pickle.loads(pickle.dumps(ex, protocol)) assert type(ex) is type(ex2) - self.assertEqual(ex.args, ex2.args) - self.assertEqual(ex.__dict__, ex2.__dict__) - self.assertEqual(str(ex), str(ex2)) + assert ex.args == ex2.args + assert ex.__dict__ == ex2.__dict__ + assert str(ex) == str(ex2) diff --git a/pint/testsuite/test_formatter.py b/pint/testsuite/test_formatter.py index c4a4d5766..9e362fc68 100644 --- a/pint/testsuite/test_formatter.py +++ b/pint/testsuite/test_formatter.py @@ -1,47 +1,48 @@ +import pytest + from pint import formatting as fmt -from pint.testsuite import QuantityTestCase -class TestFormatter(QuantityTestCase): +class TestFormatter: def test_join(self): for empty in (tuple(), []): - self.assertEqual(fmt._join("s", empty), "") - self.assertEqual(fmt._join("*", "1 2 3".split()), "1*2*3") - self.assertEqual(fmt._join("{0}*{1}", "1 2 3".split()), "1*2*3") + assert fmt._join("s", empty) == "" + assert fmt._join("*", "1 2 3".split()) == "1*2*3" + assert fmt._join("{0}*{1}", "1 2 3".split()) == "1*2*3" def test_formatter(self): - self.assertEqual(fmt.formatter(dict().items()), "") - self.assertEqual(fmt.formatter(dict(meter=1).items()), "meter") - self.assertEqual(fmt.formatter(dict(meter=-1).items()), "1 / meter") - self.assertEqual( - fmt.formatter(dict(meter=-1).items(), as_ratio=False), "meter ** -1" - ) + assert fmt.formatter(dict().items()) == "" + assert fmt.formatter(dict(meter=1).items()) == "meter" + assert fmt.formatter(dict(meter=-1).items()) == "1 / meter" + assert fmt.formatter(dict(meter=-1).items(), as_ratio=False) == "meter ** -1" - self.assertEqual( - fmt.formatter(dict(meter=-1, second=-1).items(), as_ratio=False), - "meter ** -1 * second ** -1", - ) - self.assertEqual( - fmt.formatter(dict(meter=-1, second=-1).items()), "1 / meter / second" + assert ( + fmt.formatter(dict(meter=-1, second=-1).items(), as_ratio=False) + == "meter ** -1 * second ** -1" ) - self.assertEqual( - fmt.formatter(dict(meter=-1, second=-1).items(), single_denominator=True), - "1 / (meter * second)", + assert fmt.formatter(dict(meter=-1, second=-1).items()) == "1 / meter / second" + assert ( + fmt.formatter(dict(meter=-1, second=-1).items(), single_denominator=True) + == "1 / (meter * second)" ) - self.assertEqual( - fmt.formatter(dict(meter=-1, second=-2).items()), "1 / meter / second ** 2" + assert ( + fmt.formatter(dict(meter=-1, second=-2).items()) + == "1 / meter / second ** 2" ) - self.assertEqual( - fmt.formatter(dict(meter=-1, second=-2).items(), single_denominator=True), - "1 / (meter * second ** 2)", + assert ( + fmt.formatter(dict(meter=-1, second=-2).items(), single_denominator=True) + == "1 / (meter * second ** 2)" ) def test_parse_spec(self): - self.assertEqual(fmt._parse_spec(""), "") - self.assertEqual(fmt._parse_spec(""), "") - self.assertRaises(ValueError, fmt._parse_spec, "W") - self.assertRaises(ValueError, fmt._parse_spec, "PL") + assert fmt._parse_spec("") == "" + assert fmt._parse_spec("") == "" + with pytest.raises(ValueError): + fmt._parse_spec("W") + with pytest.raises(ValueError): + fmt._parse_spec("PL") def test_format_unit(self): - self.assertEqual(fmt.format_unit("", "C"), "dimensionless") - self.assertRaises(ValueError, fmt.format_unit, "m", "W") + assert fmt.format_unit("", "C") == "dimensionless" + with pytest.raises(ValueError): + fmt.format_unit("m", "W") diff --git a/pint/testsuite/test_infer_base_unit.py b/pint/testsuite/test_infer_base_unit.py index dc3bd8253..54a472030 100644 --- a/pint/testsuite/test_infer_base_unit.py +++ b/pint/testsuite/test_infer_base_unit.py @@ -1,32 +1,30 @@ from pint import Quantity as Q -from pint.testsuite import QuantityTestCase +from pint.testsuite import helpers from pint.util import infer_base_unit -class TestInferBaseUnit(QuantityTestCase): +class TestInferBaseUnit: def test_infer_base_unit(self): from pint.util import infer_base_unit - self.assertEqual( - infer_base_unit(Q(1, "millimeter * nanometer")), Q(1, "meter**2").units - ) + assert infer_base_unit(Q(1, "millimeter * nanometer")) == Q(1, "meter**2").units def test_units_adding_to_zero(self): - self.assertEqual(infer_base_unit(Q(1, "m * mm / m / um * s")), Q(1, "s").units) + assert infer_base_unit(Q(1, "m * mm / m / um * s")) == Q(1, "s").units def test_to_compact(self): r = Q(1000000000, "m") * Q(1, "mm") / Q(1, "s") / Q(1, "ms") compact_r = r.to_compact() expected = Q(1000.0, "kilometer**2 / second**2") - self.assertQuantityAlmostEqual(compact_r, expected) + helpers.assert_quantity_almost_equal(compact_r, expected) r = (Q(1, "m") * Q(1, "mm") / Q(1, "m") / Q(2, "um") * Q(2, "s")).to_compact() - self.assertQuantityAlmostEqual(r, Q(1000, "s")) + helpers.assert_quantity_almost_equal(r, Q(1000, "s")) def test_volts(self): from pint.util import infer_base_unit r = Q(1, "V") * Q(1, "mV") / Q(1, "kV") b = infer_base_unit(r) - self.assertEqual(b, Q(1, "V").units) - self.assertQuantityAlmostEqual(r, Q(1, "uV")) + assert b == Q(1, "V").units + helpers.assert_quantity_almost_equal(r, Q(1, "uV")) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 6be33db94..4d3632608 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1,7 +1,6 @@ import copy import math import pprint -import unittest import pytest @@ -16,109 +15,108 @@ class TestIssues(QuantityTestCase): - FORCE_NDARRAY = False + kwargs = dict(autoconvert_offset_to_baseunit=False) - def setup(self): - self.ureg.autoconvert_offset_to_baseunit = False - - @unittest.expectedFailure + @pytest.mark.xfail def test_issue25(self): x = ParserHelper.from_string("10 %") - self.assertEqual(x, ParserHelper(10, {"%": 1})) + assert x == ParserHelper(10, {"%": 1}) x = ParserHelper.from_string("10 ‰") - self.assertEqual(x, ParserHelper(10, {"‰": 1})) + assert x == ParserHelper(10, {"‰": 1}) ureg.define("percent = [fraction]; offset: 0 = %") ureg.define("permille = percent / 10 = ‰") x = ureg.parse_expression("10 %") - self.assertEqual(x, ureg.Quantity(10, {"%": 1})) + assert x == ureg.Quantity(10, {"%": 1}) y = ureg.parse_expression("10 ‰") - self.assertEqual(y, ureg.Quantity(10, {"‰": 1})) - self.assertEqual(x.to("‰"), ureg.Quantity(1, {"‰": 1})) + assert y == ureg.Quantity(10, {"‰": 1}) + assert x.to("‰") == ureg.Quantity(1, {"‰": 1}) def test_issue29(self): t = 4 * ureg("mW") - self.assertEqual(t.magnitude, 4) - self.assertEqual(t._units, UnitsContainer(milliwatt=1)) - self.assertEqual(t.to("joule / second"), 4e-3 * ureg("W")) + assert t.magnitude == 4 + assert t._units == UnitsContainer(milliwatt=1) + assert t.to("joule / second") == 4e-3 * ureg("W") - @unittest.expectedFailure - @helpers.requires_numpy() + @pytest.mark.xfail + @helpers.requires_numpy def test_issue37(self): x = np.ma.masked_array([1, 2, 3], mask=[True, True, False]) q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) + assert isinstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) + assert q.units == ureg.meter.units q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) + assert isinstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) + assert q.units == ureg.meter.units m = np.ma.masked_array(2 * np.ones(3, 3)) qq = q * m - self.assertIsInstance(qq, ureg.Quantity) + assert isinstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) + assert qq.units == ureg.meter.units qq = m * q - self.assertIsInstance(qq, ureg.Quantity) + assert isinstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) + assert qq.units == ureg.meter.units - @unittest.expectedFailure - @helpers.requires_numpy() + @pytest.mark.xfail + @helpers.requires_numpy def test_issue39(self): x = np.matrix([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) q = ureg.meter * x - self.assertIsInstance(q, ureg.Quantity) + assert isinstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) + assert q.units == ureg.meter.units q = x * ureg.meter - self.assertIsInstance(q, ureg.Quantity) + assert isinstance(q, ureg.Quantity) np.testing.assert_array_equal(q.magnitude, x) - self.assertEqual(q.units, ureg.meter.units) + assert q.units == ureg.meter.units m = np.matrix(2 * np.ones(3, 3)) qq = q * m - self.assertIsInstance(qq, ureg.Quantity) + assert isinstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) + assert qq.units == ureg.meter.units qq = m * q - self.assertIsInstance(qq, ureg.Quantity) + assert isinstance(qq, ureg.Quantity) np.testing.assert_array_equal(qq.magnitude, x * m) - self.assertEqual(qq.units, ureg.meter.units) + assert qq.units == ureg.meter.units - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue44(self): x = 4.0 * ureg.dimensionless np.sqrt(x) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.sqrt([4.0] * ureg.dimensionless), [2.0] * ureg.dimensionless ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.sqrt(4.0 * ureg.dimensionless), 2.0 * ureg.dimensionless ) def test_issue45(self): import math - self.assertAlmostEqual(math.sqrt(4 * ureg.m / ureg.cm), math.sqrt(4 * 100)) - self.assertAlmostEqual(float(ureg.V / ureg.mV), 1000.0) + helpers.assert_quantity_almost_equal( + math.sqrt(4 * ureg.m / ureg.cm), math.sqrt(4 * 100) + ) + helpers.assert_quantity_almost_equal(float(ureg.V / ureg.mV), 1000.0) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue45b(self): - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( np.sin([np.pi / 2] * ureg.m / ureg.m), np.sin([np.pi / 2] * ureg.dimensionless), ) - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( np.sin([np.pi / 2] * ureg.cm / ureg.m), np.sin([np.pi / 2] * ureg.dimensionless * 0.01), ) def test_issue50(self): Q_ = ureg.Quantity - self.assertEqual(Q_(100), 100 * ureg.dimensionless) - self.assertEqual(Q_("100"), 100 * ureg.dimensionless) + assert Q_(100) == 100 * ureg.dimensionless + assert Q_("100") == 100 * ureg.dimensionless def test_issue52(self): u1 = UnitRegistry() @@ -139,63 +137,69 @@ def test_issue52(self): op.truediv, op.itruediv, ): - self.assertRaises(ValueError, fun, q1, q2) + with pytest.raises(ValueError): + fun(q1, q2) def test_issue54(self): - self.assertEqual((1 * ureg.km / ureg.m + 1).magnitude, 1001) + assert (1 * ureg.km / ureg.m + 1).magnitude == 1001 def test_issue54_related(self): - self.assertEqual(ureg.km / ureg.m, 1000) - self.assertEqual(1000, ureg.km / ureg.m) - self.assertLess(900, ureg.km / ureg.m) - self.assertGreater(1100, ureg.km / ureg.m) + assert ureg.km / ureg.m == 1000 + assert 1000 == ureg.km / ureg.m + assert 900 < ureg.km / ureg.m + assert 1100 > ureg.km / ureg.m def test_issue61(self): Q_ = ureg.Quantity for value in ({}, {"a": 3}, None): - self.assertRaises(TypeError, Q_, value) - self.assertRaises(TypeError, Q_, value, "meter") - self.assertRaises(ValueError, Q_, "", "meter") - self.assertRaises(ValueError, Q_, "") + with pytest.raises(TypeError): + Q_(value) + with pytest.raises(TypeError): + Q_(value, "meter") + with pytest.raises(ValueError): + Q_("", "meter") + with pytest.raises(ValueError): + Q_("") @helpers.requires_not_numpy() def test_issue61_notNP(self): Q_ = ureg.Quantity for value in ([1, 2, 3], (1, 2, 3)): - self.assertRaises(TypeError, Q_, value) - self.assertRaises(TypeError, Q_, value, "meter") + with pytest.raises(TypeError): + Q_(value) + with pytest.raises(TypeError): + Q_(value, "meter") def test_issue62(self): m = ureg("m**0.5") - self.assertEqual(str(m.units), "meter ** 0.5") + assert str(m.units) == "meter ** 0.5" def test_issue66(self): - self.assertEqual( - ureg.get_dimensionality(UnitsContainer({"[temperature]": 1})), - UnitsContainer({"[temperature]": 1}), + assert ureg.get_dimensionality( + UnitsContainer({"[temperature]": 1}) + ) == UnitsContainer({"[temperature]": 1}) + assert ureg.get_dimensionality(ureg.kelvin) == UnitsContainer( + {"[temperature]": 1} ) - self.assertEqual( - ureg.get_dimensionality(ureg.kelvin), UnitsContainer({"[temperature]": 1}) - ) - self.assertEqual( - ureg.get_dimensionality(ureg.degC), UnitsContainer({"[temperature]": 1}) + assert ureg.get_dimensionality(ureg.degC) == UnitsContainer( + {"[temperature]": 1} ) def test_issue66b(self): - self.assertEqual( - ureg.get_base_units(ureg.kelvin), - (1.0, ureg.Unit(UnitsContainer({"kelvin": 1}))), + assert ureg.get_base_units(ureg.kelvin) == ( + 1.0, + ureg.Unit(UnitsContainer({"kelvin": 1})), ) - self.assertEqual( - ureg.get_base_units(ureg.degC), - (1.0, ureg.Unit(UnitsContainer({"kelvin": 1}))), + assert ureg.get_base_units(ureg.degC) == ( + 1.0, + ureg.Unit(UnitsContainer({"kelvin": 1})), ) def test_issue69(self): q = ureg("m").to(ureg("in")) - self.assertEqual(q, ureg("m").to("in")) + assert q == ureg("m").to("in") - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue74(self): v1 = np.asarray([1.0, 2.0, 3.0]) v2 = np.asarray([3.0, 2.0, 1.0]) @@ -217,7 +221,7 @@ def test_issue74(self): np.testing.assert_array_equal(q1 <= q2s, v1 <= v2s) np.testing.assert_array_equal(q1 >= q2s, v1 >= v2s) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue75(self): v1 = np.asarray([1.0, 2.0, 3.0]) v2 = np.asarray([3.0, 2.0, 1.0]) @@ -238,7 +242,7 @@ def test_issue77(self): acc = (5.0 * ureg("m/s/s")).plus_minus(0.25) tim = (37.0 * ureg("s")).plus_minus(0.16) dis = acc * tim ** 2 / 2 - self.assertEqual(dis.value, acc.value * tim.value ** 2 / 2) + assert dis.value == acc.value * tim.value ** 2 / 2 def test_issue85(self): @@ -251,7 +255,7 @@ def test_issue85(self): boltmk = 1.380649e-23 * ureg.J / ureg.K vb = 2.0 * boltmk * T / m - self.assertQuantityAlmostEqual(va.to_base_units(), vb.to_base_units()) + helpers.assert_quantity_almost_equal(va.to_base_units(), vb.to_base_units()) def test_issue86(self): ureg = self.ureg @@ -273,22 +277,22 @@ def parts(q): k1m, k1u = parts(k1) - self.assertEqual(parts(q2 * q3), (q2m * q3m, q2u * q3u)) - self.assertEqual(parts(q2 / q3), (q2m / q3m, q2u / q3u)) - self.assertEqual(parts(q3 * q2), (q3m * q2m, q3u * q2u)) - self.assertEqual(parts(q3 / q2), (q3m / q2m, q3u / q2u)) - self.assertEqual(parts(q2 ** 1), (q2m ** 1, q2u ** 1)) - self.assertEqual(parts(q2 ** -1), (q2m ** -1, q2u ** -1)) - self.assertEqual(parts(q2 ** 2), (q2m ** 2, q2u ** 2)) - self.assertEqual(parts(q2 ** -2), (q2m ** -2, q2u ** -2)) - - self.assertEqual(parts(q1 * q3), (k1m * q3m, k1u * q3u)) - self.assertEqual(parts(q1 / q3), (k1m / q3m, k1u / q3u)) - self.assertEqual(parts(q3 * q1), (q3m * k1m, q3u * k1u)) - self.assertEqual(parts(q3 / q1), (q3m / k1m, q3u / k1u)) - self.assertEqual(parts(q1 ** -1), (k1m ** -1, k1u ** -1)) - self.assertEqual(parts(q1 ** 2), (k1m ** 2, k1u ** 2)) - self.assertEqual(parts(q1 ** -2), (k1m ** -2, k1u ** -2)) + assert parts(q2 * q3) == (q2m * q3m, q2u * q3u) + assert parts(q2 / q3) == (q2m / q3m, q2u / q3u) + assert parts(q3 * q2) == (q3m * q2m, q3u * q2u) + assert parts(q3 / q2) == (q3m / q2m, q3u / q2u) + assert parts(q2 ** 1) == (q2m ** 1, q2u ** 1) + assert parts(q2 ** -1) == (q2m ** -1, q2u ** -1) + assert parts(q2 ** 2) == (q2m ** 2, q2u ** 2) + assert parts(q2 ** -2) == (q2m ** -2, q2u ** -2) + + assert parts(q1 * q3) == (k1m * q3m, k1u * q3u) + assert parts(q1 / q3) == (k1m / q3m, k1u / q3u) + assert parts(q3 * q1) == (q3m * k1m, q3u * k1u) + assert parts(q3 / q1) == (q3m / k1m, q3u / k1u) + assert parts(q1 ** -1) == (k1m ** -1, k1u ** -1) + assert parts(q1 ** 2) == (k1m ** 2, k1u ** 2) + assert parts(q1 ** -2) == (k1m ** -2, k1u ** -2) def test_issues86b(self): ureg = self.ureg @@ -299,31 +303,31 @@ def test_issues86b(self): v1 = 2 * ureg.k * T1 / m v2 = 2 * ureg.k * T2 / m - self.assertQuantityAlmostEqual(v1, v2) - self.assertQuantityAlmostEqual(v1, v2.to_base_units()) - self.assertQuantityAlmostEqual(v1.to_base_units(), v2) - self.assertQuantityAlmostEqual(v1.to_base_units(), v2.to_base_units()) + helpers.assert_quantity_almost_equal(v1, v2) + helpers.assert_quantity_almost_equal(v1, v2.to_base_units()) + helpers.assert_quantity_almost_equal(v1.to_base_units(), v2) + helpers.assert_quantity_almost_equal(v1.to_base_units(), v2.to_base_units()) - @unittest.expectedFailure + @pytest.mark.xfail def test_issue86c(self): ureg = self.ureg ureg.autoconvert_offset_to_baseunit = True T = ureg.degC T = 100.0 * T - self.assertQuantityAlmostEqual(ureg.k * 2 * T, ureg.k * (2 * T)) + helpers.assert_quantity_almost_equal(ureg.k * 2 * T, ureg.k * (2 * T)) def test_issue93(self): x = 5 * ureg.meter - self.assertIsInstance(x.magnitude, int) + assert isinstance(x.magnitude, int) y = 0.1 * ureg.meter - self.assertIsInstance(y.magnitude, float) + assert isinstance(y.magnitude, float) z = 5 * ureg.meter - self.assertIsInstance(z.magnitude, int) + assert isinstance(z.magnitude, int) z += y - self.assertIsInstance(z.magnitude, float) + assert isinstance(z.magnitude, float) - self.assertQuantityAlmostEqual(x + y, 5.1 * ureg.meter) - self.assertQuantityAlmostEqual(z, 5.1 * ureg.meter) + helpers.assert_quantity_almost_equal(x + y, 5.1 * ureg.meter) + helpers.assert_quantity_almost_equal(z, 5.1 * ureg.meter) def test_issue104(self): @@ -339,40 +343,40 @@ def summer(values): return total - self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, "meter")) - self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, "meter")) - self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, "meter")) - self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, "meter")) + helpers.assert_quantity_almost_equal(summer(x), ureg.Quantity(3, "meter")) + helpers.assert_quantity_almost_equal(x[0], ureg.Quantity(1, "meter")) + helpers.assert_quantity_almost_equal(summer(y), ureg.Quantity(3, "meter")) + helpers.assert_quantity_almost_equal(y[0], ureg.Quantity(1, "meter")) def test_issue105(self): func = ureg.parse_unit_name val = list(func("meter")) - self.assertEqual(list(func("METER")), []) - self.assertEqual(val, list(func("METER", False))) + assert list(func("METER")) == [] + assert val == list(func("METER", False)) for func in (ureg.get_name, ureg.parse_expression): val = func("meter") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): func("METER") - self.assertEqual(val, func("METER", False)) + assert val == func("METER", False) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue127(self): q = [1.0, 2.0, 3.0, 4.0] * self.ureg.meter q[0] = np.nan - self.assertNotEqual(q[0], 1.0) - self.assertTrue(math.isnan(q[0].magnitude)) + assert q[0] != 1.0 + assert math.isnan(q[0].magnitude) q[1] = float("NaN") - self.assertNotEqual(q[1], 2.0) - self.assertTrue(math.isnan(q[1].magnitude)) + assert q[1] != 2.0 + assert math.isnan(q[1].magnitude) def test_issue170(self): Q_ = UnitRegistry().Quantity q = Q_("1 kHz") / Q_("100 Hz") iq = int(q) - self.assertEqual(iq, 10) - self.assertIsInstance(iq, int) + assert iq == 10 + assert isinstance(iq, int) def test_angstrom_creation(self): ureg.Quantity(2, "Å") @@ -383,57 +387,57 @@ def test_alternative_angstrom_definition(self): def test_micro_creation(self): ureg.Quantity(2, "µm") - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue171_real_imag(self): qr = [1.0, 2.0, 3.0, 4.0] * self.ureg.meter qi = [4.0, 3.0, 2.0, 1.0] * self.ureg.meter q = qr + 1j * qi - self.assertQuantityEqual(q.real, qr) - self.assertQuantityEqual(q.imag, qi) + helpers.assert_quantity_equal(q.real, qr) + helpers.assert_quantity_equal(q.imag, qi) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue171_T(self): a = np.asarray([[1.0, 2.0, 3.0, 4.0], [4.0, 3.0, 2.0, 1.0]]) q1 = a * self.ureg.meter q2 = a.T * self.ureg.meter - self.assertQuantityEqual(q1.T, q2) + helpers.assert_quantity_equal(q1.T, q2) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue250(self): a = self.ureg.V b = self.ureg.mV - self.assertEqual(np.float16(a / b), 1000.0) - self.assertEqual(np.float32(a / b), 1000.0) - self.assertEqual(np.float64(a / b), 1000.0) + assert np.float16(a / b) == 1000.0 + assert np.float32(a / b) == 1000.0 + assert np.float64(a / b) == 1000.0 if "float128" in dir(np): - self.assertEqual(np.float128(a / b), 1000.0) + assert np.float128(a / b) == 1000.0 def test_issue252(self): ur = UnitRegistry() q = ur("3 F") t = copy.deepcopy(q) u = t.to(ur.mF) - self.assertQuantityEqual(q.to(ur.mF), u) + helpers.assert_quantity_equal(q.to(ur.mF), u) def test_issue323(self): from fractions import Fraction as F - self.assertEqual((self.Q_(F(2, 3), "s")).to("ms"), self.Q_(F(2000, 3), "ms")) - self.assertEqual((self.Q_(F(2, 3), "m")).to("km"), self.Q_(F(1, 1500), "km")) + assert (self.Q_(F(2, 3), "s")).to("ms") == self.Q_(F(2000, 3), "ms") + assert (self.Q_(F(2, 3), "m")).to("km") == self.Q_(F(1, 1500), "km") def test_issue339(self): q1 = self.ureg("") - self.assertEqual(q1.magnitude, 1) - self.assertEqual(q1.units, self.ureg.dimensionless) + assert q1.magnitude == 1 + assert q1.units == self.ureg.dimensionless q2 = self.ureg("1 dimensionless") - self.assertEqual(q1, q2) + assert q1 == q2 def test_issue354_356_370(self): - self.assertEqual( - "{:~}".format(1 * self.ureg.second / self.ureg.millisecond), "1.0 s / ms" + assert ( + "{:~}".format(1 * self.ureg.second / self.ureg.millisecond) == "1.0 s / ms" ) - self.assertEqual("{:~}".format(1 * self.ureg.count), "1 count") - self.assertEqual("{:~}".format(1 * self.ureg("MiB")), "1 MiB") + assert "{:~}".format(1 * self.ureg.count) == "1 count" + assert "{:~}".format(1 * self.ureg("MiB")) == "1 MiB" def test_issue468(self): @ureg.wraps(("kg"), "meter") @@ -443,15 +447,15 @@ def f(x): x = ureg.Quantity(1.0, "meter") y = f(x) z = x * y - self.assertEqual(z, ureg.Quantity(1.0, "meter * kilogram")) + assert z == ureg.Quantity(1.0, "meter * kilogram") - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue482(self): q = self.ureg.Quantity(1, self.ureg.dimensionless) qe = np.exp(q) - self.assertIsInstance(qe, self.ureg.Quantity) + assert isinstance(qe, self.ureg.Quantity) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue483(self): ureg = self.ureg a = np.asarray([1, 2, 3]) @@ -465,19 +469,21 @@ def test_issue507(self): battery_ec = 16 * ureg.kWh / ureg._100km # noqa: F841 # ... but not with text ureg.define("_home = 4700 * kWh / year") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): home_elec_power = 1 * ureg._home # noqa: F841 # ... or with *only* underscores ureg.define("_ = 45 * km") - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): one_blank = 1 * ureg._ # noqa: F841 def test_issue523(self): src, dst = UnitsContainer({"meter": 1}), UnitsContainer({"degF": 1}) value = 10.0 convert = self.ureg.convert - self.assertRaises(DimensionalityError, convert, value, src, dst) - self.assertRaises(DimensionalityError, convert, value, dst, src) + with pytest.raises(DimensionalityError): + convert(value, src, dst) + with pytest.raises(DimensionalityError): + convert(value, dst, src) def test_issue532(self): ureg = self.ureg @@ -486,8 +492,9 @@ def test_issue532(self): def f(x): return 2 * x - self.assertEqual(f(ureg.Quantity(1, "")), 2) - self.assertRaises(DimensionalityError, f, ureg.Quantity(1, "m")) + assert f(ureg.Quantity(1, "")) == 2 + with pytest.raises(DimensionalityError): + f(ureg.Quantity(1, "m")) def test_issue625a(self): Q_ = ureg.Quantity @@ -521,11 +528,11 @@ def calculate_time_to_fall(height, gravity=Q_(9.8, "m/s^2")): lunar_module_height = Q_(10, "m") t1 = calculate_time_to_fall(lunar_module_height) # print(t1) - self.assertAlmostEqual(t1, Q_(1.4285714285714286, "s")) + assert round(abs(t1 - Q_(1.4285714285714286, "s")), 7) == 0 moon_gravity = Q_(1.625, "m/s^2") t2 = calculate_time_to_fall(lunar_module_height, moon_gravity) - self.assertAlmostEqual(t2, Q_(3.508232077228117, "s")) + assert round(abs(t2 - Q_(3.508232077228117, "s")), 7) == 0 def test_issue625b(self): Q_ = ureg.Quantity @@ -550,10 +557,10 @@ def get_displacement(time, rate=Q_(1, "m/s")): return time * rate d1 = get_displacement(Q_(2, "s")) - self.assertAlmostEqual(d1, Q_(2, "m")) + assert round(abs(d1 - Q_(2, "m")), 7) == 0 d2 = get_displacement(Q_(2, "s"), Q_(1, "deg/s")) - self.assertAlmostEqual(d2, Q_(2, " deg")) + assert round(abs(d2 - Q_(2, " deg")), 7) == 0 def test_issue625c(self): u = UnitRegistry() @@ -562,18 +569,18 @@ def test_issue625c(self): def get_product(a=2 * u.m, b=3 * u.m, c=5 * u.m): return a * b * c - self.assertEqual(get_product(a=3 * u.m), 45 * u.m ** 3) - self.assertEqual(get_product(b=2 * u.m), 20 * u.m ** 3) - self.assertEqual(get_product(c=1 * u.dimensionless), 6 * u.m ** 2) + assert get_product(a=3 * u.m) == 45 * u.m ** 3 + assert get_product(b=2 * u.m) == 20 * u.m ** 3 + assert get_product(c=1 * u.dimensionless) == 6 * u.m ** 2 def test_issue655a(self): distance = 1 * ureg.m time = 1 * ureg.s velocity = distance / time - self.assertEqual(distance.check("[length]"), True) - self.assertEqual(distance.check("[time]"), False) - self.assertEqual(velocity.check("[length] / [time]"), True) - self.assertEqual(velocity.check("1 / [time] * [length]"), True) + assert distance.check("[length]") + assert not distance.check("[time]") + assert velocity.check("[length] / [time]") + assert velocity.check("1 / [time] * [length]") def test_issue655b(self): Q_ = ureg.Quantity @@ -586,11 +593,11 @@ def pendulum_period(length, G=Q_(1, "standard_gravity")): length = Q_(1, ureg.m) # Assume earth gravity t = pendulum_period(length) - self.assertAlmostEqual(t, Q_("2.0064092925890407 second")) + assert round(abs(t - Q_("2.0064092925890407 second")), 7) == 0 # Use moon gravity moon_gravity = Q_(1.625, "m/s^2") t = pendulum_period(length, moon_gravity) - self.assertAlmostEqual(t, Q_("4.928936075204336 second")) + assert round(abs(t - Q_("4.928936075204336 second")), 7) == 0 def test_issue783(self): assert not ureg("g") == [] @@ -658,12 +665,12 @@ def test_issue912(self): def test_issue932(self): q = ureg.Quantity("1 kg") - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): q.to("joule") ureg.enable_contexts("energy", *(Context() for _ in range(20))) q.to("joule") ureg.disable_contexts() - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): q.to("joule") def test_issue960(self): @@ -681,10 +688,10 @@ def __rmul__(self, other): q = 3 * ureg.s d = MultiplicativeDictionary({4: 5, 6: 7}) assert q * d == MultiplicativeDictionary({4: 15 * ureg.s, 6: 21 * ureg.s}) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): d * q - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue973(self): """Verify that an empty array Quantity can be created through multiplication.""" q0 = np.array([]) * ureg.m # by Unit @@ -698,10 +705,11 @@ def test_issue1058(self): of same base type succeeds""" q = 1 * ureg.mg / ureg.g / ureg.kg q.ito_reduced_units() - self.assertIsInstance(q, ureg.Quantity) + assert isinstance(q, ureg.Quantity) def test_issue1062_issue1097(self): # Must not be used by any other tests + ureg = UnitRegistry() assert "nanometer" not in ureg._units for i in range(5): ctx = Context.from_lines(["@context _", "cal = 4 J"]) @@ -745,7 +753,7 @@ def test_issue1112(self): ureg.enable_contexts("c2") ureg.enable_contexts("c3") - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue1144_1102(self): # Performing operations shouldn't modify the original objects # Issue 1144 @@ -768,7 +776,7 @@ def test_issue1144_1102(self): q1 - q2 assert all(q1 == ureg.Quantity([-287.78, -32.24, -1.94], ddc)) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue_1136(self): assert (2 ** ureg.Quantity([2, 3], "") == 2 ** np.array([2, 3])).all() @@ -780,14 +788,14 @@ def test_issue1175(self): foo1 = get_application_registry().Quantity(1, "s") foo2 = pickle.loads(pickle.dumps(foo1)) - self.assertIsInstance(foo1, foo2.__class__) - self.assertIsInstance(foo2, foo1.__class__) + assert isinstance(foo1, foo2.__class__) + assert isinstance(foo2, foo1.__class__) - @helpers.requires_numpy() + @helpers.requires_numpy def test_issue1174(self): q = [1.0, -2.0, 3.0, -4.0] * self.ureg.meter - self.assertTrue(np.sign(q[0].magnitude)) - self.assertTrue(np.sign(q[1].magnitude)) + assert np.sign(q[0].magnitude) + assert np.sign(q[1].magnitude) if np is not None: diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index 0860dd7a4..77eba025b 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -1,9 +1,10 @@ +import logging import math import pytest from pint import OffsetUnitCalculusError, UnitRegistry -from pint.testsuite import QuantityTestCase +from pint.testsuite import QuantityTestCase, helpers from pint.unit import Unit, UnitsContainer @@ -18,10 +19,7 @@ def ureg(): class TestLogarithmicQuantity(QuantityTestCase): - - FORCE_NDARRAY = False - - def test_log_quantity_creation(self): + def test_log_quantity_creation(self, caplog): # Following Quantity Creation Pattern for args in ( @@ -30,42 +28,46 @@ def test_log_quantity_creation(self): (4.2, self.ureg.dBm), ): x = self.Q_(*args) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) + assert x.magnitude == 4.2 + assert x.units == UnitsContainer(decibelmilliwatt=1) x = self.Q_(self.Q_(4.2, "dBm")) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) + assert x.magnitude == 4.2 + assert x.units == UnitsContainer(decibelmilliwatt=1) x = self.Q_(4.2, UnitsContainer(decibelmilliwatt=1)) y = self.Q_(x) - self.assertEqual(x.magnitude, y.magnitude) - self.assertEqual(x.units, y.units) - self.assertIsNot(x, y) + assert x.magnitude == y.magnitude + assert x.units == y.units + assert x is not y # Using multiplications for dB units requires autoconversion to baseunits new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) x = new_reg.Quantity("4.2 * dBm") - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1)) + assert x.magnitude == 4.2 + assert x.units == UnitsContainer(decibelmilliwatt=1) + + with caplog.at_level(logging.DEBUG): + assert "wally" not in caplog.text + assert 4.2 * new_reg.dBm == new_reg.Quantity(4.2, 2 * new_reg.dBm) - with self.capture_log() as buffer: - self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm)) - self.assertEqual(len(buffer), 1) + assert len(caplog.records) == 1 def test_log_convert(self): # # 1 dB = 1/10 * bel - # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) + # helpers.assert_quantity_almost_equal(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10) # # Uncomment Bell unit in default_en.txt # ## Test dB to dB units octave - decade # 1 decade = log2(10) octave - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave") ) # ## Test dB to dB units dBm - dBu # 0 dBm = 1mW = 1e3 uW = 30 dBu - self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu")) + helpers.assert_quantity_almost_equal( + self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu"), atol=1e-7 + ) def test_mix_regular_log_units(self): # Test regular-logarithmic mixed definition, such as dB/km or dB/cm @@ -73,13 +75,15 @@ def test_mix_regular_log_units(self): # Multiplications and divisions with a mix of Logarithmic Units and regular Units is normally not possible. # The reason is that dB are considered by pint like offset units. # Multiplications and divisions that involve offset units are badly defined, so pint raises an error - with self.assertRaises(OffsetUnitCalculusError): + with pytest.raises(OffsetUnitCalculusError): (-10.0 * self.ureg.dB) / (1 * self.ureg.cm) # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base. # With this flag on multiplications and divisions are now possible: new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True) - self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm) + helpers.assert_quantity_almost_equal( + -10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm + ) log_unit_names = [ diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py index 789e47c30..323140fd5 100644 --- a/pint/testsuite/test_measurement.py +++ b/pint/testsuite/test_measurement.py @@ -1,22 +1,19 @@ +import pytest + from pint import DimensionalityError from pint.testsuite import QuantityTestCase, helpers @helpers.requires_not_uncertainties() class TestNotMeasurement(QuantityTestCase): - - FORCE_NDARRAY = False - def test_instantiate(self): M_ = self.ureg.Measurement - self.assertRaises(RuntimeError, M_, 4.0, 0.1, "s") + with pytest.raises(RuntimeError): + M_(4.0, 0.1, "s") @helpers.requires_uncertainties() class TestMeasurement(QuantityTestCase): - - FORCE_NDARRAY = False - def test_simple(self): M_ = self.ureg.Measurement M_(4.0, 0.1, "s") @@ -35,11 +32,11 @@ def test_build(self): ) for m in ms: - self.assertEqual(m.value, v) - self.assertEqual(m.error, u) - self.assertEqual(m.rel, m.error / abs(m.value)) + assert m.value == v + assert m.error == u + assert m.rel == m.error / abs(m.value) - def test_format(self): + def test_format(self, subtests): v, u = self.Q_(4.0, "s ** 2"), self.Q_(0.1, "s ** 2") m = self.ureg.Measurement(v, u) @@ -58,10 +55,10 @@ def test_format(self): ("{:.1fC}", "(4.0+/-0.1) second**2"), ("{:.1fLx}", r"\SI{4.0 +- 0.1}{\second\squared}"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_paru(self): + def test_format_paru(self, subtests): v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) @@ -73,10 +70,10 @@ def test_format_paru(self): ("{:.3uSH}", "0.2000(100) second2"), ("{:.3uSC}", "0.2000(100) second**2"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_u(self): + def test_format_u(self, subtests): v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) @@ -92,11 +89,11 @@ def test_format_u(self): ), ("{:.1uLx}", r"\SI{0.20 +- 0.01}{\second\squared}"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_percu(self): - self.test_format_perce() + def test_format_percu(self, subtests): + self.test_format_perce(subtests) v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) @@ -107,10 +104,10 @@ def test_format_percu(self): ("{:.1u%H}", "(20 ± 1)% second2"), ("{:.1u%C}", "(20+/-1)% second**2"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_perce(self): + def test_format_perce(self, subtests): v, u = self.Q_(0.20, "s ** 2"), self.Q_(0.01, "s ** 2") m = self.ureg.Measurement(v, u) for spec, result in ( @@ -123,10 +120,10 @@ def test_format_perce(self): ("{:.1ueH}", "(2.0 ± 0.1)×10-1 second2"), ("{:.1ueC}", "(2.0+/-0.1)e-01 second**2"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_exponential_pos(self): + def test_format_exponential_pos(self, subtests): # Quantities in exponential format come with their own parenthesis, don't wrap # them twice m = self.ureg.Quantity(4e20, "s^2").plus_minus(1e19) @@ -139,10 +136,10 @@ def test_format_exponential_pos(self): ("{:C}", "(4.00+/-0.10)e+20 second**2"), ("{:Lx}", r"\SI{4.00 +- 0.10 e+20}{\second\squared}"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result - def test_format_exponential_neg(self): + def test_format_exponential_neg(self, subtests): m = self.ureg.Quantity(4e-20, "s^2").plus_minus(1e-21) for spec, result in ( ("{}", "(4.00 +/- 0.10)e-20 second ** 2"), @@ -156,19 +153,19 @@ def test_format_exponential_neg(self): ("{:C}", "(4.00+/-0.10)e-20 second**2"), ("{:Lx}", r"\SI{4.00 +- 0.10 e-20}{\second\squared}"), ): - with self.subTest(spec): - self.assertEqual(spec.format(m), result) + with subtests.test(spec): + assert spec.format(m) == result def test_raise_build(self): v, u = self.Q_(1.0, "s"), self.Q_(0.1, "s") o = self.Q_(0.1, "m") M_ = self.ureg.Measurement - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): M_(v, o) - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): v.plus_minus(o) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): v.plus_minus(u, relative=True) def test_propagate_linear(self): @@ -183,35 +180,41 @@ def test_propagate_linear(self): for factor, m in zip((3, -3, 3, -3), (m1, m3, m1, m3)): r = factor * m - self.assertAlmostEqual(r.value.magnitude, factor * m.value.magnitude) - self.assertAlmostEqual(r.error.magnitude, abs(factor * m.error.magnitude)) - self.assertEqual(r.value.units, m.value.units) + helpers.assert_quantity_almost_equal( + r.value.magnitude, factor * m.value.magnitude + ) + helpers.assert_quantity_almost_equal( + r.error.magnitude, abs(factor * m.error.magnitude) + ) + assert r.value.units == m.value.units for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): r = ml + mr - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.value.magnitude, ml.value.magnitude + mr.value.magnitude ) - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.error.magnitude, - ml.error.magnitude + mr.error.magnitude - if ml is mr - else (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** 0.5, + ( + ml.error.magnitude + mr.error.magnitude + if ml is mr + else (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** 0.5 + ), ) - self.assertEqual(r.value.units, ml.value.units) + assert r.value.units == ml.value.units for ml, mr in zip((m1, m1, m1, m3), (m1, m2, m3, m3)): r = ml - mr - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.value.magnitude, ml.value.magnitude - mr.value.magnitude ) - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.error.magnitude, 0 if ml is mr else (ml.error.magnitude ** 2 + mr.error.magnitude ** 2) ** 0.5, ) - self.assertEqual(r.value.units, ml.value.units) + assert r.value.units == ml.value.units def test_propagate_product(self): @@ -228,20 +231,20 @@ def test_propagate_product(self): for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): r = ml * mr - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.value.magnitude, ml.value.magnitude * mr.value.magnitude ) - self.assertEqual(r.value.units, ml.value.units * mr.value.units) + assert r.value.units == ml.value.units * mr.value.units for ml, mr in zip((m1, m1, m1, m3, m4), (m1, m2, m3, m3, m5)): r = ml / mr - self.assertAlmostEqual( + helpers.assert_quantity_almost_equal( r.value.magnitude, ml.value.magnitude / mr.value.magnitude ) - self.assertEqual(r.value.units, ml.value.units / mr.value.units) + assert r.value.units == ml.value.units / mr.value.units def test_measurement_comparison(self): x = self.Q_(4.2, "meter") y = self.Q_(5.0, "meter").plus_minus(0.1) - self.assertTrue(x <= y) - self.assertFalse(x >= y) + assert x <= y + assert not (x >= y) diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py index 4a377d25f..63f8091a7 100644 --- a/pint/testsuite/test_non_int.py +++ b/pint/testsuite/test_non_int.py @@ -5,9 +5,10 @@ from decimal import Decimal from fractions import Fraction +import pytest + from pint import DimensionalityError, OffsetUnitCalculusError, UnitRegistry -from pint.testsuite import QuantityTestCase -from pint.testsuite.parameterized import ParameterizedTestMixin as ParameterizedTestCase +from pint.testsuite import QuantityTestCase, helpers from pint.unit import UnitsContainer @@ -18,36 +19,25 @@ def __init__(self, q): class NonIntTypeQuantityTestCase(QuantityTestCase): - - NON_INT_TYPE = None - - @classmethod - def setUpClass(cls): - cls.ureg = UnitRegistry( - force_ndarray=cls.FORCE_NDARRAY, non_int_type=cls.NON_INT_TYPE - ) - cls.Q_ = cls.ureg.Quantity - cls.U_ = cls.ureg.Unit - - def assertQuantityAlmostEqual( + def assert_quantity_almost_equal( self, first, second, rtol="1e-07", atol="0", msg=None ): if isinstance(first, self.Q_): - self.assertIsInstance(first.m, (self.NON_INT_TYPE, int)) + assert isinstance(first.m, (self.NON_INT_TYPE, int)) else: - self.assertIsInstance(first, (self.NON_INT_TYPE, int)) + assert isinstance(first, (self.NON_INT_TYPE, int)) if isinstance(second, self.Q_): - self.assertIsInstance(second.m, (self.NON_INT_TYPE, int)) + assert isinstance(second.m, (self.NON_INT_TYPE, int)) else: - self.assertIsInstance(second, (self.NON_INT_TYPE, int)) - super().assertQuantityAlmostEqual( + assert isinstance(second, (self.NON_INT_TYPE, int)) + super().assert_quantity_almost_equal( first, second, self.NON_INT_TYPE(rtol), self.NON_INT_TYPE(atol), msg ) def QP_(self, value, units): - self.assertIsInstance(value, str) + assert isinstance(value, str) return self.Q_(self.NON_INT_TYPE(value), units) @@ -65,25 +55,24 @@ def test_quantity_creation(self): (self.Q_(value, "meter"),), ): x = self.Q_(*args) - self.assertEqual(x.magnitude, value) - self.assertEqual(x.units, self.ureg.UnitsContainer(meter=1)) + assert x.magnitude == value + assert x.units == self.ureg.UnitsContainer(meter=1) x = self.Q_(value, UnitsContainer(length=1)) y = self.Q_(x) - self.assertEqual(x.magnitude, y.magnitude) - self.assertEqual(x.units, y.units) - self.assertIsNot(x, y) + assert x.magnitude == y.magnitude + assert x.units == y.units + assert x is not y x = self.Q_(value, None) - self.assertEqual(x.magnitude, value) - self.assertEqual(x.units, UnitsContainer()) + assert x.magnitude == value + assert x.units == UnitsContainer() with self.capture_log() as buffer: - self.assertEqual( - value * self.ureg.meter, - self.Q_(value, self.NON_INT_TYPE("2") * self.ureg.meter), + assert value * self.ureg.meter == self.Q_( + value, self.NON_INT_TYPE("2") * self.ureg.meter ) - self.assertEqual(len(buffer), 1) + assert len(buffer) == 1 def test_quantity_comparison(self): x = self.QP_("4.2", "meter") @@ -92,37 +81,34 @@ def test_quantity_comparison(self): j = self.QP_("5", "meter*meter") # identity for single object - self.assertTrue(x == x) - self.assertFalse(x != x) + assert x == x + assert not (x != x) # identity for multiple objects with same value - self.assertTrue(x == y) - self.assertFalse(x != y) + assert x == y + assert not (x != y) - self.assertTrue(x <= y) - self.assertTrue(x >= y) - self.assertFalse(x < y) - self.assertFalse(x > y) + assert x <= y + assert x >= y + assert not (x < y) + assert not (x > y) - self.assertFalse(x == z) - self.assertTrue(x != z) - self.assertTrue(x < z) + assert not (x == z) + assert x != z + assert x < z - self.assertTrue(z != j) + assert z != j - self.assertNotEqual(z, j) - self.assertEqual(self.QP_("0", "meter"), self.QP_("0", "centimeter")) - self.assertNotEqual(self.QP_("0", "meter"), self.QP_("0", "second")) + assert z != j + assert self.QP_("0", "meter") == self.QP_("0", "centimeter") + assert self.QP_("0", "meter") != self.QP_("0", "second") - self.assertLess(self.QP_("10", "meter"), self.QP_("5", "kilometer")) + assert self.QP_("10", "meter") < self.QP_("5", "kilometer") def test_quantity_comparison_convert(self): - self.assertEqual(self.QP_("1000", "millimeter"), self.QP_("1", "meter")) - self.assertEqual( - self.QP_("1000", "millimeter/min"), - self.Q_( - self.NON_INT_TYPE("1000") / self.NON_INT_TYPE("60"), "millimeter/s" - ), + assert self.QP_("1000", "millimeter") == self.QP_("1", "meter") + assert self.QP_("1000", "millimeter/min") == self.Q_( + self.NON_INT_TYPE("1000") / self.NON_INT_TYPE("60"), "millimeter/s" ) def test_quantity_hash(self): @@ -130,29 +116,31 @@ def test_quantity_hash(self): x2 = self.QP_("4200", "millimeter") y = self.QP_("2", "second") z = self.QP_("0.5", "hertz") - self.assertEqual(hash(x), hash(x2)) + assert hash(x) == hash(x2) # Dimensionless equality - self.assertEqual(hash(y * z), hash(1.0)) + assert hash(y * z) == hash(1.0) # Dimensionless equality from a different unit registry ureg2 = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) y2 = ureg2.Quantity(self.NON_INT_TYPE("2"), "second") z2 = ureg2.Quantity(self.NON_INT_TYPE("0.5"), "hertz") - self.assertEqual(hash(y * z), hash(y2 * z2)) + assert hash(y * z) == hash(y2 * z2) def test_to_base_units(self): x = self.Q_("1*inch") - self.assertQuantityAlmostEqual(x.to_base_units(), self.QP_("0.0254", "meter")) + helpers.assert_quantity_almost_equal( + x.to_base_units(), self.QP_("0.0254", "meter") + ) x = self.Q_("1*inch*inch") - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( x.to_base_units(), self.Q_( self.NON_INT_TYPE("0.0254") ** self.NON_INT_TYPE("2.0"), "meter*meter" ), ) x = self.Q_("1*inch/minute") - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( x.to_base_units(), self.Q_( self.NON_INT_TYPE("0.0254") / self.NON_INT_TYPE("60"), "meter/second" @@ -160,190 +148,196 @@ def test_to_base_units(self): ) def test_convert(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 inch").to("meter"), self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"), ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 meter").to("inch"), self.Q_(self.NON_INT_TYPE("2") / self.NON_INT_TYPE("0.0254"), "inch"), ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 sidereal_year").to("second"), self.QP_("63116297.5325", "second") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2.54 centimeter/second").to("inch/second"), self.Q_("1 inch/second"), ) - self.assertAlmostEqual(self.Q_("2.54 centimeter").to("inch").magnitude, 1) - self.assertAlmostEqual(self.Q_("2 second").to("millisecond").magnitude, 2000) + assert round(abs(self.Q_("2.54 centimeter").to("inch").magnitude - 1), 7) == 0 + assert ( + round(abs(self.Q_("2 second").to("millisecond").magnitude - 2000), 7) == 0 + ) def test_convert_from(self): x = self.Q_("2*inch") meter = self.ureg.meter # from quantity - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.from_(x), self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"), ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.m_from(x), self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254") ) # from unit - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.from_(self.ureg.inch), self.QP_("0.0254", "meter") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.m_from(self.ureg.inch), self.NON_INT_TYPE("0.0254") ) # from number - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.from_(2, strict=False), self.QP_("2", "meter") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.m_from(self.NON_INT_TYPE("2"), strict=False), self.NON_INT_TYPE("2") ) # from number (strict mode) - self.assertRaises(ValueError, meter.from_, self.NON_INT_TYPE("2")) - self.assertRaises(ValueError, meter.m_from, self.NON_INT_TYPE("2")) + with pytest.raises(ValueError): + meter.from_(self.NON_INT_TYPE("2")) + with pytest.raises(ValueError): + meter.m_from(self.NON_INT_TYPE("2")) def test_context_attr(self): - self.assertEqual(self.ureg.meter, self.QP_("1", "meter")) + assert self.ureg.meter == self.QP_("1", "meter") def test_both_symbol(self): - self.assertEqual(self.QP_("2", "ms"), self.QP_("2", "millisecond")) - self.assertEqual(self.QP_("2", "cm"), self.QP_("2", "centimeter")) + assert self.QP_("2", "ms") == self.QP_("2", "millisecond") + assert self.QP_("2", "cm") == self.QP_("2", "centimeter") def test_dimensionless_units(self): twopi = self.NON_INT_TYPE("2") * self.ureg.pi - self.assertAlmostEqual(self.QP_("360", "degree").to("radian").magnitude, twopi) - self.assertAlmostEqual(self.Q_(twopi, "radian"), self.QP_("360", "degree")) - self.assertEqual(self.QP_("1", "radian").dimensionality, UnitsContainer()) - self.assertTrue(self.QP_("1", "radian").dimensionless) - self.assertFalse(self.QP_("1", "radian").unitless) + assert ( + round(abs(self.QP_("360", "degree").to("radian").magnitude - twopi), 7) == 0 + ) + assert round(abs(self.Q_(twopi, "radian") - self.QP_("360", "degree")), 7) == 0 + assert self.QP_("1", "radian").dimensionality == UnitsContainer() + assert self.QP_("1", "radian").dimensionless + assert not self.QP_("1", "radian").unitless - self.assertEqual(self.QP_("1", "meter") / self.QP_("1", "meter"), 1) - self.assertEqual((self.QP_("1", "meter") / self.QP_("1", "mm")).to(""), 1000) + assert self.QP_("1", "meter") / self.QP_("1", "meter") == 1 + assert (self.QP_("1", "meter") / self.QP_("1", "mm")).to("") == 1000 - self.assertEqual(self.Q_(10) // self.QP_("360", "degree"), 1) - self.assertEqual(self.QP_("400", "degree") // self.Q_(twopi), 1) - self.assertEqual(self.QP_("400", "degree") // twopi, 1) - self.assertEqual(7 // self.QP_("360", "degree"), 1) + assert self.Q_(10) // self.QP_("360", "degree") == 1 + assert self.QP_("400", "degree") // self.Q_(twopi) == 1 + assert self.QP_("400", "degree") // twopi == 1 + assert 7 // self.QP_("360", "degree") == 1 def test_offset(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "kelvin").to("kelvin"), self.QP_("0", "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "degC").to("kelvin"), self.QP_("273.15", "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "degF").to("kelvin"), self.QP_("255.372222", "kelvin"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "kelvin").to("kelvin"), self.QP_("100", "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "degC").to("kelvin"), self.QP_("373.15", "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "degF").to("kelvin"), self.QP_("310.92777777", "kelvin"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "kelvin").to("degC"), self.QP_("-273.15", "degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "kelvin").to("degC"), self.QP_("-173.15", "degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "kelvin").to("degF"), self.QP_("-459.67", "degF"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "kelvin").to("degF"), self.QP_("-279.67", "degF"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("32", "degF").to("degC"), self.QP_("0", "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "degC").to("degF"), self.QP_("212", "degF"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("54", "degF").to("degC"), self.QP_("12.2222", "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "degC").to("degF"), self.QP_("53.6", "degF"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "kelvin").to("degC"), self.QP_("-261.15", "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "degC").to("kelvin"), self.QP_("285.15", "kelvin"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "kelvin").to("degR"), self.QP_("21.6", "degR"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "degR").to("kelvin"), self.QP_("6.66666667", "kelvin"), atol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "degC").to("degR"), self.QP_("513.27", "degR"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12", "degR").to("degC"), self.QP_("-266.483333", "degC"), atol=0.01, ) def test_offset_delta(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "delta_degC").to("kelvin"), self.QP_("0", "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("0", "delta_degF").to("kelvin"), self.QP_("0", "kelvin"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "kelvin").to("delta_degC"), self.QP_("100", "delta_degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "kelvin").to("delta_degF"), self.QP_("180", "delta_degF"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "delta_degF").to("kelvin"), self.QP_("55.55555556", "kelvin"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "delta_degC").to("delta_degF"), self.QP_("180", "delta_degF"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("100", "delta_degF").to("delta_degC"), self.QP_("55.55555556", "delta_degC"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.QP_("12.3", "delta_degC").to("delta_degF"), self.QP_("22.14", "delta_degF"), rtol=0.01, @@ -360,13 +354,13 @@ def test_pickle(self): with self.subTest(protocol=protocol, magnitude=magnitude, unit=unit): q1 = self.QP_(magnitude, unit) q2 = pickle.loads(pickle.dumps(q1, protocol)) - self.assertEqual(q1, q2) + assert q1 == q2 def test_notiter(self): # Verify that iter() crashes immediately, without needing to draw any # element from it, if the magnitude isn't iterable x = self.QP_("1", "m") - with self.assertRaises(TypeError): + with pytest.raises(TypeError): iter(x) @@ -393,10 +387,10 @@ def _test_inplace(self, operator, value1, value2, expected_result, unit=None): id2 = id(value2) value1 = operator(value1, value2) value2_cpy = copy.copy(value2) - self.assertQuantityAlmostEqual(value1, expected_result) - self.assertEqual(id1, id(value1)) - self.assertQuantityAlmostEqual(value2, value2_cpy) - self.assertEqual(id2, id(value2)) + helpers.assert_quantity_almost_equal(value1, expected_result) + assert id1 == id(value1) + helpers.assert_quantity_almost_equal(value2, value2_cpy) + assert id2 == id(value2) def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None): if isinstance(value1, str): @@ -419,11 +413,11 @@ def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None result = operator(value1, value2) - self.assertQuantityAlmostEqual(expected_result, result) - self.assertQuantityAlmostEqual(value1, value1_cpy) - self.assertQuantityAlmostEqual(value2, value2_cpy) - self.assertNotEqual(id(result), id1) - self.assertNotEqual(id(result), id2) + helpers.assert_quantity_almost_equal(expected_result, result) + helpers.assert_quantity_almost_equal(value1, value1_cpy) + helpers.assert_quantity_almost_equal(value2, value2_cpy) + assert id(result) != id1 + assert id(result) != id2 def _test_quantity_add_sub(self, unit, func): x = self.Q_(unit, "centimeter") @@ -442,9 +436,12 @@ def _test_quantity_add_sub(self, unit, func): self.Q_(unit + unit / (self.NON_INT_TYPE("2.54") * unit), "inch"), ) func(op.add, a, unit, self.Q_(unit + unit, None)) - self.assertRaises(DimensionalityError, op.add, self.NON_INT_TYPE("10"), x) - self.assertRaises(DimensionalityError, op.add, x, self.NON_INT_TYPE("10")) - self.assertRaises(DimensionalityError, op.add, x, z) + with pytest.raises(DimensionalityError): + op.add(self.NON_INT_TYPE("10"), x) + with pytest.raises(DimensionalityError): + op.add(x, self.NON_INT_TYPE("10")) + with pytest.raises(DimensionalityError): + op.add(x, z) func(op.sub, x, x, self.Q_(unit - unit, "centimeter")) func( @@ -457,9 +454,12 @@ def _test_quantity_add_sub(self, unit, func): self.Q_(unit - unit / (self.NON_INT_TYPE("2.54") * unit), "inch"), ) func(op.sub, a, unit, self.Q_(unit - unit, None)) - self.assertRaises(DimensionalityError, op.sub, self.NON_INT_TYPE("10"), x) - self.assertRaises(DimensionalityError, op.sub, x, self.NON_INT_TYPE("10")) - self.assertRaises(DimensionalityError, op.sub, x, z) + with pytest.raises(DimensionalityError): + op.sub(self.NON_INT_TYPE("10"), x) + with pytest.raises(DimensionalityError): + op.sub(x, self.NON_INT_TYPE("10")) + with pytest.raises(DimensionalityError): + op.sub(x, z) def _test_quantity_iadd_isub(self, unit, func): x = self.Q_(unit, "centimeter") @@ -476,17 +476,23 @@ def _test_quantity_iadd_isub(self, unit, func): ) func(op.iadd, y, x, self.Q_(unit + unit / self.NON_INT_TYPE("2.54"), "inch")) func(op.iadd, a, unit, self.Q_(unit + unit, None)) - self.assertRaises(DimensionalityError, op.iadd, self.NON_INT_TYPE("10"), x) - self.assertRaises(DimensionalityError, op.iadd, x, self.NON_INT_TYPE("10")) - self.assertRaises(DimensionalityError, op.iadd, x, z) + with pytest.raises(DimensionalityError): + op.iadd(self.NON_INT_TYPE("10"), x) + with pytest.raises(DimensionalityError): + op.iadd(x, self.NON_INT_TYPE("10")) + with pytest.raises(DimensionalityError): + op.iadd(x, z) func(op.isub, x, x, self.Q_(unit - unit, "centimeter")) func(op.isub, x, y, self.Q_(unit - self.NON_INT_TYPE("2.54"), "centimeter")) func(op.isub, y, x, self.Q_(unit - unit / self.NON_INT_TYPE("2.54"), "inch")) func(op.isub, a, unit, self.Q_(unit - unit, None)) - self.assertRaises(DimensionalityError, op.sub, self.NON_INT_TYPE("10"), x) - self.assertRaises(DimensionalityError, op.sub, x, self.NON_INT_TYPE("10")) - self.assertRaises(DimensionalityError, op.sub, x, z) + with pytest.raises(DimensionalityError): + op.sub(self.NON_INT_TYPE("10"), x) + with pytest.raises(DimensionalityError): + op.sub(x, self.NON_INT_TYPE("10")) + with pytest.raises(DimensionalityError): + op.sub(x, z) def _test_quantity_mul_div(self, unit, func): func(op.mul, unit * self.NON_INT_TYPE("10"), "4.2*meter", "42*meter", unit) @@ -511,12 +517,18 @@ def _test_quantity_imul_idiv(self, unit, func): def _test_quantity_floordiv(self, unit, func): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, op.floordiv, a, b) - self.assertRaises(DimensionalityError, op.floordiv, self.NON_INT_TYPE("3"), b) - self.assertRaises(DimensionalityError, op.floordiv, a, self.NON_INT_TYPE("3")) - self.assertRaises(DimensionalityError, op.ifloordiv, a, b) - self.assertRaises(DimensionalityError, op.ifloordiv, self.NON_INT_TYPE("3"), b) - self.assertRaises(DimensionalityError, op.ifloordiv, a, self.NON_INT_TYPE("3")) + with pytest.raises(DimensionalityError): + op.floordiv(a, b) + with pytest.raises(DimensionalityError): + op.floordiv(self.NON_INT_TYPE("3"), b) + with pytest.raises(DimensionalityError): + op.floordiv(a, self.NON_INT_TYPE("3")) + with pytest.raises(DimensionalityError): + op.ifloordiv(a, b) + with pytest.raises(DimensionalityError): + op.ifloordiv(self.NON_INT_TYPE("3"), b) + with pytest.raises(DimensionalityError): + op.ifloordiv(a, self.NON_INT_TYPE("3")) func( op.floordiv, unit * self.NON_INT_TYPE("10"), @@ -529,12 +541,18 @@ def _test_quantity_floordiv(self, unit, func): def _test_quantity_mod(self, unit, func): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, op.mod, a, b) - self.assertRaises(DimensionalityError, op.mod, 3, b) - self.assertRaises(DimensionalityError, op.mod, a, 3) - self.assertRaises(DimensionalityError, op.imod, a, b) - self.assertRaises(DimensionalityError, op.imod, 3, b) - self.assertRaises(DimensionalityError, op.imod, a, 3) + with pytest.raises(DimensionalityError): + op.mod(a, b) + with pytest.raises(DimensionalityError): + op.mod(3, b) + with pytest.raises(DimensionalityError): + op.mod(a, 3) + with pytest.raises(DimensionalityError): + op.imod(a, b) + with pytest.raises(DimensionalityError): + op.imod(3, b) + with pytest.raises(DimensionalityError): + op.imod(a, 3) func( op.mod, unit * self.NON_INT_TYPE("10"), @@ -560,25 +578,25 @@ def _test_quantity_divmod_one(self, a, b): b = self.Q_(b) q, r = divmod(a, b) - self.assertEqual(q, a // b) - self.assertEqual(r, a % b) - self.assertQuantityEqual(a, (q * b) + r) - self.assertEqual(q, math.floor(q)) + assert q == a // b + assert r == a % b + helpers.assert_quantity_equal(a, (q * b) + r) + assert q == math.floor(q) if b > (0 * b): - self.assertTrue((0 * b) <= r < b) + assert (0 * b) <= r < b else: - self.assertTrue((0 * b) >= r > b) + assert (0 * b) >= r > b if isinstance(a, self.Q_): - self.assertEqual(r.units, a.units) + assert r.units == a.units else: - self.assertTrue(r.unitless) - self.assertTrue(q.unitless) + assert r.unitless + assert q.unitless copy_a = copy.copy(a) a %= b - self.assertEqual(a, r) + assert a == r copy_a //= b - self.assertEqual(copy_a, q) + assert copy_a == q def _test_quantity_divmod(self): self._test_quantity_divmod_one("10*meter", "4.2*inch") @@ -602,9 +620,12 @@ def _test_quantity_divmod(self): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, divmod, a, b) - self.assertRaises(DimensionalityError, divmod, 3, b) - self.assertRaises(DimensionalityError, divmod, a, 3) + with pytest.raises(DimensionalityError): + divmod(a, b) + with pytest.raises(DimensionalityError): + divmod(3, b) + with pytest.raises(DimensionalityError): + divmod(a, 3) def _test_numeric(self, unit, ifunc): self._test_quantity_add_sub(unit, self._test_not_inplace) @@ -627,28 +648,30 @@ def test_quantity_abs_round(self): zy = self.Q_(fun(y.magnitude), "meter") rx = fun(x) ry = fun(y) - self.assertEqual(rx, zx, "while testing {0}".format(fun)) - self.assertEqual(ry, zy, "while testing {0}".format(fun)) - self.assertIsNot(rx, zx, "while testing {0}".format(fun)) - self.assertIsNot(ry, zy, "while testing {0}".format(fun)) + assert rx == zx, "while testing {0}".format(fun) + assert ry == zy, "while testing {0}".format(fun) + assert rx is not zx, "while testing {0}".format(fun) + assert ry is not zy, "while testing {0}".format(fun) def test_quantity_float_complex(self): x = self.QP_("-4.2", None) y = self.QP_("4.2", None) z = self.QP_("1", "meter") for fun in (float, complex): - self.assertEqual(fun(x), fun(x.magnitude)) - self.assertEqual(fun(y), fun(y.magnitude)) - self.assertRaises(DimensionalityError, fun, z) + assert fun(x) == fun(x.magnitude) + assert fun(y) == fun(y.magnitude) + with pytest.raises(DimensionalityError): + fun(z) def test_not_inplace(self): self._test_numeric(self.NON_INT_TYPE("1.0"), self._test_not_inplace) -class _TestOffsetUnitMath(ParameterizedTestCase): - def setup(self): - self.ureg.autoconvert_offset_to_baseunit = False - self.ureg.default_as_delta = True +class _TestOffsetUnitMath: + @classmethod + def setup_class(cls): + cls.ureg.autoconvert_offset_to_baseunit = False + cls.ureg.default_as_delta = True additions = [ # --- input tuple -------------------- | -- expected result -- @@ -690,7 +713,7 @@ def setup(self): ((("100", "delta_degF"), ("10", "delta_degF")), ("110", "delta_degF")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) + @pytest.mark.parametrize(("input", "expected_output"), additions) def test_addition(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple @@ -698,11 +721,12 @@ def test_addition(self, input_tuple, expected): # update input tuple with new values to have correct values on failure input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.add, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.add(q1, q2) else: expected = self.QP_(*expected) - self.assertEqual(op.add(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.add(q1, q2), expected, atol="0.01") + assert op.add(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.add(q1, q2), expected, atol="0.01") subtractions = [ ((("100", "kelvin"), ("10", "kelvin")), ("90", "kelvin")), @@ -743,18 +767,19 @@ def test_addition(self, input_tuple, expected): ((("100", "delta_degF"), ("10", "delta_degF")), ("90", "delta_degF")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) + @pytest.mark.parametrize(("input", "expected_output"), subtractions) def test_subtraction(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.QP_(*qin1), self.QP_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.sub, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.sub(q1, q2) else: expected = self.QP_(*expected) - self.assertEqual(op.sub(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.sub(q1, q2), expected, atol=0.01) + assert op.sub(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.sub(q1, q2), expected, atol=0.01) multiplications = [ ((("100", "kelvin"), ("10", "kelvin")), ("1000", "kelvin**2")), @@ -801,18 +826,19 @@ def test_subtraction(self, input_tuple, expected): ((("100", "delta_degF"), ("10", "delta_degF")), ("1000", "delta_degF**2")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) + @pytest.mark.parametrize(("input", "expected_output"), multiplications) def test_multiplication(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.QP_(*qin1), self.QP_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(q1, q2) else: expected = self.QP_(*expected) - self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + assert op.mul(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01) divisions = [ ((("100", "kelvin"), ("10", "kelvin")), ("10", "")), @@ -859,18 +885,21 @@ def test_multiplication(self, input_tuple, expected): ((("100", "delta_degF"), ("10", "delta_degF")), ("10", "")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) + @pytest.mark.parametrize(("input", "expected_output"), divisions) def test_truedivision(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.QP_(*qin1), self.QP_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.truediv, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.truediv(q1, q2) else: expected = self.QP_(*expected) - self.assertEqual(op.truediv(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.truediv(q1, q2), expected, atol=0.01) + assert op.truediv(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal( + op.truediv(q1, q2), expected, atol=0.01 + ) multiplications_with_autoconvert_to_baseunit = [ ((("100", "kelvin"), ("10", "degC")), ("28315.0", "kelvin**2")), @@ -895,7 +924,7 @@ def test_truedivision(self, input_tuple, expected): ((("100", "delta_degF"), ("10", "degF")), ("26092.78", "delta_degF*kelvin")), ] - @ParameterizedTestCase.parameterize( + @pytest.mark.parametrize( ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit ) def test_multiplication_with_autoconvert(self, input_tuple, expected): @@ -904,11 +933,12 @@ def test_multiplication_with_autoconvert(self, input_tuple, expected): q1, q2 = self.QP_(*qin1), self.QP_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(q1, q2) else: expected = self.QP_(*expected) - self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + assert op.mul(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01) multiplications_with_scalar = [ ((("10", "kelvin"), "2"), ("20.0", "kelvin")), @@ -920,9 +950,7 @@ def test_multiplication_with_autoconvert(self, input_tuple, expected): ((("10", "degC**-2"), "2"), "error"), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), multiplications_with_scalar - ) + @pytest.mark.parametrize(("input", "expected_output"), multiplications_with_scalar) def test_multiplication_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -932,11 +960,14 @@ def test_multiplication_with_scalar(self, input_tuple, expected): in1, in2 = in1, self.QP_(*in2) input_tuple = in1, in2 # update input_tuple for better tracebacks if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, in1, in2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(in1, in2) else: expected = self.QP_(*expected) - self.assertEqual(op.mul(in1, in2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(in1, in2), expected, atol="0.01") + assert op.mul(in1, in2).units == expected.units + helpers.assert_quantity_almost_equal( + op.mul(in1, in2), expected, atol="0.01" + ) divisions_with_scalar = [ # without / with autoconvert to base unit ((("10", "kelvin"), "2"), [("5.0", "kelvin"), ("5.0", "kelvin")]), @@ -950,9 +981,7 @@ def test_multiplication_with_scalar(self, input_tuple, expected): (("2", ("10", "degC**-2")), ["error", "error"]), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), divisions_with_scalar - ) + @pytest.mark.parametrize(("input", "expected_output"), divisions_with_scalar) def test_division_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -965,11 +994,12 @@ def test_division_with_scalar(self, input_tuple, expected): for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode if expected_copy[i] == "error": - self.assertRaises(OffsetUnitCalculusError, op.truediv, in1, in2) + with pytest.raises(OffsetUnitCalculusError): + op.truediv(in1, in2) else: expected = self.QP_(*expected_copy[i]) - self.assertEqual(op.truediv(in1, in2).units, expected.units) - self.assertQuantityAlmostEqual(op.truediv(in1, in2), expected) + assert op.truediv(in1, in2).units == expected.units + helpers.assert_quantity_almost_equal(op.truediv(in1, in2), expected) exponentiation = [ # resuls without / with autoconvert ((("10", "degC"), "1"), [("10", "degC"), ("10", "degC")]), @@ -993,7 +1023,7 @@ def test_division_with_scalar(self, input_tuple, expected): # ), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) + @pytest.mark.parametrize(("input", "expected_output"), exponentiation) def test_exponentiation(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1008,70 +1038,69 @@ def test_exponentiation(self, input_tuple, expected): for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode if expected_copy[i] == "error": - self.assertRaises( - (OffsetUnitCalculusError, DimensionalityError), op.pow, in1, in2 - ) + with pytest.raises((OffsetUnitCalculusError, DimensionalityError)): + op.pow(in1, in2) else: if type(expected_copy[i]) is tuple: expected = self.QP_(*expected_copy[i]) - self.assertEqual(op.pow(in1, in2).units, expected.units) + assert op.pow(in1, in2).units == expected.units else: expected = expected_copy[i] - self.assertQuantityAlmostEqual(op.pow(in1, in2), expected) + helpers.assert_quantity_almost_equal(op.pow(in1, in2), expected) class NonIntTypeQuantityTestQuantityFloat(_TestBasic, NonIntTypeQuantityTestCase): - NON_INT_TYPE = float + kwargs = dict(non_int_type=float) class NonIntTypeQuantityTestQuantityBasicMathFloat( _TestQuantityBasicMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = float + kwargs = dict(non_int_type=float) class NonIntTypeQuantityTestOffsetUnitMathFloat( _TestOffsetUnitMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = float + kwargs = dict(non_int_type=float) class NonIntTypeQuantityTestQuantityDecimal(_TestBasic, NonIntTypeQuantityTestCase): - NON_INT_TYPE = Decimal + kwargs = dict(non_int_type=Decimal) class NonIntTypeQuantityTestQuantityBasicMathDecimal( _TestQuantityBasicMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = Decimal + kwargs = dict(non_int_type=Decimal) class NonIntTypeQuantityTestOffsetUnitMathDecimal( _TestOffsetUnitMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = Decimal + kwargs = dict(non_int_type=Decimal) class NonIntTypeQuantityTestQuantityFraction(_TestBasic, NonIntTypeQuantityTestCase): - NON_INT_TYPE = Fraction + kwargs = dict(non_int_type=Fraction) class NonIntTypeQuantityTestQuantityBasicMathFraction( _TestQuantityBasicMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = Fraction + kwargs = dict(non_int_type=Fraction) class NonIntTypeQuantityTestOffsetUnitMathFraction( _TestOffsetUnitMath, NonIntTypeQuantityTestCase ): - NON_INT_TYPE = Fraction + kwargs = dict(non_int_type=Fraction) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index cb4c01621..de59362d2 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1,27 +1,30 @@ import copy import operator as op import pickle -import unittest import warnings +import pytest + from pint import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning from pint.compat import np -from pint.testsuite import QuantityTestCase, helpers +from pint.testsuite import helpers from pint.testsuite.test_umath import TestUFuncs -@helpers.requires_numpy() -class TestNumpyMethods(QuantityTestCase): - - FORCE_NDARRAY = True - +@helpers.requires_numpy +class TestNumpyMethods: @classmethod - def setUpClass(cls): + def setup_class(cls): from pint import _DEFAULT_REGISTRY cls.ureg = _DEFAULT_REGISTRY cls.Q_ = cls.ureg.Quantity + @classmethod + def teardown_class(cls): + cls.ureg = None + cls.Q_ = None + @property def q(self): return [[1, 2], [3, 4]] * self.ureg.m @@ -45,30 +48,30 @@ def q_temperature(self): def assertNDArrayEqual(self, actual, desired): # Assert that the given arrays are equal, and are not Quantities np.testing.assert_array_equal(actual, desired) - self.assertFalse(isinstance(actual, self.Q_)) - self.assertFalse(isinstance(desired, self.Q_)) + assert not isinstance(actual, self.Q_) + assert not isinstance(desired, self.Q_) class TestNumpyArrayCreation(TestNumpyMethods): # https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_ones_like(self): self.assertNDArrayEqual(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_zeros_like(self): self.assertNDArrayEqual(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_empty_like(self): ret = np.empty_like(self.q) - self.assertEqual(ret.shape, (2, 2)) - self.assertTrue(isinstance(ret, np.ndarray)) + assert ret.shape == (2, 2) + assert isinstance(ret, np.ndarray) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_full_like(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.full_like(self.q, self.Q_(0, self.ureg.degC)), self.Q_([[0, 0], [0, 0]], self.ureg.degC), ) @@ -85,66 +88,72 @@ class TestNumpyArrayManipulation(TestNumpyMethods): # Changing array shape def test_flatten(self): - self.assertQuantityEqual(self.q.flatten(), [1, 2, 3, 4] * self.ureg.m) + helpers.assert_quantity_equal(self.q.flatten(), [1, 2, 3, 4] * self.ureg.m) def test_flat(self): for q, v in zip(self.q.flat, [1, 2, 3, 4]): - self.assertEqual(q, v * self.ureg.m) + assert q == v * self.ureg.m def test_reshape(self): - self.assertQuantityEqual(self.q.reshape([1, 4]), [[1, 2, 3, 4]] * self.ureg.m) + helpers.assert_quantity_equal( + self.q.reshape([1, 4]), [[1, 2, 3, 4]] * self.ureg.m + ) def test_ravel(self): - self.assertQuantityEqual(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) + helpers.assert_quantity_equal(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_ravel_numpy_func(self): - self.assertQuantityEqual(np.ravel(self.q), [1, 2, 3, 4] * self.ureg.m) + helpers.assert_quantity_equal(np.ravel(self.q), [1, 2, 3, 4] * self.ureg.m) # Transpose-like operations - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_moveaxis(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.moveaxis(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_rollaxis(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.rollaxis(self.q, 1), np.array([[1, 2], [3, 4]]).T * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_swapaxes(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.swapaxes(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m ) def test_transpose(self): - self.assertQuantityEqual(self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m) + helpers.assert_quantity_equal( + self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_transpose_numpy_func(self): - self.assertQuantityEqual(np.transpose(self.q), [[1, 3], [2, 4]] * self.ureg.m) + helpers.assert_quantity_equal( + np.transpose(self.q), [[1, 3], [2, 4]] * self.ureg.m + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_flip_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m ) # Changing number of dimensions - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_atleast_1d(self): actual = np.atleast_1d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = (self.Q_(np.array([0]), self.ureg.degC), self.q.flatten()) for ind_actual, ind_expected in zip(actual, expected): - self.assertQuantityEqual(ind_actual, ind_expected) - self.assertQuantityEqual(np.atleast_1d(self.q), self.q) + helpers.assert_quantity_equal(ind_actual, ind_expected) + helpers.assert_quantity_equal(np.atleast_1d(self.q), self.q) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_atleast_2d(self): actual = np.atleast_2d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = ( @@ -152,10 +161,10 @@ def test_atleast_2d(self): np.array([[1, 2, 3, 4]]) * self.ureg.m, ) for ind_actual, ind_expected in zip(actual, expected): - self.assertQuantityEqual(ind_actual, ind_expected) - self.assertQuantityEqual(np.atleast_2d(self.q), self.q) + helpers.assert_quantity_equal(ind_actual, ind_expected) + helpers.assert_quantity_equal(np.atleast_2d(self.q), self.q) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_atleast_3d(self): actual = np.atleast_3d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = ( @@ -163,42 +172,42 @@ def test_atleast_3d(self): np.array([[[1], [2], [3], [4]]]) * self.ureg.m, ) for ind_actual, ind_expected in zip(actual, expected): - self.assertQuantityEqual(ind_actual, ind_expected) - self.assertQuantityEqual( + helpers.assert_quantity_equal(ind_actual, ind_expected) + helpers.assert_quantity_equal( np.atleast_3d(self.q), np.array([[[1], [2]], [[3], [4]]]) * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_broadcast_to(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.broadcast_to(self.q[:, 1], (2, 2)), np.array([[2, 4], [2, 4]]) * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_expand_dims(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.expand_dims(self.q, 0), np.array([[[1, 2], [3, 4]]]) * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_squeeze(self): - self.assertQuantityEqual(np.squeeze(self.q), self.q) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.squeeze(self.q), self.q) + helpers.assert_quantity_equal( self.q.reshape([1, 4]).squeeze(), [1, 2, 3, 4] * self.ureg.m ) # Changing number of dimensions # Joining arrays - @helpers.requires_array_function_protocol() - def test_concat_stack(self): + @helpers.requires_array_function_protocol + def test_concat_stack(self, subtests): for func in (np.concatenate, np.stack, np.hstack, np.vstack, np.dstack): - with self.subTest(func=func): - self.assertQuantityEqual( + with subtests.test(func=func): + helpers.assert_quantity_equal( func([self.q] * 2), self.Q_(func([self.q.m] * 2), self.ureg.m) ) # One or more of the args is a bare array full of zeros or NaNs - self.assertQuantityEqual( + helpers.assert_quantity_equal( func([self.q_zero_or_nan.m, self.q]), self.Q_(func([self.q_zero_or_nan.m, self.q.m]), self.ureg.m), ) @@ -206,21 +215,21 @@ def test_concat_stack(self): # non-NaN element nz = self.q_zero_or_nan nz.m[0, 0] = 1 - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): func([nz.m, self.q]) - @helpers.requires_array_function_protocol() - def test_block_column_stack(self): + @helpers.requires_array_function_protocol + def test_block_column_stack(self, subtests): for func in (np.block, np.column_stack): - with self.subTest(func=func): + with subtests.test(func=func): - self.assertQuantityEqual( + helpers.assert_quantity_equal( func([self.q[:, 0], self.q[:, 1]]), self.Q_(func([self.q[:, 0].m, self.q[:, 1].m]), self.ureg.m), ) # One or more of the args is a bare array full of zeros or NaNs - self.assertQuantityEqual( + helpers.assert_quantity_equal( func( [ self.q_zero_or_nan[:, 0].m, @@ -243,12 +252,12 @@ def test_block_column_stack(self): # non-NaN element nz = self.q_zero_or_nan nz.m[0, 0] = 1 - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): func([nz[:, 0].m, self.q[:, 0]]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_append(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), [[1, 2], [3, 4], [0, 0]] * self.ureg.m, ) @@ -256,175 +265,191 @@ def test_append(self): def test_astype(self): actual = self.q.astype(np.float32) expected = self.Q_(np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32), "m") - self.assertQuantityEqual(actual, expected) - self.assertEqual(actual.m.dtype, expected.m.dtype) + helpers.assert_quantity_equal(actual, expected) + assert actual.m.dtype == expected.m.dtype def test_item(self): - self.assertQuantityEqual(self.Q_([[0]], "m").item(), 0 * self.ureg.m) + helpers.assert_quantity_equal(self.Q_([[0]], "m").item(), 0 * self.ureg.m) class TestNumpyMathematicalFunctions(TestNumpyMethods): # https://www.numpy.org/devdocs/reference/routines.math.html # Trigonometric functions - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_unwrap(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.unwrap([0, 3 * np.pi] * self.ureg.radians), [0, np.pi] ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.unwrap([0, 540] * self.ureg.deg), [0, 180] * self.ureg.deg ) # Rounding - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_fix(self): - self.assertQuantityEqual(np.fix(3.14 * self.ureg.m), 3.0 * self.ureg.m) - self.assertQuantityEqual(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.fix(3.14 * self.ureg.m), 3.0 * self.ureg.m) + helpers.assert_quantity_equal(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) + helpers.assert_quantity_equal( np.fix([2.1, 2.9, -2.1, -2.9] * self.ureg.m), [2.0, 2.0, -2.0, -2.0] * self.ureg.m, ) # Sums, products, differences - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_prod(self): axis = 0 where = [[True, False], [True, True]] - self.assertQuantityEqual(self.q.prod(), 24 * self.ureg.m ** 4) - self.assertQuantityEqual(self.q.prod(axis=axis), [3, 8] * self.ureg.m ** 2) - self.assertQuantityEqual(self.q.prod(where=where), 12 * self.ureg.m ** 3) + helpers.assert_quantity_equal(self.q.prod(), 24 * self.ureg.m ** 4) + helpers.assert_quantity_equal(self.q.prod(axis=axis), [3, 8] * self.ureg.m ** 2) + helpers.assert_quantity_equal(self.q.prod(where=where), 12 * self.ureg.m ** 3) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_prod_numpy_func(self): axis = 0 where = [[True, False], [True, True]] - self.assertQuantityEqual(np.prod(self.q), 24 * self.ureg.m ** 4) - self.assertQuantityEqual(np.prod(self.q, axis=axis), [3, 8] * self.ureg.m ** 2) - self.assertQuantityEqual(np.prod(self.q, where=where), 12 * self.ureg.m ** 3) + helpers.assert_quantity_equal(np.prod(self.q), 24 * self.ureg.m ** 4) + helpers.assert_quantity_equal( + np.prod(self.q, axis=axis), [3, 8] * self.ureg.m ** 2 + ) + helpers.assert_quantity_equal( + np.prod(self.q, where=where), 12 * self.ureg.m ** 3 + ) - self.assertRaises(DimensionalityError, np.prod, self.q, axis=axis, where=where) - self.assertQuantityEqual( + with pytest.raises(DimensionalityError): + np.prod(self.q, axis=axis, where=where) + helpers.assert_quantity_equal( np.prod(self.q, axis=axis, where=[[True, False], [False, True]]), [1, 4] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.prod(self.q, axis=axis, where=[True, False]), [3, 1] * self.ureg.m ** 2 ) def test_sum(self): - self.assertEqual(self.q.sum(), 10 * self.ureg.m) - self.assertQuantityEqual(self.q.sum(0), [4, 6] * self.ureg.m) - self.assertQuantityEqual(self.q.sum(1), [3, 7] * self.ureg.m) + assert self.q.sum() == 10 * self.ureg.m + helpers.assert_quantity_equal(self.q.sum(0), [4, 6] * self.ureg.m) + helpers.assert_quantity_equal(self.q.sum(1), [3, 7] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_sum_numpy_func(self): - self.assertQuantityEqual(np.sum(self.q, axis=0), [4, 6] * self.ureg.m) - self.assertRaises(OffsetUnitCalculusError, np.sum, self.q_temperature) + helpers.assert_quantity_equal(np.sum(self.q, axis=0), [4, 6] * self.ureg.m) + with pytest.raises(OffsetUnitCalculusError): + np.sum(self.q_temperature) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nansum_numpy_func(self): - self.assertQuantityEqual(np.nansum(self.q_nan, axis=0), [4, 2] * self.ureg.m) + helpers.assert_quantity_equal( + np.nansum(self.q_nan, axis=0), [4, 2] * self.ureg.m + ) def test_cumprod(self): - self.assertRaises(DimensionalityError, self.q.cumprod) - self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) + with pytest.raises(DimensionalityError): + self.q.cumprod() + helpers.assert_quantity_equal((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_cumprod_numpy_func(self): - self.assertRaises(DimensionalityError, np.cumprod, self.q) - self.assertRaises(DimensionalityError, np.cumproduct, self.q) - self.assertQuantityEqual(np.cumprod(self.q / self.ureg.m), [1, 2, 6, 24]) - self.assertQuantityEqual(np.cumproduct(self.q / self.ureg.m), [1, 2, 6, 24]) - self.assertQuantityEqual( + with pytest.raises(DimensionalityError): + np.cumprod(self.q) + with pytest.raises(DimensionalityError): + np.cumproduct(self.q) + helpers.assert_quantity_equal(np.cumprod(self.q / self.ureg.m), [1, 2, 6, 24]) + helpers.assert_quantity_equal( + np.cumproduct(self.q / self.ureg.m), [1, 2, 6, 24] + ) + helpers.assert_quantity_equal( np.cumprod(self.q / self.ureg.m, axis=1), [[1, 2], [3, 12]] ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nancumprod_numpy_func(self): - self.assertRaises(DimensionalityError, np.nancumprod, self.q_nan) - self.assertQuantityEqual(np.nancumprod(self.q_nan / self.ureg.m), [1, 2, 6, 6]) + with pytest.raises(DimensionalityError): + np.nancumprod(self.q_nan) + helpers.assert_quantity_equal( + np.nancumprod(self.q_nan / self.ureg.m), [1, 2, 6, 6] + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_diff(self): - self.assertQuantityEqual(np.diff(self.q, 1), [[1], [1]] * self.ureg.m) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.diff(self.q, 1), [[1], [1]] * self.ureg.m) + helpers.assert_quantity_equal( np.diff(self.q_temperature, 1), [[1], [1]] * self.ureg.delta_degC ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_ediff1d(self): - self.assertQuantityEqual(np.ediff1d(self.q), [1, 1, 1] * self.ureg.m) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.ediff1d(self.q), [1, 1, 1] * self.ureg.m) + helpers.assert_quantity_equal( np.ediff1d(self.q_temperature), [1, 1, 1] * self.ureg.delta_degC ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_gradient(self): grad = np.gradient([[1, 1], [3, 4]] * self.ureg.m, 1 * self.ureg.J) - self.assertQuantityEqual( + helpers.assert_quantity_equal( grad[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.m / self.ureg.J ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( grad[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.m / self.ureg.J ) grad = np.gradient(self.Q_([[1, 1], [3, 4]], self.ureg.degC), 1 * self.ureg.J) - self.assertQuantityEqual( + helpers.assert_quantity_equal( grad[0], [[2.0, 3.0], [2.0, 3.0]] * self.ureg.delta_degC / self.ureg.J ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( grad[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.delta_degC / self.ureg.J ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_cross(self): a = [[3, -3, 1]] * self.ureg.kPa b = [[4, 9, 2]] * self.ureg.m ** 2 - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m ** 2 ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_trapz(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.trapz([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), 7.5 * self.ureg.J * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_dot(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.ravel().dot(np.array([1, 0, 0, 1])), 5 * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_dot_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_einsum(self): a = np.arange(25).reshape(5, 5) * self.ureg.m b = np.arange(5) * self.ureg.m - self.assertQuantityEqual(np.einsum("ii", a), 60 * self.ureg.m) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.einsum("ii", a), 60 * self.ureg.m) + helpers.assert_quantity_equal( np.einsum("ii->i", a), np.array([0, 6, 12, 18, 24]) * self.ureg.m ) - self.assertQuantityEqual(np.einsum("i,i", b, b), 30 * self.ureg.m ** 2) - self.assertQuantityEqual( + helpers.assert_quantity_equal(np.einsum("i,i", b, b), 30 * self.ureg.m ** 2) + helpers.assert_quantity_equal( np.einsum("ij,j", a, b), np.array([30, 80, 130, 180, 230]) * self.ureg.m ** 2, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_solve(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.linalg.solve(self.q, [[3], [7]] * self.ureg.s), self.Q_([[1], [1]], "m / s"), ) @@ -433,18 +458,20 @@ def test_solve(self): def test_addition_with_scalar(self): a = np.array([0, 1, 2]) b = 10.0 * self.ureg("gram/kilogram") - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( a + b, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless) ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( b + a, self.Q_([0.01, 1.01, 2.01], self.ureg.dimensionless) ) def test_addition_with_incompatible_scalar(self): a = np.array([0, 1, 2]) b = 1.0 * self.ureg.m - self.assertRaises(DimensionalityError, op.add, a, b) - self.assertRaises(DimensionalityError, op.add, b, a) + with pytest.raises(DimensionalityError): + op.add(a, b) + with pytest.raises(DimensionalityError): + op.add(b, a) def test_power(self): arr = np.array(range(3), dtype=np.float) @@ -452,33 +479,36 @@ def test_power(self): for op_ in [op.pow, op.ipow, np.power]: q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, 2.0, q_cp) + with pytest.raises(DimensionalityError): + op_(2.0, q_cp) arr_cp = copy.copy(arr) arr_cp = copy.copy(arr) q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, q_cp, arr_cp) + with pytest.raises(DimensionalityError): + op_(q_cp, arr_cp) q_cp = copy.copy(q) q2_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op_, q_cp, q2_cp) + with pytest.raises(DimensionalityError): + op_(q_cp, q2_cp) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.power(self.q, self.Q_(2)), self.Q_([[1, 4], [9, 16]], "m**2") ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q ** self.Q_(2), self.Q_([[1, 4], [9, 16]], "m**2") ) self.assertNDArrayEqual(arr ** self.Q_(2), np.array([0, 1, 4])) def test_sqrt(self): q = self.Q_(100, "m**2") - self.assertQuantityEqual(np.sqrt(q), self.Q_(10, "m")) + helpers.assert_quantity_equal(np.sqrt(q), self.Q_(10, "m")) def test_cbrt(self): q = self.Q_(1000, "m**3") - self.assertQuantityEqual(np.cbrt(q), self.Q_(10, "m")) + helpers.assert_quantity_equal(np.cbrt(q), self.Q_(10, "m")) - @unittest.expectedFailure - @helpers.requires_numpy() + @pytest.mark.xfail + @helpers.requires_numpy def test_exponentiation_array_exp_2(self): arr = np.array(range(3), dtype=np.float) # q = self.Q_(copy.copy(arr), None) @@ -486,100 +516,101 @@ def test_exponentiation_array_exp_2(self): arr_cp = copy.copy(arr) q_cp = copy.copy(q) # this fails as expected since numpy 1.8.0 but... - self.assertRaises(DimensionalityError, op.pow, arr_cp, q_cp) + with pytest.raises(DimensionalityError): + op.pow(arr_cp, q_cp) # ..not for op.ipow ! # q_cp is treated as if it is an array. The units are ignored. # Quantity.__ipow__ is never called arr_cp = copy.copy(arr) q_cp = copy.copy(q) - self.assertRaises(DimensionalityError, op.ipow, arr_cp, q_cp) + with pytest.raises(DimensionalityError): + op.ipow(arr_cp, q_cp) class TestNumpyUnclassified(TestNumpyMethods): def test_tolist(self): - self.assertEqual( - self.q.tolist(), - [[1 * self.ureg.m, 2 * self.ureg.m], [3 * self.ureg.m, 4 * self.ureg.m]], - ) - self.assertEqual(self.q_scalar.tolist(), 5 * self.ureg.m) - - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): (5 * self.ureg.m).tolist() + assert self.q.tolist() == [ + [1 * self.ureg.m, 2 * self.ureg.m], + [3 * self.ureg.m, 4 * self.ureg.m], + ] + def test_fill(self): tmp = self.q tmp.fill(6 * self.ureg.ft) - self.assertQuantityEqual(tmp, [[6, 6], [6, 6]] * self.ureg.ft) + helpers.assert_quantity_equal(tmp, [[6, 6], [6, 6]] * self.ureg.ft) tmp.fill(5 * self.ureg.m) - self.assertQuantityEqual(tmp, [[5, 5], [5, 5]] * self.ureg.m) + helpers.assert_quantity_equal(tmp, [[5, 5], [5, 5]] * self.ureg.m) def test_take(self): - self.assertQuantityEqual(self.q.take([0, 1, 2, 3]), self.q.flatten()) + helpers.assert_quantity_equal(self.q.take([0, 1, 2, 3]), self.q.flatten()) def test_put(self): q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m q.put([0, 2], [10.0, 20.0] * self.ureg.m) - self.assertQuantityEqual(q, [10.0, 2.0, 20.0, 4.0] * self.ureg.m) + helpers.assert_quantity_equal(q, [10.0, 2.0, 20.0, 4.0] * self.ureg.m) q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m q.put([0, 2], [1.0, 2.0] * self.ureg.mm) - self.assertQuantityEqual(q, [0.001, 2.0, 0.002, 4.0] * self.ureg.m) + helpers.assert_quantity_equal(q, [0.001, 2.0, 0.002, 4.0] * self.ureg.m) q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m / self.ureg.mm q.put([0, 2], [1.0, 2.0]) - self.assertQuantityEqual( + helpers.assert_quantity_equal( q, [0.001, 2.0, 0.002, 4.0] * self.ureg.m / self.ureg.mm ) q = [1.0, 2.0, 3.0, 4.0] * self.ureg.m - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): q.put([0, 2], [4.0, 6.0] * self.ureg.J) - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): q.put([0, 2], [4.0, 6.0]) def test_repeat(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.repeat(2), [1, 1, 2, 2, 3, 3, 4, 4] * self.ureg.m ) def test_sort(self): q = [4, 5, 2, 3, 1, 6] * self.ureg.m q.sort() - self.assertQuantityEqual(q, [1, 2, 3, 4, 5, 6] * self.ureg.m) + helpers.assert_quantity_equal(q, [1, 2, 3, 4, 5, 6] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_sort_numpy_func(self): q = [4, 5, 2, 3, 1, 6] * self.ureg.m - self.assertQuantityEqual(np.sort(q), [1, 2, 3, 4, 5, 6] * self.ureg.m) + helpers.assert_quantity_equal(np.sort(q), [1, 2, 3, 4, 5, 6] * self.ureg.m) def test_argsort(self): q = [1, 4, 5, 6, 2, 9] * self.ureg.MeV self.assertNDArrayEqual(q.argsort(), [0, 4, 1, 2, 3, 5]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_argsort_numpy_func(self): self.assertNDArrayEqual(np.argsort(self.q, axis=0), np.array([[0, 0], [1, 1]])) def test_diagonal(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m - self.assertQuantityEqual(q.diagonal(offset=1), [2, 3] * self.ureg.m) + helpers.assert_quantity_equal(q.diagonal(offset=1), [2, 3] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_diagonal_numpy_func(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m - self.assertQuantityEqual(np.diagonal(q, offset=-1), [1, 2] * self.ureg.m) + helpers.assert_quantity_equal(np.diagonal(q, offset=-1), [1, 2] * self.ureg.m) def test_compress(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.compress([False, True], axis=0), [[3, 4]] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.compress([False, True], axis=1), [[2], [4]] * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_compress_nep18(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.compress([False, True], self.q, axis=1), [[2], [4]] * self.ureg.m ) @@ -587,9 +618,10 @@ def test_searchsorted(self): q = self.q.flatten() self.assertNDArrayEqual(q.searchsorted([1.5, 2.5] * self.ureg.m), [1, 2]) q = self.q.flatten() - self.assertRaises(DimensionalityError, q.searchsorted, [1.5, 2.5]) + with pytest.raises(DimensionalityError): + q.searchsorted([1.5, 2.5]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" q = self.q.flatten() @@ -599,265 +631,278 @@ def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m self.assertNDArrayEqual(q.nonzero()[0], [0, 2, 3, 5]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m self.assertNDArrayEqual(np.nonzero(q)[0], [0, 2, 3, 5]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_any_numpy_func(self): q = [0, 1] * self.ureg.m - self.assertTrue(np.any(q)) - self.assertRaises(ValueError, np.any, self.q_temperature) + assert np.any(q) + with pytest.raises(ValueError): + np.any(self.q_temperature) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_all_numpy_func(self): q = [0, 1] * self.ureg.m - self.assertFalse(np.all(q)) - self.assertRaises(ValueError, np.all, self.q_temperature) + assert not np.all(q) + with pytest.raises(ValueError): + np.all(self.q_temperature) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_count_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m - self.assertEqual(np.count_nonzero(q), 4) + assert np.count_nonzero(q) == 4 def test_max(self): - self.assertEqual(self.q.max(), 4 * self.ureg.m) + assert self.q.max() == 4 * self.ureg.m def test_max_numpy_func(self): - self.assertEqual(np.max(self.q), 4 * self.ureg.m) + assert np.max(self.q) == 4 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_max_with_axis_arg(self): - self.assertQuantityEqual(np.max(self.q, axis=1), [2, 4] * self.ureg.m) + helpers.assert_quantity_equal(np.max(self.q, axis=1), [2, 4] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_max_with_initial_arg(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.max(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanmax(self): - self.assertEqual(np.nanmax(self.q_nan), 3 * self.ureg.m) + assert np.nanmax(self.q_nan) == 3 * self.ureg.m def test_argmax(self): - self.assertEqual(self.q.argmax(), 3) + assert self.q.argmax() == 3 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_argmax_numpy_func(self): self.assertNDArrayEqual(np.argmax(self.q, axis=0), np.array([1, 1])) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanargmax_numpy_func(self): self.assertNDArrayEqual(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) def test_maximum(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.maximum(self.q, self.Q_([0, 5], "m")), self.Q_([[1, 5], [3, 5]], "m") ) def test_min(self): - self.assertEqual(self.q.min(), 1 * self.ureg.m) + assert self.q.min() == 1 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_min_numpy_func(self): - self.assertEqual(np.min(self.q), 1 * self.ureg.m) + assert np.min(self.q) == 1 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_min_with_axis_arg(self): - self.assertQuantityEqual(np.min(self.q, axis=1), [1, 3] * self.ureg.m) + helpers.assert_quantity_equal(np.min(self.q, axis=1), [1, 3] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_min_with_initial_arg(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.min(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[1, 2], [3, 3]] * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanmin(self): - self.assertEqual(np.nanmin(self.q_nan), 1 * self.ureg.m) + assert np.nanmin(self.q_nan) == 1 * self.ureg.m def test_argmin(self): - self.assertEqual(self.q.argmin(), 0) + assert self.q.argmin() == 0 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_argmin_numpy_func(self): self.assertNDArrayEqual(np.argmin(self.q, axis=0), np.array([0, 0])) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanargmin_numpy_func(self): self.assertNDArrayEqual(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) def test_minimum(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.minimum(self.q, self.Q_([0, 5], "m")), self.Q_([[0, 2], [0, 4]], "m") ) def test_ptp(self): - self.assertEqual(self.q.ptp(), 3 * self.ureg.m) + assert self.q.ptp() == 3 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_ptp_numpy_func(self): - self.assertQuantityEqual(np.ptp(self.q, axis=0), [2, 2] * self.ureg.m) + helpers.assert_quantity_equal(np.ptp(self.q, axis=0), [2, 2] * self.ureg.m) def test_clip(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.clip(max=2 * self.ureg.m), [[1, 2], [2, 2]] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.clip(min=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( self.q.clip(min=2 * self.ureg.m, max=3 * self.ureg.m), [[2, 2], [3, 3]] * self.ureg.m, ) - self.assertRaises(DimensionalityError, self.q.clip, self.ureg.J) - self.assertRaises(DimensionalityError, self.q.clip, 1) + with pytest.raises(DimensionalityError): + self.q.clip(self.ureg.J) + with pytest.raises(DimensionalityError): + self.q.clip(1) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_clip_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.clip(self.q, 150 * self.ureg.cm, None), [[1.5, 2], [3, 4]] * self.ureg.m ) def test_round(self): q = [1, 1.33, 5.67, 22] * self.ureg.m - self.assertQuantityEqual(q.round(0), [1, 1, 6, 22] * self.ureg.m) - self.assertQuantityEqual(q.round(-1), [0, 0, 10, 20] * self.ureg.m) - self.assertQuantityEqual(q.round(1), [1, 1.3, 5.7, 22] * self.ureg.m) + helpers.assert_quantity_equal(q.round(0), [1, 1, 6, 22] * self.ureg.m) + helpers.assert_quantity_equal(q.round(-1), [0, 0, 10, 20] * self.ureg.m) + helpers.assert_quantity_equal(q.round(1), [1, 1.3, 5.7, 22] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_round_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.round_(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m ) def test_trace(self): - self.assertEqual(self.q.trace(), (1 + 4) * self.ureg.m) + assert self.q.trace() == (1 + 4) * self.ureg.m def test_cumsum(self): - self.assertQuantityEqual(self.q.cumsum(), [1, 3, 6, 10] * self.ureg.m) + helpers.assert_quantity_equal(self.q.cumsum(), [1, 3, 6, 10] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_cumsum_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.cumsum(self.q, axis=0), [[1, 2], [4, 6]] * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nancumsum_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.nancumsum(self.q_nan, axis=0), [[1, 2], [4, 2]] * self.ureg.m ) def test_mean(self): - self.assertEqual(self.q.mean(), 2.5 * self.ureg.m) + assert self.q.mean() == 2.5 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_mean_numpy_func(self): - self.assertEqual(np.mean(self.q), 2.5 * self.ureg.m) - self.assertEqual(np.mean(self.q_temperature), self.Q_(2.5, self.ureg.degC)) + assert np.mean(self.q) == 2.5 * self.ureg.m + assert np.mean(self.q_temperature) == self.Q_(2.5, self.ureg.degC) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanmean_numpy_func(self): - self.assertEqual(np.nanmean(self.q_nan), 2 * self.ureg.m) + assert np.nanmean(self.q_nan) == 2 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_average_numpy_func(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.average(self.q, axis=0, weights=[1, 2]), [2.33333, 3.33333] * self.ureg.m, rtol=1e-5, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_median_numpy_func(self): - self.assertEqual(np.median(self.q), 2.5 * self.ureg.m) + assert np.median(self.q) == 2.5 * self.ureg.m - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanmedian_numpy_func(self): - self.assertEqual(np.nanmedian(self.q_nan), 2 * self.ureg.m) + assert np.nanmedian(self.q_nan) == 2 * self.ureg.m def test_var(self): - self.assertEqual(self.q.var(), 1.25 * self.ureg.m ** 2) + assert self.q.var() == 1.25 * self.ureg.m ** 2 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_var_numpy_func(self): - self.assertEqual(np.var(self.q), 1.25 * self.ureg.m ** 2) + assert np.var(self.q) == 1.25 * self.ureg.m ** 2 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanvar_numpy_func(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.nanvar(self.q_nan), 0.66667 * self.ureg.m ** 2, rtol=1e-5 ) def test_std(self): - self.assertQuantityAlmostEqual(self.q.std(), 1.11803 * self.ureg.m, rtol=1e-5) + helpers.assert_quantity_almost_equal( + self.q.std(), 1.11803 * self.ureg.m, rtol=1e-5 + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_std_numpy_func(self): - self.assertQuantityAlmostEqual(np.std(self.q), 1.11803 * self.ureg.m, rtol=1e-5) - self.assertRaises(OffsetUnitCalculusError, np.std, self.q_temperature) + helpers.assert_quantity_almost_equal( + np.std(self.q), 1.11803 * self.ureg.m, rtol=1e-5 + ) + with pytest.raises(OffsetUnitCalculusError): + np.std(self.q_temperature) def test_cumprod(self): - self.assertRaises(DimensionalityError, self.q.cumprod) - self.assertQuantityEqual((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) + with pytest.raises(DimensionalityError): + self.q.cumprod() + helpers.assert_quantity_equal((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanstd_numpy_func(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5 ) def test_conj(self): - self.assertQuantityEqual((self.q * (1 + 1j)).conj(), self.q * (1 - 1j)) - self.assertQuantityEqual((self.q * (1 + 1j)).conjugate(), self.q * (1 - 1j)) + helpers.assert_quantity_equal((self.q * (1 + 1j)).conj(), self.q * (1 - 1j)) + helpers.assert_quantity_equal( + (self.q * (1 + 1j)).conjugate(), self.q * (1 - 1j) + ) def test_getitem(self): - self.assertRaises(IndexError, self.q.__getitem__, (0, 10)) - self.assertQuantityEqual(self.q[0], [1, 2] * self.ureg.m) - self.assertEqual(self.q[1, 1], 4 * self.ureg.m) + with pytest.raises(IndexError): + self.q.__getitem__((0, 10)) + helpers.assert_quantity_equal(self.q[0], [1, 2] * self.ureg.m) + assert self.q[1, 1] == 4 * self.ureg.m def test_setitem(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): self.q[0, 0] = 1 - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): self.q[0, 0] = 1 * self.ureg.J - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): self.q[0] = 1 - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): self.q[0] = np.ndarray([1, 2]) - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): self.q[0] = 1 * self.ureg.J q = self.q.copy() q[0] = 1 * self.ureg.m - self.assertQuantityEqual(q, [[1, 1], [3, 4]] * self.ureg.m) + helpers.assert_quantity_equal(q, [[1, 1], [3, 4]] * self.ureg.m) q = self.q.copy() q[...] = 1 * self.ureg.m - self.assertQuantityEqual(q, [[1, 1], [1, 1]] * self.ureg.m) + helpers.assert_quantity_equal(q, [[1, 1], [1, 1]] * self.ureg.m) q = self.q.copy() q[:] = 1 * self.ureg.m - self.assertQuantityEqual(q, [[1, 1], [1, 1]] * self.ureg.m) + helpers.assert_quantity_equal(q, [[1, 1], [1, 1]] * self.ureg.m) # check and see that dimensionless num bers work correctly q = [0, 1, 2, 3] * self.ureg.dimensionless q[0] = 1 - self.assertQuantityEqual(q, np.asarray([1, 1, 2, 3])) + helpers.assert_quantity_equal(q, np.asarray([1, 1, 2, 3])) q[0] = self.ureg.m / self.ureg.mm - self.assertQuantityEqual(q, np.asarray([1000, 1, 2, 3])) + helpers.assert_quantity_equal(q, np.asarray([1000, 1, 2, 3])) q = [0.0, 1.0, 2.0, 3.0] * self.ureg.m / self.ureg.mm q[0] = 1.0 - self.assertQuantityEqual(q, [0.001, 1, 2, 3] * self.ureg.m / self.ureg.mm) + helpers.assert_quantity_equal(q, [0.001, 1, 2, 3] * self.ureg.m / self.ureg.mm) # Check that this properly masks the first item without warning q = self.ureg.Quantity( @@ -871,28 +916,28 @@ def test_setitem(self): def test_iterator(self): for q, v in zip(self.q.flatten(), [1, 2, 3, 4]): - self.assertEqual(q, v * self.ureg.m) + assert q == v * self.ureg.m def test_iterable(self): - self.assertTrue(np.iterable(self.q)) - self.assertFalse(np.iterable(1 * self.ureg.m)) + assert np.iterable(self.q) + assert not np.iterable(1 * self.ureg.m) def test_reversible_op(self): """ """ x = self.q.magnitude u = self.Q_(np.ones(x.shape)) - self.assertQuantityEqual(x / self.q, u * x / self.q) - self.assertQuantityEqual(x * self.q, u * x * self.q) - self.assertQuantityEqual(x + u, u + x) - self.assertQuantityEqual(x - u, -(u - x)) + helpers.assert_quantity_equal(x / self.q, u * x / self.q) + helpers.assert_quantity_equal(x * self.q, u * x * self.q) + helpers.assert_quantity_equal(x + u, u + x) + helpers.assert_quantity_equal(x - u, -(u - x)) - def test_pickle(self): + def test_pickle(self, subtests): for protocol in range(pickle.HIGHEST_PROTOCOL + 1): - with self.subTest(protocol): + with subtests.test(protocol): q1 = [10, 20] * self.ureg.m q2 = pickle.loads(pickle.dumps(q1, protocol)) self.assertNDArrayEqual(q1.magnitude, q2.magnitude) - self.assertEqual(q1.units, q2.units) + assert q1.units == q2.units def test_equal(self): x = self.q.magnitude @@ -900,9 +945,9 @@ def test_equal(self): true = np.ones_like(x, dtype=np.bool_) false = np.zeros_like(x, dtype=np.bool_) - self.assertQuantityEqual(u, u) - self.assertQuantityEqual(u == u, u.magnitude == u.magnitude) - self.assertQuantityEqual(u == 1, u.magnitude == 1) + helpers.assert_quantity_equal(u, u) + helpers.assert_quantity_equal(u == u, u.magnitude == u.magnitude) + helpers.assert_quantity_equal(u == 1, u.magnitude == 1) v = self.Q_(np.zeros(x.shape), "m") w = self.Q_(np.ones(x.shape), "m") @@ -919,51 +964,51 @@ def test_equal(self): def test_shape(self): u = self.Q_(np.arange(12)) u.shape = 4, 3 - self.assertEqual(u.magnitude.shape, (4, 3)) + assert u.magnitude.shape == (4, 3) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_shape_numpy_func(self): - self.assertEqual(np.shape(self.q), (2, 2)) + assert np.shape(self.q) == (2, 2) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_alen_numpy_func(self): - self.assertEqual(np.alen(self.q), 2) + assert np.alen(self.q) == 2 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_ndim_numpy_func(self): - self.assertEqual(np.ndim(self.q), 2) + assert np.ndim(self.q) == 2 - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_copy_numpy_func(self): q_copy = np.copy(self.q) - self.assertQuantityEqual(self.q, q_copy) - self.assertIsNot(self.q, q_copy) + helpers.assert_quantity_equal(self.q, q_copy) + assert self.q is not q_copy - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_trim_zeros_numpy_func(self): q = [0, 4, 3, 0, 2, 2, 0, 0, 0] * self.ureg.m - self.assertQuantityEqual(np.trim_zeros(q), [4, 3, 0, 2, 2] * self.ureg.m) + helpers.assert_quantity_equal(np.trim_zeros(q), [4, 3, 0, 2, 2] * self.ureg.m) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_result_type_numpy_func(self): - self.assertEqual(np.result_type(self.q), np.dtype("int")) + assert np.result_type(self.q) == np.dtype("int") - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nan_to_num_numpy_func(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.nan_to_num(self.q_nan, nan=-999 * self.ureg.mm), [[1, 2], [3, -0.999]] * self.ureg.m, ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_meshgrid_numpy_func(self): x = [1, 2] * self.ureg.m y = [0, 50, 100] * self.ureg.mm xx, yy = np.meshgrid(x, y) - self.assertQuantityEqual(xx, [[1, 2], [1, 2], [1, 2]] * self.ureg.m) - self.assertQuantityEqual(yy, [[0, 0], [50, 50], [100, 100]] * self.ureg.mm) + helpers.assert_quantity_equal(xx, [[1, 2], [1, 2], [1, 2]] * self.ureg.m) + helpers.assert_quantity_equal(yy, [[0, 0], [50, 50], [100, 100]] * self.ureg.mm) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_isclose_numpy_func(self): q2 = [[1000.05, 2000], [3000.00007, 4001]] * self.ureg.mm self.assertNDArrayEqual( @@ -974,12 +1019,12 @@ def test_isclose_numpy_func(self): np.array([[False, True], [True, False]]), ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_interp_numpy_func(self): x = [1, 4] * self.ureg.m xp = np.linspace(0, 3, 5) * self.ureg.m fp = self.Q_([0, 5, 10, 15, 20], self.ureg.degC) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.interp(x, xp, fp), self.Q_([6.66667, 20.0], self.ureg.degC), rtol=1e-5 ) @@ -987,10 +1032,12 @@ def test_interp_numpy_func(self): xp_ = np.linspace(0, 3, 5) fp_ = [0, 5, 10, 15, 20] - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( np.interp(x_, xp_, fp), self.Q_([6.6667, 20.0], self.ureg.degC), rtol=1e-5 ) - self.assertQuantityAlmostEqual(np.interp(x, xp, fp_), [6.6667, 20.0], rtol=1e-5) + helpers.assert_quantity_almost_equal( + np.interp(x, xp, fp_), [6.6667, 20.0], rtol=1e-5 + ) def test_comparisons(self): self.assertNDArrayEqual( @@ -1000,51 +1047,50 @@ def test_comparisons(self): self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]]) ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_where(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 2 * self.ureg.m, self.q, 20 * self.ureg.m), [[20, 2], [3, 4]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 2 * self.ureg.m, self.q, 0), [[0, 2], [3, 4]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 2 * self.ureg.m, self.q, np.nan), [[np.nan, 2], [3, 4]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 3 * self.ureg.m, 0, self.q), [[1, 2], [0, 0]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 3 * self.ureg.m, np.nan, self.q), [[1, 2], [np.nan, np.nan]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 2 * self.ureg.m, self.q, np.array(np.nan)), [[np.nan, 2], [3, 4]] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.where(self.q >= 3 * self.ureg.m, np.array(np.nan), self.q), [[1, 2], [np.nan, np.nan]] * self.ureg.m, ) - self.assertRaises( - DimensionalityError, - np.where, - self.q < 2 * self.ureg.m, - self.q, - 0 * self.ureg.J, - ) + with pytest.raises(DimensionalityError): + np.where( + self.q < 2 * self.ureg.m, + self.q, + 0 * self.ureg.J, + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_fabs(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], "m") ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_isin(self): self.assertNDArrayEqual( np.isin(self.q, self.Q_([0, 2, 4], "m")), @@ -1065,116 +1111,121 @@ def test_isin(self): np.isin(self.q / self.ureg.cm, [1, 3]), np.array([[True, False], [True, False]]), ) - self.assertRaises(ValueError, np.isin, self.q.m, self.q) + with pytest.raises(ValueError): + np.isin(self.q.m, self.q) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_percentile(self): - self.assertQuantityEqual(np.percentile(self.q, 25), self.Q_(1.75, "m")) + helpers.assert_quantity_equal(np.percentile(self.q, 25), self.Q_(1.75, "m")) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanpercentile(self): - self.assertQuantityEqual(np.nanpercentile(self.q_nan, 25), self.Q_(1.5, "m")) + helpers.assert_quantity_equal( + np.nanpercentile(self.q_nan, 25), self.Q_(1.5, "m") + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_quantile(self): - self.assertQuantityEqual(np.quantile(self.q, 0.25), self.Q_(1.75, "m")) + helpers.assert_quantity_equal(np.quantile(self.q, 0.25), self.Q_(1.75, "m")) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_nanquantile(self): - self.assertQuantityEqual(np.nanquantile(self.q_nan, 0.25), self.Q_(1.5, "m")) + helpers.assert_quantity_equal( + np.nanquantile(self.q_nan, 0.25), self.Q_(1.5, "m") + ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_copyto(self): a = self.q.m q = copy.copy(self.q) np.copyto(q, 2 * q, where=[True, False]) - self.assertQuantityEqual(q, self.Q_([[2, 2], [6, 4]], "m")) + helpers.assert_quantity_equal(q, self.Q_([[2, 2], [6, 4]], "m")) np.copyto(q, 0, where=[[False, False], [True, False]]) - self.assertQuantityEqual(q, self.Q_([[2, 2], [0, 4]], "m")) + helpers.assert_quantity_equal(q, self.Q_([[2, 2], [0, 4]], "m")) np.copyto(a, q) self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_tile(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_rot90(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.rot90(self.q), np.array([[2, 4], [1, 3]]) * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_insert(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.insert(self.q, 1, 0 * self.ureg.m, axis=1), np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m, ) def test_ndarray_downcast(self): - with self.assertWarns(UnitStrippedWarning): + with pytest.warns(UnitStrippedWarning): np.asarray(self.q) def test_ndarray_downcast_with_dtype(self): - with self.assertWarns(UnitStrippedWarning): + with pytest.warns(UnitStrippedWarning): qarr = np.asarray(self.q, dtype=np.float64) - self.assertEqual(qarr.dtype, np.float64) + assert qarr.dtype == np.float64 def test_array_protocol_unavailable(self): for attr in ("__array_struct__", "__array_interface__"): - self.assertRaises(AttributeError, getattr, self.q, attr) + with pytest.raises(AttributeError): + getattr(self.q, attr) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_resize(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.resize(self.q, (2, 4)), [[1, 2, 3, 4], [1, 2, 3, 4]] * self.ureg.m ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol 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( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "constant"), [0, 0, 1, 2, 3, 4, 5, 0, 0, 0] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( 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( + helpers.assert_quantity_equal( 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( + with pytest.raises(DimensionalityError): + np.pad(a, (2, 3), "constant", constant_values=4) + helpers.assert_quantity_equal( np.pad(a, (2, 3), "edge"), [1, 1, 1, 2, 3, 4, 5, 5, 5, 5] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "linear_ramp"), [0, 0, 1, 2, 3, 4, 5, 3, 1, 0] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( 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, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2,), "maximum"), [5, 5, 1, 2, 3, 4, 5, 5, 5] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2,), "mean"), [3, 3, 1, 2, 3, 4, 5, 3, 3] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2,), "median"), [3, 3, 1, 2, 3, 4, 5, 3, 3] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(self.q, ((3, 2), (2, 3)), "minimum"), [ [1, 1, 1, 2, 1, 1, 1], @@ -1187,21 +1238,21 @@ def test_pad(self): ] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "reflect"), [3, 2, 1, 2, 3, 4, 5, 4, 3, 2] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "reflect", reflect_type="odd"), [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "symmetric"), [2, 1, 1, 2, 3, 4, 5, 5, 4, 3] * self.ureg.m ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "symmetric", reflect_type="odd"), [0, 1, 1, 2, 3, 4, 5, 5, 6, 7] * self.ureg.m, ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(a, (2, 3), "wrap"), [4, 5, 1, 2, 3, 4, 5, 1, 2, 3] * self.ureg.m ) @@ -1211,7 +1262,7 @@ def pad_with(vector, pad_width, iaxis, kwargs): vector[-pad_width[1] :] = pad_value b = self.Q_(np.arange(6).reshape((2, 3)), "degC") - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(b, 2, pad_with), self.Q_( [ @@ -1225,7 +1276,7 @@ def pad_with(vector, pad_width, iaxis, kwargs): "degC", ), ) - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.pad(b, 2, pad_with, padder=100), self.Q_( [ @@ -1240,24 +1291,22 @@ def pad_with(vector, pad_width, iaxis, kwargs): ), ) # Note: Does not support Quantity pad_with vectorized callable use - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_allclose(self): - self.assertTrue( - np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.m) - ) - self.assertFalse( - np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.mm) + assert np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.m) + assert not np.allclose( + [1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.mm ) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_intersect1d(self): - self.assertQuantityEqual( + helpers.assert_quantity_equal( np.intersect1d([1, 3, 4, 3] * self.ureg.m, [3, 1, 2, 1] * self.ureg.m), [1, 3] * self.ureg.m, ) -@unittest.skip +@pytest.mark.skip class TestBitTwiddlingUfuncs(TestUFuncs): """Universal functions (ufuncs) > Bittwiddling functions diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 3a9453d35..732ac61a8 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -1,5 +1,7 @@ from unittest.mock import patch +import pytest + import pint.numpy_func from pint import DimensionalityError, OffsetUnitCalculusError from pint.compat import np @@ -12,6 +14,7 @@ numpy_wrap, unwrap_and_wrap_consistent_units, ) +from pint.testsuite import helpers from pint.testsuite.test_numpy import TestNumpyMethods @@ -24,45 +27,43 @@ def test_implements(self): def test_function(): pass - self.assertEqual(pint.numpy_func.HANDLED_FUNCTIONS["test"], test_function) + assert pint.numpy_func.HANDLED_FUNCTIONS["test"] == test_function # Test for ufuncs @implements("test", "ufunc") def test_ufunc(): pass - self.assertEqual(pint.numpy_func.HANDLED_UFUNCS["test"], test_ufunc) + assert pint.numpy_func.HANDLED_UFUNCS["test"] == test_ufunc # Test for invalid func type - with self.assertRaises(ValueError): + with pytest.raises(ValueError): @implements("test", "invalid") def test_invalid(): pass def test_is_quantity(self): - self.assertTrue(_is_quantity(self.Q_(0))) - self.assertTrue(_is_quantity(np.arange(4) * self.ureg.m)) - self.assertFalse(_is_quantity(1.0)) - self.assertFalse(_is_quantity(np.array([1, 1, 2, 3, 5, 8]))) - self.assertFalse(_is_quantity("not-a-quantity")) + assert _is_quantity(self.Q_(0)) + assert _is_quantity(np.arange(4) * self.ureg.m) + assert not _is_quantity(1.0) + assert not _is_quantity(np.array([1, 1, 2, 3, 5, 8])) + assert not _is_quantity("not-a-quantity") # TODO (#905 follow-up): test other duck arrays that wrap or are wrapped by Pint def test_is_sequence_with_quantity_elements(self): - self.assertTrue( - _is_sequence_with_quantity_elements( - (self.Q_(0, "m"), self.Q_(32.0, "degF")) - ) - ) - self.assertTrue(_is_sequence_with_quantity_elements(np.arange(4) * self.ureg.m)) - self.assertTrue(_is_sequence_with_quantity_elements((self.Q_(0), 0))) - self.assertTrue(_is_sequence_with_quantity_elements((0, self.Q_(0)))) - self.assertFalse(_is_sequence_with_quantity_elements([1, 3, 5])) - self.assertFalse(_is_sequence_with_quantity_elements(9 * self.ureg.m)) - self.assertFalse(_is_sequence_with_quantity_elements(np.arange(4))) - self.assertFalse(_is_sequence_with_quantity_elements("0123")) - self.assertFalse(_is_sequence_with_quantity_elements([])) - self.assertFalse(_is_sequence_with_quantity_elements(np.array([]))) + assert _is_sequence_with_quantity_elements( + (self.Q_(0, "m"), self.Q_(32.0, "degF")) + ) + assert _is_sequence_with_quantity_elements(np.arange(4) * self.ureg.m) + assert _is_sequence_with_quantity_elements((self.Q_(0), 0)) + assert _is_sequence_with_quantity_elements((0, self.Q_(0))) + assert not _is_sequence_with_quantity_elements([1, 3, 5]) + assert not _is_sequence_with_quantity_elements(9 * self.ureg.m) + assert not _is_sequence_with_quantity_elements(np.arange(4)) + assert not _is_sequence_with_quantity_elements("0123") + assert not _is_sequence_with_quantity_elements([]) + assert not _is_sequence_with_quantity_elements(np.array([])) def test_convert_to_consistent_units_with_pre_calc_units(self): args, kwargs = convert_to_consistent_units( @@ -73,33 +74,31 @@ def test_convert_to_consistent_units_with_pre_calc_units(self): a=6378 * self.ureg.km, pre_calc_units=self.ureg.meter, ) - self.assertEqual(args[0], 0.5) + assert args[0] == 0.5 self.assertNDArrayEqual(args[1], np.array([[0, 1], [2, 3]])) self.assertNDArrayEqual(args[2], np.array([42])) - self.assertEqual(args[3][0], 0) - self.assertEqual(args[3][1], 10) - self.assertEqual(kwargs["a"], 6.378e6) + assert args[3][0] == 0 + assert args[3][1] == 10 + assert kwargs["a"] == 6.378e6 def test_convert_to_consistent_units_with_dimensionless(self): args, kwargs = convert_to_consistent_units( np.arange(2), pre_calc_units=self.ureg.g / self.ureg.kg ) self.assertNDArrayEqual(args[0], np.array([0, 1000])) - self.assertEqual(kwargs, {}) + assert kwargs == {} def test_convert_to_consistent_units_with_dimensionality_error(self): - self.assertRaises( - DimensionalityError, - convert_to_consistent_units, - self.Q_(32.0, "degF"), - pre_calc_units=self.ureg.meter, - ) - self.assertRaises( - DimensionalityError, - convert_to_consistent_units, - np.arange(4), - pre_calc_units=self.ureg.meter, - ) + with pytest.raises(DimensionalityError): + convert_to_consistent_units( + self.Q_(32.0, "degF"), + pre_calc_units=self.ureg.meter, + ) + with pytest.raises(DimensionalityError): + convert_to_consistent_units( + np.arange(4), + pre_calc_units=self.ureg.meter, + ) def test_convert_to_consistent_units_without_pre_calc_units(self): args, kwargs = convert_to_consistent_units( @@ -107,10 +106,10 @@ def test_convert_to_consistent_units_without_pre_calc_units(self): [1, 2, 3, 5, 7] * self.ureg.m, pre_calc_units=None, ) - self.assertEqual(args[0][0], 0) - self.assertEqual(args[0][1], 10) + assert args[0][0] == 0 + assert args[0][1] == 10 self.assertNDArrayEqual(args[1], np.array([1, 2, 3, 5, 7])) - self.assertEqual(kwargs, {}) + assert kwargs == {} def test_unwrap_and_wrap_constistent_units(self): (a,), output_wrap_a = unwrap_and_wrap_consistent_units([2, 4, 8] * self.ureg.m) @@ -120,81 +119,75 @@ def test_unwrap_and_wrap_constistent_units(self): self.assertNDArrayEqual(a, np.array([2, 4, 8])) self.assertNDArrayEqual(b, np.array([0, 1000, 2000, 3000])) - self.assertEqual(c, 1) + assert c == 1 - self.assertQuantityEqual(output_wrap_a(0), 0 * self.ureg.m) - self.assertQuantityEqual(output_wrap_c(0), self.Q_(0, "g/kg")) + helpers.assert_quantity_equal(output_wrap_a(0), 0 * self.ureg.m) + helpers.assert_quantity_equal(output_wrap_c(0), self.Q_(0, "g/kg")) def test_op_output_unit_sum(self): - self.assertEqual(get_op_output_unit("sum", self.ureg.m), self.ureg.m) - self.assertRaises( - OffsetUnitCalculusError, get_op_output_unit, "sum", self.ureg.degC - ) + assert get_op_output_unit("sum", self.ureg.m) == self.ureg.m + with pytest.raises(OffsetUnitCalculusError): + get_op_output_unit("sum", self.ureg.degC) def test_op_output_unit_mul(self): - self.assertEqual( + assert ( get_op_output_unit( "mul", self.ureg.s, (self.Q_(1, "m"), self.Q_(1, "m**2")) - ), - self.ureg.m ** 3, + ) + == self.ureg.m ** 3 ) def test_op_output_unit_delta(self): - self.assertEqual(get_op_output_unit("delta", self.ureg.m), self.ureg.m) - self.assertEqual( - get_op_output_unit("delta", self.ureg.degC), self.ureg.delta_degC - ) + assert get_op_output_unit("delta", self.ureg.m) == self.ureg.m + assert get_op_output_unit("delta", self.ureg.degC) == self.ureg.delta_degC def test_op_output_unit_delta_div(self): - self.assertEqual( + assert ( get_op_output_unit( "delta,div", self.ureg.m, (self.Q_(1, "m"), self.Q_(1, "s")) - ), - self.ureg.m / self.ureg.s, + ) + == self.ureg.m / self.ureg.s ) - self.assertEqual( + assert ( get_op_output_unit( "delta,div", self.ureg.degC, (self.Q_(1, "degC"), self.Q_(1, "m")) - ), - self.ureg.delta_degC / self.ureg.m, + ) + == self.ureg.delta_degC / self.ureg.m ) def test_op_output_unit_div(self): - self.assertEqual( + assert ( get_op_output_unit( "div", self.ureg.m, (self.Q_(1, "m"), self.Q_(1, "s"), self.Q_(1, "K")) - ), - self.ureg.m / self.ureg.s / self.ureg.K, + ) + == self.ureg.m / self.ureg.s / self.ureg.K ) - self.assertEqual( - get_op_output_unit("div", self.ureg.s, (1, self.Q_(1, "s"))), - self.ureg.s ** -1, + assert ( + get_op_output_unit("div", self.ureg.s, (1, self.Q_(1, "s"))) + == self.ureg.s ** -1 ) def test_op_output_unit_variance(self): - self.assertEqual(get_op_output_unit("variance", self.ureg.m), self.ureg.m ** 2) - self.assertRaises( - OffsetUnitCalculusError, get_op_output_unit, "variance", self.ureg.degC - ) + assert get_op_output_unit("variance", self.ureg.m) == self.ureg.m ** 2 + with pytest.raises(OffsetUnitCalculusError): + get_op_output_unit("variance", self.ureg.degC) def test_op_output_unit_square(self): - self.assertEqual(get_op_output_unit("square", self.ureg.m), self.ureg.m ** 2) + assert get_op_output_unit("square", self.ureg.m) == self.ureg.m ** 2 def test_op_output_unit_sqrt(self): - self.assertEqual(get_op_output_unit("sqrt", self.ureg.m), self.ureg.m ** 0.5) + assert get_op_output_unit("sqrt", self.ureg.m) == self.ureg.m ** 0.5 def test_op_output_unit_reciprocal(self): - self.assertEqual( - get_op_output_unit("reciprocal", self.ureg.m), self.ureg.m ** -1 - ) + assert get_op_output_unit("reciprocal", self.ureg.m) == self.ureg.m ** -1 def test_op_output_unit_size(self): - self.assertEqual( - get_op_output_unit("size", self.ureg.m, size=3), self.ureg.m ** 3 - ) - self.assertRaises(ValueError, get_op_output_unit, "size", self.ureg.m) + assert get_op_output_unit("size", self.ureg.m, size=3) == self.ureg.m ** 3 + with pytest.raises(ValueError): + get_op_output_unit("size", self.ureg.m) def test_numpy_wrap(self): - self.assertRaises(ValueError, numpy_wrap, "invalid", np.ones, [], {}, []) + with pytest.raises(ValueError): + numpy_wrap("invalid", np.ones, [], {}, []) # TODO (#905 follow-up): test that NotImplemented is returned when upcast types # present diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py index 20f9b46da..237293367 100644 --- a/pint/testsuite/test_pint_eval.py +++ b/pint/testsuite/test_pint_eval.py @@ -1,12 +1,10 @@ -import unittest - from pint.compat import tokenizer from pint.pint_eval import build_eval_tree -class TestPintEval(unittest.TestCase): +class TestPintEval: def _test_one(self, input_text, parsed): - self.assertEqual(build_eval_tree(tokenizer(input_text)).to_string(), parsed) + assert build_eval_tree(tokenizer(input_text)).to_string() == parsed def test_build_eval_tree(self): self._test_one("3", "3") diff --git a/pint/testsuite/test_pitheorem.py b/pint/testsuite/test_pitheorem.py index 40abd8c2e..1a4996279 100644 --- a/pint/testsuite/test_pitheorem.py +++ b/pint/testsuite/test_pitheorem.py @@ -1,28 +1,24 @@ import itertools +import logging from pint import pi_theorem from pint.testsuite import QuantityTestCase class TestPiTheorem(QuantityTestCase): - - FORCE_NDARRAY = False - - def test_simple(self): + def test_simple(self, caplog): # simple movement - with self.capture_log() as buffer: - self.assertEqual( - pi_theorem({"V": "m/s", "T": "s", "L": "m"}), - [{"V": 1, "T": 1, "L": -1}], - ) + with caplog.at_level(logging.DEBUG): + assert pi_theorem({"V": "m/s", "T": "s", "L": "m"}) == [ + {"V": 1, "T": 1, "L": -1} + ] # pendulum - self.assertEqual( - pi_theorem({"T": "s", "M": "grams", "L": "m", "g": "m/s**2"}), - [{"g": 1, "T": 2, "L": -1}], - ) - self.assertEqual(len(buffer), 7) + assert pi_theorem({"T": "s", "M": "grams", "L": "m", "g": "m/s**2"}) == [ + {"g": 1, "T": 2, "L": -1} + ] + assert len(caplog.records) == 7 def test_inputs(self): V = "km/hour" @@ -39,7 +35,6 @@ def test_inputs(self): qv = fv(V) qt = ft(T) ql = ft(L) - self.assertEqual( - self.ureg.pi_theorem({"V": qv, "T": qt, "L": ql}), - [{"V": 1.0, "T": 1.0, "L": -1.0}], - ) + assert self.ureg.pi_theorem({"V": qv, "T": qt, "L": ql}) == [ + {"V": 1.0, "T": 1.0, "L": -1.0} + ] diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 8b6a19ef3..fb1fc5d25 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -1,11 +1,14 @@ import copy import datetime +import logging import math import operator as op import pickle import warnings from unittest.mock import patch +import pytest + from pint import ( DimensionalityError, OffsetUnitCalculusError, @@ -15,7 +18,6 @@ ) from pint.compat import np from pint.testsuite import QuantityTestCase, helpers -from pint.testsuite.parameterized import ParameterizedTestCase from pint.unit import UnitsContainer @@ -27,9 +29,9 @@ def __init__(self, q): class TestQuantity(QuantityTestCase): - FORCE_NDARRAY = False + kwargs = dict(autoconvert_offset_to_baseunit=False) - def test_quantity_creation(self): + def test_quantity_creation(self, caplog): for args in ( (4.2, "meter"), (4.2, UnitsContainer(meter=1)), @@ -39,30 +41,31 @@ def test_quantity_creation(self): (self.Q_(4.2, "meter"),), ): x = self.Q_(*args) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer(meter=1)) + assert x.magnitude == 4.2 + assert x.units == UnitsContainer(meter=1) x = self.Q_(4.2, UnitsContainer(length=1)) y = self.Q_(x) - self.assertEqual(x.magnitude, y.magnitude) - self.assertEqual(x.units, y.units) - self.assertIsNot(x, y) + assert x.magnitude == y.magnitude + assert x.units == y.units + assert x is not y x = self.Q_(4.2, None) - self.assertEqual(x.magnitude, 4.2) - self.assertEqual(x.units, UnitsContainer()) + assert x.magnitude == 4.2 + assert x.units == UnitsContainer() - with self.capture_log() as buffer: - self.assertEqual(4.2 * self.ureg.meter, self.Q_(4.2, 2 * self.ureg.meter)) - self.assertEqual(len(buffer), 1) + with caplog.at_level(logging.DEBUG): + assert 4.2 * self.ureg.meter == self.Q_(4.2, 2 * self.ureg.meter) + assert len(caplog.records) == 1 def test_quantity_bool(self): - self.assertTrue(self.Q_(1, None)) - self.assertTrue(self.Q_(1, "meter")) - self.assertFalse(self.Q_(0, None)) - self.assertFalse(self.Q_(0, "meter")) - self.assertRaises(ValueError, bool, self.Q_(0, "degC")) - self.assertFalse(self.Q_(0, "delta_degC")) + assert self.Q_(1, None) + assert self.Q_(1, "meter") + assert not self.Q_(0, None) + assert not self.Q_(0, "meter") + with pytest.raises(ValueError): + bool(self.Q_(0, "degC")) + assert not self.Q_(0, "delta_degC") def test_quantity_comparison(self): x = self.Q_(4.2, "meter") @@ -75,63 +78,61 @@ def test_quantity_comparison(self): m = Quantity(5, "meter") # Include a comparison to a directly created Quantity # identity for single object - self.assertTrue(x == x) - self.assertFalse(x != x) + assert x == x + assert not (x != x) # identity for multiple objects with same value - self.assertTrue(x == y) - self.assertFalse(x != y) + assert x == y + assert not (x != y) - self.assertTrue(x <= y) - self.assertTrue(x >= y) - self.assertFalse(x < y) - self.assertFalse(x > y) + assert x <= y + assert x >= y + assert not (x < y) + assert not (x > y) - self.assertFalse(x == z) - self.assertTrue(x != z) - self.assertTrue(x < z) + assert not (x == z) + assert x != z + assert x < z # Compare with items to the separate application registry - self.assertTrue(k >= m) # These should both be from application registry - with self.assertRaises(ValueError): + assert k >= m # These should both be from application registry + with pytest.raises(ValueError): z > m # One from local registry, one from application registry - self.assertTrue(z != j) + assert z != j - self.assertNotEqual(z, j) - self.assertEqual(self.Q_(0, "meter"), self.Q_(0, "centimeter")) - self.assertNotEqual(self.Q_(0, "meter"), self.Q_(0, "second")) + assert z != j + assert self.Q_(0, "meter") == self.Q_(0, "centimeter") + assert self.Q_(0, "meter") != self.Q_(0, "second") - self.assertLess(self.Q_(10, "meter"), self.Q_(5, "kilometer")) + assert self.Q_(10, "meter") < self.Q_(5, "kilometer") def test_quantity_comparison_convert(self): - self.assertEqual(self.Q_(1000, "millimeter"), self.Q_(1, "meter")) - self.assertEqual( - self.Q_(1000, "millimeter/min"), self.Q_(1000 / 60, "millimeter/s") - ) + assert self.Q_(1000, "millimeter") == self.Q_(1, "meter") + assert self.Q_(1000, "millimeter/min") == self.Q_(1000 / 60, "millimeter/s") def test_quantity_repr(self): x = self.Q_(4.2, UnitsContainer(meter=1)) - self.assertEqual(str(x), "4.2 meter") - self.assertEqual(repr(x), "") + assert str(x) == "4.2 meter" + assert repr(x) == "" def test_quantity_hash(self): x = self.Q_(4.2, "meter") x2 = self.Q_(4200, "millimeter") y = self.Q_(2, "second") z = self.Q_(0.5, "hertz") - self.assertEqual(hash(x), hash(x2)) + assert hash(x) == hash(x2) # Dimensionless equality - self.assertEqual(hash(y * z), hash(1.0)) + assert hash(y * z) == hash(1.0) # Dimensionless equality from a different unit registry - ureg2 = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) + ureg2 = UnitRegistry(**self.kwargs) y2 = ureg2.Quantity(2, "second") z2 = ureg2.Quantity(0.5, "hertz") - self.assertEqual(hash(y * z), hash(y2 * z2)) + assert hash(y * z) == hash(y2 * z2) - def test_quantity_format(self): + def test_quantity_format(self, subtests): x = self.Q_(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( ("{}", str(x)), @@ -161,15 +162,15 @@ def test_quantity_format(self): ("{:C~}", "4.12345678 kg*m**2/s"), ("{:Lx}", r"\SI[]{4.12345678}{\kilo\gram\meter\squared\per\second}"), ): - with self.subTest(spec): - self.assertEqual(spec.format(x), result) + with subtests.test(spec): + assert spec.format(x) == result # Check the special case that prevents e.g. '3 1 / second' x = self.Q_(3, UnitsContainer(second=-1)) - self.assertEqual(f"{x}", "3 / second") + assert f"{x}" == "3 / second" - @helpers.requires_numpy() - def test_quantity_array_format(self): + @helpers.requires_numpy + def test_quantity_array_format(self, subtests): x = self.Q_( np.array([1e-16, 1.0000001, 10000000.0, 1e12, np.nan, np.inf]), "kg * m ** 2", @@ -201,30 +202,30 @@ def test_quantity_array_format(self): ), ), ): - with self.subTest(spec): - self.assertEqual(spec.format(x), result) + with subtests.test(spec): + assert spec.format(x) == result def test_format_compact(self): q1 = (200e-9 * self.ureg.s).to_compact() q1b = self.Q_(200.0, "nanosecond") - self.assertAlmostEqual(q1.magnitude, q1b.magnitude) - self.assertEqual(q1.units, q1b.units) + assert round(abs(q1.magnitude - q1b.magnitude), 7) == 0 + assert q1.units == q1b.units q2 = (1e-2 * self.ureg("kg m/s^2")).to_compact("N") q2b = self.Q_(10.0, "millinewton") - self.assertEqual(q2.magnitude, q2b.magnitude) - self.assertEqual(q2.units, q2b.units) + assert q2.magnitude == q2b.magnitude + assert q2.units == q2b.units q3 = (-1000.0 * self.ureg("meters")).to_compact() q3b = self.Q_(-1.0, "kilometer") - self.assertEqual(q3.magnitude, q3b.magnitude) - self.assertEqual(q3.units, q3b.units) + assert q3.magnitude == q3b.magnitude + assert q3.units == q3b.units - self.assertEqual(f"{q1:#.1f}", f"{q1b}") - self.assertEqual(f"{q2:#.1f}", f"{q2b}") - self.assertEqual(f"{q3:#.1f}", f"{q3b}") + assert f"{q1:#.1f}" == f"{q1b}" + assert f"{q2:#.1f}" == f"{q2b}" + assert f"{q3:#.1f}" == f"{q3b}" - def test_default_formatting(self): + def test_default_formatting(self, subtests): ureg = UnitRegistry() x = ureg.Quantity(4.12345678, UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( @@ -241,21 +242,21 @@ def test_default_formatting(self): ("H~", "4.12345678 kg m2/s"), ("C~", "4.12345678 kg*m**2/s"), ): - with self.subTest(spec): + with subtests.test(spec): ureg.default_format = spec - self.assertEqual(f"{x}", result) + assert f"{x}" == result def test_exponent_formatting(self): ureg = UnitRegistry() x = ureg.Quantity(1e20, "meter") - 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") + assert f"{x:~H}" == r"1×1020 m" + assert f"{x:~L}" == r"1\times 10^{20}\ \mathrm{m}" + assert f"{x:~P}" == r"1×10²⁰ m" x /= 1e40 - 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") + assert f"{x:~H}" == r"1×10-20 m" + assert f"{x:~L}" == r"1\times 10^{-20}\ \mathrm{m}" + assert f"{x:~P}" == r"1×10⁻²⁰ m" def test_ipython(self): alltext = [] @@ -274,54 +275,57 @@ def pretty(cls, data): ureg = UnitRegistry() x = 3.5 * ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), "3.5 kilogram meter2/second") - self.assertEqual( - x._repr_latex_(), - r"$3.5\ \frac{\mathrm{kilogram} \cdot " - r"\mathrm{meter}^{2}}{\mathrm{second}}$", + assert x._repr_html_() == "3.5 kilogram meter2/second" + assert ( + x._repr_latex_() == r"$3.5\ \frac{\mathrm{kilogram} \cdot " + r"\mathrm{meter}^{2}}{\mathrm{second}}$" ) x._repr_pretty_(Pretty, False) - self.assertEqual("".join(alltext), "3.5 kilogram·meter²/second") + assert "".join(alltext) == "3.5 kilogram·meter²/second" ureg.default_format = "~" - 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}}$", + assert x._repr_html_() == "3.5 kg m2/s" + assert ( + x._repr_latex_() == r"$3.5\ \frac{\mathrm{kg} \cdot " + r"\mathrm{m}^{2}}{\mathrm{s}}$" ) alltext = [] x._repr_pretty_(Pretty, False) - self.assertEqual("".join(alltext), "3.5 kg·m²/s") + assert "".join(alltext) == "3.5 kg·m²/s" def test_to_base_units(self): x = self.Q_("1*inch") - self.assertQuantityAlmostEqual(x.to_base_units(), self.Q_(0.0254, "meter")) + helpers.assert_quantity_almost_equal( + x.to_base_units(), self.Q_(0.0254, "meter") + ) x = self.Q_("1*inch*inch") - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( x.to_base_units(), self.Q_(0.0254 ** 2.0, "meter*meter") ) x = self.Q_("1*inch/minute") - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( x.to_base_units(), self.Q_(0.0254 / 60.0, "meter/second") ) def test_convert(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 inch").to("meter"), self.Q_(2.0 * 0.0254, "meter") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 meter").to("inch"), self.Q_(2.0 / 0.0254, "inch") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2 sidereal_year").to("second"), self.Q_(63116297.5325, "second") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_("2.54 centimeter/second").to("inch/second"), self.Q_("1 inch/second"), ) - self.assertAlmostEqual(self.Q_("2.54 centimeter").to("inch").magnitude, 1) - self.assertAlmostEqual(self.Q_("2 second").to("millisecond").magnitude, 2000) + assert round(abs(self.Q_("2.54 centimeter").to("inch").magnitude - 1), 7) == 0 + assert ( + round(abs(self.Q_("2 second").to("millisecond").magnitude - 2000), 7) == 0 + ) - @helpers.requires_numpy() + @helpers.requires_numpy def test_convert_numpy(self): # Conversions with single units take a different codepath than @@ -335,187 +339,194 @@ def test_convert_numpy(self): q = self.Q_(a, src) qac = self.Q_(ac, src).to(dst) r = q.to(dst) - self.assertQuantityAlmostEqual(qac, r) - self.assertIsNot(r, q) - self.assertIsNot(r._magnitude, a) + helpers.assert_quantity_almost_equal(qac, r) + assert r is not q + assert r._magnitude is not a def test_convert_from(self): x = self.Q_("2*inch") meter = self.ureg.meter # from quantity - self.assertQuantityAlmostEqual(meter.from_(x), self.Q_(2.0 * 0.0254, "meter")) - self.assertQuantityAlmostEqual(meter.m_from(x), 2.0 * 0.0254) + helpers.assert_quantity_almost_equal( + meter.from_(x), self.Q_(2.0 * 0.0254, "meter") + ) + helpers.assert_quantity_almost_equal(meter.m_from(x), 2.0 * 0.0254) # from unit - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.from_(self.ureg.inch), self.Q_(0.0254, "meter") ) - self.assertQuantityAlmostEqual(meter.m_from(self.ureg.inch), 0.0254) + helpers.assert_quantity_almost_equal(meter.m_from(self.ureg.inch), 0.0254) # from number - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( meter.from_(2, strict=False), self.Q_(2.0, "meter") ) - self.assertQuantityAlmostEqual(meter.m_from(2, strict=False), 2.0) + helpers.assert_quantity_almost_equal(meter.m_from(2, strict=False), 2.0) # from number (strict mode) - self.assertRaises(ValueError, meter.from_, 2) - self.assertRaises(ValueError, meter.m_from, 2) + with pytest.raises(ValueError): + meter.from_(2) + with pytest.raises(ValueError): + meter.m_from(2) - @helpers.requires_numpy() + @helpers.requires_numpy def test_retain_unit(self): # Test that methods correctly retain units and do not degrade into # ordinary ndarrays. List contained in __copy_units. a = np.ones((3, 2)) q = self.Q_(a, "km") - self.assertEqual(q.u, q.reshape(2, 3).u) - self.assertEqual(q.u, q.swapaxes(0, 1).u) - self.assertEqual(q.u, q.mean().u) - self.assertEqual(q.u, np.compress((q == q[0, 0]).any(0), q).u) + assert q.u == q.reshape(2, 3).u + assert q.u == q.swapaxes(0, 1).u + assert q.u == q.mean().u + assert q.u == np.compress((q == q[0, 0]).any(0), q).u def test_context_attr(self): - self.assertEqual(self.ureg.meter, self.Q_(1, "meter")) + assert self.ureg.meter == self.Q_(1, "meter") def test_both_symbol(self): - self.assertEqual(self.Q_(2, "ms"), self.Q_(2, "millisecond")) - self.assertEqual(self.Q_(2, "cm"), self.Q_(2, "centimeter")) + assert self.Q_(2, "ms") == self.Q_(2, "millisecond") + assert self.Q_(2, "cm") == self.Q_(2, "centimeter") def test_dimensionless_units(self): - self.assertAlmostEqual( - self.Q_(360, "degree").to("radian").magnitude, 2 * math.pi + assert ( + round(abs(self.Q_(360, "degree").to("radian").magnitude - 2 * math.pi), 7) + == 0 + ) + assert ( + round(abs(self.Q_(2 * math.pi, "radian") - self.Q_(360, "degree")), 7) == 0 ) - self.assertAlmostEqual(self.Q_(2 * math.pi, "radian"), self.Q_(360, "degree")) - self.assertEqual(self.Q_(1, "radian").dimensionality, UnitsContainer()) - self.assertTrue(self.Q_(1, "radian").dimensionless) - self.assertFalse(self.Q_(1, "radian").unitless) + assert self.Q_(1, "radian").dimensionality == UnitsContainer() + assert self.Q_(1, "radian").dimensionless + assert not self.Q_(1, "radian").unitless - self.assertEqual(self.Q_(1, "meter") / self.Q_(1, "meter"), 1) - self.assertEqual((self.Q_(1, "meter") / self.Q_(1, "mm")).to(""), 1000) + assert self.Q_(1, "meter") / self.Q_(1, "meter") == 1 + assert (self.Q_(1, "meter") / self.Q_(1, "mm")).to("") == 1000 - self.assertEqual(self.Q_(10) // self.Q_(360, "degree"), 1) - self.assertEqual(self.Q_(400, "degree") // self.Q_(2 * math.pi), 1) - self.assertEqual(self.Q_(400, "degree") // (2 * math.pi), 1) - self.assertEqual(7 // self.Q_(360, "degree"), 1) + assert self.Q_(10) // self.Q_(360, "degree") == 1 + assert self.Q_(400, "degree") // self.Q_(2 * math.pi) == 1 + assert self.Q_(400, "degree") // (2 * math.pi) == 1 + assert 7 // self.Q_(360, "degree") == 1 def test_offset(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "kelvin").to("kelvin"), self.Q_(0, "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "degC").to("kelvin"), self.Q_(273.15, "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "degF").to("kelvin"), self.Q_(255.372222, "kelvin"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "kelvin").to("kelvin"), self.Q_(100, "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "degC").to("kelvin"), self.Q_(373.15, "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "degF").to("kelvin"), self.Q_(310.92777777, "kelvin"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "kelvin").to("degC"), self.Q_(-273.15, "degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "kelvin").to("degC"), self.Q_(-173.15, "degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "kelvin").to("degF"), self.Q_(-459.67, "degF"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "kelvin").to("degF"), self.Q_(-279.67, "degF"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(32, "degF").to("degC"), self.Q_(0, "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "degC").to("degF"), self.Q_(212, "degF"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(54, "degF").to("degC"), self.Q_(12.2222, "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "degC").to("degF"), self.Q_(53.6, "degF"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "kelvin").to("degC"), self.Q_(-261.15, "degC"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "degC").to("kelvin"), self.Q_(285.15, "kelvin"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "kelvin").to("degR"), self.Q_(21.6, "degR"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "degR").to("kelvin"), self.Q_(6.66666667, "kelvin"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "degC").to("degR"), self.Q_(513.27, "degR"), atol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12, "degR").to("degC"), self.Q_(-266.483333, "degC"), atol=0.01 ) def test_offset_delta(self): - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "delta_degC").to("kelvin"), self.Q_(0, "kelvin") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(0, "delta_degF").to("kelvin"), self.Q_(0, "kelvin"), rtol=0.01 ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "kelvin").to("delta_degC"), self.Q_(100, "delta_degC") ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "kelvin").to("delta_degF"), self.Q_(180, "delta_degF"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "delta_degF").to("kelvin"), self.Q_(55.55555556, "kelvin"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "delta_degC").to("delta_degF"), self.Q_(180, "delta_degF"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(100, "delta_degF").to("delta_degC"), self.Q_(55.55555556, "delta_degC"), rtol=0.01, ) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( self.Q_(12.3, "delta_degC").to("delta_degF"), self.Q_(22.14, "delta_degF"), rtol=0.01, ) - def test_pickle(self): + def test_pickle(self, subtests): for protocol in range(pickle.HIGHEST_PROTOCOL + 1): for magnitude, unit in ((32, ""), (2.4, ""), (32, "m/s"), (2.4, "m/s")): - with self.subTest(protocol=protocol, magnitude=magnitude, unit=unit): + with subtests.test(protocol=protocol, magnitude=magnitude, unit=unit): q1 = self.Q_(magnitude, unit) q2 = pickle.loads(pickle.dumps(q1, protocol)) - self.assertEqual(q1, q2) + assert q1 == q2 - @helpers.requires_numpy() + @helpers.requires_numpy def test_from_sequence(self): u_array_ref = self.Q_([200, 1000], "g") u_array_ref_reversed = self.Q_([1000, 200], "g") @@ -523,36 +534,36 @@ def test_from_sequence(self): u_seq_reversed = u_seq[::-1] u_array = self.Q_.from_sequence(u_seq) - self.assertTrue(all(u_array == u_array_ref)) + assert all(u_array == u_array_ref) u_array_2 = self.Q_.from_sequence(u_seq_reversed) - self.assertTrue(all(u_array_2 == u_array_ref_reversed)) - self.assertFalse(u_array_2.u == u_array_ref_reversed.u) + assert all(u_array_2 == u_array_ref_reversed) + assert not (u_array_2.u == u_array_ref_reversed.u) u_array_3 = self.Q_.from_sequence(u_seq_reversed, units="g") - self.assertTrue(all(u_array_3 == u_array_ref_reversed)) - self.assertTrue(u_array_3.u == u_array_ref_reversed.u) + assert all(u_array_3 == u_array_ref_reversed) + assert u_array_3.u == u_array_ref_reversed.u - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.Q_.from_sequence([]) u_array_5 = self.Q_.from_list(u_seq) - self.assertTrue(all(u_array_5 == u_array_ref)) + assert all(u_array_5 == u_array_ref) - @helpers.requires_numpy() + @helpers.requires_numpy def test_iter(self): # Verify that iteration gives element as Quantity with same units x = self.Q_([0, 1, 2, 3], "m") - self.assertQuantityEqual(next(iter(x)), self.Q_(0, "m")) + helpers.assert_quantity_equal(next(iter(x)), self.Q_(0, "m")) def test_notiter(self): # Verify that iter() crashes immediately, without needing to draw any # element from it, if the magnitude isn't iterable x = self.Q_(1, "m") - with self.assertRaises(TypeError): + with pytest.raises(TypeError): iter(x) - @helpers.requires_array_function_protocol() + @helpers.requires_array_function_protocol def test_no_longer_array_function_warning_on_creation(self): # Test that warning is no longer raised on first creation with warnings.catch_warnings(): @@ -561,88 +572,87 @@ def test_no_longer_array_function_warning_on_creation(self): @helpers.requires_not_numpy() def test_no_ndarray_coercion_without_numpy(self): - self.assertRaises(ValueError, self.Q_(1, "m").__array__) + with pytest.raises(ValueError): + self.Q_(1, "m").__array__() @patch("pint.compat.upcast_types", [FakeWrapper]) def test_upcast_type_rejection_on_creation(self): - self.assertRaises(TypeError, self.Q_, FakeWrapper(42), "m") - self.assertEqual(FakeWrapper(self.Q_(42, "m")).q, self.Q_(42, "m")) + with pytest.raises(TypeError): + self.Q_(FakeWrapper(42), "m") + assert FakeWrapper(self.Q_(42, "m")).q == self.Q_(42, "m") class TestQuantityToCompact(QuantityTestCase): def assertQuantityAlmostIdentical(self, q1, q2): - self.assertEqual(q1.units, q2.units) - self.assertAlmostEqual(q1.magnitude, q2.magnitude) + assert q1.units == q2.units + assert round(abs(q1.magnitude - q2.magnitude), 7) == 0 - def compareQuantity_compact(self, q, expected_compact, unit=None): - self.assertQuantityAlmostIdentical(q.to_compact(unit=unit), expected_compact) + def compare_quantity_compact(self, q, expected_compact, unit=None): + helpers.assert_quantity_almost_equal(q.to_compact(unit=unit), expected_compact) def test_dimensionally_simple_units(self): ureg = self.ureg - self.compareQuantity_compact(1 * ureg.m, 1 * ureg.m) - self.compareQuantity_compact(1e-9 * ureg.m, 1 * ureg.nm) + self.compare_quantity_compact(1 * ureg.m, 1 * ureg.m) + self.compare_quantity_compact(1e-9 * ureg.m, 1 * ureg.nm) def test_power_units(self): ureg = self.ureg - self.compareQuantity_compact(900 * ureg.m ** 2, 900 * ureg.m ** 2) - self.compareQuantity_compact(1e7 * ureg.m ** 2, 10 * ureg.km ** 2) + self.compare_quantity_compact(900 * ureg.m ** 2, 900 * ureg.m ** 2) + self.compare_quantity_compact(1e7 * ureg.m ** 2, 10 * ureg.km ** 2) def test_inverse_units(self): ureg = self.ureg - self.compareQuantity_compact(1 / ureg.m, 1 / ureg.m) - self.compareQuantity_compact(100e9 / ureg.m, 100 / ureg.nm) + self.compare_quantity_compact(1 / ureg.m, 1 / ureg.m) + self.compare_quantity_compact(100e9 / ureg.m, 100 / ureg.nm) def test_inverse_square_units(self): ureg = self.ureg - self.compareQuantity_compact(1 / ureg.m ** 2, 1 / ureg.m ** 2) - self.compareQuantity_compact(1e11 / ureg.m ** 2, 1e5 / ureg.mm ** 2) + self.compare_quantity_compact(1 / ureg.m ** 2, 1 / ureg.m ** 2) + self.compare_quantity_compact(1e11 / ureg.m ** 2, 1e5 / ureg.mm ** 2) def test_fractional_units(self): ureg = self.ureg # Typing denominator first to provoke potential error - self.compareQuantity_compact(20e3 * ureg("hr^(-1) m"), 20 * ureg.km / ureg.hr) + self.compare_quantity_compact(20e3 * ureg("hr^(-1) m"), 20 * ureg.km / ureg.hr) def test_fractional_exponent_units(self): ureg = self.ureg - self.compareQuantity_compact(1 * ureg.m ** 0.5, 1 * ureg.m ** 0.5) - self.compareQuantity_compact(1e-2 * ureg.m ** 0.5, 10 * ureg.um ** 0.5) + self.compare_quantity_compact(1 * ureg.m ** 0.5, 1 * ureg.m ** 0.5) + self.compare_quantity_compact(1e-2 * ureg.m ** 0.5, 10 * ureg.um ** 0.5) def test_derived_units(self): ureg = self.ureg - self.compareQuantity_compact(0.5 * ureg.megabyte, 500 * ureg.kilobyte) - self.compareQuantity_compact(1e-11 * ureg.N, 10 * ureg.pN) + self.compare_quantity_compact(0.5 * ureg.megabyte, 500 * ureg.kilobyte) + self.compare_quantity_compact(1e-11 * ureg.N, 10 * ureg.pN) def test_unit_parameter(self): ureg = self.ureg - self.compareQuantity_compact( + self.compare_quantity_compact( self.Q_(100e-9, "kg m / s^2"), 100 * ureg.nN, ureg.N ) - self.compareQuantity_compact( + self.compare_quantity_compact( self.Q_(101.3e3, "kg/m/s^2"), 101.3 * ureg.kPa, ureg.Pa ) def test_limits_magnitudes(self): ureg = self.ureg - self.compareQuantity_compact(0 * ureg.m, 0 * ureg.m) - self.compareQuantity_compact(float("inf") * ureg.m, float("inf") * ureg.m) + self.compare_quantity_compact(0 * ureg.m, 0 * ureg.m) + self.compare_quantity_compact(float("inf") * ureg.m, float("inf") * ureg.m) def test_nonnumeric_magnitudes(self): ureg = self.ureg x = "some string" * ureg.m - with self.assertWarns(RuntimeWarning): - self.compareQuantity_compact(x, x) + with pytest.warns(RuntimeWarning): + self.compare_quantity_compact(x, x) def test_very_large_to_compact(self): # This should not raise an IndexError - self.compareQuantity_compact( + self.compare_quantity_compact( self.Q_(10000, "yottameter"), self.Q_(10 ** 28, "meter").to_compact() ) class TestQuantityBasicMath(QuantityTestCase): - - FORCE_NDARRAY = False - def _test_inplace(self, operator, value1, value2, expected_result, unit=None): if isinstance(value1, str): value1 = self.Q_(value1) @@ -662,10 +672,10 @@ def _test_inplace(self, operator, value1, value2, expected_result, unit=None): id2 = id(value2) value1 = operator(value1, value2) value2_cpy = copy.copy(value2) - self.assertQuantityAlmostEqual(value1, expected_result) - self.assertEqual(id1, id(value1)) - self.assertQuantityAlmostEqual(value2, value2_cpy) - self.assertEqual(id2, id(value2)) + helpers.assert_quantity_almost_equal(value1, expected_result) + assert id1 == id(value1) + helpers.assert_quantity_almost_equal(value2, value2_cpy) + assert id2 == id(value2) def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None): if isinstance(value1, str): @@ -688,11 +698,11 @@ def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None result = operator(value1, value2) - self.assertQuantityAlmostEqual(expected_result, result) - self.assertQuantityAlmostEqual(value1, value1_cpy) - self.assertQuantityAlmostEqual(value2, value2_cpy) - self.assertNotEqual(id(result), id1) - self.assertNotEqual(id(result), id2) + helpers.assert_quantity_almost_equal(expected_result, result) + helpers.assert_quantity_almost_equal(value1, value1_cpy) + helpers.assert_quantity_almost_equal(value2, value2_cpy) + assert id(result) != id1 + assert id(result) != id2 def _test_quantity_add_sub(self, unit, func): x = self.Q_(unit, "centimeter") @@ -704,17 +714,23 @@ def _test_quantity_add_sub(self, unit, func): func(op.add, x, y, self.Q_(unit + 2.54 * unit, "centimeter")) func(op.add, y, x, self.Q_(unit + unit / (2.54 * unit), "inch")) func(op.add, a, unit, self.Q_(unit + unit, None)) - self.assertRaises(DimensionalityError, op.add, 10, x) - self.assertRaises(DimensionalityError, op.add, x, 10) - self.assertRaises(DimensionalityError, op.add, x, z) + with pytest.raises(DimensionalityError): + op.add(10, x) + with pytest.raises(DimensionalityError): + op.add(x, 10) + with pytest.raises(DimensionalityError): + op.add(x, z) func(op.sub, x, x, self.Q_(unit - unit, "centimeter")) func(op.sub, x, y, self.Q_(unit - 2.54 * unit, "centimeter")) func(op.sub, y, x, self.Q_(unit - unit / (2.54 * unit), "inch")) func(op.sub, a, unit, self.Q_(unit - unit, None)) - self.assertRaises(DimensionalityError, op.sub, 10, x) - self.assertRaises(DimensionalityError, op.sub, x, 10) - self.assertRaises(DimensionalityError, op.sub, x, z) + with pytest.raises(DimensionalityError): + op.sub(10, x) + with pytest.raises(DimensionalityError): + op.sub(x, 10) + with pytest.raises(DimensionalityError): + op.sub(x, z) def _test_quantity_iadd_isub(self, unit, func): x = self.Q_(unit, "centimeter") @@ -726,17 +742,23 @@ def _test_quantity_iadd_isub(self, unit, func): func(op.iadd, x, y, self.Q_(unit + 2.54 * unit, "centimeter")) func(op.iadd, y, x, self.Q_(unit + unit / 2.54, "inch")) func(op.iadd, a, unit, self.Q_(unit + unit, None)) - self.assertRaises(DimensionalityError, op.iadd, 10, x) - self.assertRaises(DimensionalityError, op.iadd, x, 10) - self.assertRaises(DimensionalityError, op.iadd, x, z) + with pytest.raises(DimensionalityError): + op.iadd(10, x) + with pytest.raises(DimensionalityError): + op.iadd(x, 10) + with pytest.raises(DimensionalityError): + op.iadd(x, z) func(op.isub, x, x, self.Q_(unit - unit, "centimeter")) func(op.isub, x, y, self.Q_(unit - 2.54, "centimeter")) func(op.isub, y, x, self.Q_(unit - unit / 2.54, "inch")) func(op.isub, a, unit, self.Q_(unit - unit, None)) - self.assertRaises(DimensionalityError, op.sub, 10, x) - self.assertRaises(DimensionalityError, op.sub, x, 10) - self.assertRaises(DimensionalityError, op.sub, x, z) + with pytest.raises(DimensionalityError): + op.sub(10, x) + with pytest.raises(DimensionalityError): + op.sub(x, 10) + with pytest.raises(DimensionalityError): + op.sub(x, z) def _test_quantity_mul_div(self, unit, func): func(op.mul, unit * 10.0, "4.2*meter", "42*meter", unit) @@ -757,24 +779,36 @@ def _test_quantity_imul_idiv(self, unit, func): def _test_quantity_floordiv(self, unit, func): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, op.floordiv, a, b) - self.assertRaises(DimensionalityError, op.floordiv, 3, b) - self.assertRaises(DimensionalityError, op.floordiv, a, 3) - self.assertRaises(DimensionalityError, op.ifloordiv, a, b) - self.assertRaises(DimensionalityError, op.ifloordiv, 3, b) - self.assertRaises(DimensionalityError, op.ifloordiv, a, 3) + with pytest.raises(DimensionalityError): + op.floordiv(a, b) + with pytest.raises(DimensionalityError): + op.floordiv(3, b) + with pytest.raises(DimensionalityError): + op.floordiv(a, 3) + with pytest.raises(DimensionalityError): + op.ifloordiv(a, b) + with pytest.raises(DimensionalityError): + op.ifloordiv(3, b) + with pytest.raises(DimensionalityError): + op.ifloordiv(a, 3) func(op.floordiv, unit * 10.0, "4.2*meter/meter", 2, unit) func(op.floordiv, "10*meter", "4.2*inch", 93, unit) def _test_quantity_mod(self, unit, func): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, op.mod, a, b) - self.assertRaises(DimensionalityError, op.mod, 3, b) - self.assertRaises(DimensionalityError, op.mod, a, 3) - self.assertRaises(DimensionalityError, op.imod, a, b) - self.assertRaises(DimensionalityError, op.imod, 3, b) - self.assertRaises(DimensionalityError, op.imod, a, 3) + with pytest.raises(DimensionalityError): + op.mod(a, b) + with pytest.raises(DimensionalityError): + op.mod(3, b) + with pytest.raises(DimensionalityError): + op.mod(a, 3) + with pytest.raises(DimensionalityError): + op.imod(a, b) + with pytest.raises(DimensionalityError): + op.imod(3, b) + with pytest.raises(DimensionalityError): + op.imod(a, 3) func(op.mod, unit * 10.0, "4.2*meter/meter", 1.6, unit) def _test_quantity_ifloordiv(self, unit, func): @@ -788,25 +822,25 @@ def _test_quantity_divmod_one(self, a, b): b = self.Q_(b) q, r = divmod(a, b) - self.assertEqual(q, a // b) - self.assertEqual(r, a % b) - self.assertEqual(a, (q * b) + r) - self.assertEqual(q, math.floor(q)) + assert q == a // b + assert r == a % b + assert a == (q * b) + r + assert q == math.floor(q) if b > (0 * b): - self.assertTrue((0 * b) <= r < b) + assert (0 * b) <= r < b else: - self.assertTrue((0 * b) >= r > b) + assert (0 * b) >= r > b if isinstance(a, self.Q_): - self.assertEqual(r.units, a.units) + assert r.units == a.units else: - self.assertTrue(r.unitless) - self.assertTrue(q.unitless) + assert r.unitless + assert q.unitless copy_a = copy.copy(a) a %= b - self.assertEqual(a, r) + assert a == r copy_a //= b - self.assertEqual(copy_a, q) + assert copy_a == q def _test_quantity_divmod(self): self._test_quantity_divmod_one("10*meter", "4.2*inch") @@ -822,9 +856,12 @@ def _test_quantity_divmod(self): a = self.Q_("10*meter") b = self.Q_("3*second") - self.assertRaises(DimensionalityError, divmod, a, b) - self.assertRaises(DimensionalityError, divmod, 3, b) - self.assertRaises(DimensionalityError, divmod, a, 3) + with pytest.raises(DimensionalityError): + divmod(a, b) + with pytest.raises(DimensionalityError): + divmod(3, b) + with pytest.raises(DimensionalityError): + divmod(a, 3) def _test_numeric(self, unit, ifunc): self._test_quantity_add_sub(unit, self._test_not_inplace) @@ -844,7 +881,7 @@ def test_fraction(self): self._test_numeric(fractions.Fraction(1, 1), self._test_not_inplace) - @helpers.requires_numpy() + @helpers.requires_numpy def test_nparray(self): self._test_numeric(np.ones((1, 3)), self._test_inplace) @@ -858,176 +895,160 @@ def test_quantity_abs_round(self): zy = self.Q_(fun(y.magnitude), "meter") rx = fun(x) ry = fun(y) - self.assertEqual(rx, zx, "while testing {0}".format(fun)) - self.assertEqual(ry, zy, "while testing {0}".format(fun)) - self.assertIsNot(rx, zx, "while testing {0}".format(fun)) - self.assertIsNot(ry, zy, "while testing {0}".format(fun)) + assert rx == zx, "while testing {0}".format(fun) + assert ry == zy, "while testing {0}".format(fun) + assert rx is not zx, "while testing {0}".format(fun) + assert ry is not zy, "while testing {0}".format(fun) def test_quantity_float_complex(self): x = self.Q_(-4.2, None) y = self.Q_(4.2, None) z = self.Q_(1, "meter") for fun in (float, complex): - self.assertEqual(fun(x), fun(x.magnitude)) - self.assertEqual(fun(y), fun(y.magnitude)) - self.assertRaises(DimensionalityError, fun, z) + assert fun(x) == fun(x.magnitude) + assert fun(y) == fun(y.magnitude) + with pytest.raises(DimensionalityError): + fun(z) class TestQuantityNeutralAdd(QuantityTestCase): """Addition to zero or NaN is allowed between a Quantity and a non-Quantity""" - FORCE_NDARRAY = False - def test_bare_zero(self): v = self.Q_(2.0, "m") - self.assertEqual(v + 0, v) - self.assertEqual(v - 0, v) - self.assertEqual(0 + v, v) - self.assertEqual(0 - v, -v) + assert v + 0 == v + assert v - 0 == v + assert 0 + v == v + assert 0 - v == -v def test_bare_zero_inplace(self): v = self.Q_(2.0, "m") v2 = self.Q_(2.0, "m") v2 += 0 - self.assertEqual(v2, v) + assert v2 == v v2 = self.Q_(2.0, "m") v2 -= 0 - self.assertEqual(v2, v) + assert v2 == v v2 = 0 v2 += v - self.assertEqual(v2, v) + assert v2 == v v2 = 0 v2 -= v - self.assertEqual(v2, -v) + assert v2 == -v def test_bare_nan(self): v = self.Q_(2.0, "m") - self.assertQuantityEqual(v + math.nan, self.Q_(math.nan, v.units)) - self.assertQuantityEqual(v - math.nan, self.Q_(math.nan, v.units)) - self.assertQuantityEqual(math.nan + v, self.Q_(math.nan, v.units)) - self.assertQuantityEqual(math.nan - v, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v + math.nan, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v - math.nan, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(math.nan + v, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(math.nan - v, self.Q_(math.nan, v.units)) def test_bare_nan_inplace(self): v = self.Q_(2.0, "m") v2 = self.Q_(2.0, "m") v2 += math.nan - self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v2, self.Q_(math.nan, v.units)) v2 = self.Q_(2.0, "m") v2 -= math.nan - self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v2, self.Q_(math.nan, v.units)) v2 = math.nan v2 += v - self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v2, self.Q_(math.nan, v.units)) v2 = math.nan v2 -= v - self.assertQuantityEqual(v2, self.Q_(math.nan, v.units)) + helpers.assert_quantity_equal(v2, self.Q_(math.nan, v.units)) - @helpers.requires_numpy() + @helpers.requires_numpy def test_bare_zero_or_nan_numpy(self): z = np.array([0.0, np.nan]) v = self.Q_([1.0, 2.0], "m") e = self.Q_([1.0, np.nan], "m") - self.assertQuantityEqual(z + v, e) - self.assertQuantityEqual(z - v, -e) - self.assertQuantityEqual(v + z, e) - self.assertQuantityEqual(v - z, e) + helpers.assert_quantity_equal(z + v, e) + helpers.assert_quantity_equal(z - v, -e) + helpers.assert_quantity_equal(v + z, e) + helpers.assert_quantity_equal(v - z, e) # If any element is non-zero and non-NaN, raise DimensionalityError nz = np.array([0.0, 1.0]) - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): nz + v - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): nz - v - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): v + nz - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): v - nz # Mismatched shape z = np.array([0.0, np.nan, 0.0]) v = self.Q_([1.0, 2.0], "m") for x, y in ((z, v), (v, z)): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): x + y - with self.assertRaises(ValueError): + with pytest.raises(ValueError): x - y - @helpers.requires_numpy() + @helpers.requires_numpy def test_bare_zero_or_nan_numpy_inplace(self): z = np.array([0.0, np.nan]) v = self.Q_([1.0, 2.0], "m") e = self.Q_([1.0, np.nan], "m") v += z - self.assertQuantityEqual(v, e) + helpers.assert_quantity_equal(v, e) v = self.Q_([1.0, 2.0], "m") v -= z - self.assertQuantityEqual(v, e) + helpers.assert_quantity_equal(v, e) v = self.Q_([1.0, 2.0], "m") z = np.array([0.0, np.nan]) z += v - self.assertQuantityEqual(z, e) + helpers.assert_quantity_equal(z, e) v = self.Q_([1.0, 2.0], "m") z = np.array([0.0, np.nan]) z -= v - self.assertQuantityEqual(z, -e) + helpers.assert_quantity_equal(z, -e) class TestDimensions(QuantityTestCase): - - FORCE_NDARRAY = False - def test_get_dimensionality(self): get = self.ureg.get_dimensionality - self.assertEqual(get("[time]"), UnitsContainer({"[time]": 1})) - self.assertEqual( - get(UnitsContainer({"[time]": 1})), UnitsContainer({"[time]": 1}) - ) - self.assertEqual(get("seconds"), UnitsContainer({"[time]": 1})) - self.assertEqual( - get(UnitsContainer({"seconds": 1})), UnitsContainer({"[time]": 1}) - ) - self.assertEqual(get("[speed]"), UnitsContainer({"[length]": 1, "[time]": -1})) - self.assertEqual( - get("[acceleration]"), UnitsContainer({"[length]": 1, "[time]": -2}) - ) + assert get("[time]") == UnitsContainer({"[time]": 1}) + assert get(UnitsContainer({"[time]": 1})) == UnitsContainer({"[time]": 1}) + assert get("seconds") == UnitsContainer({"[time]": 1}) + assert get(UnitsContainer({"seconds": 1})) == UnitsContainer({"[time]": 1}) + assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1}) + assert get("[acceleration]") == UnitsContainer({"[length]": 1, "[time]": -2}) def test_dimensionality(self): x = self.Q_(42, "centimeter") x.to_base_units() x = self.Q_(42, "meter*second") - self.assertEqual( - x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 1.0}) - ) + assert x.dimensionality == UnitsContainer({"[length]": 1.0, "[time]": 1.0}) x = self.Q_(42, "meter*second*second") - self.assertEqual( - x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 2.0}) - ) + assert x.dimensionality == UnitsContainer({"[length]": 1.0, "[time]": 2.0}) x = self.Q_(42, "inch*second*second") - self.assertEqual( - x.dimensionality, UnitsContainer({"[length]": 1.0, "[time]": 2.0}) - ) - self.assertTrue(self.Q_(42, None).dimensionless) - self.assertFalse(self.Q_(42, "meter").dimensionless) - self.assertTrue((self.Q_(42, "meter") / self.Q_(1, "meter")).dimensionless) - self.assertFalse((self.Q_(42, "meter") / self.Q_(1, "second")).dimensionless) - self.assertTrue((self.Q_(42, "meter") / self.Q_(1, "inch")).dimensionless) + assert x.dimensionality == UnitsContainer({"[length]": 1.0, "[time]": 2.0}) + assert self.Q_(42, None).dimensionless + assert not self.Q_(42, "meter").dimensionless + assert (self.Q_(42, "meter") / self.Q_(1, "meter")).dimensionless + assert not (self.Q_(42, "meter") / self.Q_(1, "second")).dimensionless + assert (self.Q_(42, "meter") / self.Q_(1, "inch")).dimensionless def test_inclusion(self): dim = self.Q_(42, "meter").dimensionality - self.assertTrue("[length]" in dim) - self.assertFalse("[time]" in dim) + assert "[length]" in dim + assert not ("[time]" in dim) dim = (self.Q_(42, "meter") / self.Q_(11, "second")).dimensionality - self.assertTrue("[length]" in dim) - self.assertTrue("[time]" in dim) + assert "[length]" in dim + assert "[time]" in dim dim = self.Q_(20.785, "J/(mol)").dimensionality for dimension in ("[length]", "[mass]", "[substance]", "[time]"): - self.assertTrue(dimension in dim) - self.assertFalse("[angle]" in dim) + assert dimension in dim + assert not ("[angle]" in dim) class TestQuantityWithDefaultRegistry(TestDimensions): @classmethod - def setUpClass(cls): + def setup_class(cls): from pint import _DEFAULT_REGISTRY cls.ureg = _DEFAULT_REGISTRY @@ -1036,17 +1057,19 @@ def setUpClass(cls): class TestDimensionsWithDefaultRegistry(TestDimensions): @classmethod - def setUpClass(cls): + def setup_class(cls): from pint import _DEFAULT_REGISTRY cls.ureg = _DEFAULT_REGISTRY cls.Q_ = cls.ureg.Quantity -class TestOffsetUnitMath(QuantityTestCase, ParameterizedTestCase): - def setup(self): - self.ureg.autoconvert_offset_to_baseunit = False - self.ureg.default_as_delta = True +class TestOffsetUnitMath(QuantityTestCase): + @classmethod + def setup_class(cls): + super().setup_class() + cls.ureg.autoconvert_offset_to_baseunit = False + cls.ureg.default_as_delta = True additions = [ # --- input tuple -------------------- | -- expected result -- @@ -1088,7 +1111,7 @@ def setup(self): (((100, "delta_degF"), (10, "delta_degF")), (110, "delta_degF")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) + @pytest.mark.parametrize(("input_tuple", "expected"), additions) def test_addition(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple @@ -1096,14 +1119,15 @@ def test_addition(self, input_tuple, expected): # update input tuple with new values to have correct values on failure input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.add, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.add(q1, q2) else: expected = self.Q_(*expected) - self.assertEqual(op.add(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.add(q1, q2), expected, atol=0.01) + assert op.add(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.add(q1, q2), expected, atol=0.01) - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), additions) + @helpers.requires_numpy + @pytest.mark.parametrize(("input_tuple", "expected"), additions) def test_inplace_addition(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple @@ -1117,12 +1141,15 @@ def test_inplace_addition(self, input_tuple, expected): q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.iadd, q1_cp, q2) + with pytest.raises(OffsetUnitCalculusError): + op.iadd(q1_cp, q2) else: expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] - self.assertEqual(op.iadd(q1_cp, q2).units, Q_(*expected).units) + assert op.iadd(q1_cp, q2).units == Q_(*expected).units q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.iadd(q1_cp, q2), Q_(*expected), atol=0.01) + helpers.assert_quantity_almost_equal( + op.iadd(q1_cp, q2), Q_(*expected), atol=0.01 + ) subtractions = [ (((100, "kelvin"), (10, "kelvin")), (90, "kelvin")), @@ -1163,22 +1190,23 @@ def test_inplace_addition(self, input_tuple, expected): (((100, "delta_degF"), (10, "delta_degF")), (90, "delta_degF")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) + @pytest.mark.parametrize(("input_tuple", "expected"), subtractions) def test_subtraction(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.sub, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.sub(q1, q2) else: expected = self.Q_(*expected) - self.assertEqual(op.sub(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.sub(q1, q2), expected, atol=0.01) + assert op.sub(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.sub(q1, q2), expected, atol=0.01) - # @unittest.expectedFailure - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), subtractions) + # @pytest.mark.xfail + @helpers.requires_numpy + @pytest.mark.parametrize(("input_tuple", "expected"), subtractions) def test_inplace_subtraction(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple @@ -1192,12 +1220,15 @@ def test_inplace_subtraction(self, input_tuple, expected): q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.isub, q1_cp, q2) + with pytest.raises(OffsetUnitCalculusError): + op.isub(q1_cp, q2) else: expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] - self.assertEqual(op.isub(q1_cp, q2).units, Q_(*expected).units) + assert op.isub(q1_cp, q2).units == Q_(*expected).units q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.isub(q1_cp, q2), Q_(*expected), atol=0.01) + helpers.assert_quantity_almost_equal( + op.isub(q1_cp, q2), Q_(*expected), atol=0.01 + ) multiplications = [ (((100, "kelvin"), (10, "kelvin")), (1000, "kelvin**2")), @@ -1238,21 +1269,22 @@ def test_inplace_subtraction(self, input_tuple, expected): (((100, "delta_degF"), (10, "delta_degF")), (1000, "delta_degF**2")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) + @pytest.mark.parametrize(("input_tuple", "expected"), multiplications) def test_multiplication(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(q1, q2) else: expected = self.Q_(*expected) - self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + assert op.mul(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01) - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), multiplications) + @helpers.requires_numpy + @pytest.mark.parametrize(("input_tuple", "expected"), multiplications) def test_inplace_multiplication(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple @@ -1266,12 +1298,15 @@ def test_inplace_multiplication(self, input_tuple, expected): q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.imul, q1_cp, q2) + with pytest.raises(OffsetUnitCalculusError): + op.imul(q1_cp, q2) else: expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] - self.assertEqual(op.imul(q1_cp, q2).units, Q_(*expected).units) + assert op.imul(q1_cp, q2).units == Q_(*expected).units q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), atol=0.01) + helpers.assert_quantity_almost_equal( + op.imul(q1_cp, q2), Q_(*expected), atol=0.01 + ) divisions = [ (((100, "kelvin"), (10, "kelvin")), (10, "")), @@ -1312,21 +1347,24 @@ def test_inplace_multiplication(self, input_tuple, expected): (((100, "delta_degF"), (10, "delta_degF")), (10, "")), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) + @pytest.mark.parametrize(("input_tuple", "expected"), divisions) def test_truedivision(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False qin1, qin2 = input_tuple q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.truediv, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.truediv(q1, q2) else: expected = self.Q_(*expected) - self.assertEqual(op.truediv(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.truediv(q1, q2), expected, atol=0.01) + assert op.truediv(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal( + op.truediv(q1, q2), expected, atol=0.01 + ) - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), divisions) + @helpers.requires_numpy + @pytest.mark.parametrize(("input_tuple", "expected"), divisions) def test_inplace_truedivision(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = False (q1v, q1u), (q2v, q2u) = input_tuple @@ -1340,12 +1378,13 @@ def test_inplace_truedivision(self, input_tuple, expected): q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.itruediv, q1_cp, q2) + with pytest.raises(OffsetUnitCalculusError): + op.itruediv(q1_cp, q2) else: expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] - self.assertEqual(op.itruediv(q1_cp, q2).units, Q_(*expected).units) + assert op.itruediv(q1_cp, q2).units == Q_(*expected).units q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( op.itruediv(q1_cp, q2), Q_(*expected), atol=0.01 ) @@ -1372,8 +1411,8 @@ def test_inplace_truedivision(self, input_tuple, expected): (((100, "delta_degF"), (10, "degF")), (26092.78, "delta_degF*kelvin")), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit + @pytest.mark.parametrize( + ("input_tuple", "expected"), multiplications_with_autoconvert_to_baseunit ) def test_multiplication_with_autoconvert(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = True @@ -1381,15 +1420,16 @@ def test_multiplication_with_autoconvert(self, input_tuple, expected): q1, q2 = self.Q_(*qin1), self.Q_(*qin2) input_tuple = q1, q2 if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, q1, q2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(q1, q2) else: expected = self.Q_(*expected) - self.assertEqual(op.mul(q1, q2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(q1, q2), expected, atol=0.01) + assert op.mul(q1, q2).units == expected.units + helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01) - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit + @helpers.requires_numpy + @pytest.mark.parametrize( + ("input_tuple", "expected"), multiplications_with_autoconvert_to_baseunit ) def test_inplace_multiplication_with_autoconvert(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = True @@ -1404,12 +1444,15 @@ def test_inplace_multiplication_with_autoconvert(self, input_tuple, expected): q1, q2 = Q_(*qin1), Q_(*qin2) q1_cp = copy.copy(q1) if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.imul, q1_cp, q2) + with pytest.raises(OffsetUnitCalculusError): + op.imul(q1_cp, q2) else: expected = np.array([expected[0]] * 2, dtype=np.float), expected[1] - self.assertEqual(op.imul(q1_cp, q2).units, Q_(*expected).units) + assert op.imul(q1_cp, q2).units == Q_(*expected).units q1_cp = copy.copy(q1) - self.assertQuantityAlmostEqual(op.imul(q1_cp, q2), Q_(*expected), atol=0.01) + helpers.assert_quantity_almost_equal( + op.imul(q1_cp, q2), Q_(*expected), atol=0.01 + ) multiplications_with_scalar = [ (((10, "kelvin"), 2), (20.0, "kelvin")), @@ -1421,9 +1464,7 @@ def test_inplace_multiplication_with_autoconvert(self, input_tuple, expected): (((10, "degC**-2"), 2), "error"), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), multiplications_with_scalar - ) + @pytest.mark.parametrize(("input_tuple", "expected"), multiplications_with_scalar) def test_multiplication_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1433,11 +1474,12 @@ def test_multiplication_with_scalar(self, input_tuple, expected): in1, in2 = in1, self.Q_(*in2) input_tuple = in1, in2 # update input_tuple for better tracebacks if expected == "error": - self.assertRaises(OffsetUnitCalculusError, op.mul, in1, in2) + with pytest.raises(OffsetUnitCalculusError): + op.mul(in1, in2) else: expected = self.Q_(*expected) - self.assertEqual(op.mul(in1, in2).units, expected.units) - self.assertQuantityAlmostEqual(op.mul(in1, in2), expected, atol=0.01) + assert op.mul(in1, in2).units == expected.units + helpers.assert_quantity_almost_equal(op.mul(in1, in2), expected, atol=0.01) divisions_with_scalar = [ # without / with autoconvert to base unit (((10, "kelvin"), 2), [(5.0, "kelvin"), (5.0, "kelvin")]), @@ -1451,9 +1493,7 @@ def test_multiplication_with_scalar(self, input_tuple, expected): ((2, (10, "degC**-2")), ["error", "error"]), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), divisions_with_scalar - ) + @pytest.mark.parametrize(("input_tuple", "expected"), divisions_with_scalar) def test_division_with_scalar(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1466,11 +1506,12 @@ def test_division_with_scalar(self, input_tuple, expected): for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode if expected_copy[i] == "error": - self.assertRaises(OffsetUnitCalculusError, op.truediv, in1, in2) + with pytest.raises(OffsetUnitCalculusError): + op.truediv(in1, in2) else: expected = self.Q_(*expected_copy[i]) - self.assertEqual(op.truediv(in1, in2).units, expected.units) - self.assertQuantityAlmostEqual(op.truediv(in1, in2), expected) + assert op.truediv(in1, in2).units == expected.units + helpers.assert_quantity_almost_equal(op.truediv(in1, in2), expected) exponentiation = [ # resuls without / with autoconvert (((10, "degC"), 1), [(10, "degC"), (10, "degC")]), @@ -1491,7 +1532,7 @@ def test_division_with_scalar(self, input_tuple, expected): ), ] - @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) + @pytest.mark.parametrize(("input_tuple", "expected"), exponentiation) def test_exponentiation(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1506,19 +1547,18 @@ def test_exponentiation(self, input_tuple, expected): for i, mode in enumerate([False, True]): self.ureg.autoconvert_offset_to_baseunit = mode if expected_copy[i] == "error": - self.assertRaises( - (OffsetUnitCalculusError, DimensionalityError), op.pow, in1, in2 - ) + with pytest.raises((OffsetUnitCalculusError, DimensionalityError)): + op.pow(in1, in2) else: if type(expected_copy[i]) is tuple: expected = self.Q_(*expected_copy[i]) - self.assertEqual(op.pow(in1, in2).units, expected.units) + assert op.pow(in1, in2).units == expected.units else: expected = expected_copy[i] - self.assertQuantityAlmostEqual(op.pow(in1, in2), expected) + helpers.assert_quantity_almost_equal(op.pow(in1, in2), expected) - @helpers.requires_numpy() - @ParameterizedTestCase.parameterize(("input", "expected_output"), exponentiation) + @helpers.requires_numpy + @pytest.mark.parametrize(("input_tuple", "expected"), exponentiation) def test_inplace_exponentiation(self, input_tuple, expected): self.ureg.default_as_delta = False in1, in2 = input_tuple @@ -1538,21 +1578,20 @@ def test_inplace_exponentiation(self, input_tuple, expected): self.ureg.autoconvert_offset_to_baseunit = mode in1_cp = copy.copy(in1) if expected_copy[i] == "error": - self.assertRaises( - (OffsetUnitCalculusError, DimensionalityError), op.ipow, in1_cp, in2 - ) + with pytest.raises((OffsetUnitCalculusError, DimensionalityError)): + op.ipow(in1_cp, in2) else: if type(expected_copy[i]) is tuple: expected = self.Q_( np.array([expected_copy[i][0]] * 2, dtype=np.float), expected_copy[i][1], ) - self.assertEqual(op.ipow(in1_cp, in2).units, expected.units) + assert op.ipow(in1_cp, in2).units == expected.units else: expected = np.array([expected_copy[i]] * 2, dtype=np.float) in1_cp = copy.copy(in1) - self.assertQuantityAlmostEqual(op.ipow(in1_cp, in2), expected) + helpers.assert_quantity_almost_equal(op.ipow(in1_cp, in2), expected) # matmul is only a ufunc since 1.16 @helpers.requires_numpy_at_least("1.16") @@ -1560,12 +1599,12 @@ def test_matmul_with_numpy(self): A = [[1, 2], [3, 4]] * self.ureg.m B = np.array([[0, -1], [-1, 0]]) b = [[1], [0]] * self.ureg.m - self.assertQuantityEqual(A @ B, [[-2, -1], [-4, -3]] * self.ureg.m) - self.assertQuantityEqual(A @ b, [[1], [3]] * self.ureg.m ** 2) - self.assertQuantityEqual(B @ b, [[0], [-1]] * self.ureg.m) + helpers.assert_quantity_equal(A @ B, [[-2, -1], [-4, -3]] * self.ureg.m) + helpers.assert_quantity_equal(A @ b, [[1], [3]] * self.ureg.m ** 2) + helpers.assert_quantity_equal(B @ b, [[0], [-1]] * self.ureg.m) -class TestDimensionReduction(QuantityTestCase): +class TestDimensionReduction: def _calc_mass(self, ureg): density = 3 * ureg.g / ureg.L volume = 32 * ureg.milliliter @@ -1579,59 +1618,59 @@ def _icalc_mass(self, ureg): def test_mul_and_div_reduction(self): ureg = UnitRegistry(auto_reduce_dimensions=True) mass = self._calc_mass(ureg) - self.assertEqual(mass.units, ureg.g) + assert mass.units == ureg.g ureg = UnitRegistry(auto_reduce_dimensions=False) mass = self._calc_mass(ureg) - self.assertEqual(mass.units, ureg.g / ureg.L * ureg.milliliter) + assert mass.units == ureg.g / ureg.L * ureg.milliliter - @helpers.requires_numpy() + @helpers.requires_numpy def test_imul_and_div_reduction(self): ureg = UnitRegistry(auto_reduce_dimensions=True, force_ndarray=True) mass = self._icalc_mass(ureg) - self.assertEqual(mass.units, ureg.g) + assert mass.units == ureg.g ureg = UnitRegistry(auto_reduce_dimensions=False, force_ndarray=True) mass = self._icalc_mass(ureg) - self.assertEqual(mass.units, ureg.g / ureg.L * ureg.milliliter) + assert mass.units == ureg.g / ureg.L * ureg.milliliter def test_reduction_to_dimensionless(self): ureg = UnitRegistry(auto_reduce_dimensions=True) x = (10 * ureg.feet) / (3 * ureg.inches) - self.assertEqual(x.units, UnitsContainer({})) + assert x.units == UnitsContainer({}) ureg = UnitRegistry(auto_reduce_dimensions=False) x = (10 * ureg.feet) / (3 * ureg.inches) - self.assertEqual(x.units, ureg.feet / ureg.inches) + assert x.units == ureg.feet / ureg.inches def test_nocoerce_creation(self): ureg = UnitRegistry(auto_reduce_dimensions=True) x = 1 * ureg.foot - self.assertEqual(x.units, ureg.foot) + assert x.units == ureg.foot class TestTimedelta(QuantityTestCase): def test_add_sub(self): d = datetime.datetime(year=1968, month=1, day=10, hour=3, minute=42, second=24) after = d + 3 * self.ureg.second - self.assertEqual(d + datetime.timedelta(seconds=3), after) + assert d + datetime.timedelta(seconds=3) == after after = 3 * self.ureg.second + d - self.assertEqual(d + datetime.timedelta(seconds=3), after) + assert d + datetime.timedelta(seconds=3) == after after = d - 3 * self.ureg.second - self.assertEqual(d - datetime.timedelta(seconds=3), after) - with self.assertRaises(DimensionalityError): + assert d - datetime.timedelta(seconds=3) == after + with pytest.raises(DimensionalityError): 3 * self.ureg.second - d def test_iadd_isub(self): d = datetime.datetime(year=1968, month=1, day=10, hour=3, minute=42, second=24) after = copy.copy(d) after += 3 * self.ureg.second - self.assertEqual(d + datetime.timedelta(seconds=3), after) + assert d + datetime.timedelta(seconds=3) == after after = 3 * self.ureg.second after += d - self.assertEqual(d + datetime.timedelta(seconds=3), after) + assert d + datetime.timedelta(seconds=3) == after after = copy.copy(d) after -= 3 * self.ureg.second - self.assertEqual(d - datetime.timedelta(seconds=3), after) + assert d - datetime.timedelta(seconds=3) == after after = 3 * self.ureg.second - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): after -= d @@ -1642,19 +1681,19 @@ class TestCompareNeutral(QuantityTestCase): def test_equal_zero(self): self.ureg.autoconvert_offset_to_baseunit = False - self.assertTrue(self.Q_(0, "J") == 0) - self.assertFalse(self.Q_(0, "J") == self.Q_(0, "")) - self.assertFalse(self.Q_(5, "J") == 0) + assert self.Q_(0, "J") == 0 + assert not (self.Q_(0, "J") == self.Q_(0, "")) + assert not (self.Q_(5, "J") == 0) def test_equal_nan(self): # nan == nan returns False self.ureg.autoconvert_offset_to_baseunit = False - self.assertFalse(self.Q_(math.nan, "J") == 0) - self.assertFalse(self.Q_(math.nan, "J") == math.nan) - self.assertFalse(self.Q_(math.nan, "J") == self.Q_(math.nan, "")) - self.assertFalse(self.Q_(5, "J") == math.nan) + assert not (self.Q_(math.nan, "J") == 0) + assert not (self.Q_(math.nan, "J") == math.nan) + assert not (self.Q_(math.nan, "J") == self.Q_(math.nan, "")) + assert not (self.Q_(5, "J") == math.nan) - @helpers.requires_numpy() + @helpers.requires_numpy def test_equal_zero_nan_NP(self): self.ureg.autoconvert_offset_to_baseunit = False aeq = np.testing.assert_array_equal @@ -1664,7 +1703,7 @@ def test_equal_zero_nan_NP(self): self.Q_([0, 1, 2], "J") == np.array([0, 0, np.nan]), np.asarray([True, False, False]), ) - self.assertFalse(self.Q_(np.arange(4), "J") == np.zeros(3)) + assert not (self.Q_(np.arange(4), "J") == np.zeros(3)) def test_offset_equal_zero(self): ureg = self.ureg @@ -1672,10 +1711,13 @@ def test_offset_equal_zero(self): q0 = ureg.Quantity(-273.15, "degC") q1 = ureg.Quantity(0, "degC") q2 = ureg.Quantity(5, "degC") - self.assertRaises(OffsetUnitCalculusError, q0.__eq__, 0) - self.assertRaises(OffsetUnitCalculusError, q1.__eq__, 0) - self.assertRaises(OffsetUnitCalculusError, q2.__eq__, 0) - self.assertFalse(q0 == ureg.Quantity(0, "")) + with pytest.raises(OffsetUnitCalculusError): + q0.__eq__(0) + with pytest.raises(OffsetUnitCalculusError): + q1.__eq__(0) + with pytest.raises(OffsetUnitCalculusError): + q2.__eq__(0) + assert not (q0 == ureg.Quantity(0, "")) def test_offset_autoconvert_equal_zero(self): ureg = self.ureg @@ -1683,10 +1725,10 @@ def test_offset_autoconvert_equal_zero(self): q0 = ureg.Quantity(-273.15, "degC") q1 = ureg.Quantity(0, "degC") q2 = ureg.Quantity(5, "degC") - self.assertTrue(q0 == 0) - self.assertFalse(q1 == 0) - self.assertFalse(q2 == 0) - self.assertFalse(q0 == ureg.Quantity(0, "")) + assert q0 == 0 + assert not (q1 == 0) + assert not (q2 == 0) + assert not (q0 == ureg.Quantity(0, "")) def test_gt_zero(self): self.ureg.autoconvert_offset_to_baseunit = False @@ -1695,12 +1737,12 @@ def test_gt_zero(self): q0less = self.Q_(0, "") qpos = self.Q_(5, "J") qneg = self.Q_(-5, "J") - self.assertTrue(qpos > q0) - self.assertTrue(qpos > 0) - self.assertFalse(qneg > 0) - with self.assertRaises(DimensionalityError): + assert qpos > q0 + assert qpos > 0 + assert not (qneg > 0) + with pytest.raises(DimensionalityError): qpos > q0less - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): qpos > q0m def test_gt_nan(self): @@ -1709,14 +1751,14 @@ def test_gt_nan(self): qnm = self.Q_(math.nan, "m") qnless = self.Q_(math.nan, "") qpos = self.Q_(5, "J") - self.assertFalse(qpos > qn) - self.assertFalse(qpos > math.nan) - with self.assertRaises(DimensionalityError): + assert not (qpos > qn) + assert not (qpos > math.nan) + with pytest.raises(DimensionalityError): qpos > qnless - with self.assertRaises(DimensionalityError): + with pytest.raises(DimensionalityError): qpos > qnm - @helpers.requires_numpy() + @helpers.requires_numpy def test_gt_zero_nan_NP(self): self.ureg.autoconvert_offset_to_baseunit = False qpos = self.Q_(5, "J") @@ -1728,7 +1770,7 @@ def test_gt_zero_nan_NP(self): self.Q_(np.arange(-2, 3), "J") > np.array([np.nan, 0, 0, 0, np.nan]), np.asarray([False, False, False, True, False]), ) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.Q_(np.arange(-1, 2), "J") > np.zeros(4) def test_offset_gt_zero(self): @@ -1737,10 +1779,14 @@ def test_offset_gt_zero(self): q0 = ureg.Quantity(-273.15, "degC") q1 = ureg.Quantity(0, "degC") q2 = ureg.Quantity(5, "degC") - self.assertRaises(OffsetUnitCalculusError, q0.__gt__, 0) - self.assertRaises(OffsetUnitCalculusError, q1.__gt__, 0) - self.assertRaises(OffsetUnitCalculusError, q2.__gt__, 0) - self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) + with pytest.raises(OffsetUnitCalculusError): + q0.__gt__(0) + with pytest.raises(OffsetUnitCalculusError): + q1.__gt__(0) + with pytest.raises(OffsetUnitCalculusError): + q2.__gt__(0) + with pytest.raises(DimensionalityError): + q1.__gt__(ureg.Quantity(0, "")) def test_offset_autoconvert_gt_zero(self): ureg = self.ureg @@ -1748,7 +1794,8 @@ def test_offset_autoconvert_gt_zero(self): q0 = ureg.Quantity(-273.15, "degC") q1 = ureg.Quantity(0, "degC") q2 = ureg.Quantity(5, "degC") - self.assertFalse(q0 > 0) - self.assertTrue(q1 > 0) - self.assertTrue(q2 > 0) - self.assertRaises(DimensionalityError, q1.__gt__, ureg.Quantity(0, "")) + assert not (q0 > 0) + assert q1 > 0 + assert q2 > 0 + with pytest.raises(DimensionalityError): + q1.__gt__(ureg.Quantity(0, "")) diff --git a/pint/testsuite/test_systems.py b/pint/testsuite/test_systems.py index ffe084dff..92d77a997 100644 --- a/pint/testsuite/test_systems.py +++ b/pint/testsuite/test_systems.py @@ -1,8 +1,10 @@ +import pytest + from pint import UnitRegistry from pint.testsuite import QuantityTestCase -class TestGroup(QuantityTestCase): +class TestGroup: def _build_empty_reg_root(self): ureg = UnitRegistry(None) grp = ureg.get_group("root") @@ -13,13 +15,13 @@ def test_units_programatically(self): ureg, root = self._build_empty_reg_root() d = ureg._groups - self.assertEqual(root._used_groups, set()) - self.assertEqual(root._used_by, set()) + assert root._used_groups == set() + assert root._used_by == set() root.add_units("meter", "second", "meter") - self.assertEqual(root._unit_names, {"meter", "second"}) - self.assertEqual(root.members, {"meter", "second"}) + assert root._unit_names == {"meter", "second"} + assert root.members == {"meter", "second"} - self.assertEqual(d.keys(), {"root"}) + assert d.keys() == {"root"} def test_cyclic(self): ureg, root = self._build_empty_reg_root() @@ -27,22 +29,25 @@ def test_cyclic(self): g3 = ureg.Group("g3") g2.add_groups("g3") - self.assertRaises(ValueError, g2.add_groups, "root") - self.assertRaises(ValueError, g3.add_groups, "g2") - self.assertRaises(ValueError, g3.add_groups, "root") + with pytest.raises(ValueError): + g2.add_groups("root") + with pytest.raises(ValueError): + g3.add_groups("g2") + with pytest.raises(ValueError): + g3.add_groups("root") def test_groups_programatically(self): ureg, root = self._build_empty_reg_root() d = ureg._groups g2 = ureg.Group("g2") - self.assertEqual(d.keys(), {"root", "g2"}) + assert d.keys() == {"root", "g2"} - self.assertEqual(root._used_groups, {"g2"}) - self.assertEqual(root._used_by, set()) + assert root._used_groups == {"g2"} + assert root._used_by == set() - self.assertEqual(g2._used_groups, set()) - self.assertEqual(g2._used_by, {"root"}) + assert g2._used_groups == set() + assert g2._used_by == {"root"} def test_simple(self): lines = ["@group mygroup", "meter", "second"] @@ -52,13 +57,13 @@ def test_simple(self): grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(d.keys(), {"root", "mygroup"}) + assert d.keys() == {"root", "mygroup"} - self.assertEqual(grp.name, "mygroup") - self.assertEqual(grp._unit_names, {"meter", "second"}) - self.assertEqual(grp._used_groups, set()) - self.assertEqual(grp._used_by, {root.name}) - self.assertEqual(grp.members, frozenset(["meter", "second"])) + assert grp.name == "mygroup" + assert grp._unit_names == {"meter", "second"} + assert grp._used_groups == set() + assert grp._used_by == {root.name} + assert grp.members == frozenset(["meter", "second"]) def test_using1(self): lines = ["@group mygroup using group1", "meter", "second"] @@ -66,10 +71,10 @@ def test_using1(self): ureg, root = self._build_empty_reg_root() ureg.Group("group1") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, "mygroup") - self.assertEqual(grp._unit_names, {"meter", "second"}) - self.assertEqual(grp._used_groups, {"group1"}) - self.assertEqual(grp.members, frozenset(["meter", "second"])) + assert grp.name == "mygroup" + assert grp._unit_names == {"meter", "second"} + assert grp._used_groups == {"group1"} + assert grp.members == frozenset(["meter", "second"]) def test_using2(self): lines = ["@group mygroup using group1,group2", "meter", "second"] @@ -78,10 +83,10 @@ def test_using2(self): ureg.Group("group1") ureg.Group("group2") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, "mygroup") - self.assertEqual(grp._unit_names, {"meter", "second"}) - self.assertEqual(grp._used_groups, {"group1", "group2"}) - self.assertEqual(grp.members, frozenset(["meter", "second"])) + assert grp.name == "mygroup" + assert grp._unit_names == {"meter", "second"} + assert grp._used_groups == {"group1", "group2"} + assert grp.members == frozenset(["meter", "second"]) def test_spaces(self): lines = ["@group mygroup using group1 , group2", " meter ", " second "] @@ -90,10 +95,10 @@ def test_spaces(self): ureg.Group("group1") ureg.Group("group2") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertEqual(grp.name, "mygroup") - self.assertEqual(grp._unit_names, {"meter", "second"}) - self.assertEqual(grp._used_groups, {"group1", "group2"}) - self.assertEqual(grp.members, frozenset(["meter", "second"])) + assert grp.name == "mygroup" + assert grp._unit_names == {"meter", "second"} + assert grp._used_groups == {"group1", "group2"} + assert grp.members == frozenset(["meter", "second"]) def test_invalidate_members(self): lines = ["@group mygroup using group1", "meter", "second"] @@ -101,17 +106,17 @@ def test_invalidate_members(self): ureg, root = self._build_empty_reg_root() ureg.Group("group1") grp = ureg.Group.from_lines(lines, lambda x: None) - self.assertIs(root._computed_members, None) - self.assertIs(grp._computed_members, None) - self.assertEqual(grp.members, frozenset(["meter", "second"])) - self.assertIs(root._computed_members, None) - self.assertIsNot(grp._computed_members, None) - self.assertEqual(root.members, frozenset(["meter", "second"])) - self.assertIsNot(root._computed_members, None) - self.assertIsNot(grp._computed_members, None) + assert root._computed_members is None + assert grp._computed_members is None + assert grp.members == frozenset(["meter", "second"]) + assert root._computed_members is None + assert grp._computed_members is not None + assert root.members == frozenset(["meter", "second"]) + assert root._computed_members is not None + assert grp._computed_members is not None grp.invalidate_members() - self.assertIs(root._computed_members, None) - self.assertIs(grp._computed_members, None) + assert root._computed_members is None + assert grp._computed_members is None def test_with_defintions(self): lines = [ @@ -129,7 +134,7 @@ def define(ud): ureg, root = self._build_empty_reg_root() ureg.Group.from_lines(lines, define) - self.assertEqual(["kings_leg", "kings_head"], defs) + assert ["kings_leg", "kings_head"] == defs def test_members_including(self): ureg, root = self._build_empty_reg_root() @@ -144,10 +149,10 @@ def test_members_including(self): g3.add_units("meter", "second") g3.add_groups("group1", "group2") - self.assertEqual(root.members, frozenset(["meter", "second", "newton", "inch"])) - self.assertEqual(g1.members, frozenset(["second", "inch"])) - self.assertEqual(g2.members, frozenset(["second", "newton"])) - self.assertEqual(g3.members, frozenset(["meter", "second", "newton", "inch"])) + assert root.members == frozenset(["meter", "second", "newton", "inch"]) + assert g1.members == frozenset(["second", "inch"]) + assert g2.members == frozenset(["second", "newton"]) + assert g3.members == frozenset(["meter", "second", "newton", "inch"]) def test_get_compatible_units(self): ureg = UnitRegistry() @@ -155,7 +160,7 @@ def test_get_compatible_units(self): g = ureg.get_group("test-imperial") g.add_units("inch", "yard", "pint") c = ureg.get_compatible_units("meter", "test-imperial") - self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) + assert c == frozenset([ureg.inch, ureg.yard]) class TestSystem(QuantityTestCase): @@ -185,7 +190,7 @@ def test_members_group(self): ureg, root = self._build_empty_reg_root() root.add_units("second") s = ureg.System.from_lines(lines, lambda x: x) - self.assertEqual(s.members, frozenset(["second"])) + assert s.members == frozenset(["second"]) def test_get_compatible_units(self): sysname = "mysys1" @@ -195,13 +200,13 @@ def test_get_compatible_units(self): g.add_units("inch", "yard", "pint") c = ureg.get_compatible_units("meter", "test-imperial") - self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) + assert c == frozenset([ureg.inch, ureg.yard]) lines = ["@system %s using test-imperial" % sysname, "inch"] ureg.System.from_lines(lines, lambda x: x) c = ureg.get_compatible_units("meter", sysname) - self.assertEqual(c, frozenset([ureg.inch, ureg.yard])) + assert c == frozenset([ureg.inch, ureg.yard]) def test_get_base_units(self): sysname = "mysys2" @@ -217,12 +222,12 @@ def test_get_base_units(self): # base_factor, destination_units c = ureg.get_base_units("inch", system=sysname) - self.assertAlmostEqual(c[0], 1) - self.assertEqual(c[1], {"inch": 1}) + assert round(abs(c[0] - 1), 7) == 0 + assert c[1] == {"inch": 1} c = ureg.get_base_units("cm", system=sysname) - self.assertAlmostEqual(c[0], 1.0 / 2.54) - self.assertEqual(c[1], {"inch": 1}) + assert round(abs(c[0] - 1.0 / 2.54), 7) == 0 + assert c[1] == {"inch": 1} def test_get_base_units_different_exponent(self): sysname = "mysys3" @@ -239,20 +244,20 @@ def test_get_base_units_different_exponent(self): # base_factor, destination_units c = ureg.get_base_units("inch", system=sysname) - self.assertAlmostEqual(c[0], 0.326, places=3) - self.assertEqual(c[1], {"pint": 1.0 / 3}) + assert round(abs(c[0] - 0.326), 3) == 0 + assert c[1] == {"pint": 1.0 / 3} c = ureg.get_base_units("cm", system=sysname) - self.assertAlmostEqual(c[0], 0.1283, places=3) - self.assertEqual(c[1], {"pint": 1.0 / 3}) + assert round(abs(c[0] - 0.1283), 3) == 0 + assert c[1] == {"pint": 1.0 / 3} c = ureg.get_base_units("inch**2", system=sysname) - self.assertAlmostEqual(c[0], 0.326 ** 2, places=3) - self.assertEqual(c[1], {"pint": 2.0 / 3}) + assert round(abs(c[0] - 0.326 ** 2), 3) == 0 + assert c[1] == {"pint": 2.0 / 3} c = ureg.get_base_units("cm**2", system=sysname) - self.assertAlmostEqual(c[0], 0.1283 ** 2, places=3) - self.assertEqual(c[1], {"pint": 2.0 / 3}) + assert round(abs(c[0] - 0.1283 ** 2), 3) == 0 + assert c[1] == {"pint": 2.0 / 3} def test_get_base_units_relation(self): sysname = "mysys4" @@ -267,12 +272,12 @@ def test_get_base_units_relation(self): ureg._systems[s.name] = s # base_factor, destination_units c = ureg.get_base_units("inch", system=sysname) - self.assertAlmostEqual(c[0], 0.056, places=2) - self.assertEqual(c[1], {"mph": 1, "second": 1}) + assert round(abs(c[0] - 0.056), 2) == 0 + assert c[1] == {"mph": 1, "second": 1} c = ureg.get_base_units("kph", system=sysname) - self.assertAlmostEqual(c[0], 0.6213, places=3) - self.assertEqual(c[1], {"mph": 1}) + assert round(abs(c[0] - 0.6213), 3) == 0 + assert c[1] == {"mph": 1} def test_members_nowarning(self): ureg = self.ureg diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py index e11f7d8fb..bc2db9b5f 100644 --- a/pint/testsuite/test_umath.py +++ b/pint/testsuite/test_umath.py @@ -1,6 +1,8 @@ -from pint import DimensionalityError +import pytest + +from pint import DimensionalityError, UnitRegistry from pint.compat import np -from pint.testsuite import QuantityTestCase, helpers +from pint.testsuite import helpers # Following http://docs.scipy.org/doc/numpy/reference/ufuncs.html @@ -8,10 +10,17 @@ pi = np.pi -@helpers.requires_numpy() -class TestUFuncs(QuantityTestCase): +@helpers.requires_numpy +class TestUFuncs: + @classmethod + def setup_class(cls): + cls.ureg = UnitRegistry(force_ndarray=True) + cls.Q_ = cls.ureg.Quantity - FORCE_NDARRAY = True + @classmethod + def teardown_class(cls): + cls.ureg = None + cls.Q_ = None @property def qless(self): @@ -84,12 +93,10 @@ def _test1( if ou is not None: res = self.Q_(res, ou) - self.assertQuantityAlmostEqual(qm, res, rtol=rtol, msg=err_msg) + helpers.assert_quantity_almost_equal(qm, res, rtol=rtol, msg=err_msg) for x1 in raise_with: - with self.assertRaises( - DimensionalityError, msg=f"At {func.__name__} with {x1}" - ): + with pytest.raises(DimensionalityError): func(x1) def _testn(self, func, ok_with, raise_with=(), results=None): @@ -170,10 +177,10 @@ def _test1_2o( if ou is not None: re = self.Q_(re, ou) - self.assertQuantityAlmostEqual(qm, re, rtol=rtol, msg=err_msg) + helpers.assert_quantity_almost_equal(qm, re, rtol=rtol, msg=err_msg) for x1 in raise_with: - with self.assertRaises(ValueError, msg=f"At {func.__name__} with {x1}"): + with pytest.raises(ValueError): func(x1) def _test2( @@ -239,12 +246,10 @@ def _test2( if ou is not None: res = self.Q_(res, ou) - self.assertQuantityAlmostEqual(qm, res, rtol=rtol, msg=err_msg) + helpers.assert_quantity_almost_equal(qm, res, rtol=rtol, msg=err_msg) for x2 in raise_with: - with self.assertRaises( - DimensionalityError, msg=f"At {func.__name__} with {x1} and {x2}" - ): + with pytest.raises(DimensionalityError): func(x1, x2) def _testn2(self, func, x1, ok_with, raise_with=()): @@ -268,7 +273,7 @@ def _testn2(self, func, x1, ok_with, raise_with=()): self._test2(func, x1, ok_with, raise_with, output_units=None) -@helpers.requires_numpy() +@helpers.requires_numpy class TestMathUfuncs(TestUFuncs): """Universal functions (ufunc) > Math operations @@ -416,7 +421,7 @@ def test_reciprocal(self): self._test1(np.reciprocal, (self.q2, self.qs, self.qless, self.qi), (), -1) -@helpers.requires_numpy() +@helpers.requires_numpy class TestTrigUfuncs(TestUFuncs): """Universal functions (ufunc) > Trigonometric functions @@ -552,13 +557,9 @@ def test_arctan2(self): ) def test_hypot(self): - self.assertTrue( - np.hypot(3.0 * self.ureg.m, 4.0 * self.ureg.m) == 5.0 * self.ureg.m - ) - self.assertTrue( - np.hypot(3.0 * self.ureg.m, 400.0 * self.ureg.cm) == 5.0 * self.ureg.m - ) - with self.assertRaises(DimensionalityError): + assert np.hypot(3.0 * self.ureg.m, 4.0 * self.ureg.m) == 5.0 * self.ureg.m + assert np.hypot(3.0 * self.ureg.m, 400.0 * self.ureg.cm) == 5.0 * self.ureg.m + with pytest.raises(DimensionalityError): np.hypot(1.0 * self.ureg.m, 2.0 * self.ureg.J) def test_sinh(self): diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index dcb3e97a1..c729333c1 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -1,8 +1,11 @@ import copy import functools +import logging import math import re +import pytest + from pint import ( DefinitionSyntaxError, DimensionalityError, @@ -11,27 +14,27 @@ ) from pint.compat import np from pint.registry import LazyRegistry, UnitRegistry -from pint.testsuite import CaseInsensitveQuantityTestCase, QuantityTestCase, helpers -from pint.testsuite.parameterized import ParameterizedTestCase +from pint.testsuite import QuantityTestCase, helpers from pint.util import ParserHelper, UnitsContainer class TestUnit(QuantityTestCase): def test_creation(self): for arg in ("meter", UnitsContainer(meter=1), self.U_("m")): - self.assertEqual(self.U_(arg)._units, UnitsContainer(meter=1)) - self.assertRaises(TypeError, self.U_, 1) + assert self.U_(arg)._units == UnitsContainer(meter=1) + with pytest.raises(TypeError): + self.U_(1) def test_deepcopy(self): x = self.U_(UnitsContainer(meter=1)) - self.assertEqual(x, copy.deepcopy(x)) + assert x == copy.deepcopy(x) def test_unit_repr(self): x = self.U_(UnitsContainer(meter=1)) - self.assertEqual(str(x), "meter") - self.assertEqual(repr(x), "") + assert str(x) == "meter" + assert repr(x) == "" - def test_unit_formatting(self): + def test_unit_formatting(self, subtests): x = self.U_(UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( ("{}", str(x)), @@ -51,10 +54,10 @@ def test_unit_formatting(self): ("{:H~}", "kg m2/s"), ("{:C~}", "kg*m**2/s"), ): - with self.subTest(spec): - self.assertEqual(spec.format(x), result) + with subtests.test(spec): + assert spec.format(x) == result - def test_unit_default_formatting(self): + def test_unit_default_formatting(self, subtests): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) for spec, result in ( @@ -71,11 +74,11 @@ def test_unit_default_formatting(self): ("H~", "kg m2/s"), ("C~", "kg*m**2/s"), ): - with self.subTest(spec): + with subtests.test(spec): ureg.default_format = spec - self.assertEqual(f"{x}", result, f"Failed for {spec}, {result}") + assert f"{x}" == result, f"Failed for {spec}, {result}" - def test_unit_formatting_snake_case(self): + def test_unit_formatting_snake_case(self, subtests): # Test that snake_case units are escaped where appropriate ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(oil_barrel=1)) @@ -90,9 +93,9 @@ def test_unit_formatting_snake_case(self): ("H~", "oil_bbl"), ("C~", "oil_bbl"), ): - with self.subTest(spec): + with subtests.test(spec): ureg.default_format = spec - self.assertEqual(f"{x}", result, f"Failed for {spec}, {result}") + assert f"{x}" == result, f"Failed for {spec}, {result}" def test_ipython(self): alltext = [] @@ -104,118 +107,119 @@ def text(text): ureg = UnitRegistry() x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1)) - self.assertEqual(x._repr_html_(), "kilogram meter2/second") - self.assertEqual( - x._repr_latex_(), - r"$\frac{\mathrm{kilogram} \cdot " r"\mathrm{meter}^{2}}{\mathrm{second}}$", + assert x._repr_html_() == "kilogram meter2/second" + assert ( + x._repr_latex_() == r"$\frac{\mathrm{kilogram} \cdot " + r"\mathrm{meter}^{2}}{\mathrm{second}}$" ) x._repr_pretty_(Pretty, False) - self.assertEqual("".join(alltext), "kilogram·meter²/second") + assert "".join(alltext) == "kilogram·meter²/second" ureg.default_format = "~" - self.assertEqual(x._repr_html_(), "kg m2/s") - self.assertEqual( - x._repr_latex_(), r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" + assert x._repr_html_() == "kg m2/s" + assert ( + x._repr_latex_() == r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" ) alltext = [] x._repr_pretty_(Pretty, False) - self.assertEqual("".join(alltext), "kg·m²/s") + assert "".join(alltext) == "kg·m²/s" def test_unit_mul(self): x = self.U_("m") - self.assertEqual(x * 1, self.Q_(1, "m")) - self.assertEqual(x * 0.5, self.Q_(0.5, "m")) - self.assertEqual(x * self.Q_(1, "m"), self.Q_(1, "m**2")) - self.assertEqual(1 * x, self.Q_(1, "m")) + assert x * 1 == self.Q_(1, "m") + assert x * 0.5 == self.Q_(0.5, "m") + assert x * self.Q_(1, "m") == self.Q_(1, "m**2") + assert 1 * x == self.Q_(1, "m") def test_unit_div(self): x = self.U_("m") - self.assertEqual(x / 1, self.Q_(1, "m")) - self.assertEqual(x / 0.5, self.Q_(2.0, "m")) - self.assertEqual(x / self.Q_(1, "m"), self.Q_(1)) + assert x / 1 == self.Q_(1, "m") + assert x / 0.5 == self.Q_(2.0, "m") + assert x / self.Q_(1, "m") == self.Q_(1) def test_unit_rdiv(self): x = self.U_("m") - self.assertEqual(1 / x, self.Q_(1, "1/m")) + assert 1 / x == self.Q_(1, "1/m") def test_unit_pow(self): x = self.U_("m") - self.assertEqual(x ** 2, self.U_("m**2")) + assert x ** 2 == self.U_("m**2") def test_unit_hash(self): x = self.U_("m") - self.assertEqual(hash(x), hash(x._units)) + assert hash(x) == hash(x._units) def test_unit_eqs(self): x = self.U_("m") - self.assertEqual(x, self.U_("m")) - self.assertNotEqual(x, self.U_("cm")) + assert x == self.U_("m") + assert x != self.U_("cm") - self.assertEqual(x, self.Q_(1, "m")) - self.assertNotEqual(x, self.Q_(2, "m")) + assert x == self.Q_(1, "m") + assert x != self.Q_(2, "m") - self.assertEqual(x, UnitsContainer({"meter": 1})) + assert x == UnitsContainer({"meter": 1}) y = self.U_("cm/m") - self.assertEqual(y, 0.01) + assert y == 0.01 - self.assertEqual(self.U_("byte") == self.U_("byte"), True) - self.assertEqual(self.U_("byte") != self.U_("byte"), False) + assert self.U_("byte") == self.U_("byte") + assert not (self.U_("byte") != self.U_("byte")) def test_unit_cmp(self): x = self.U_("m") - self.assertLess(x, self.U_("km")) - self.assertGreater(x, self.U_("mm")) + assert x < self.U_("km") + assert x > self.U_("mm") y = self.U_("m/mm") - self.assertGreater(y, 1) - self.assertLess(y, 1e6) + assert y > 1 + assert y < 1e6 def test_dimensionality(self): x = self.U_("m") - self.assertEqual(x.dimensionality, UnitsContainer({"[length]": 1})) + assert x.dimensionality == UnitsContainer({"[length]": 1}) def test_dimensionless(self): - self.assertTrue(self.U_("m/mm").dimensionless) - self.assertFalse(self.U_("m").dimensionless) + assert self.U_("m/mm").dimensionless + assert not self.U_("m").dimensionless def test_unit_casting(self): - self.assertEqual(int(self.U_("m/mm")), 1000) - self.assertEqual(float(self.U_("mm/m")), 1e-3) - self.assertEqual(complex(self.U_("mm/mm")), 1 + 0j) + assert int(self.U_("m/mm")) == 1000 + assert float(self.U_("mm/m")) == 1e-3 + assert complex(self.U_("mm/mm")) == 1 + 0j - @helpers.requires_numpy() + @helpers.requires_numpy def test_array_interface(self): import numpy as np x = self.U_("m") arr = np.ones(10) - self.assertQuantityEqual(arr * x, self.Q_(arr, "m")) - self.assertQuantityEqual(arr / x, self.Q_(arr, "1/m")) - self.assertQuantityEqual(x / arr, self.Q_(arr, "m")) + helpers.assert_quantity_equal(arr * x, self.Q_(arr, "m")) + helpers.assert_quantity_equal(arr / x, self.Q_(arr, "1/m")) + helpers.assert_quantity_equal(x / arr, self.Q_(arr, "m")) class TestRegistry(QuantityTestCase): - - FORCE_NDARRAY = False - - def setup(self): - self.ureg.autoconvert_offset_to_baseunit = False + @classmethod + def setup_class(cls): + super().setup_class() + cls.ureg.autoconvert_offset_to_baseunit = False def test_base(self): ureg = UnitRegistry(None) ureg.define("meter = [length]") - self.assertRaises(DefinitionSyntaxError, ureg.define, "meter = [length]") - self.assertRaises(TypeError, ureg.define, list()) + with pytest.raises(DefinitionSyntaxError): + ureg.define("meter = [length]") + with pytest.raises(TypeError): + ureg.define(list()) ureg.define("degC = kelvin; offset: 273.15") def test_define(self): ureg = UnitRegistry(None) - self.assertIsInstance(dir(ureg), list) - self.assertGreater(len(dir(ureg)), 0) + assert isinstance(dir(ureg), list) + assert len(dir(ureg)) > 0 def test_load(self): import pkg_resources @@ -225,10 +229,9 @@ def test_load(self): data = pkg_resources.resource_filename(unit.__name__, "default_en.txt") ureg1 = UnitRegistry() ureg2 = UnitRegistry(data) - self.assertEqual(dir(ureg1), dir(ureg2)) - self.assertRaises( - ValueError, UnitRegistry(None).load_definitions, "notexisting" - ) + assert dir(ureg1) == dir(ureg2) + with pytest.raises(ValueError): + UnitRegistry(None).load_definitions("notexisting") def test_default_format(self): ureg = UnitRegistry() @@ -237,130 +240,109 @@ def test_default_format(self): s2 = f"{q:~}" ureg.default_format = "~" s3 = f"{q}" - self.assertEqual(s2, s3) - self.assertNotEqual(s1, s3) - self.assertEqual(ureg.default_format, "~") + assert s2 == s3 + assert s1 != s3 + assert ureg.default_format == "~" def test_iterate(self): ureg = UnitRegistry() - self.assertTrue("meter" in list(ureg)) + assert "meter" in list(ureg) def test_parse_number(self): - self.assertEqual(self.ureg.parse_expression("pi"), math.pi) - self.assertEqual(self.ureg.parse_expression("x", x=2), 2) - self.assertEqual(self.ureg.parse_expression("x", x=2.3), 2.3) - self.assertEqual(self.ureg.parse_expression("x * y", x=2.3, y=3), 2.3 * 3) - self.assertEqual(self.ureg.parse_expression("x", x=(1 + 1j)), (1 + 1j)) + assert self.ureg.parse_expression("pi") == math.pi + assert self.ureg.parse_expression("x", x=2) == 2 + assert self.ureg.parse_expression("x", x=2.3) == 2.3 + assert self.ureg.parse_expression("x * y", x=2.3, y=3) == 2.3 * 3 + assert self.ureg.parse_expression("x", x=(1 + 1j)) == (1 + 1j) def test_parse_single(self): - self.assertEqual( - self.ureg.parse_expression("meter"), self.Q_(1, UnitsContainer(meter=1.0)) + assert self.ureg.parse_expression("meter") == self.Q_( + 1, UnitsContainer(meter=1.0) ) - self.assertEqual( - self.ureg.parse_expression("second"), self.Q_(1, UnitsContainer(second=1.0)) + assert self.ureg.parse_expression("second") == self.Q_( + 1, UnitsContainer(second=1.0) ) def test_parse_alias(self): - self.assertEqual( - self.ureg.parse_expression("metre"), self.Q_(1, UnitsContainer(meter=1.0)) + assert self.ureg.parse_expression("metre") == self.Q_( + 1, UnitsContainer(meter=1.0) ) def test_parse_plural(self): - self.assertEqual( - self.ureg.parse_expression("meters"), self.Q_(1, UnitsContainer(meter=1.0)) + assert self.ureg.parse_expression("meters") == self.Q_( + 1, UnitsContainer(meter=1.0) ) def test_parse_prefix(self): - self.assertEqual( - self.ureg.parse_expression("kilometer"), - self.Q_(1, UnitsContainer(kilometer=1.0)), + assert self.ureg.parse_expression("kilometer") == self.Q_( + 1, UnitsContainer(kilometer=1.0) ) def test_parse_complex(self): - self.assertEqual( - self.ureg.parse_expression("kilometre"), - self.Q_(1, UnitsContainer(kilometer=1.0)), + assert self.ureg.parse_expression("kilometre") == self.Q_( + 1, UnitsContainer(kilometer=1.0) ) - self.assertEqual( - self.ureg.parse_expression("kilometres"), - self.Q_(1, UnitsContainer(kilometer=1.0)), + assert self.ureg.parse_expression("kilometres") == self.Q_( + 1, UnitsContainer(kilometer=1.0) ) def test_parse_mul_div(self): - self.assertEqual( - self.ureg.parse_expression("meter*meter"), - self.Q_(1, UnitsContainer(meter=2.0)), + assert self.ureg.parse_expression("meter*meter") == self.Q_( + 1, UnitsContainer(meter=2.0) ) - self.assertEqual( - self.ureg.parse_expression("meter**2"), - self.Q_(1, UnitsContainer(meter=2.0)), + assert self.ureg.parse_expression("meter**2") == self.Q_( + 1, UnitsContainer(meter=2.0) ) - self.assertEqual( - self.ureg.parse_expression("meter*second"), - self.Q_(1, UnitsContainer(meter=1.0, second=1)), + assert self.ureg.parse_expression("meter*second") == self.Q_( + 1, UnitsContainer(meter=1.0, second=1) ) - self.assertEqual( - self.ureg.parse_expression("meter/second"), - self.Q_(1, UnitsContainer(meter=1.0, second=-1)), + assert self.ureg.parse_expression("meter/second") == self.Q_( + 1, UnitsContainer(meter=1.0, second=-1) ) - self.assertEqual( - self.ureg.parse_expression("meter/second**2"), - self.Q_(1, UnitsContainer(meter=1.0, second=-2)), + assert self.ureg.parse_expression("meter/second**2") == self.Q_( + 1, UnitsContainer(meter=1.0, second=-2) ) def test_parse_pretty(self): - self.assertEqual( - self.ureg.parse_expression("meter/second²"), - self.Q_(1, UnitsContainer(meter=1.0, second=-2)), + assert self.ureg.parse_expression("meter/second²") == self.Q_( + 1, UnitsContainer(meter=1.0, second=-2) ) - self.assertEqual( - self.ureg.parse_expression("m³/s³"), - self.Q_(1, UnitsContainer(meter=3.0, second=-3)), + assert self.ureg.parse_expression("m³/s³") == self.Q_( + 1, UnitsContainer(meter=3.0, second=-3) ) - self.assertEqual( - self.ureg.parse_expression("meter² · second"), - self.Q_(1, UnitsContainer(meter=2.0, second=1)), + assert self.ureg.parse_expression("meter² · second") == self.Q_( + 1, UnitsContainer(meter=2.0, second=1) ) - self.assertEqual( - self.ureg.parse_expression("meter⁰.⁵·second"), - self.Q_(1, UnitsContainer(meter=0.5, second=1)), + assert self.ureg.parse_expression("meter⁰.⁵·second") == self.Q_( + 1, UnitsContainer(meter=0.5, second=1) ) - self.assertEqual( - self.ureg.parse_expression("meter³⁷/second⁴.³²¹"), - self.Q_(1, UnitsContainer(meter=37, second=-4.321)), + assert self.ureg.parse_expression("meter³⁷/second⁴.³²¹") == self.Q_( + 1, UnitsContainer(meter=37, second=-4.321) ) def test_parse_factor(self): - self.assertEqual( - self.ureg.parse_expression("42*meter"), - self.Q_(42, UnitsContainer(meter=1.0)), + assert self.ureg.parse_expression("42*meter") == self.Q_( + 42, UnitsContainer(meter=1.0) ) - self.assertEqual( - self.ureg.parse_expression("meter*42"), - self.Q_(42, UnitsContainer(meter=1.0)), + assert self.ureg.parse_expression("meter*42") == self.Q_( + 42, UnitsContainer(meter=1.0) ) def test_rep_and_parse(self): q = self.Q_(1, "g/(m**2*s)") - self.assertEqual(self.Q_(q.magnitude, str(q.units)), q) + assert self.Q_(q.magnitude, str(q.units)) == q def test_as_delta(self): parse = self.ureg.parse_units - self.assertEqual(parse("kelvin", as_delta=True), UnitsContainer(kelvin=1)) - self.assertEqual(parse("kelvin", as_delta=False), UnitsContainer(kelvin=1)) - self.assertEqual( - parse("kelvin**(-1)", as_delta=True), UnitsContainer(kelvin=-1) - ) - self.assertEqual( - parse("kelvin**(-1)", as_delta=False), UnitsContainer(kelvin=-1) - ) - self.assertEqual(parse("kelvin**2", as_delta=True), UnitsContainer(kelvin=2)) - self.assertEqual(parse("kelvin**2", as_delta=False), UnitsContainer(kelvin=2)) - self.assertEqual( - parse("kelvin*meter", as_delta=True), UnitsContainer(kelvin=1, meter=1) - ) - self.assertEqual( - parse("kelvin*meter", as_delta=False), UnitsContainer(kelvin=1, meter=1) + assert parse("kelvin", as_delta=True) == UnitsContainer(kelvin=1) + assert parse("kelvin", as_delta=False) == UnitsContainer(kelvin=1) + assert parse("kelvin**(-1)", as_delta=True) == UnitsContainer(kelvin=-1) + assert parse("kelvin**(-1)", as_delta=False) == UnitsContainer(kelvin=-1) + assert parse("kelvin**2", as_delta=True) == UnitsContainer(kelvin=2) + assert parse("kelvin**2", as_delta=False) == UnitsContainer(kelvin=2) + assert parse("kelvin*meter", as_delta=True) == UnitsContainer(kelvin=1, meter=1) + assert parse("kelvin*meter", as_delta=False) == UnitsContainer( + kelvin=1, meter=1 ) def test_parse_expression_with_preprocessor(self): @@ -373,22 +355,19 @@ def test_parse_expression_with_preprocessor(self): ) ) # Test equality - self.assertEqual( - self.ureg.parse_expression("42 m2"), self.Q_(42, UnitsContainer(meter=2.0)) + assert self.ureg.parse_expression("42 m2") == self.Q_( + 42, UnitsContainer(meter=2.0) ) - self.assertEqual( - self.ureg.parse_expression("1e6 Hz s-2"), - self.Q_(1e6, UnitsContainer(second=-3.0)), + assert self.ureg.parse_expression("1e6 Hz s-2") == self.Q_( + 1e6, UnitsContainer(second=-3.0) ) - self.assertEqual( - self.ureg.parse_expression("3 metre3"), - self.Q_(3, UnitsContainer(meter=3.0)), + assert self.ureg.parse_expression("3 metre3") == self.Q_( + 3, UnitsContainer(meter=3.0) ) # Clean up and test previously expected value self.ureg.preprocessors.pop() - self.assertEqual( - self.ureg.parse_expression("1e6 Hz s-2"), - self.Q_(999998.0, UnitsContainer()), + assert self.ureg.parse_expression("1e6 Hz s-2") == self.Q_( + 999998.0, UnitsContainer() ) def test_parse_unit_with_preprocessor(self): @@ -401,36 +380,38 @@ def test_parse_unit_with_preprocessor(self): ) ) # Test equality - self.assertEqual(self.ureg.parse_units("m2"), UnitsContainer(meter=2.0)) - self.assertEqual(self.ureg.parse_units("m-2"), UnitsContainer(meter=-2.0)) + assert self.ureg.parse_units("m2") == UnitsContainer(meter=2.0) + assert self.ureg.parse_units("m-2") == UnitsContainer(meter=-2.0) # Clean up self.ureg.preprocessors.pop() def test_name(self): - self.assertRaises(UndefinedUnitError, self.ureg.get_name, "asdf") + with pytest.raises(UndefinedUnitError): + self.ureg.get_name("asdf") def test_symbol(self): - self.assertRaises(UndefinedUnitError, self.ureg.get_symbol, "asdf") + with pytest.raises(UndefinedUnitError): + self.ureg.get_symbol("asdf") - self.assertEqual(self.ureg.get_symbol("meter"), "m") - self.assertEqual(self.ureg.get_symbol("second"), "s") - self.assertEqual(self.ureg.get_symbol("hertz"), "Hz") + assert self.ureg.get_symbol("meter") == "m" + assert self.ureg.get_symbol("second") == "s" + assert self.ureg.get_symbol("hertz") == "Hz" - self.assertEqual(self.ureg.get_symbol("kilometer"), "km") - self.assertEqual(self.ureg.get_symbol("megahertz"), "MHz") - self.assertEqual(self.ureg.get_symbol("millisecond"), "ms") + assert self.ureg.get_symbol("kilometer") == "km" + assert self.ureg.get_symbol("megahertz") == "MHz" + assert self.ureg.get_symbol("millisecond") == "ms" def test_imperial_symbol(self): - self.assertEqual(self.ureg.get_symbol("inch"), "in") - self.assertEqual(self.ureg.get_symbol("foot"), "ft") - self.assertEqual(self.ureg.get_symbol("inches"), "in") - self.assertEqual(self.ureg.get_symbol("feet"), "ft") - self.assertEqual(self.ureg.get_symbol("international_foot"), "ft") - self.assertEqual(self.ureg.get_symbol("international_inch"), "in") + assert self.ureg.get_symbol("inch") == "in" + assert self.ureg.get_symbol("foot") == "ft" + assert self.ureg.get_symbol("inches") == "in" + assert self.ureg.get_symbol("feet") == "ft" + assert self.ureg.get_symbol("international_foot") == "ft" + assert self.ureg.get_symbol("international_inch") == "in" def test_pint(self): - self.assertLess(self.ureg.pint, self.ureg.liter) - self.assertLess(self.ureg.pint, self.ureg.imperial_pint) + assert self.ureg.pint < self.ureg.liter + assert self.ureg.pint < self.ureg.imperial_pint def test_wraps(self): def func(x): @@ -438,78 +419,90 @@ def func(x): ureg = self.ureg - self.assertRaises(TypeError, ureg.wraps, (3 * ureg.meter, [None])) - self.assertRaises(TypeError, ureg.wraps, (None, [3 * ureg.meter])) + with pytest.raises(TypeError): + ureg.wraps((3 * ureg.meter, [None])) + with pytest.raises(TypeError): + ureg.wraps((None, [3 * ureg.meter])) f0 = ureg.wraps(None, [None])(func) - self.assertEqual(f0(3.0), 3.0) + assert f0(3.0) == 3.0 f0 = ureg.wraps(None, None)(func) - self.assertEqual(f0(3.0), 3.0) + assert f0(3.0) == 3.0 f1 = ureg.wraps(None, ["meter"])(func) - self.assertRaises(ValueError, f1, 3.0) - self.assertEqual(f1(3.0 * ureg.centimeter), 0.03) - self.assertEqual(f1(3.0 * ureg.meter), 3.0) - self.assertRaises(DimensionalityError, f1, 3 * ureg.second) + with pytest.raises(ValueError): + f1(3.0) + assert f1(3.0 * ureg.centimeter) == 0.03 + assert f1(3.0 * ureg.meter) == 3.0 + with pytest.raises(DimensionalityError): + f1(3 * ureg.second) f1b = ureg.wraps(None, [ureg.meter])(func) - self.assertRaises(ValueError, f1b, 3.0) - self.assertEqual(f1b(3.0 * ureg.centimeter), 0.03) - self.assertEqual(f1b(3.0 * ureg.meter), 3.0) - self.assertRaises(DimensionalityError, f1b, 3 * ureg.second) + with pytest.raises(ValueError): + f1b(3.0) + assert f1b(3.0 * ureg.centimeter) == 0.03 + assert f1b(3.0 * ureg.meter) == 3.0 + with pytest.raises(DimensionalityError): + f1b(3 * ureg.second) f1c = ureg.wraps("meter", [ureg.meter])(func) - self.assertEqual(f1c(3.0 * ureg.centimeter), 0.03 * ureg.meter) - self.assertEqual(f1c(3.0 * ureg.meter), 3.0 * ureg.meter) - self.assertRaises(DimensionalityError, f1c, 3 * ureg.second) + assert f1c(3.0 * ureg.centimeter) == 0.03 * ureg.meter + assert f1c(3.0 * ureg.meter) == 3.0 * ureg.meter + with pytest.raises(DimensionalityError): + f1c(3 * ureg.second) f1d = ureg.wraps(ureg.meter, [ureg.meter])(func) - self.assertEqual(f1d(3.0 * ureg.centimeter), 0.03 * ureg.meter) - self.assertEqual(f1d(3.0 * ureg.meter), 3.0 * ureg.meter) - self.assertRaises(DimensionalityError, f1d, 3 * ureg.second) + assert f1d(3.0 * ureg.centimeter) == 0.03 * ureg.meter + assert f1d(3.0 * ureg.meter) == 3.0 * ureg.meter + with pytest.raises(DimensionalityError): + f1d(3 * ureg.second) f1 = ureg.wraps(None, "meter")(func) - self.assertRaises(ValueError, f1, 3.0) - self.assertEqual(f1(3.0 * ureg.centimeter), 0.03) - self.assertEqual(f1(3.0 * ureg.meter), 3.0) - self.assertRaises(DimensionalityError, f1, 3 * ureg.second) + with pytest.raises(ValueError): + f1(3.0) + assert f1(3.0 * ureg.centimeter) == 0.03 + assert f1(3.0 * ureg.meter) == 3.0 + with pytest.raises(DimensionalityError): + f1(3 * ureg.second) f2 = ureg.wraps("centimeter", ["meter"])(func) - self.assertRaises(ValueError, f2, 3.0) - self.assertEqual(f2(3.0 * ureg.centimeter), 0.03 * ureg.centimeter) - self.assertEqual(f2(3.0 * ureg.meter), 3 * ureg.centimeter) + with pytest.raises(ValueError): + f2(3.0) + assert f2(3.0 * ureg.centimeter) == 0.03 * ureg.centimeter + assert f2(3.0 * ureg.meter) == 3 * ureg.centimeter f3 = ureg.wraps("centimeter", ["meter"], strict=False)(func) - self.assertEqual(f3(3), 3 * ureg.centimeter) - self.assertEqual(f3(3.0 * ureg.centimeter), 0.03 * ureg.centimeter) - self.assertEqual(f3(3.0 * ureg.meter), 3.0 * ureg.centimeter) + assert f3(3) == 3 * ureg.centimeter + assert f3(3.0 * ureg.centimeter) == 0.03 * ureg.centimeter + assert f3(3.0 * ureg.meter) == 3.0 * ureg.centimeter def gfunc(x, y): return x + y g0 = ureg.wraps(None, [None, None])(gfunc) - self.assertEqual(g0(3, 1), 4) + assert g0(3, 1) == 4 g1 = ureg.wraps(None, ["meter", "centimeter"])(gfunc) - self.assertRaises(ValueError, g1, 3 * ureg.meter, 1) - self.assertEqual(g1(3 * ureg.meter, 1 * ureg.centimeter), 4) - self.assertEqual(g1(3 * ureg.meter, 1 * ureg.meter), 3 + 100) + with pytest.raises(ValueError): + g1(3 * ureg.meter, 1) + assert g1(3 * ureg.meter, 1 * ureg.centimeter) == 4 + assert g1(3 * ureg.meter, 1 * ureg.meter) == 3 + 100 def hfunc(x, y): return x, y h0 = ureg.wraps(None, [None, None])(hfunc) - self.assertEqual(h0(3, 1), (3, 1)) + assert h0(3, 1) == (3, 1) h1 = ureg.wraps(["meter", "centimeter"], [None, None])(hfunc) - self.assertEqual(h1(3, 1), [3 * ureg.meter, 1 * ureg.cm]) + assert h1(3, 1) == [3 * ureg.meter, 1 * ureg.cm] h2 = ureg.wraps(("meter", "centimeter"), [None, None])(hfunc) - self.assertEqual(h2(3, 1), (3 * ureg.meter, 1 * ureg.cm)) + assert h2(3, 1) == (3 * ureg.meter, 1 * ureg.cm) h3 = ureg.wraps((None,), (None, None))(hfunc) - self.assertEqual(h3(3, 1), (3, 1)) + assert h3(3, 1) == (3, 1) def test_wrap_referencing(self): @@ -527,29 +520,27 @@ def gfunc3(x, y): rst = 3.0 * ureg.meter + 1.0 * ureg.centimeter g0 = ureg.wraps("=A", ["=A", "=A"])(gfunc) - self.assertEqual(g0(3.0 * ureg.meter, 1.0 * ureg.centimeter), rst.to("meter")) - self.assertEqual(g0(3, 1), 4) + assert g0(3.0 * ureg.meter, 1.0 * ureg.centimeter) == rst.to("meter") + assert g0(3, 1) == 4 g1 = ureg.wraps("=A", ["=A", "=A"])(gfunc) - self.assertEqual( - g1(3.0 * ureg.meter, 1.0 * ureg.centimeter), rst.to("centimeter") - ) + assert g1(3.0 * ureg.meter, 1.0 * ureg.centimeter) == rst.to("centimeter") g2 = ureg.wraps("=A", ["=A", "=A"])(gfunc) - self.assertEqual(g2(3.0 * ureg.meter, 1.0 * ureg.centimeter), rst.to("meter")) + assert g2(3.0 * ureg.meter, 1.0 * ureg.centimeter) == rst.to("meter") g3 = ureg.wraps("=A**2", ["=A", "=A**2"])(gfunc2) a = 3.0 * ureg.meter b = (2.0 * ureg.centimeter) ** 2 - self.assertEqual(g3(a, b), gfunc2(a, b)) - self.assertEqual(g3(3, 2), gfunc2(3, 2)) + assert g3(a, b) == gfunc2(a, b) + assert g3(3, 2) == gfunc2(3, 2) g4 = ureg.wraps("=A**2 * B", ["=A", "=B"])(gfunc3) - self.assertEqual( - g4(3.0 * ureg.meter, 2.0 * ureg.second), ureg("(3*meter)**2 * 2 *second") + assert g4(3.0 * ureg.meter, 2.0 * ureg.second) == ureg( + "(3*meter)**2 * 2 *second" ) - self.assertEqual(g4(3.0 * ureg.meter, 2.0), ureg("(3*meter)**2 * 2")) - self.assertEqual(g4(3.0, 2.0 * ureg.second), ureg("3**2 * 2 * second")) + assert g4(3.0 * ureg.meter, 2.0) == ureg("(3*meter)**2 * 2") + assert g4(3.0, 2.0 * ureg.second) == ureg("3**2 * 2 * second") def test_check(self): def func(x): @@ -558,55 +549,62 @@ def func(x): ureg = self.ureg f0 = ureg.check("[length]")(func) - self.assertRaises(DimensionalityError, f0, 3.0) - self.assertEqual(f0(3.0 * ureg.centimeter), 0.03 * ureg.meter) - self.assertRaises(DimensionalityError, f0, 3.0 * ureg.kilogram) + with pytest.raises(DimensionalityError): + f0(3.0) + assert f0(3.0 * ureg.centimeter) == 0.03 * ureg.meter + with pytest.raises(DimensionalityError): + f0(3.0 * ureg.kilogram) f0b = ureg.check(ureg.meter)(func) - self.assertRaises(DimensionalityError, f0b, 3.0) - self.assertEqual(f0b(3.0 * ureg.centimeter), 0.03 * ureg.meter) - self.assertRaises(DimensionalityError, f0b, 3.0 * ureg.kilogram) + with pytest.raises(DimensionalityError): + f0b(3.0) + assert f0b(3.0 * ureg.centimeter) == 0.03 * ureg.meter + with pytest.raises(DimensionalityError): + f0b(3.0 * ureg.kilogram) def gfunc(x, y): return x / y g0 = ureg.check(None, None)(gfunc) - self.assertEqual(g0(6, 2), 3) - self.assertEqual(g0(6 * ureg.parsec, 2), 3 * ureg.parsec) + assert g0(6, 2) == 3 + assert g0(6 * ureg.parsec, 2) == 3 * ureg.parsec g1 = ureg.check("[speed]", "[time]")(gfunc) - self.assertRaises(DimensionalityError, g1, 3.0, 1) - self.assertRaises(DimensionalityError, g1, 1 * ureg.parsec, 1 * ureg.angstrom) - self.assertRaises(TypeError, g1, 1 * ureg.km / ureg.hour, 1 * ureg.hour, 3.0) - self.assertEqual( - g1(3.6 * ureg.km / ureg.hour, 1 * ureg.second), - 1 * ureg.meter / ureg.second ** 2, - ) - - self.assertRaises(TypeError, ureg.check("[speed]"), gfunc) - self.assertRaises(TypeError, ureg.check("[speed]", "[time]", "[mass]"), gfunc) + with pytest.raises(DimensionalityError): + g1(3.0, 1) + with pytest.raises(DimensionalityError): + g1(1 * ureg.parsec, 1 * ureg.angstrom) + with pytest.raises(TypeError): + g1(1 * ureg.km / ureg.hour, 1 * ureg.hour, 3.0) + assert ( + g1(3.6 * ureg.km / ureg.hour, 1 * ureg.second) + == 1 * ureg.meter / ureg.second ** 2 + ) + + with pytest.raises(TypeError): + ureg.check("[speed]")(gfunc) + with pytest.raises(TypeError): + ureg.check("[speed]", "[time]", "[mass]")(gfunc) def test_to_ref_vs_to(self): self.ureg.autoconvert_offset_to_baseunit = True q = 8.0 * self.ureg.inch t = 8.0 * self.ureg.degF dt = 8.0 * self.ureg.delta_degF - self.assertEqual( - q.to("yard").magnitude, self.ureg._units["inch"].converter.to_reference(8.0) - ) - self.assertEqual( - t.to("kelvin").magnitude, - self.ureg._units["degF"].converter.to_reference(8.0), - ) - self.assertEqual( - dt.to("kelvin").magnitude, - self.ureg._units["delta_degF"].converter.to_reference(8.0), - ) - - def test_redefinition(self): + assert q.to("yard").magnitude == self.ureg._units[ + "inch" + ].converter.to_reference(8.0) + assert t.to("kelvin").magnitude == self.ureg._units[ + "degF" + ].converter.to_reference(8.0) + assert dt.to("kelvin").magnitude == self.ureg._units[ + "delta_degF" + ].converter.to_reference(8.0) + + def test_redefinition(self, caplog): d = UnitRegistry().define - with self.capture_log() as buffer: + with caplog.at_level(logging.DEBUG): d("meter = [fruits]") d("kilo- = 1000") d("[speed] = [vegetables]") @@ -615,16 +613,15 @@ def test_redefinition(self): d("bla = 3.2 meter = inch") d("myk- = 1000 = kilo-") - self.assertEqual(len(buffer), 5) + assert len(caplog.records) == 5 def test_convert_parse_str(self): ureg = self.ureg - self.assertEqual( - ureg.convert(1, "meter", "inch"), - ureg.convert(1, UnitsContainer(meter=1), UnitsContainer(inch=1)), + assert ureg.convert(1, "meter", "inch") == ureg.convert( + 1, UnitsContainer(meter=1), UnitsContainer(inch=1) ) - @helpers.requires_numpy() + @helpers.requires_numpy def test_convert_inplace(self): ureg = self.ureg @@ -640,11 +637,11 @@ def test_convert_inplace(self): r1 = ureg.convert(a, src, dst) np.testing.assert_allclose(r1, v * ac) - self.assertIsNot(r1, a) + assert r1 is not a r2 = ureg.convert(a, src, dst, inplace=True) np.testing.assert_allclose(r2, v * ac) - self.assertIs(r2, a) + assert r2 is a def test_repeated_convert(self): # Because of caching, repeated conversions were failing. @@ -660,114 +657,98 @@ def test_singular_SI_prefix_convert(self): def test_parse_units(self): ureg = self.ureg - self.assertEqual(ureg.parse_units(""), ureg.Unit("")) - self.assertRaises(ValueError, ureg.parse_units, "2 * meter") + assert ureg.parse_units("") == ureg.Unit("") + with pytest.raises(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")], - ) + assert 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")], - ) + assert 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")], - ], - ) + assert 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")], - ], - ) + assert 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")], + ] def test_case_sensitivity(self): ureg = self.ureg # Default - self.assertRaises(UndefinedUnitError, ureg.parse_units, "Meter") - self.assertRaises(UndefinedUnitError, ureg.parse_units, "j") + with pytest.raises(UndefinedUnitError): + ureg.parse_units("Meter") + with pytest.raises(UndefinedUnitError): + ureg.parse_units("j") # Force True - self.assertRaises( - UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True - ) - self.assertRaises( - UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True - ) + with pytest.raises(UndefinedUnitError): + ureg.parse_units("Meter", case_sensitive=True) + with pytest.raises(UndefinedUnitError): + ureg.parse_units("j", case_sensitive=True) # Force False - self.assertEqual( - ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1) - ) - self.assertEqual( - ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1) + assert ureg.parse_units("Meter", case_sensitive=False) == UnitsContainer( + meter=1 ) + assert ureg.parse_units("j", case_sensitive=False) == UnitsContainer(joule=1) -class TestCaseInsensitiveRegistry(CaseInsensitveQuantityTestCase): +class TestCaseInsensitiveRegistry(QuantityTestCase): + + kwargs = dict(case_sensitive=False) + def test_case_sensitivity(self): ureg = self.ureg # Default - self.assertEqual(ureg.parse_units("Meter"), UnitsContainer(meter=1)) - self.assertEqual(ureg.parse_units("j"), UnitsContainer(joule=1)) + assert ureg.parse_units("Meter") == UnitsContainer(meter=1) + assert ureg.parse_units("j") == UnitsContainer(joule=1) # Force True - self.assertRaises( - UndefinedUnitError, ureg.parse_units, "Meter", case_sensitive=True - ) - self.assertRaises( - UndefinedUnitError, ureg.parse_units, "j", case_sensitive=True - ) + with pytest.raises(UndefinedUnitError): + ureg.parse_units("Meter", case_sensitive=True) + with pytest.raises(UndefinedUnitError): + ureg.parse_units("j", case_sensitive=True) # Force False - self.assertEqual( - ureg.parse_units("Meter", case_sensitive=False), UnitsContainer(meter=1) - ) - self.assertEqual( - ureg.parse_units("j", case_sensitive=False), UnitsContainer(joule=1) + assert ureg.parse_units("Meter", case_sensitive=False) == UnitsContainer( + meter=1 ) + assert ureg.parse_units("j", case_sensitive=False) == UnitsContainer(joule=1) class TestCompatibleUnits(QuantityTestCase): - FORCE_NDARRAY = False - - def setUp(self): - super().setUp() - self.ureg = UnitRegistry(force_ndarray=self.FORCE_NDARRAY) - self.Q_ = self.ureg.Quantity - self.U_ = self.ureg.Unit - def _test(self, input_units): gd = self.ureg.get_dimensionality dim = gd(input_units) equiv = self.ureg.get_compatible_units(input_units) for eq in equiv: - self.assertEqual(gd(eq), dim) - self.assertEqual(equiv, self.ureg.get_compatible_units(dim)) + assert gd(eq) == dim + assert equiv == self.ureg.get_compatible_units(dim) def _test2(self, units1, units2): equiv1 = self.ureg.get_compatible_units(units1) equiv2 = self.ureg.get_compatible_units(units2) - self.assertEqual(equiv1, equiv2) + assert equiv1 == equiv2 def test_many(self): self._test(self.ureg.meter) @@ -793,31 +774,30 @@ def test_context_sp(self): for eq in equiv: dim = gd(eq) result.add(dim) - self.assertIn(dim, valid) + assert dim in valid - self.assertEqual(len(result), len(valid)) + assert len(result) == len(valid) def test_get_base_units(self): ureg = UnitRegistry() - self.assertEqual(ureg.get_base_units(""), (1, ureg.Unit(""))) - self.assertEqual(ureg.get_base_units("pi"), (math.pi, ureg.Unit(""))) - self.assertEqual(ureg.get_base_units("ln10"), (math.log(10), ureg.Unit(""))) - self.assertEqual( - ureg.get_base_units("meter"), ureg.get_base_units(ParserHelper(meter=1)) + assert ureg.get_base_units("") == (1, ureg.Unit("")) + assert ureg.get_base_units("pi") == (math.pi, ureg.Unit("")) + assert ureg.get_base_units("ln10") == (math.log(10), ureg.Unit("")) + assert ureg.get_base_units("meter") == ureg.get_base_units( + ParserHelper(meter=1) ) def test_get_compatible_units(self): ureg = UnitRegistry() - self.assertEqual(ureg.get_compatible_units(""), frozenset()) - self.assertEqual( - ureg.get_compatible_units("meter"), - ureg.get_compatible_units(ParserHelper(meter=1)), + assert ureg.get_compatible_units("") == frozenset() + assert ureg.get_compatible_units("meter") == ureg.get_compatible_units( + ParserHelper(meter=1) ) class TestRegistryWithDefaultRegistry(TestRegistry): @classmethod - def setUpClass(cls): + def setup_class(cls): from pint import _DEFAULT_REGISTRY cls.ureg = _DEFAULT_REGISTRY @@ -826,25 +806,31 @@ def setUpClass(cls): def test_lazy(self): x = LazyRegistry() x.test = "test" - self.assertIsInstance(x, UnitRegistry) + assert isinstance(x, UnitRegistry) y = LazyRegistry() y("meter") - self.assertIsInstance(y, UnitRegistry) + assert isinstance(y, UnitRegistry) def test_redefinition(self): d = self.ureg.define - self.assertRaises(DefinitionSyntaxError, d, "meter = [time]") - self.assertRaises(RedefinitionError, d, "meter = [newdim]") - self.assertRaises(RedefinitionError, d, "kilo- = 1000") - self.assertRaises(RedefinitionError, d, "[speed] = [length]") + with pytest.raises(DefinitionSyntaxError): + d("meter = [time]") + with pytest.raises(RedefinitionError): + d("meter = [newdim]") + with pytest.raises(RedefinitionError): + d("kilo- = 1000") + with pytest.raises(RedefinitionError): + d("[speed] = [length]") # aliases - self.assertIn("inch", self.ureg._units) - self.assertRaises(RedefinitionError, d, "bla = 3.2 meter = inch") - self.assertRaises(RedefinitionError, d, "myk- = 1000 = kilo-") + assert "inch" in self.ureg._units + with pytest.raises(RedefinitionError): + d("bla = 3.2 meter = inch") + with pytest.raises(RedefinitionError): + d("myk- = 1000 = kilo-") -class TestConvertWithOffset(QuantityTestCase, ParameterizedTestCase): +class TestConvertWithOffset(QuantityTestCase): # The dicts in convert_with_offset are used to create a UnitsContainer. # We create UnitsContainer to avoid any auto-conversion of units. @@ -894,24 +880,24 @@ class TestConvertWithOffset(QuantityTestCase, ParameterizedTestCase): (({"degC": 1, "kelvin": 1}, {"kelvin": 2}), "error"), ] - @ParameterizedTestCase.parameterize( - ("input", "expected_output"), convert_with_offset - ) + @pytest.mark.parametrize(("input_tuple", "expected"), convert_with_offset) def test_to_and_from_offset_units(self, input_tuple, expected): src, dst = input_tuple src, dst = UnitsContainer(src), UnitsContainer(dst) value = 10.0 convert = self.ureg.convert if isinstance(expected, str): - self.assertRaises(DimensionalityError, convert, value, src, dst) + with pytest.raises(DimensionalityError): + convert(value, src, dst) if src != dst: - self.assertRaises(DimensionalityError, convert, value, dst, src) + with pytest.raises(DimensionalityError): + convert(value, dst, src) else: - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( convert(value, src, dst), expected, atol=0.001 ) if src != dst: - self.assertQuantityAlmostEqual( + helpers.assert_quantity_almost_equal( convert(expected, dst, src), value, atol=0.001 ) @@ -933,13 +919,17 @@ def test_alias(self): # Test that new aliases work # Test that pre-existing aliases and symbol are not eliminated for a in ("can", "alias1", "alias2", "alias3", "alias4", "alias5"): - self.assertEqual(ureg.Unit(a), ureg.Unit("canonical")) + assert ureg.Unit(a) == ureg.Unit("canonical") # Test that aliases defined multiple times are not duplicated - self.assertEqual( - ureg._units["canonical"].aliases, - ("alias1", "alias2", "alias3", "alias4", "alias5"), + assert ureg._units["canonical"].aliases == ( + "alias1", + "alias2", + "alias3", + "alias4", + "alias5", ) # Define against unknown name - self.assertRaises(KeyError, ureg.define, "@alias notexist = something") + with pytest.raises(KeyError): + ureg.define("@alias notexist = something") diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py index 5f7117312..d2eebe59a 100644 --- a/pint/testsuite/test_util.py +++ b/pint/testsuite/test_util.py @@ -3,7 +3,8 @@ import math import operator as op -from pint.testsuite import BaseTestCase, QuantityTestCase +import pytest + from pint.util import ( ParserHelper, UnitsContainer, @@ -19,7 +20,7 @@ ) -class TestUnitsContainer(QuantityTestCase): +class TestUnitsContainer: def _test_inplace(self, operator, value1, value2, expected_result): value1 = copy.copy(value1) value2 = copy.copy(value2) @@ -27,11 +28,11 @@ def _test_inplace(self, operator, value1, value2, expected_result): id2 = id(value2) value1 = operator(value1, value2) value2_cpy = copy.copy(value2) - self.assertEqual(value1, expected_result) + assert value1 == expected_result # Inplace operation creates copies - self.assertNotEqual(id1, id(value1)) - self.assertEqual(value2, value2_cpy) - self.assertEqual(id2, id(value2)) + assert id1 != id(value1) + assert value2 == value2_cpy + assert id2 == id(value2) def _test_not_inplace(self, operator, value1, value2, expected_result): id1 = id(value1) @@ -42,48 +43,48 @@ def _test_not_inplace(self, operator, value1, value2, expected_result): result = operator(value1, value2) - self.assertEqual(expected_result, result) - self.assertEqual(value1, value1_cpy) - self.assertEqual(value2, value2_cpy) - self.assertNotEqual(id(result), id1) - self.assertNotEqual(id(result), id2) + assert expected_result == result + assert value1 == value1_cpy + assert value2 == value2_cpy + assert id(result) != id1 + assert id(result) != id2 def test_unitcontainer_creation(self): x = UnitsContainer(meter=1, second=2) y = UnitsContainer({"meter": 1, "second": 2}) - self.assertIsInstance(x["meter"], int) - self.assertEqual(x, y) - self.assertIsNot(x, y) + assert isinstance(x["meter"], int) + assert x == y + assert x is not y z = copy.copy(x) - self.assertEqual(x, z) - self.assertIsNot(x, z) + assert x == z + assert x is not z z = UnitsContainer(x) - self.assertEqual(x, z) - self.assertIsNot(x, z) + assert x == z + assert x is not z def test_unitcontainer_repr(self): x = UnitsContainer() - self.assertEqual(str(x), "dimensionless") - self.assertEqual(repr(x), "") + assert str(x) == "dimensionless" + assert repr(x) == "" x = UnitsContainer(meter=1, second=2) - self.assertEqual(str(x), "meter * second ** 2") - self.assertEqual(repr(x), "") + assert str(x) == "meter * second ** 2" + assert repr(x) == "" x = UnitsContainer(meter=1, second=2.5) - self.assertEqual(str(x), "meter * second ** 2.5") - self.assertEqual(repr(x), "") + assert str(x) == "meter * second ** 2.5" + assert repr(x) == "" def test_unitcontainer_bool(self): - self.assertTrue(UnitsContainer(meter=1, second=2)) - self.assertFalse(UnitsContainer()) + assert UnitsContainer(meter=1, second=2) + assert not UnitsContainer() def test_unitcontainer_comp(self): x = UnitsContainer(meter=1, second=2) y = UnitsContainer(meter=1.0, second=2) z = UnitsContainer(meter=1, second=3) - self.assertTrue(x == y) - self.assertFalse(x != y) - self.assertFalse(x == z) - self.assertTrue(x != z) + assert x == y + assert not (x != y) + assert not (x == z) + assert x != z def test_unitcontainer_arithmetic(self): x = UnitsContainer(meter=1) @@ -104,72 +105,75 @@ def test_string_comparison(self): x = UnitsContainer(meter=1) y = UnitsContainer(second=1) z = UnitsContainer(meter=1, second=-2) - self.assertEqual(x, "meter") - self.assertEqual("meter", x) - self.assertNotEqual(x, "meter ** 2") - self.assertNotEqual(x, "meter * meter") - self.assertNotEqual(x, "second") - self.assertEqual(y, "second") - self.assertEqual(z, "meter/second/second") + assert x == "meter" + assert "meter" == x + assert x != "meter ** 2" + assert x != "meter * meter" + assert x != "second" + assert y == "second" + assert z == "meter/second/second" def test_invalid(self): - self.assertRaises(TypeError, UnitsContainer, {1: 2}) - self.assertRaises(TypeError, UnitsContainer, {"1": "2"}) + with pytest.raises(TypeError): + UnitsContainer({1: 2}) + with pytest.raises(TypeError): + UnitsContainer({"1": "2"}) d = UnitsContainer() - self.assertRaises(TypeError, d.__mul__, list()) - self.assertRaises(TypeError, d.__pow__, list()) - self.assertRaises(TypeError, d.__truediv__, list()) - self.assertRaises(TypeError, d.__rtruediv__, list()) + with pytest.raises(TypeError): + d.__mul__(list()) + with pytest.raises(TypeError): + d.__pow__(list()) + with pytest.raises(TypeError): + d.__truediv__(list()) + with pytest.raises(TypeError): + d.__rtruediv__(list()) -class TestToUnitsContainer(BaseTestCase): +class TestToUnitsContainer: def test_str_conversion(self): - self.assertEqual(to_units_container("m"), UnitsContainer(m=1)) + assert to_units_container("m") == UnitsContainer(m=1) def test_uc_conversion(self): a = UnitsContainer(m=1) - self.assertIs(to_units_container(a), a) + assert to_units_container(a) is a def test_quantity_conversion(self): from pint.registry import UnitRegistry ureg = UnitRegistry() - self.assertEqual( - to_units_container(ureg.Quantity(1, UnitsContainer(m=1))), - UnitsContainer(m=1), - ) + assert to_units_container( + ureg.Quantity(1, UnitsContainer(m=1)) + ) == UnitsContainer(m=1) def test_unit_conversion(self): from pint import Unit - self.assertEqual( - to_units_container(Unit(UnitsContainer(m=1))), UnitsContainer(m=1) - ) + assert to_units_container(Unit(UnitsContainer(m=1))) == UnitsContainer(m=1) def test_dict_conversion(self): - self.assertEqual(to_units_container(dict(m=1)), UnitsContainer(m=1)) + assert to_units_container(dict(m=1)) == UnitsContainer(m=1) -class TestParseHelper(BaseTestCase): +class TestParseHelper: def test_basic(self): # Parse Helper ar mutables, so we build one everytime x = lambda: ParserHelper(1, meter=2) xp = lambda: ParserHelper(1, meter=2) y = lambda: ParserHelper(2, meter=2) - self.assertEqual(x(), xp()) - self.assertNotEqual(x(), y()) - self.assertEqual(ParserHelper.from_string(""), ParserHelper()) - self.assertEqual(repr(x()), "") + assert x() == xp() + assert x() != y() + assert ParserHelper.from_string("") == ParserHelper() + assert repr(x()) == "" - self.assertEqual(ParserHelper(2), 2) + assert ParserHelper(2) == 2 - self.assertEqual(x(), dict(meter=2)) - self.assertEqual(x(), "meter ** 2") - self.assertNotEqual(y(), dict(meter=2)) - self.assertNotEqual(y(), "meter ** 2") + assert x() == dict(meter=2) + assert x() == "meter ** 2" + assert y() != dict(meter=2) + assert y() != "meter ** 2" - self.assertNotEqual(xp(), object()) + assert xp() != object() def test_calculate(self): # Parse Helper ar mutables, so we build one everytime @@ -177,43 +181,43 @@ def test_calculate(self): y = lambda: ParserHelper(2.0, meter=-2) z = lambda: ParserHelper(2.0, meter=2) - self.assertEqual(x() * 4.0, ParserHelper(4.0, meter=2)) - self.assertEqual(x() * y(), ParserHelper(2.0)) - self.assertEqual(x() * "second", ParserHelper(1.0, meter=2, second=1)) + assert x() * 4.0 == ParserHelper(4.0, meter=2) + assert x() * y() == ParserHelper(2.0) + assert x() * "second" == ParserHelper(1.0, meter=2, second=1) - self.assertEqual(x() / 4.0, ParserHelper(0.25, meter=2)) - self.assertEqual(x() / "second", ParserHelper(1.0, meter=2, second=-1)) - self.assertEqual(x() / z(), ParserHelper(0.5)) + assert x() / 4.0 == ParserHelper(0.25, meter=2) + assert x() / "second" == ParserHelper(1.0, meter=2, second=-1) + assert x() / z() == ParserHelper(0.5) - self.assertEqual(4.0 / z(), ParserHelper(2.0, meter=-2)) - self.assertEqual("seconds" / z(), ParserHelper(0.5, seconds=1, meter=-2)) - self.assertEqual(dict(seconds=1) / z(), ParserHelper(0.5, seconds=1, meter=-2)) + assert 4.0 / z() == ParserHelper(2.0, meter=-2) + assert "seconds" / z() == ParserHelper(0.5, seconds=1, meter=-2) + assert dict(seconds=1) / z() == ParserHelper(0.5, seconds=1, meter=-2) def _test_eval_token(self, expected, expression, use_decimal=False): token = next(tokenizer(expression)) actual = ParserHelper.eval_token(token, use_decimal=use_decimal) - self.assertEqual(expected, actual) - self.assertEqual(type(expected), type(actual)) + assert expected == actual + assert type(expected) == type(actual) def test_eval_token(self): self._test_eval_token(1000.0, "1e3") self._test_eval_token(1000.0, "1E3") self._test_eval_token(1000, "1000") - def test_nan(self): + def test_nan(self, subtests): for s in ("nan", "NAN", "NaN", "123 NaN nan NAN 456"): - with self.subTest(s): + with subtests.test(s): p = ParserHelper.from_string(s + " kg") assert math.isnan(p.scale) - self.assertEqual(dict(p), {"kg": 1}) + assert dict(p) == {"kg": 1} -class TestStringProcessor(BaseTestCase): +class TestStringProcessor: def _test(self, bef, aft): for pattern in ("{}", "+{}+"): b = pattern.format(bef) a = pattern.format(aft) - self.assertEqual(string_preprocessor(b), a) + assert string_preprocessor(b) == a def test_square_cube(self): self._test("bcd^3", "bcd**3") @@ -267,95 +271,98 @@ def test_names(self): self._test("water_60F", "water_60F") -class TestGraph(BaseTestCase): +class TestGraph: def test_start_not_in_graph(self): g = collections.defaultdict(set) g[1] = {2} g[2] = {3} - self.assertIs(find_connected_nodes(g, 9), None) + assert find_connected_nodes(g, 9) is None def test_shortest_path(self): g = collections.defaultdict(set) g[1] = {2} g[2] = {3} p = find_shortest_path(g, 1, 2) - self.assertEqual(p, [1, 2]) + assert p == [1, 2] p = find_shortest_path(g, 1, 3) - self.assertEqual(p, [1, 2, 3]) + assert p == [1, 2, 3] p = find_shortest_path(g, 3, 1) - self.assertIs(p, None) + assert p is None g = collections.defaultdict(set) g[1] = {2} g[2] = {3, 1} g[3] = {2} p = find_shortest_path(g, 1, 2) - self.assertEqual(p, [1, 2]) + assert p == [1, 2] p = find_shortest_path(g, 1, 3) - self.assertEqual(p, [1, 2, 3]) + assert p == [1, 2, 3] p = find_shortest_path(g, 3, 1) - self.assertEqual(p, [3, 2, 1]) + assert p == [3, 2, 1] p = find_shortest_path(g, 2, 1) - self.assertEqual(p, [2, 1]) + assert p == [2, 1] -class TestMatrix(BaseTestCase): +class TestMatrix: def test_matrix_to_string(self): - self.assertEqual( - matrix_to_string([[1, 2], [3, 4]], row_headers=None, col_headers=None), - "1\t2\n" "3\t4", + assert ( + matrix_to_string([[1, 2], [3, 4]], row_headers=None, col_headers=None) + == "1\t2\n" + "3\t4" ) - self.assertEqual( + assert ( matrix_to_string( [[1, 2], [3, 4]], row_headers=None, col_headers=None, fmtfun=lambda x: f"{x:.2f}", - ), - "1.00\t2.00\n" "3.00\t4.00", + ) + == "1.00\t2.00\n" + "3.00\t4.00" ) - self.assertEqual( - matrix_to_string( - [[1, 2], [3, 4]], row_headers=["c", "d"], col_headers=None - ), - "c\t1\t2\n" "d\t3\t4", + assert ( + matrix_to_string([[1, 2], [3, 4]], row_headers=["c", "d"], col_headers=None) + == "c\t1\t2\n" + "d\t3\t4" ) - self.assertEqual( - matrix_to_string( - [[1, 2], [3, 4]], row_headers=None, col_headers=["a", "b"] - ), - "a\tb\n" "1\t2\n" "3\t4", + assert ( + matrix_to_string([[1, 2], [3, 4]], row_headers=None, col_headers=["a", "b"]) + == "a\tb\n" + "1\t2\n" + "3\t4" ) - self.assertEqual( + assert ( matrix_to_string( [[1, 2], [3, 4]], row_headers=["c", "d"], col_headers=["a", "b"] - ), - "\ta\tb\n" "c\t1\t2\n" "d\t3\t4", + ) + == "\ta\tb\n" + "c\t1\t2\n" + "d\t3\t4" ) def test_transpose(self): - self.assertEqual(transpose([[1, 2], [3, 4]]), [[1, 3], [2, 4]]) + assert transpose([[1, 2], [3, 4]]) == [[1, 3], [2, 4]] -class TestOtherUtils(BaseTestCase): +class TestOtherUtils: def test_iterable(self): # Test with list, string, generator, and scalar - self.assertTrue(iterable([0, 1, 2, 3])) - self.assertTrue(iterable("test")) - self.assertTrue(iterable((i for i in range(5)))) - self.assertFalse(iterable(0)) + assert iterable([0, 1, 2, 3]) + assert iterable("test") + assert iterable((i for i in range(5))) + assert not iterable(0) def test_sized(self): # Test with list, string, generator, and scalar - self.assertTrue(sized([0, 1, 2, 3])) - self.assertTrue(sized("test")) - self.assertFalse(sized((i for i in range(5)))) - self.assertFalse(sized(0)) + assert sized([0, 1, 2, 3]) + assert sized("test") + assert not sized((i for i in range(5))) + assert not sized(0) diff --git a/setup.cfg b/setup.cfg index b97221685..4947cd9d2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,11 @@ scripts = pint/pint-convert [options.extras_require] numpy = numpy >= 1.14 uncertainties = uncertainties >= 3.0 -test = pytest; pytest-mpl; pytest-cov +test = + pytest + pytest-mpl + pytest-cov + pytest-subtests [options.package_data] pint = default_en.txt; constants_en.txt From 3ba20d75f97d4b887a0ce3dcf015c46deb85c1d8 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 16 Jan 2021 13:41:19 -0300 Subject: [PATCH 585/612] Some info in contributing regarding tests --- docs/contributing.rst | 57 ++++++++++++++++++++++++++++++++++----- pint/testsuite/helpers.py | 3 ++- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 19157f1ea..96ddc94c9 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -3,8 +3,17 @@ Contributing to Pint ==================== -You can contribute in different ways: +Pint uses (and thanks): +- github_ to host the code +- travis_ to test all commits and PRs. +- coveralls_ to monitor coverage test coverage +- readthedocs_ to host the documentation. +- `bors-ng`_ as a merge bot and therefore every PR is tested before merging. +- black_, isort_ and flake8_ as code linters and pre-commit_ to enforce them. +- pytest_ to write tests +- sphinx_ to write docs. +You can contribute in different ways: Report issues ------------- @@ -19,18 +28,23 @@ Contribute code To contribute fixes, code or documentation to Pint, fork Pint in github_ and submit the changes using a pull request against the **master** branch. -- If you are fixing a bug, add a test to test_issues.py, or amend/enrich the general - test suite to cover the use case. -- If you are submitting new code, add tests and documentation. +- If you are submitting new code, add tests (see below) and documentation. - Write "Closes #" in the PR description or a comment, as described in the `github docs`_. - Log the change in the CHANGES file. -- Execute ``black -t py36 . && isort -rc . && flake8`` and resolve any issues. - -Pint uses `bors-ng` as a merge bot and therefore every PR is tested before merging. +- Execute ``pre-commit run --all-files`` and resolve any issues. In any case, feel free to use the `issue tracker`_ to discuss ideas for new features or improvements. +Notice that we will not merge a PR if tests are failing. In certain cases tests pass in your +machine but not in travis. There might be multiple reasons for this but these are some of +the most common + +- Your new code does not work for other Python or Numpy versions. +- The documentation is not being built properly or the examples in the docs are + not working. +- linters are reporting that the code does no adhere to the standards. + Setting up your environment --------------------------- @@ -44,6 +58,26 @@ environment on Linux or OSX with the following commands:: $ source venv/bin/activate $ pip install -e . $ pip install -r requirements_docs.txt + $ pip install pre-commit # This step and the next are optional but recommended. + $ pre-commit install + + +Writing tests +------------- + +We use pytest_ for testing. If you contribute code you need to add tests: + +- If you are fixing a bug, add a test to `test_issues.py`, or amend/enrich the general + test suite to cover the use case. +- If you are adding a new feature, add a test in the appropiate place. There is usually + a `test_X.py` for each `X.py` file. There are some other test files that deal with + individual/specific features. If in doubt, ask. +- Prefer functions to classes. +- When using classes, derive from `QuantityTestCase`. +- Use `parametrize` as much as possible. +- Use `fixtures` (see conftest.py) instead of instantiating the registry yourself. +- When your test does not modify the registry, use `sess_registry` fixture. + Running tests and building documentation ---------------------------------------- @@ -94,3 +128,12 @@ features that work best as an extension pacakage versus direct inclusion in Pint .. _`issue tracker`: https://github.com/hgrecco/pint/issues .. _`bors-ng`: https://github.com/bors-ng/bors-ng .. _`github docs`: https://help.github.com/articles/closing-issues-via-commit-messages/ +.. _travis: https://travis-ci.com/ +.. _coveralls: https://coveralls.io/ +.. _readthedocs: https://readthedocs.org/ +.. _pre-commit: https://pre-commit.com/ +.. _black: https://black.readthedocs.io/en/stable/ +.. _isort: https://pycqa.github.io/isort/ +.. _flake8: https://flake8.pycqa.org/en/latest/ +.. _pytest: https://docs.pytest.org/en/stable/ +.. _sphinx: https://www.sphinx-doc.org/en/master/ \ No newline at end of file diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 0ec405a44..f67b73397 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -7,6 +7,7 @@ from numbers import Number import pytest +from _pytest.mark.structures import store_mark from pint import Quantity from pint.compat import ndarray, np @@ -169,7 +170,7 @@ def assert_quantity_almost_equal(first, second, rtol=1e-07, atol=0, msg=None): def multi_mark(*funcs): def _deco(f): for func in funcs: - pytest.mark.store_mark(f, func) + store_mark(f, func) return _deco From da6d95388d7a207d532f015f6c2e9a49d126d184 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 16 Jan 2021 13:55:39 -0300 Subject: [PATCH 586/612] Fixed some helpers --- docs/contributing.rst | 1 + pint/testsuite/helpers.py | 48 ++++---- pint/testsuite/test_numpy.py | 190 ++++++++++++++++---------------- pint/testsuite/test_quantity.py | 2 +- 4 files changed, 116 insertions(+), 125 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 96ddc94c9..841de0b8c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -76,6 +76,7 @@ We use pytest_ for testing. If you contribute code you need to add tests: - When using classes, derive from `QuantityTestCase`. - Use `parametrize` as much as possible. - Use `fixtures` (see conftest.py) instead of instantiating the registry yourself. +- Checkout `helpers.py` for some convenience functions before reinventing the wheel. - When your test does not modify the registry, use `sess_registry` fixture. diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index f67b73397..2f4064bed 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -7,7 +7,6 @@ from numbers import Number import pytest -from _pytest.mark.structures import store_mark from pint import Quantity from pint.compat import ndarray, np @@ -166,54 +165,45 @@ def assert_quantity_almost_equal(first, second, rtol=1e-07, atol=0, msg=None): assert abs(m1 - m2) <= max(rtol * max(abs(m1), abs(m2)), atol), msg -# Requirements -def multi_mark(*funcs): - def _deco(f): - for func in funcs: - store_mark(f, func) - - return _deco - - requires_numpy = pytest.mark.skipif(not HAS_NUMPY, reason="Requires NumPy") requires_not_numpy = pytest.mark.skipif( HAS_NUMPY, reason="Requires NumPy not to be installed." ) -requires_array_function_protocol = multi_mark( - requires_numpy, - pytest.mark.skipif( + +def requires_array_function_protocol(): + if not HAS_NUMPY: + return pytest.mark.skip("Requires NumPy") + return pytest.mark.skipif( not HAS_NUMPY_ARRAY_FUNCTION, reason="Requires __array_function__ protocol to be enabled", - ), -) + ) + -requires_not_array_function_protocol = multi_mark( - requires_not_numpy, - pytest.mark.skipif( +def requires_not_array_function_protocol(): + if not HAS_NUMPY: + return pytest.mark.skip("Requires NumPy") + return pytest.mark.skipif( HAS_NUMPY_ARRAY_FUNCTION, reason="Requires __array_function__ protocol to be unavailable or disabled", - ), -) + ) def requires_numpy_previous_than(version): - return multi_mark( - requires_numpy, - pytest.mark.skip( + if not HAS_NUMPY: + return pytest.mark.skip("Requires NumPy") + return pytest.mark.skipif( not LooseVersion(NUMPY_VER) < LooseVersion(version), reason="Requires NumPy < %s" % version, - ), - ) + ) def requires_numpy_at_least(version): - return multi_mark( - requires_numpy, - pytest.mark.skip( + if not HAS_NUMPY: + return pytest.mark.skip("Requires NumPy") + return pytest.mark.skipif( not LooseVersion(NUMPY_VER) >= LooseVersion(version), reason="Requires NumPy >= %s" % version, - ), ) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index de59362d2..beea69d32 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -55,21 +55,21 @@ def assertNDArrayEqual(self, actual, desired): class TestNumpyArrayCreation(TestNumpyMethods): # https://docs.scipy.org/doc/numpy/reference/routines.array-creation.html - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_ones_like(self): self.assertNDArrayEqual(np.ones_like(self.q), np.array([[1, 1], [1, 1]])) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_zeros_like(self): self.assertNDArrayEqual(np.zeros_like(self.q), np.array([[0, 0], [0, 0]])) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_empty_like(self): ret = np.empty_like(self.q) assert ret.shape == (2, 2) assert isinstance(ret, np.ndarray) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_full_like(self): helpers.assert_quantity_equal( np.full_like(self.q, self.Q_(0, self.ureg.degC)), @@ -102,25 +102,25 @@ def test_reshape(self): def test_ravel(self): helpers.assert_quantity_equal(self.q.ravel(), [1, 2, 3, 4] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_ravel_numpy_func(self): helpers.assert_quantity_equal(np.ravel(self.q), [1, 2, 3, 4] * self.ureg.m) # Transpose-like operations - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_moveaxis(self): helpers.assert_quantity_equal( np.moveaxis(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_rollaxis(self): helpers.assert_quantity_equal( np.rollaxis(self.q, 1), np.array([[1, 2], [3, 4]]).T * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_swapaxes(self): helpers.assert_quantity_equal( np.swapaxes(self.q, 1, 0), np.array([[1, 2], [3, 4]]).T * self.ureg.m @@ -131,13 +131,13 @@ def test_transpose(self): self.q.transpose(), [[1, 3], [2, 4]] * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_transpose_numpy_func(self): helpers.assert_quantity_equal( np.transpose(self.q), [[1, 3], [2, 4]] * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_flip_numpy_func(self): helpers.assert_quantity_equal( np.flip(self.q, axis=0), [[3, 4], [1, 2]] * self.ureg.m @@ -145,7 +145,7 @@ def test_flip_numpy_func(self): # Changing number of dimensions - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_atleast_1d(self): actual = np.atleast_1d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = (self.Q_(np.array([0]), self.ureg.degC), self.q.flatten()) @@ -153,7 +153,7 @@ def test_atleast_1d(self): helpers.assert_quantity_equal(ind_actual, ind_expected) helpers.assert_quantity_equal(np.atleast_1d(self.q), self.q) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_atleast_2d(self): actual = np.atleast_2d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = ( @@ -164,7 +164,7 @@ def test_atleast_2d(self): helpers.assert_quantity_equal(ind_actual, ind_expected) helpers.assert_quantity_equal(np.atleast_2d(self.q), self.q) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_atleast_3d(self): actual = np.atleast_3d(self.Q_(0, self.ureg.degC), self.q.flatten()) expected = ( @@ -177,20 +177,20 @@ def test_atleast_3d(self): np.atleast_3d(self.q), np.array([[[1], [2]], [[3], [4]]]) * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_broadcast_to(self): helpers.assert_quantity_equal( np.broadcast_to(self.q[:, 1], (2, 2)), np.array([[2, 4], [2, 4]]) * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_expand_dims(self): helpers.assert_quantity_equal( np.expand_dims(self.q, 0), np.array([[[1, 2], [3, 4]]]) * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_squeeze(self): helpers.assert_quantity_equal(np.squeeze(self.q), self.q) helpers.assert_quantity_equal( @@ -199,7 +199,7 @@ def test_squeeze(self): # Changing number of dimensions # Joining arrays - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_concat_stack(self, subtests): for func in (np.concatenate, np.stack, np.hstack, np.vstack, np.dstack): with subtests.test(func=func): @@ -218,7 +218,7 @@ def test_concat_stack(self, subtests): with pytest.raises(DimensionalityError): func([nz.m, self.q]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_block_column_stack(self, subtests): for func in (np.block, np.column_stack): with subtests.test(func=func): @@ -255,7 +255,7 @@ def test_block_column_stack(self, subtests): with pytest.raises(DimensionalityError): func([nz[:, 0].m, self.q[:, 0]]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_append(self): helpers.assert_quantity_equal( np.append(self.q, [[0, 0]] * self.ureg.m, axis=0), @@ -275,7 +275,7 @@ def test_item(self): class TestNumpyMathematicalFunctions(TestNumpyMethods): # https://www.numpy.org/devdocs/reference/routines.math.html # Trigonometric functions - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_unwrap(self): helpers.assert_quantity_equal( np.unwrap([0, 3 * np.pi] * self.ureg.radians), [0, np.pi] @@ -286,7 +286,7 @@ def test_unwrap(self): # Rounding - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_fix(self): helpers.assert_quantity_equal(np.fix(3.14 * self.ureg.m), 3.0 * self.ureg.m) helpers.assert_quantity_equal(np.fix(3.0 * self.ureg.m), 3.0 * self.ureg.m) @@ -297,7 +297,7 @@ def test_fix(self): # Sums, products, differences - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_prod(self): axis = 0 where = [[True, False], [True, True]] @@ -306,7 +306,7 @@ def test_prod(self): helpers.assert_quantity_equal(self.q.prod(axis=axis), [3, 8] * self.ureg.m ** 2) helpers.assert_quantity_equal(self.q.prod(where=where), 12 * self.ureg.m ** 3) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_prod_numpy_func(self): axis = 0 where = [[True, False], [True, True]] @@ -334,13 +334,13 @@ def test_sum(self): helpers.assert_quantity_equal(self.q.sum(0), [4, 6] * self.ureg.m) helpers.assert_quantity_equal(self.q.sum(1), [3, 7] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_sum_numpy_func(self): helpers.assert_quantity_equal(np.sum(self.q, axis=0), [4, 6] * self.ureg.m) with pytest.raises(OffsetUnitCalculusError): np.sum(self.q_temperature) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nansum_numpy_func(self): helpers.assert_quantity_equal( np.nansum(self.q_nan, axis=0), [4, 2] * self.ureg.m @@ -351,7 +351,7 @@ def test_cumprod(self): self.q.cumprod() helpers.assert_quantity_equal((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_cumprod_numpy_func(self): with pytest.raises(DimensionalityError): np.cumprod(self.q) @@ -365,7 +365,7 @@ def test_cumprod_numpy_func(self): np.cumprod(self.q / self.ureg.m, axis=1), [[1, 2], [3, 12]] ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nancumprod_numpy_func(self): with pytest.raises(DimensionalityError): np.nancumprod(self.q_nan) @@ -373,21 +373,21 @@ def test_nancumprod_numpy_func(self): np.nancumprod(self.q_nan / self.ureg.m), [1, 2, 6, 6] ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_diff(self): helpers.assert_quantity_equal(np.diff(self.q, 1), [[1], [1]] * self.ureg.m) helpers.assert_quantity_equal( np.diff(self.q_temperature, 1), [[1], [1]] * self.ureg.delta_degC ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_ediff1d(self): helpers.assert_quantity_equal(np.ediff1d(self.q), [1, 1, 1] * self.ureg.m) helpers.assert_quantity_equal( np.ediff1d(self.q_temperature), [1, 1, 1] * self.ureg.delta_degC ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_gradient(self): grad = np.gradient([[1, 1], [3, 4]] * self.ureg.m, 1 * self.ureg.J) helpers.assert_quantity_equal( @@ -405,7 +405,7 @@ def test_gradient(self): grad[1], [[0.0, 0.0], [1.0, 1.0]] * self.ureg.delta_degC / self.ureg.J ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_cross(self): a = [[3, -3, 1]] * self.ureg.kPa b = [[4, 9, 2]] * self.ureg.m ** 2 @@ -413,27 +413,27 @@ def test_cross(self): np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m ** 2 ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_trapz(self): helpers.assert_quantity_equal( np.trapz([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), 7.5 * self.ureg.J * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_dot(self): helpers.assert_quantity_equal( self.q.ravel().dot(np.array([1, 0, 0, 1])), 5 * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_dot_numpy_func(self): helpers.assert_quantity_equal( np.dot(self.q.ravel(), [0, 0, 1, 0] * self.ureg.dimensionless), 3 * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_einsum(self): a = np.arange(25).reshape(5, 5) * self.ureg.m b = np.arange(5) * self.ureg.m @@ -447,7 +447,7 @@ def test_einsum(self): np.array([30, 80, 130, 180, 230]) * self.ureg.m ** 2, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_solve(self): helpers.assert_quantity_almost_equal( np.linalg.solve(self.q, [[3], [7]] * self.ureg.s), @@ -578,7 +578,7 @@ def test_sort(self): q.sort() helpers.assert_quantity_equal(q, [1, 2, 3, 4, 5, 6] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_sort_numpy_func(self): q = [4, 5, 2, 3, 1, 6] * self.ureg.m helpers.assert_quantity_equal(np.sort(q), [1, 2, 3, 4, 5, 6] * self.ureg.m) @@ -587,7 +587,7 @@ def test_argsort(self): q = [1, 4, 5, 6, 2, 9] * self.ureg.MeV self.assertNDArrayEqual(q.argsort(), [0, 4, 1, 2, 3, 5]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_argsort_numpy_func(self): self.assertNDArrayEqual(np.argsort(self.q, axis=0), np.array([[0, 0], [1, 1]])) @@ -595,7 +595,7 @@ def test_diagonal(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m helpers.assert_quantity_equal(q.diagonal(offset=1), [2, 3] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_diagonal_numpy_func(self): q = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] * self.ureg.m helpers.assert_quantity_equal(np.diagonal(q, offset=-1), [1, 2] * self.ureg.m) @@ -608,7 +608,7 @@ def test_compress(self): self.q.compress([False, True], axis=1), [[2], [4]] * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_compress_nep18(self): helpers.assert_quantity_equal( np.compress([False, True], self.q, axis=1), [[2], [4]] * self.ureg.m @@ -621,7 +621,7 @@ def test_searchsorted(self): with pytest.raises(DimensionalityError): q.searchsorted([1.5, 2.5]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_searchsorted_numpy_func(self): """Test searchsorted as numpy function.""" q = self.q.flatten() @@ -631,26 +631,26 @@ def test_nonzero(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m self.assertNDArrayEqual(q.nonzero()[0], [0, 2, 3, 5]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m self.assertNDArrayEqual(np.nonzero(q)[0], [0, 2, 3, 5]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_any_numpy_func(self): q = [0, 1] * self.ureg.m assert np.any(q) with pytest.raises(ValueError): np.any(self.q_temperature) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_all_numpy_func(self): q = [0, 1] * self.ureg.m assert not np.all(q) with pytest.raises(ValueError): np.all(self.q_temperature) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_count_nonzero_numpy_func(self): q = [1, 0, 5, 6, 0, 9] * self.ureg.m assert np.count_nonzero(q) == 4 @@ -661,29 +661,29 @@ def test_max(self): def test_max_numpy_func(self): assert np.max(self.q) == 4 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_max_with_axis_arg(self): helpers.assert_quantity_equal(np.max(self.q, axis=1), [2, 4] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_max_with_initial_arg(self): helpers.assert_quantity_equal( np.max(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[3, 3], [3, 4]] * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanmax(self): assert np.nanmax(self.q_nan) == 3 * self.ureg.m def test_argmax(self): assert self.q.argmax() == 3 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_argmax_numpy_func(self): self.assertNDArrayEqual(np.argmax(self.q, axis=0), np.array([1, 1])) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanargmax_numpy_func(self): self.assertNDArrayEqual(np.nanargmax(self.q_nan, axis=0), np.array([1, 0])) @@ -695,33 +695,33 @@ def test_maximum(self): def test_min(self): assert self.q.min() == 1 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_min_numpy_func(self): assert np.min(self.q) == 1 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_min_with_axis_arg(self): helpers.assert_quantity_equal(np.min(self.q, axis=1), [1, 3] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_min_with_initial_arg(self): helpers.assert_quantity_equal( np.min(self.q[..., None], axis=2, initial=3 * self.ureg.m), [[1, 2], [3, 3]] * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanmin(self): assert np.nanmin(self.q_nan) == 1 * self.ureg.m def test_argmin(self): assert self.q.argmin() == 0 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_argmin_numpy_func(self): self.assertNDArrayEqual(np.argmin(self.q, axis=0), np.array([0, 0])) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanargmin_numpy_func(self): self.assertNDArrayEqual(np.nanargmin(self.q_nan, axis=0), np.array([0, 0])) @@ -733,7 +733,7 @@ def test_minimum(self): def test_ptp(self): assert self.q.ptp() == 3 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_ptp_numpy_func(self): helpers.assert_quantity_equal(np.ptp(self.q, axis=0), [2, 2] * self.ureg.m) @@ -753,7 +753,7 @@ def test_clip(self): with pytest.raises(DimensionalityError): self.q.clip(1) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_clip_numpy_func(self): helpers.assert_quantity_equal( np.clip(self.q, 150 * self.ureg.cm, None), [[1.5, 2], [3, 4]] * self.ureg.m @@ -765,7 +765,7 @@ def test_round(self): helpers.assert_quantity_equal(q.round(-1), [0, 0, 10, 20] * self.ureg.m) helpers.assert_quantity_equal(q.round(1), [1, 1.3, 5.7, 22] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_round_numpy_func(self): helpers.assert_quantity_equal( np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m @@ -780,13 +780,13 @@ def test_trace(self): def test_cumsum(self): helpers.assert_quantity_equal(self.q.cumsum(), [1, 3, 6, 10] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_cumsum_numpy_func(self): helpers.assert_quantity_equal( np.cumsum(self.q, axis=0), [[1, 2], [4, 6]] * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nancumsum_numpy_func(self): helpers.assert_quantity_equal( np.nancumsum(self.q_nan, axis=0), [[1, 2], [4, 2]] * self.ureg.m @@ -795,16 +795,16 @@ def test_nancumsum_numpy_func(self): def test_mean(self): assert self.q.mean() == 2.5 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_mean_numpy_func(self): assert np.mean(self.q) == 2.5 * self.ureg.m assert np.mean(self.q_temperature) == self.Q_(2.5, self.ureg.degC) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanmean_numpy_func(self): assert np.nanmean(self.q_nan) == 2 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_average_numpy_func(self): helpers.assert_quantity_almost_equal( np.average(self.q, axis=0, weights=[1, 2]), @@ -812,22 +812,22 @@ def test_average_numpy_func(self): rtol=1e-5, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_median_numpy_func(self): assert np.median(self.q) == 2.5 * self.ureg.m - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanmedian_numpy_func(self): assert np.nanmedian(self.q_nan) == 2 * self.ureg.m def test_var(self): assert self.q.var() == 1.25 * self.ureg.m ** 2 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_var_numpy_func(self): assert np.var(self.q) == 1.25 * self.ureg.m ** 2 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanvar_numpy_func(self): helpers.assert_quantity_almost_equal( np.nanvar(self.q_nan), 0.66667 * self.ureg.m ** 2, rtol=1e-5 @@ -838,7 +838,7 @@ def test_std(self): self.q.std(), 1.11803 * self.ureg.m, rtol=1e-5 ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_std_numpy_func(self): helpers.assert_quantity_almost_equal( np.std(self.q), 1.11803 * self.ureg.m, rtol=1e-5 @@ -851,7 +851,7 @@ def test_cumprod(self): self.q.cumprod() helpers.assert_quantity_equal((self.q / self.ureg.m).cumprod(), [1, 2, 6, 24]) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanstd_numpy_func(self): helpers.assert_quantity_almost_equal( np.nanstd(self.q_nan), 0.81650 * self.ureg.m, rtol=1e-5 @@ -966,41 +966,41 @@ def test_shape(self): u.shape = 4, 3 assert u.magnitude.shape == (4, 3) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_shape_numpy_func(self): assert np.shape(self.q) == (2, 2) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_alen_numpy_func(self): assert np.alen(self.q) == 2 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_ndim_numpy_func(self): assert np.ndim(self.q) == 2 - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_copy_numpy_func(self): q_copy = np.copy(self.q) helpers.assert_quantity_equal(self.q, q_copy) assert self.q is not q_copy - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_trim_zeros_numpy_func(self): q = [0, 4, 3, 0, 2, 2, 0, 0, 0] * self.ureg.m helpers.assert_quantity_equal(np.trim_zeros(q), [4, 3, 0, 2, 2] * self.ureg.m) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_result_type_numpy_func(self): assert np.result_type(self.q) == np.dtype("int") - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nan_to_num_numpy_func(self): helpers.assert_quantity_equal( np.nan_to_num(self.q_nan, nan=-999 * self.ureg.mm), [[1, 2], [3, -0.999]] * self.ureg.m, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_meshgrid_numpy_func(self): x = [1, 2] * self.ureg.m y = [0, 50, 100] * self.ureg.mm @@ -1008,7 +1008,7 @@ def test_meshgrid_numpy_func(self): helpers.assert_quantity_equal(xx, [[1, 2], [1, 2], [1, 2]] * self.ureg.m) helpers.assert_quantity_equal(yy, [[0, 0], [50, 50], [100, 100]] * self.ureg.mm) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_isclose_numpy_func(self): q2 = [[1000.05, 2000], [3000.00007, 4001]] * self.ureg.mm self.assertNDArrayEqual( @@ -1019,7 +1019,7 @@ def test_isclose_numpy_func(self): np.array([[False, True], [True, False]]), ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_interp_numpy_func(self): x = [1, 4] * self.ureg.m xp = np.linspace(0, 3, 5) * self.ureg.m @@ -1047,7 +1047,7 @@ def test_comparisons(self): self.q < 2 * self.ureg.m, np.array([[True, False], [False, False]]) ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_where(self): helpers.assert_quantity_equal( np.where(self.q >= 2 * self.ureg.m, self.q, 20 * self.ureg.m), @@ -1084,13 +1084,13 @@ def test_where(self): 0 * self.ureg.J, ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_fabs(self): helpers.assert_quantity_equal( np.fabs(self.q - 2 * self.ureg.m), self.Q_([[1, 0], [1, 2]], "m") ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_isin(self): self.assertNDArrayEqual( np.isin(self.q, self.Q_([0, 2, 4], "m")), @@ -1114,27 +1114,27 @@ def test_isin(self): with pytest.raises(ValueError): np.isin(self.q.m, self.q) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_percentile(self): helpers.assert_quantity_equal(np.percentile(self.q, 25), self.Q_(1.75, "m")) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanpercentile(self): helpers.assert_quantity_equal( np.nanpercentile(self.q_nan, 25), self.Q_(1.5, "m") ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_quantile(self): helpers.assert_quantity_equal(np.quantile(self.q, 0.25), self.Q_(1.75, "m")) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_nanquantile(self): helpers.assert_quantity_equal( np.nanquantile(self.q_nan, 0.25), self.Q_(1.5, "m") ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_copyto(self): a = self.q.m q = copy.copy(self.q) @@ -1145,19 +1145,19 @@ def test_copyto(self): np.copyto(a, q) self.assertNDArrayEqual(a, np.array([[2, 2], [0, 4]])) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_tile(self): helpers.assert_quantity_equal( np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_rot90(self): helpers.assert_quantity_equal( np.rot90(self.q), np.array([[2, 4], [1, 3]]) * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_insert(self): helpers.assert_quantity_equal( np.insert(self.q, 1, 0 * self.ureg.m, axis=1), @@ -1178,13 +1178,13 @@ def test_array_protocol_unavailable(self): with pytest.raises(AttributeError): getattr(self.q, attr) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_resize(self): helpers.assert_quantity_equal( np.resize(self.q, (2, 4)), [[1, 2, 3, 4], [1, 2, 3, 4]] * self.ureg.m ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_pad(self): # Tests reproduced with modification from NumPy documentation a = [1, 2, 3, 4, 5] * self.ureg.m @@ -1291,14 +1291,14 @@ def pad_with(vector, pad_width, iaxis, kwargs): ), ) # Note: Does not support Quantity pad_with vectorized callable use - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_allclose(self): assert np.allclose([1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.m) assert not np.allclose( [1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.mm ) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_intersect1d(self): helpers.assert_quantity_equal( np.intersect1d([1, 3, 4, 3] * self.ureg.m, [3, 1, 2, 1] * self.ureg.m), diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index fb1fc5d25..aa6326303 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -563,7 +563,7 @@ def test_notiter(self): with pytest.raises(TypeError): iter(x) - @helpers.requires_array_function_protocol + @helpers.requires_array_function_protocol() def test_no_longer_array_function_warning_on_creation(self): # Test that warning is no longer raised on first creation with warnings.catch_warnings(): From 4548e512a9486abe4476b340d3b303679a10e51c Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 16 Jan 2021 19:10:23 -0300 Subject: [PATCH 587/612] Linters --- pint/testsuite/helpers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index 2f4064bed..a0753f541 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -193,17 +193,17 @@ def requires_numpy_previous_than(version): if not HAS_NUMPY: return pytest.mark.skip("Requires NumPy") return pytest.mark.skipif( - not LooseVersion(NUMPY_VER) < LooseVersion(version), - reason="Requires NumPy < %s" % version, - ) + not LooseVersion(NUMPY_VER) < LooseVersion(version), + reason="Requires NumPy < %s" % version, + ) def requires_numpy_at_least(version): if not HAS_NUMPY: return pytest.mark.skip("Requires NumPy") return pytest.mark.skipif( - not LooseVersion(NUMPY_VER) >= LooseVersion(version), - reason="Requires NumPy >= %s" % version, + not LooseVersion(NUMPY_VER) >= LooseVersion(version), + reason="Requires NumPy >= %s" % version, ) From 227604061c13b8d5d8be33393b834515391e9097 Mon Sep 17 00:00:00 2001 From: Hernan Date: Sat, 16 Jan 2021 21:31:31 -0300 Subject: [PATCH 588/612] Fixed tests broken in merge master --- pint/testsuite/test_contexts.py | 63 +++++++++++++++++++++++++++------ pint/testsuite/test_issues.py | 1 - 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py index 852482888..91070cf4d 100644 --- a/pint/testsuite/test_contexts.py +++ b/pint/testsuite/test_contexts.py @@ -839,7 +839,7 @@ def test_define_nan(): assert q.units.dimensionality == {"[currency]": 1} assert q.to("GBP").magnitude == 10 assert math.isnan(q.to("USD").magnitude) - assert round(abs(q.to("USD", "c").magnitude - 10 * 1.18 * 1.11), 7) == 0 + assert math.isclose(q.to("USD", "c").magnitude, 10 * 1.18 * 1.11) def test_non_multiplicative(subtests): @@ -863,20 +863,28 @@ def test_non_multiplicative(subtests): k = ureg.Quantity(100, "kelvin") with subtests.test("baseline"): - assert round(abs(k.to("fahrenheit").magnitude - (100 - 255) * 9 / 5), 7) == 0 - assert round(abs(k.to("bogodegrees").magnitude - 100 / 9), 7) == 0 + helpers.assert_quantity_almost_equal( + k.to("fahrenheit").magnitude, (100 - 255) * 9 / 5 + ) + helpers.assert_quantity_almost_equal(k.to("bogodegrees").magnitude, 100 / 9) with subtests.test("nonmult_to_nonmult"): with ureg.context("nonmult_to_nonmult"): - assert round(abs(k.to("fahrenheit").magnitude - (100 - 123) / 7), 7) == 0 + helpers.assert_quantity_almost_equal( + k.to("fahrenheit").magnitude, (100 - 123) / 7 + ) with subtests.test("nonmult_to_mult"): with ureg.context("nonmult_to_mult"): - assert round(abs(k.to("fahrenheit").magnitude - 100 / 123), 7) == 0 + helpers.assert_quantity_almost_equal( + k.to("fahrenheit").magnitude, 100 / 123 + ) with subtests.test("mult_to_nonmult"): with ureg.context("mult_to_nonmult"): - assert round(abs(k.to("bogodegrees").magnitude - (100 - 123) / 5), 7) == 0 + helpers.assert_quantity_almost_equal( + k.to("bogodegrees").magnitude, (100 - 123) / 5 + ) def test_stack_contexts(): @@ -912,28 +920,49 @@ def test_stack_contexts(): assert q.to("d", "c1", "c2").magnitude == 3 # c2 takes precedence +def test_err_change_base_unit(): + ureg = UnitRegistry( + """ + foo = [d1] + bar = [d2] + + @context c + bar = foo + @end + """.splitlines() + ) + + expected = "Can't redefine a base unit to a derived one" + with pytest.raises(ValueError, match=expected): + ureg.enable_contexts("c") + + def test_err_to_base_unit(): expected = "Can't define base units within a context" with pytest.raises(DefinitionSyntaxError, match=expected): Context.from_lines(["@context c", "x = [d]"]) -def test_err_change_base_unit(): +def test_err_change_dimensionality(): ureg = UnitRegistry( """ foo = [d1] bar = [d2] + baz = foo @context c - bar = foo + baz = bar @end """.splitlines() ) - expected = "Can't redefine a base unit to a derived one" + expected = re.escape( + "Can't change dimensionality of baz from [d1] to [d2] in a context" + ) with pytest.raises(ValueError, match=expected): ureg.enable_contexts("c") + def test_err_cyclic_dependency(): ureg = UnitRegistry( """ @@ -990,4 +1019,18 @@ def test_err_redefine_with_prefix(): expected = "Can't redefine a unit with a prefix: kilopound" with pytest.raises(ValueError, match=expected): - ureg.enable_contexts("c") \ No newline at end of file + ureg.enable_contexts("c") + + +def test_err_new_unit(): + ureg = UnitRegistry( + """ + foo = [d] + @context c + bar = foo + @end + """.splitlines() + ) + expected = "'bar' is not defined in the unit registry" + with pytest.raises(UndefinedUnitError, match=expected): + ureg.enable_contexts("c") diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 45611ac8f..52309f23a 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -102,7 +102,6 @@ def test_issue45(self): ) helpers.assert_quantity_almost_equal(float(ureg.V / ureg.mV), 1000.0) - @helpers.requires_numpy def test_issue45b(self): helpers.assert_quantity_almost_equal( From a58852cadb4c5759195854d47b2834d3e2b65993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Mon, 18 Jan 2021 21:15:00 +0100 Subject: [PATCH 589/612] Add github actions --- .github/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++++ .github/workflows/docs.yml | 46 ++++++++++++++++++++ .github/workflows/lint.yml | 17 ++++++++ CHANGES | 1 + requirements_docs.txt | 2 +- 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..676b456c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + numpy: [null, "numpy>=1.14,<2.0.0"] + uncertainties: [null, "uncertainties==3.0.1", "uncertainties>=3.0.1,<4.0.0"] + extras: [null] + include: + - python-version: 3.6 + numpy: numpy==1.14.6 + extras: matplotlib==2.2.5 + - python-version: 3.8 + numpy: "numpy" + uncertainties: "uncertainties" + extras: "sparse xarray netCDF4 dask[complete] graphviz babel==2.8" + - python-version: 3.9 + numpy: numpy + + runs-on: ubuntu-latest + + env: + TEST_OPTS: "-rfsxEX -s --cov=pint --cov-config=.coveragerc" + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 100 + + - name: Get tags + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: echo "::set-output name=dir::$(pip cache dir)" + + - name: Setup caching + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: pip-${{ matrix.python-version }} + restore-keys: | + pip-${{ matrix.python-version }} + + - name: Install numpy + if: ${{ matrix.numpy != null }} + run: pip install "${{matrix.numpy}}" + + - name: Install uncertainties + if: ${{ matrix.uncertainties != null }} + run: pip install "${{matrix.uncertainties}}" + + - name: Install extras + if: ${{ matrix.extras != null }} + run: pip install ${{matrix.extras}} + + - name: Install dependencies + run: | + sudo apt install -y graphviz + pip install .[test] + + - name: Run Tests + run: | + pytest $TEST_OPTS + + - name: Coverage report + run: coverage report -m + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..5ed4fe7aa --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,46 @@ +name: Documentation Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 100 + + - name: Get tags + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Get pip cache dir + id: pip-cache + run: echo "::set-output name=dir::$(pip cache dir)" + + - name: Setup pip cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: pip-docs + restore-keys: pip-docs + + - name: Install dependencies + run: | + sudo apt install -y pandoc + pip install --upgrade pip setuptools wheel + pip install -r "requirements_docs.txt" + pip install docutils==0.14 commonmark==0.8.1 recommonmark==0.5.0 babel==2.8 + pip install . + + - name: Build documentation + run: sphinx-build -n -j auto -b html -d build/doctrees docs build/html + + - name: Doc Tests + run: sphinx-build -a -j auto -b doctest -d build/doctrees docs build/doctest + \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..b10a674fb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - name: Lint + uses: pre-commit/action@v2.0.0 + with: + extra_args: --all-files --show-diff-on-failure \ No newline at end of file diff --git a/CHANGES b/CHANGES index 5bb632bb3..b93b83bcd 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Pint Changelog (Issue #1195, thanks jules-ch) - UnitsContainer returns false if other is str and cannnot be parsed (Issue #1179, thanks rfrowe) +- Add Github Actions CI. (Issue #1236) 0.16.1 (2020-09-22) ------------------- diff --git a/requirements_docs.txt b/requirements_docs.txt index b5fb8a86a..248faa885 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -3,7 +3,7 @@ matplotlib nbsphinx numpy pytest -pandas>=1.0.4 +pandas==1.1.3 pint-pandas jupyter_client ipykernel From 25ba6176d90b9463c863e2167032f9260255b99d Mon Sep 17 00:00:00 2001 From: Hernan Date: Wed, 20 Jan 2021 13:30:36 -0300 Subject: [PATCH 590/612] Remove .travis.yml --- .travis.yml | 83 ----------------------------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d81d89fab..000000000 --- a/.travis.yml +++ /dev/null @@ -1,83 +0,0 @@ -language: python - -branches: - # prevent bors temporary branches to be built - except: - - staging.tmp - - trying.tmp - -env: - # This project adheres to NEP-29 - # https://numpy.org/neps/nep-0029-deprecation_policy.html - - # Refer to https://docs.scipy.org/doc/numpy/release.html for - # min/max Python version supported by numpy - # Refer to history of https://github.com/lebigot/uncertainties/blob/master/setup.py - # for min/max Python versions supported by uncertainties - - - PKGS="python=3.8 pre-commit" - # Pinned packages to match readthedocs CI https://readthedocs.org/projects/pint/ - - PKGS="python=3.7 ipython matplotlib nbsphinx numpy pandas==1.1.3 jupyter_client ipykernel python-graphviz graphviz xarray sparse dask[complete] sphinx Pygments==2.3.1 docutils==0.14 alabaster commonmark==0.8.1 recommonmark==0.5.0 babel==2.8" - - PKGS="python=3.6" - - PKGS="python=3.7" - - PKGS="python=3.8" - - PKGS="python=3.6 uncertainties=3.0" - - PKGS="python=3.7 uncertainties=3.0" - - PKGS="python=3.6 numpy=1.14 matplotlib" - - PKGS="python=3.7 numpy=1.14 matplotlib" - - PKGS="python=3.8 numpy=1.17 matplotlib" - - PKGS="python=3.6 numpy=1.14 uncertainties=3.0" - - PKGS="python=3.7 numpy=1.14 uncertainties=3.0" - - PKGS="python=3.6 numpy uncertainties" - - PKGS="python=3.7 numpy uncertainties" - - PKGS="python=3.8 numpy uncertainties" - - PKGS="python=3.8 numpy uncertainties sparse xarray netCDF4" - - # TODO: pandas tests - # - PKGS="python=3.7 numpy pandas uncertainties pandas" - -before_install: - - sudo apt-get update - - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - - bash miniconda.sh -b -p $HOME/miniconda - - export PATH="$HOME/miniconda/bin:$PATH" - - hash -r - - conda config --set always_yes yes --set changeps1 no - - conda config --add channels conda-forge - - conda update -q conda - # Useful for debugging any issues with conda - - conda info -a - - # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda - # But broke travis 2019-08 - # - sudo rm -rf /dev/shm - # - sudo ln -s /run/shm /dev/shm - - export TEST_OPTS="-rfsxEX -s --cov=pint --cov-config=.coveragerc" - -install: - - conda create -n travis $PKGS pytest pytest-cov coveralls pytest-subtests - - source activate travis - - if [[ $PKGS =~ pandas ]]; then PANDAS=1; else PANDAS=0; fi - - if [[ $PKGS =~ pre-commit ]]; then LINT=1; else LINT=0; fi - - if [[ $PKGS =~ sphinx ]]; then DOCS=1; else DOCS=0; fi - - if [[ $PKGS =~ matplotlib && $DOCS == 0 ]]; then pip install pytest-mpl; export TEST_OPTS="$TEST_OPTS --mpl"; fi - - if [[ $DOCS == 1 ]]; then pip install pint-pandas; fi - # this is superslow but suck it up until updates to pandas are made - # - if [[ $PANDAS == '1' ]]; then pip install numpy cython pytest pytest-cov nbval; pip install git+https://github.com/pandas-dev/pandas.git@bdb7a1603f1e0948ca0cab011987f616e7296167; python -c 'import pandas; print(pandas.__version__)'; fi - - conda list - - pip install . - -script: - # if we're doing the pandas tests and hence have pytest available, we can - # simply use it to run all the tests - # - if [[ $PANDAS == '1' ]]; then python -bb -m coverage run -p --source=pint --omit="*test*","*compat*" -m py.test -rfsxEX; fi - # test notebooks too if pandas available - # - if [[ $PANDAS == '1' ]]; then pip install -e .; pytest --nbval notebooks/*; fi - - if [[ $PANDAS == 0 && $LINT == 0 && $DOCS == 0 ]]; then python -bb -m pytest $TEST_OPTS; fi - - if [[ $LINT == 1 ]]; then pre-commit run --all-files; fi - - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -n -j auto -b html -d build/doctrees docs build/html; fi - - if [[ $DOCS == 1 ]]; then PYTHONPATH=$PWD sphinx-build -a -j auto -b doctest -d build/doctrees docs build/doctest; fi - - if [[ $LINT == 0 && $DOCS == 0 ]]; then coverage report -m; fi - -after_success: - - coveralls --verbose From f08859f9c66dacc295bd6400b46056ffdf5fe2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Wed, 20 Jan 2021 17:38:43 +0100 Subject: [PATCH 591/612] Fix coveralls upload --- .github/workflows/ci.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 676b456c3..dc5235245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,12 +75,27 @@ jobs: - name: Coverage report run: coverage report -m - finish: + - name: Coveralls Parallel + env: + COVERALLS_FLAG_NAME: ${{ matrix.test-number }} + COVERALLS_PARALLEL: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_SERVICE_NAME: github + run: | + pip install coveralls + coveralls + + coveralls: needs: test runs-on: ubuntu-latest steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v1.1.2 + - uses: actions/setup-python@v2 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true \ No newline at end of file + python-version: 3.x + - name: Coveralls Finished + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_SERVICE_NAME: github + run: | + pip install coveralls + coveralls --finish From 397022713184adb0acd9eedf15d1df236db39a7a Mon Sep 17 00:00:00 2001 From: Hernan Date: Tue, 16 Feb 2021 00:55:52 -0300 Subject: [PATCH 592/612] Fixed badges --- BADGES.rst | 29 ----------------------------- README.rst | 8 ++++++-- 2 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 BADGES.rst diff --git a/BADGES.rst b/BADGES.rst deleted file mode 100644 index a4c936937..000000000 --- a/BADGES.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. image:: https://img.shields.io/pypi/v/pint.svg - :target: https://pypi.python.org/pypi/pint - :alt: Latest Version - -.. image:: https://readthedocs.org/projects/pip/badge/ - :target: http://pint.readthedocs.org/ - :alt: Documentation - -.. image:: https://img.shields.io/pypi/l/pint.svg - :target: https://pypi.python.org/pypi/pint - :alt: License - -.. image:: https://img.shields.io/pypi/pyversions/pint.svg - :target: https://pypi.python.org/pypi/pint - :alt: Python Versions - -.. image:: https://travis-ci.org/hgrecco/pint.svg?branch=master - :target: https://travis-ci.org/hgrecco/pint - :alt: CI - -.. image:: https://coveralls.io/repos/github/hgrecco/pint/badge.svg?branch=master - :target: https://coveralls.io/github/hgrecco/pint?branch=master - :alt: Coverage - -.. image:: https://readthedocs.org/projects/pint/badge/ - :target: http://pint.readthedocs.org/ - :alt: Docs - - diff --git a/README.rst b/README.rst index 97cda301a..3ed1938ad 100644 --- a/README.rst +++ b/README.rst @@ -14,10 +14,14 @@ :target: https://pypi.python.org/pypi/pint :alt: Python Versions -.. image:: https://travis-ci.org/hgrecco/pint.svg?branch=master - :target: https://travis-ci.org/hgrecco/pint +.. image:: https://github.com/hgrecco/pint/workflows/CI/badge.svg + :target: https://github.com/hgrecco/pint/actions?query=workflow%3ACI :alt: CI +.. image:: https://github.com/hgrecco/pint/workflows/Lint/badge.svg + :target: https://github.com/hgrecco/pint/actions?query=workflow%3ALint + :alt: LINTER + .. image:: https://coveralls.io/repos/github/hgrecco/pint/badge.svg?branch=master :target: https://coveralls.io/github/hgrecco/pint?branch=master :alt: Coverage From 146f4f16a6268860e0f27c1e129df0ac341eebb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Tue, 16 Feb 2021 17:06:03 +0100 Subject: [PATCH 593/612] Fix numpy.linalg.solve units output. Update get_op_output_unit with new type invdiv. It outputs the product of the following units over the first one in the args list. Update tests with values & np.dot(A, x) == b. Where x = np.linalg.solve(A, b) Closes #1246 --- CHANGES | 1 + pint/numpy_func.py | 14 ++++++++++++-- pint/testsuite/test_numpy.py | 11 +++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index b93b83bcd..cf50d1eca 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ Pint Changelog - UnitsContainer returns false if other is str and cannnot be parsed (Issue #1179, thanks rfrowe) - Add Github Actions CI. (Issue #1236) +- Fix numpy.linalg.solve unit output. (Issue #1246) 0.16.1 (2020-09-22) ------------------- diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 1dce04449..c335f3d2f 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -148,6 +148,7 @@ def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): - "sqrt": square root of `first_input_units` - "reciprocal": reciprocal of `first_input_units` - "size": `first_input_units` raised to the power of `size` + - "invdiv": inverse of `div`, product of all following units divided by first argument unit Parameters ---------- @@ -205,7 +206,15 @@ def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): if size is None: raise ValueError('size argument must be given when unit_op=="size"') result_unit = first_input_units ** size - + elif unit_op == "invdiv": + # Start with first arg in numerator, all others in denominator + product = getattr( + all_args[0], "units", first_input_units._REGISTRY.parse_units("") + ) + for x in all_args[1:]: + if hasattr(x, "units"): + product /= x.units + result_unit = product ** -1 else: raise ValueError("Output unit method {} not understood".format(unit_op)) @@ -304,6 +313,7 @@ def implementation(*args, **kwargs): "delta", "delta,div", "div", + "invdiv", "variance", "square", "sqrt", @@ -878,7 +888,7 @@ def implementation(a, *args, **kwargs): for func_str in ["gradient"]: implement_func("function", func_str, input_units=None, output_unit="delta,div") for func_str in ["linalg.solve"]: - implement_func("function", func_str, input_units=None, output_unit="div") + implement_func("function", func_str, input_units=None, output_unit="invdiv") for func_str in ["var", "nanvar"]: implement_func("function", func_str, input_units=None, output_unit="variance") diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index beea69d32..44d42714c 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -449,10 +449,13 @@ def test_einsum(self): @helpers.requires_array_function_protocol() def test_solve(self): - helpers.assert_quantity_almost_equal( - np.linalg.solve(self.q, [[3], [7]] * self.ureg.s), - self.Q_([[1], [1]], "m / s"), - ) + A = self.q + b = [[3], [7]] * self.ureg.s + x = np.linalg.solve(A, b) + + helpers.assert_quantity_almost_equal(x, self.Q_([[1], [1]], "s / m")) + + helpers.assert_quantity_almost_equal(np.dot(A, x), b) # Arithmetic operations def test_addition_with_scalar(self): From 59b8b0a19385a710f41ff16253a37371b19a5647 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 22 Feb 2021 09:43:36 -0300 Subject: [PATCH 594/612] Update bors.toml to GHA --- .github/workflows/ci.yml | 2 +- .github/workflows/docs.yml | 4 ++-- bors.toml | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc5235245..e8da0fce1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: [push, pull_request] jobs: - test: + test-linux: strategy: matrix: python-version: [3.6, 3.7, 3.8] diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5ed4fe7aa..726a7de37 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Documentation Build on: [push, pull_request] jobs: - build: + docbuild: runs-on: ubuntu-latest steps: @@ -43,4 +43,4 @@ jobs: - name: Doc Tests run: sphinx-build -a -j auto -b doctest -d build/doctrees docs build/doctest - \ No newline at end of file + diff --git a/bors.toml b/bors.toml index 7b67f3062..4240e1acd 100644 --- a/bors.toml +++ b/bors.toml @@ -1,4 +1,8 @@ status = [ - "continuous-integration/travis-ci/push", + "test-linux", + "docbuild", + "lint" ] - +delete_merged_branches = true +timeout_sec = 10800 +block_labels = [ "do-not-merge-yet" ] From 8d80cec02ee1ae2c8c136dadcaa2de740eb7d0c9 Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 22 Feb 2021 11:53:30 -0300 Subject: [PATCH 595/612] Fix ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8da0fce1..33494f8e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: coveralls coveralls: - needs: test + needs: test-linux runs-on: ubuntu-latest steps: - uses: actions/setup-python@v2 From 6cca736d75bc2ccedc8f4b9a415eeb118979f056 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 21:58:07 +0000 Subject: [PATCH 596/612] remove pintpandas import --- pint/__init__.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index 067aa317d..c3b63130d 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -36,17 +36,6 @@ # Backport for Python < 3.8 from importlib_metadata import version -try: - from pint_pandas import PintArray, PintType - - del PintType - del PintArray - - _HAS_PINT_PANDAS = True -except ImportError: - _HAS_PINT_PANDAS = False - _, _pint_pandas_error, _ = sys.exc_info() - try: # pragma: no cover __version__ = version("pint") except Exception: # pragma: no cover From e251a5271bbccc8f6f4b587e1f97b7e738af86b8 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 22:01:17 +0000 Subject: [PATCH 597/612] lint --- pint/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pint/__init__.py b/pint/__init__.py index c3b63130d..3e9addfe0 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -11,8 +11,6 @@ :license: BSD, see LICENSE for more details. """ -import sys - from .context import Context from .errors import ( # noqa: F401 DefinitionSyntaxError, From 9f412c1ad7af3a70ee2d3bf85af350c3123e2008 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 22:03:29 +0000 Subject: [PATCH 598/612] notebook --- docs/pint-pandas.ipynb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index 2052f1fed..7b9e1efb3 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -43,9 +43,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", - "\n", - "First some imports (you don't need to import `pint_pandas` for this to work)" + "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n" ] }, { @@ -55,7 +53,8 @@ "outputs": [], "source": [ "import pandas as pd \n", - "import pint" + "import pint\n", + "import pint_pandas", ] }, { From 1b64963dae9aad4509c40bfb6f749070ea4c73d4 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 23:45:52 +0000 Subject: [PATCH 599/612] plotting --- docs/pint-pandas.ipynb | 104 ++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index 7b9e1efb3..b4e7bc792 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -43,7 +43,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n" + "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", + "\n", + "First some imports (you don't need to import `pint_pandas` for this to work)" ] }, { @@ -53,8 +55,7 @@ "outputs": [], "source": [ "import pandas as pd \n", - "import pint\n", - "import pint_pandas", + "import pint" ] }, { @@ -217,12 +218,13 @@ "metadata": {}, "outputs": [], "source": [ - "test_data = '''speed,mech power,torque,rail pressure,fuel flow rate,fluid power\n", - "rpm,kW,N m,bar,l/min,kW\n", - "1000.0,,10.0,1000.0,10.0,\n", - "1100.0,,10.0,100000000.0,10.0,\n", - "1200.0,,10.0,1000.0,10.0,\n", - "1200.0,,10.0,1000.0,10.0,'''" + "test_data = '''ShaftSpeedIndex,rpm,1200,1200,1200,1600,1600,1600,2300,2300,2300\n", + "pump,,A,B,C,A,B,C,A,B,C\n", + "ShaftSpeed,rpm,1200,1200,1200,1600,1600,1600,2300,2300,2300\n", + "FlowRate,m^3 h^-1,8.72,9.28,9.31,11.61,12.78,13.51,18.32,17.90,19.23\n", + "DifferentialPressure,kPa,162.03,144.16,136.47,286.86,241.41,204.21,533.17,526.74,440.76\n", + "ShaftPower,kW,1.32,1.23,1.18,3.09,2.78,2.50,8.59,8.51,7.61\n", + "Efficiency,dimensionless,30.60,31.16,30.70,30.72,31.83,31.81,32.52,31.67,32.05'''" ] }, { @@ -239,7 +241,7 @@ "metadata": {}, "outputs": [], "source": [ - "df = pd.read_csv(io.StringIO(test_data), header=[0, 1])\n", + "df = pd.read_csv(io.StringIO(test_data), header=[0, 1], index_col = [0,1]).T\n", "# df = pd.read_csv(\"/path/to/test_data.csv\", header=[0, 1])\n", "df" ] @@ -274,7 +276,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As previously, operations between DataFrame columns are unit aware" + "Let's confirm the units have been parsed correctly" ] }, { @@ -283,7 +285,14 @@ "metadata": {}, "outputs": [], "source": [ - "df_.speed * df_.torque" + "df_.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here the h in m^3 h^-1 has been parsed as the planck constant. Let's change the unit to hours." ] }, { @@ -292,7 +301,15 @@ "metadata": {}, "outputs": [], "source": [ - "df_" + "df_['FlowRate'] = pint_pandas.PintArray(df_['FlowRate'].values.quantity.m, dtype = \"pint[m^3/hr]\")\n", + "df_.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As previously, operations between DataFrame columns are unit aware" ] }, { @@ -301,8 +318,17 @@ "metadata": {}, "outputs": [], "source": [ - "df_['mech power'] = df_.speed * df_.torque\n", - "df_['fluid power'] = df_['fuel flow rate'] * df_['rail pressure']\n", + "df_.ShaftPower / df_.ShaftSpeed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_['ShaftTorque'] = df_.ShaftPower / df_.ShaftSpeed\n", + "df_['FluidPower'] = df_['FlowRate'] * df_['DifferentialPressure']\n", "df_" ] }, @@ -335,8 +361,9 @@ "metadata": {}, "outputs": [], "source": [ - "df_['fluid power'] = df_['fluid power'].pint.to(\"kW\")\n", - "df_['mech power'] = df_['mech power'].pint.to(\"kW\")\n", + "df_['FluidPower'] = df_['FluidPower'].pint.to(\"kW\")\n", + "df_['FlowRate'] = df_['FlowRate'].pint.to(\"L/s\")\n", + "df_['ShaftTorque'] = df_['ShaftTorque'].pint.to(\"N m\")\n", "df_.pint.dequantify()" ] }, @@ -373,6 +400,49 @@ "df_.pint.to_base_units().pint.dequantify()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting\n", + "Pint's matplotlib support allows columns with the same dimensionality to be plotted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pint_pandas.PintType.ureg.setup_matplotlib()\n", + "ax = df_[['ShaftPower', 'FluidPower']].unstack(\"pump\").plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ax.yaxis.units" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that indexes cannot store PintArrays, so don't contain unit information" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(ax.xaxis.units)" + ] + }, { "cell_type": "markdown", "metadata": {}, From 718f6782fedd1527effb714b120191627b78821b Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 22:01:17 +0000 Subject: [PATCH 600/612] lint From 0ea5b90e2ee274b7547637ea1c4866b638fbd117 Mon Sep 17 00:00:00 2001 From: andrewgsavage Date: Mon, 1 Mar 2021 23:58:55 +0000 Subject: [PATCH 601/612] notebook --- docs/pint-pandas.ipynb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pint-pandas.ipynb b/docs/pint-pandas.ipynb index b4e7bc792..63f7ddb71 100644 --- a/docs/pint-pandas.ipynb +++ b/docs/pint-pandas.ipynb @@ -45,7 +45,7 @@ "source": [ "This example will show the simplist way to use pandas with pint and the underlying objects. It's slightly fiddly as you are not reading from a file. A more normal use case is given in Reading a csv.\n", "\n", - "First some imports (you don't need to import `pint_pandas` for this to work)" + "First some imports" ] }, { @@ -55,7 +55,8 @@ "outputs": [], "source": [ "import pandas as pd \n", - "import pint" + "import pint\n", + "import pint_pandas", ] }, { From 47efc0b125bf5dc6cb6f16872ae5f35b6554b1fd Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 01:51:09 +0100 Subject: [PATCH 602/612] add a test for sliding_window_view --- pint/testsuite/test_numpy.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 44d42714c..eb5c53564 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1154,6 +1154,13 @@ def test_tile(self): np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m ) + @helpers.requires_array_function_protocol() + def test_sliding_window_view(self): + q = self.Q_([[1, 2, 2, 1], [2, 1, 1, 2], [1, 2, 2, 1]], "m") + actual = np.lib.stride_tricks.sliding_window_view(q, window_shape=(3, 3)) + expected = self.Q_([[[[1, 2, 2], [2, 1, 1], [1, 2, 2]], [[2, 2, 1], [1, 1, 2], [2, 2, 1]]]], "m") + helpers.assert_quantity_equal(actual, expected) + @helpers.requires_array_function_protocol() def test_rot90(self): helpers.assert_quantity_equal( From ef5abc9acc1040d9597ae04727f11893f262e796 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 01:51:24 +0100 Subject: [PATCH 603/612] implement lib.stride_tricks.sliding_window_view --- pint/numpy_func.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index c335f3d2f..da74383e6 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -717,7 +717,13 @@ def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output if np is None: return - func = getattr(np, func_str, None) + if "." not in func_str: + func = getattr(np, func_str, None) + else: + func = np + for part in func_str.split("."): + func = getattr(func, part) + # if NumPy does not implement it, do not implement it either if func is None: return @@ -790,6 +796,7 @@ def implementation(*args, **kwargs): ("compress", "a", True), ("linspace", ["start", "stop"], True), ("tile", "A", True), + ("lib.stride_tricks.sliding_window_view", "x", True), ("rot90", "m", True), ("insert", ["arr", "values"], True), ("resize", "a", True), From 5520af0120049fa00dd140bcbe3d050b77dd805a Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 01:56:03 +0100 Subject: [PATCH 604/612] black --- pint/testsuite/test_numpy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index eb5c53564..0f9feb182 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1158,7 +1158,10 @@ def test_tile(self): def test_sliding_window_view(self): q = self.Q_([[1, 2, 2, 1], [2, 1, 1, 2], [1, 2, 2, 1]], "m") actual = np.lib.stride_tricks.sliding_window_view(q, window_shape=(3, 3)) - expected = self.Q_([[[[1, 2, 2], [2, 1, 1], [1, 2, 2]], [[2, 2, 1], [1, 1, 2], [2, 2, 1]]]], "m") + expected = self.Q_( + [[[[1, 2, 2], [2, 1, 1], [1, 2, 2]], [[2, 2, 1], [1, 1, 2], [2, 2, 1]]]], + "m", + ) helpers.assert_quantity_equal(actual, expected) @helpers.requires_array_function_protocol() From fe8c9a6cc990b6cc24a654619f45e24252a65cbb Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 01:58:09 +0100 Subject: [PATCH 605/612] require numpy >= 1.20 --- pint/testsuite/test_numpy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index 0f9feb182..60d8707fe 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -1154,6 +1154,7 @@ def test_tile(self): np.tile(self.q, 2), np.array([[1, 2, 1, 2], [3, 4, 3, 4]]) * self.ureg.m ) + @helpers.requires_numpy_at_least("1.20") @helpers.requires_array_function_protocol() def test_sliding_window_view(self): q = self.Q_([[1, 2, 2, 1], [2, 1, 1, 2], [1, 2, 2, 1]], "m") From 74d33843d411ea4108c4121a90724332ba3d5f0c Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 02:03:36 +0100 Subject: [PATCH 606/612] provide a default to also work with earlier numpy versions --- pint/numpy_func.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index da74383e6..544348f8f 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -720,9 +720,11 @@ def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output if "." not in func_str: func = getattr(np, func_str, None) else: - func = np - for part in func_str.split("."): - func = getattr(func, part) + parts = func_str.split(".") + module = np + for part in parts[:-1]: + module = getattr(module, part, None) + func = getattr(module, part[-1], None) # if NumPy does not implement it, do not implement it either if func is None: From 3a4ee2ef782b549ae2e13a8e1c28360fe2fcc4cb Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 02:08:17 +0100 Subject: [PATCH 607/612] fix typo --- pint/numpy_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pint/numpy_func.py b/pint/numpy_func.py index 544348f8f..38aab1ae5 100644 --- a/pint/numpy_func.py +++ b/pint/numpy_func.py @@ -724,7 +724,7 @@ def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output module = np for part in parts[:-1]: module = getattr(module, part, None) - func = getattr(module, part[-1], None) + func = getattr(module, parts[-1], None) # if NumPy does not implement it, do not implement it either if func is None: From 7c5e567b1c88ee4d2d2fcb7d8cec7dc626da20cd Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Mar 2021 02:22:41 +0100 Subject: [PATCH 608/612] update CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index e3ef610a6..c0cb09291 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,7 @@ Pint Changelog (Issue #1179, thanks rfrowe) - Add Github Actions CI. (Issue #1236) - Fix numpy.linalg.solve unit output. (Issue #1246) +- Support numpy.lib.stride_tricks.sliding_window_view. (Issue #1255) 0.16.1 (2020-09-22) From e9e8958450344f03829c84659758b5626a642957 Mon Sep 17 00:00:00 2001 From: Maciej Grela Date: Mon, 8 Mar 2021 22:45:07 +0100 Subject: [PATCH 609/612] Add the Wh unit for battery capacity measurements --- pint/default_en.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pint/default_en.txt b/pint/default_en.txt index 7e8c69071..b63d9fe50 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -397,6 +397,7 @@ coulomb = ampere * second = C abcoulomb = 10 * C = abC faraday = e * N_A * mole conventional_coulomb_90 = K_J90 * R_K90 / (K_J * R_K) * coulomb = C_90 +ampere_hour = ampere * hour = Ah # Electric potential [electric_potential] = [energy] / [charge] From 5a140ab5fa5a2b282a64c578c5eb786569751ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jules=20Ch=C3=A9ron?= Date: Tue, 16 Mar 2021 18:56:55 +0100 Subject: [PATCH 610/612] NEP29 supports in docs --- docs/numpy.ipynb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/numpy.ipynb b/docs/numpy.ipynb index 5edf125bf..c221b5382 100644 --- a/docs/numpy.ipynb +++ b/docs/numpy.ipynb @@ -12,6 +12,9 @@ "quite convenient to use [NumPy ndarray](http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html) (or [ndarray-like types supporting NEP-18](https://numpy.org/neps/nep-0018-array-function-protocol.html)),\n", "and therefore these are the array types supported by Pint.\n", "\n", + "Pint follows Numpy's recommendation ([NEP29](https://numpy.org/neps/nep-0029-deprecation_policy.html)) for minimal Numpy/Python versions support across the Scientific Python ecosystem.\n", + "This ensures compatibility with other third party libraries (matplotlib, pandas, scipy).\n", + "\n", "First, we import the relevant packages:" ] }, From 5c2ee4cb031fdc80af67e274dff76eb83c6f3a5f Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 22 Mar 2021 00:59:56 -0300 Subject: [PATCH 611/612] Update CHANGES --- CHANGES | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index c0cb09291..720c9d113 100644 --- a/CHANGES +++ b/CHANGES @@ -4,19 +4,32 @@ Pint Changelog 0.17 (unreleased) ----------------- +- Add the Wh unit for battery capacity measurements + (PR #1260, thanks Maciej Grela) - Fix issue with reducable dimensionless units when using power (Quantity**ndarray) (Issue #1185) - Fix comparisons between Quantities and Measurements. - (Issue #1134, thanks lewisamarshall) -- Implemented benchmarks based on airspeed velocity. -- Fix tolist function with scalar ndarray. - (Issue #1195, thanks jules-ch) + (Issue #1134, thanks lewisamarshall) - UnitsContainer returns false if other is str and cannnot be parsed (Issue #1179, thanks rfrowe) -- Add Github Actions CI. (Issue #1236) - Fix numpy.linalg.solve unit output. (Issue #1246) - Support numpy.lib.stride_tricks.sliding_window_view. (Issue #1255) - +- NEP29 Support docs. +- Move all tests to pytest. +- Fix to __pow__ and __ipow__ +- Migrate to Github Actions. + (Issue #1236) +- Update linter to use pre-commit. +- Quantity comparisons now ensure other is Quantity. +- Add sign function compatibility. + (thanks Robin Tesse) +- Fix scalar to ndarray tolist. +- Fix tolist function with scalar ndarray. + (Issue #1195, thanks jules-ch) +- Corrected typos and dacstrings +- Implements a first benchmark suite in airspeed velocity (asv). +- Power for pseudo-dimensionless units. + (Issue #1185, thanks Kevin Fuhr) 0.16.1 (2020-09-22) ------------------- From 636961a8ac988f5af25799ffdd041da725554bfb Mon Sep 17 00:00:00 2001 From: Hernan Date: Mon, 22 Mar 2021 01:01:45 -0300 Subject: [PATCH 612/612] Preparing release 0.17 --- CHANGES | 2 +- version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 720c9d113..6139187de 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ Pint Changelog ============== -0.17 (unreleased) +0.17 (2021-03-22) ----------------- - Add the Wh unit for battery capacity measurements diff --git a/version.py b/version.py index 40c1ce586..787d5f883 100644 --- a/version.py +++ b/version.py @@ -2,5 +2,5 @@ # flake8: noqa # fmt: off -__version__ = '0.17.dev0' +__version__ = '0.17' # fmt: on