Repository: yt-project/unyt

Issue:
unyt_quantity.from_string not working on simple expressions, e.g. "g**2/cm**2"
I cannot evaluate simple expressions such as "g\*\*2/cm\*\*2" with` unyt_quantity.from_string`. What am I missing?

```python
import unyt
print(unyt.__version__) # 'v2.9.3'
print(unyt.unyt_quantity.from_string("1e-3 g/cm**3"))  # correct: '0.001 g/cm**3' (taken from docs)
print(unyt.unyt_quantity.from_string("1e-3 g**2 / cm**2")) # wrong: '0.001 g**2'
print(unyt.unyt_quantity.from_string("g/cm**2")) # correct: '1 g/cm**2'
print(unyt.unyt_quantity.from_string("1 g**2/cm**2")) # wrong: '1.0 g**2'
print(unyt.unyt_quantity.from_string("g**2/cm**2")) # ValueError: Received invalid quantity expression 'g**2/cm**2'.
```

* unyt version: v2.9.3
* Python version: 3.9.7
* Operating System: debian 11

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 538fd85..156473a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -28,7 +28,7 @@ repos:
     hooks:
     - id: black
 -   repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.11.4
     hooks:
     - id: isort
       name: isort
diff --git a/unyt/array.py b/unyt/array.py
index 03d3688..df6f691 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -145,7 +145,7 @@ _NUMB_PATTERN = (
 )
 # *all* greek letters are considered valid unit string elements.
 # This may be an overshoot. We rely on unyt.Unit to do the actual validation
-_UNIT_PATTERN = r"([α-ωΑ-Ωa-zA-Z]+(\*\*([+/-]?[0-9]+)|[*/])?)+"
+_UNIT_PATTERN = r"((\s*[*/]\s*)?[α-ωΑ-Ωa-zA-Z]+(\*\*([+-]?\d+|\([+-]?\d+\)))?)+"
 _QUAN_PATTERN = rf"{_NUMB_PATTERN}\s*{_UNIT_PATTERN}"
 _NUMB_REGEXP = re.compile(_NUMB_PATTERN)
 _UNIT_REGEXP = re.compile(_UNIT_PATTERN)
@@ -1395,13 +1395,15 @@ class unyt_array(np.ndarray):
         elif re.fullmatch(_UNIT_REGEXP, v):
             num = 1
             unit = Unit(re.match(_UNIT_REGEXP, v).group())
-        elif not re.match(_QUAN_REGEXP, v):
+        elif not re.fullmatch(_QUAN_REGEXP, v):
             raise ValueError(f"Received invalid quantity expression '{s}'.")
         else:
             res = re.search(_NUMB_REGEXP, v)
             num = res.group()
             res = re.search(_UNIT_REGEXP, v[res.span()[1] :])
-            unit = res.group()
+            unit = res.group().strip()
+            if unit.startswith(("/", "*")):
+                unit = f"1{unit}"
         try:
             num = int(num)
         except ValueError:


Test patch:
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index cf3a268..4db8ed5 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -2631,15 +2631,30 @@ def test_string_formatting():
         ("-inf g", -float("inf") * Unit("g"), "-inf g"),
         ("1", 1.0 * Unit(), "1 dimensionless"),
         ("g", 1.0 * Unit("g"), "1 g"),
+        # from https://github.com/yt-project/unyt/issues/361
+        ("1 g**2/cm**2", 1.0 * Unit("g") ** 2 / Unit("cm") ** 2, "1 g**2/cm**2"),
+        ("g**2/cm**2", 1.0 * Unit("g") ** 2 / Unit("cm") ** 2, "1 g**2/cm**2"),
+        ("1*cm**2", 1.0 * Unit("cm") ** 2, "1 cm**2"),
+        ("1/cm**2", 1.0 / Unit("cm") ** 2, "1 cm**(-2)"),
+        ("1 / cm**2", 1.0 / Unit("cm") ** 2, "1 cm**(-2)"),
+        (
+            "1e-3 g**2 / cm**2",
+            1e-3 * Unit("g") ** 2 / Unit("cm") ** 2,
+            "0.001 g**2/cm**2",
+        ),
     ],
 )
 def test_valid_quantity_from_string(s, expected, normalized):
     actual = unyt_quantity.from_string(s)
-    if "nan" in s:
-        assert actual != expected
-    else:
+    assert actual.to_string() == normalized
+    roundtrip = unyt_quantity.from_string(actual.to_string())
+
+    if "nan" not in s:
         assert actual == expected
+        assert roundtrip == expected
+
     assert actual.to_string() == normalized
+    assert roundtrip.to_string() == normalized
 
 
 @pytest.mark.parametrize(
@@ -2650,6 +2665,9 @@ def test_valid_quantity_from_string(s, expected, normalized):
         "cm10",
         "cm 10.",
         ".cm",
+        "1cm**(-1",
+        "1cm**/2",
+        "1cm**3 hello",
     ],
 )
 def test_invalid_expression_quantity_from_string(s):