Repository: astropy/astropy

Issue:
QTable cannot take `dimensionless_unscaled` when creating table from `data`
### Description

Attempting to create an table with a column in units `dimensionless_unscaled` leads to a crash, because a guard against setting a column attribute to `np.ma.masked` or `None` triggers a comparison between `np.ma.masked` and Unit which eventually resolves to a `ZeroDivisionError`.

### Expected behavior

A `QTable` is created. 

### How to Reproduce

```python
from astropy.table import QTable, Column
import numpy as np
data = np.ones((5,2))
tt = QTable(data=data, names=["a","weight"],units={"weight":u.dimensionless_unscaled})
```
Results in 
```
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[15], line 2
      1 data = np.ones((5,2))
----> 2 tt = QTable(data=data, names=["a","weight"],units={"weight":u.dimensionless_unscaled})
      3 tt

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/astropy/table/table.py:887, in Table.__init__(self, data, masked, names, dtype, meta, copy, rows, copy_indices, units, descriptions, **kwargs)
    884 if self.masked not in (None, True, False):
    885     raise TypeError("masked property must be None, True or False")
--> 887 self._set_column_attribute("unit", units)
    888 self._set_column_attribute("description", descriptions)

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/astropy/table/table.py:921, in Table._set_column_attribute(self, attr, values)
    918     if value.strip() == "":
    919         value = None
--> 921 if value not in (np.ma.masked, None):
    922     col = self[name]
    923     if attr == "unit" and isinstance(col, Quantity):
    924         # Update the Quantity unit in-place

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/numpy/ma/core.py:4182, in MaskedArray.__eq__(self, other)
   4171 def __eq__(self, other):
   4172     """Check whether other equals self elementwise.
   4173 
   4174     When either of the elements is masked, the result is masked as well,
   (...)
   4180     and other considered equal only if both were fully masked.
   4181     """
-> 4182     return self._comparison(other, operator.eq)

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/numpy/ma/core.py:4138, in MaskedArray._comparison(self, other, compare)
   4134 else:
   4135     # For regular arrays, just use the data as they come.
   4136     sdata = self.data
-> 4138 check = compare(sdata, odata)
   4140 if isinstance(check, (np.bool_, bool)):
   4141     return masked if mask else check

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/astropy/units/core.py:955, in UnitBase.__eq__(self, other)
    952     return NotImplemented
    954 try:
--> 955     return is_effectively_unity(self._to(other))
    956 except UnitsError:
    957     return False

File ~/miniconda3/envs/datapipe-testbench/lib/python3.11/site-packages/astropy/units/core.py:1157, in UnitBase._to(self, other)
   1147     # Check quickly whether equivalent.  This is faster than
   1148     # `is_equivalent`, because it doesn't generate the entire
   1149     # physical type list of both units.  In other words it "fails
   1150     # fast".
   1151     if self_decomposed.powers == other_decomposed.powers and all(
   1152         self_base is other_base
   1153         for (self_base, other_base) in zip(
   1154             self_decomposed.bases, other_decomposed.bases
   1155         )
   1156     ):
-> 1157         return self_decomposed.scale / other_decomposed.scale
   1159 raise UnitConversionError(f"'{self!r}' is not a scaled version of '{other!r}'")

ZeroDivisionError: float division by zero
```



### Versions

```python
import astropy
try:
    astropy.system_info()
except AttributeError:
    import platform; print(platform.platform())
    import sys; print("Python", sys.version)
    import astropy; print("astropy", astropy.__version__)
    import numpy; print("Numpy", numpy.__version__)
    import erfa; print("pyerfa", erfa.__version__)
    try:
        import scipy
        print("Scipy", scipy.__version__)
    except ImportError:
        print("Scipy not installed")
    try:
        import matplotlib
        print("Matplotlib", matplotlib.__version__)
    except ImportError:
        print("Matplotlib not installed")
```
```
Linux-5.15.0-119-generic-x86_64-with-glibc2.35
Python 3.11.8 | packaged by conda-forge | (main, Feb 16 2024, 20:53:32) [GCC 12.3.0]
astropy 5.3.4
Numpy 1.26.4
pyerfa 2.0.0
Scipy 1.11.4
Matplotlib 3.8.3
```


