Repository: hgrecco/pint

Issue:
Add aliases to already existing definitions
I'd like to write a custom definitions file that adds new aliases to definitions from defaults_en.txt - as opposed to creating entirely new units.

However, I could not find a way to do it without scouting for the original definition line and writing it again whole.

For example, defaults_en.txt contains:
```
calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th
```

If I want to add an alias, I have to write in my custom definitions file:
```
calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th = CUSTOM_CAL
```
whereas I'd like to just write something like:
```
calorie = ... = CUSTOM_CAL
```

Comments:

Patch:
diff --git a/docs/defining.rst b/docs/defining.rst
index 50c177b..7aba5bb 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 fecc9e0..8adabe7 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
-# =================
-# <canonical name> = <relation to another unit> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+# Syntax
+# ======
+# Units
+# -----
+# <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
 #
 # 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
+# --------
+# <prefix>- = <amount> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+#
+# Example:
+#     deca- =  1e+1  = da- = deka-
+#
+#
+# Derived dimensions
+# ------------------
+# [dimension name] = <relation to other dimensions>
+#
+# 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 <canonical name or previous alias> = <alias> [ = <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 e510a1b..811a471 100644
--- a/pint/definitions.py
+++ b/pint/definitions.py
@@ -52,6 +52,12 @@ class Definition(object):
                     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,10 @@ class Definition(object):
     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
     def converter(self):
         return self._converter
@@ -166,3 +176,12 @@ class DimensionDefinition(Definition):
 
         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 0129ba5..1a24442 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -54,7 +54,7 @@ from .util import (logger, pi_theorem, solve_dependencies, ParserHelper,
 
 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 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
         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 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
         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..
 
@@ -367,7 +379,7 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
 
         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()


Test patch:
diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py
index 3b73fd8..8a66a4e 100644
--- a/pint/testsuite/test_definitions.py
+++ b/pint/testsuite/test_definitions.py
@@ -5,7 +5,7 @@ from __future__ import division, unicode_literals, print_function, absolute_impo
 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 @@ class TestDefinition(BaseTestCase):
         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 5ce35c5..9c6bf51 100644
--- a/pint/testsuite/test_unit.py
+++ b/pint/testsuite/test_unit.py
@@ -648,3 +648,30 @@ class TestConvertWithOffset(QuantityTestCase, ParameterizedTestCase):
             if src != dst:
                 self.assertQuantityAlmostEqual(convert(expected, dst, src),
                                                value, atol=0.001)
+
+    def test_alias(self):
+        # 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")