Discussion:
github-actions[bot]: Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  - [ ] Do the proposed changes actually accomplish desired goals?
  - [ ] Do the proposed changes follow the [Astropy coding guidelines](https://docs.astropy.org/en/latest/development/codeguide.html)?
  - [ ] Are tests added/updated as required? If so, do they follow the [Astropy testing guidelines](https://docs.astropy.org/en/latest/development/testguide.html)?
  - [ ] Are docs added/updated as required? If so, do they follow the [Astropy documentation guidelines](https://docs.astropy.org/en/latest/development/docguide.html)?
  - [ ] Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for [rebase](https://docs.astropy.org/en/latest/development/development_details.html#rebase-if-necessary) and [squash](https://docs.astropy.org/en/latest/development/development_details.html#squash-if-necessary).
  - [ ] Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the [bot](https://docs.astropy.org/en/latest/development/development_details.html#pre-commit).
  - [ ] Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  - [ ] Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  - [ ] At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) *before* merge.

Patch:
diff --git a/astropy/table/table.py b/astropy/table/table.py
index 75e179fca6..0c1382f5ce 100644
--- a/astropy/table/table.py
+++ b/astropy/table/table.py
@@ -938,7 +938,7 @@ class Table:
                 if value.strip() == "":
                     value = None
 
-            if value not in (np.ma.masked, None):
+            if value is not None and value is not np.ma.masked:
                 col = self[name]
                 if attr == "unit" and isinstance(col, Quantity):
                     # Update the Quantity unit in-place
diff --git a/astropy/units/core.py b/astropy/units/core.py
index 4d72912003..7bb637e81f 100644
--- a/astropy/units/core.py
+++ b/astropy/units/core.py
@@ -21,7 +21,7 @@ from astropy.utils.exceptions import AstropyWarning
 from astropy.utils.misc import isiterable
 
 from . import format as unit_format
-from .errors import UnitConversionError, UnitsError, UnitsWarning
+from .errors import UnitConversionError, UnitScaleError, UnitsError, UnitsWarning
 from .utils import (
     is_effectively_unity,
     resolve_fractions,
@@ -2128,7 +2128,7 @@ class _UnitMetaClass(type):
                 return UnrecognizedUnit(s)
 
         elif isinstance(s, (int, float, np.floating, np.integer)):
-            return CompositeUnit(s, [], [], _error_check=False)
+            return CompositeUnit(s, [], [])
 
         elif isinstance(s, tuple):
             from .structured import StructuredUnit
@@ -2293,6 +2293,11 @@ class CompositeUnit(UnitBase):
     powers : sequence of numbers
         A sequence of powers (in parallel with ``bases``) for each
         of the base units.
+
+    Raises
+    ------
+    UnitScaleError
+        If the scale is zero.
     """
 
     _decomposed_cache = None
@@ -2313,6 +2318,8 @@ class CompositeUnit(UnitBase):
         # kwarg `_error_check` is False, the error checking is turned
         # off.
         if _error_check:
+            if scale == 0:
+                raise UnitScaleError("cannot create a unit with a scale of 0.")
             for base in bases:
                 if not isinstance(base, UnitBase):
                     raise TypeError("bases must be sequence of UnitBase instances")
diff --git a/docs/changes/table/17048.bugfix.rst b/docs/changes/table/17048.bugfix.rst
new file mode 100644
index 0000000000..ba38075417
--- /dev/null
+++ b/docs/changes/table/17048.bugfix.rst
@@ -0,0 +1,2 @@
+Ensure that initializing a ``QTable`` with explicit units` also succeeds if
+one of the units is ``u.one``.
diff --git a/docs/changes/units/17048.bugfix.rst b/docs/changes/units/17048.bugfix.rst
new file mode 100644
index 0000000000..e80b27aaa4
--- /dev/null
+++ b/docs/changes/units/17048.bugfix.rst
@@ -0,0 +1,4 @@
+An exception is now raised if it is attempted to create a unit with a
+scale of zero, avoiding bugs further downstream (including surprising
+ones, such as a comparison of ``np.ma.masked == u.one`` leading to
+a ``ZeroDivisionError``).


Test patch:
diff --git a/astropy/table/tests/test_table.py b/astropy/table/tests/test_table.py
index 0514469120..02b19764ab 100644
--- a/astropy/table/tests/test_table.py
+++ b/astropy/table/tests/test_table.py
@@ -3533,3 +3533,11 @@ def test_table_columns_update_deprecation():
         ),
     ):
         Table().columns.update({"a": [0]})
+
+
+def test_qtable_with_explicit_units():
+    # Regression test for gh-17047; the problem was that the dimensionless
+    # unit ended up being compared to np.ma.masked.  See also
+    # astropy/units/tests/test_units.py::test_comparison_dimensionless_with_np_ma_masked
+    tt = QTable(data=[[1.0, 2.0, 3.0]], names=["weight"], units={"weight": u.one})
+    assert tt["weight"].unit == u.dimensionless_unscaled
diff --git a/astropy/units/tests/test_units.py b/astropy/units/tests/test_units.py
index c81075f6be..0e36e83cf6 100644
--- a/astropy/units/tests/test_units.py
+++ b/astropy/units/tests/test_units.py
@@ -1022,3 +1022,23 @@ def test_dask_arrays():
 def test_get_format_name_deprecation():
     with pytest.warns(AstropyDeprecationWarning, match=r"Use to_string\(\) instead\.$"):
         assert u.m.get_format_name("fits") == "m"
+
+
+def test_comparison_dimensionless_with_np_ma_masked():
+    # Found to be a problem indirectly in gh-17047;
+    # The path np.ma.masked.__eq__(u.dimensionless_unscaled)
+    # used to give a ZeroDivisionError.
+    comparison = u.dimensionless_unscaled == np.ma.masked
+    assert comparison is np.ma.masked
+
+
+def test_error_on_conversion_of_zero_to_unit():
+    # Found to be a problem indirectly in gh-17047; we allow conversion
+    # of numbers to units, but should not allow 0.
+    with pytest.raises(u.UnitScaleError, match="cannot create.*scale of 0"):
+        u.Unit(0)
+    with pytest.raises(u.UnitScaleError, match="cannot create.*scale of 0"):
+        u.dimensionless_unscaled.to(0)
+    # Also check some that do work.
+    assert u.dimensionless_unscaled.to(0.125) == 8
+    assert u.dimensionless_unscaled.to(8) == 0.125