Repository: astropy/astropy

Issue:
``Column.pprint`` fails for scalars
### Description

Raises TypeError because object is scalar and the code expects an array object.

### Expected behavior

If it can be made into a Column, ``pprint`` works.

### Actual behavior

See Description.

### Steps to Reproduce

```python
import astropy.units as u
from astropy.table import Column

Column(1).pprint()
```
TypeError: len() of unsized object

### System Details

macOS-10.16-x86_64-i386-64bit
Python 3.9.5 (default, May 18 2021, 12:31:01) 
[Clang 10.0.0 ]
Numpy 1.21.4
pyerfa 2.0.0.1
astropy 5.1.dev207+gbfb9252df.d20211130
Scipy 1.7.1
Matplotlib 3.3.4


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#astropy-documentation-rules-and-guidelines)?
  - [ ] 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/workflow/development_workflow.html#rebase-if-necessary) and [squash](https://docs.astropy.org/en/latest/development/workflow/development_workflow.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/workflow/development_workflow.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?
  - [ ] Is a milestone set? Milestone must be set but we cannot check for it on Actions; do not let the green checkmark fool you.
  - [ ] 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.
pllim: Opt to not backport for now but if the final working solution is simple, then we can rethink the backport. Thanks!
neutrinoceros: @pllim actually I think it's good now. Hopefully this is simple enough !
neutrinoceros: @mhvk can I ask you to review this again ?
taldcroft: @neutrinoceros - can you do some quick performance  checking to see how much speed impact there is to `Column` getitem? I would not want to slow down astropy for this fix.
neutrinoceros: @mhvk if we want to just go with your suggestion (just typesetting the number), then this patch is sufficient and we can close #15754 as "not a bug". In that case I'll just need to complete my test
taldcroft: @neutrinoceros - is there any solution that does not require changing that Cython mixin code? I haven't dug into this, but what is driving that exactly? Can't we just do something trivial in `pprint()` to catch this like what @mhvk said `if self.ndim == 0; return <something-simple>`?

I'm a little hesitant to sacrifice 3-4% performance for this bugfix given the low likelihood of normal users hitting it.
neutrinoceros: @taldcroft it's very hard to inspect what happening exactly before the cython function is called because the callsite is actually visited many times before it crashes *and* it happens in repr-related code, making it difficult/impossible to inspect variables in a debugger REPL, so patching the Cython function is the only way I was able to find so far.
Is it possible that my benchmark isn't representative of actual user code that may be impacted ? In other words, do we know of an actual performance-critical use case ?
taldcroft: @neutrinoceros - below is a patch to your PR that passes the new tests. This reverts the Cython update and deals with a scalar column only in the pprint code.
```
In [2]: c = Column(0, name="scalar")

In [3]: c.pprint()  # Not obvious it is scalar, but mostly we just want to prevent a crash
scalar
------
     0
```

It also occurred to me that the Cython update would have the undesired effect of ignoring the index in getitem. I didn't try, but I think that code would mean that `c[250] == 0` for the above column would be `True` instead of raising `IndexError`.

Here is the diff:

```
diff --git a/astropy/table/_column_mixins.pyx b/astropy/table/_column_mixins.pyx
index bdd075528b..5ab4fe66d3 100644
--- a/astropy/table/_column_mixins.pyx
+++ b/astropy/table/_column_mixins.pyx
@@ -52,9 +52,6 @@ ctypedef object (*item_getter)(object, object)
 
 
 cdef inline object base_getitem(object self, object item, item_getter getitem):
-    if (<np.ndarray>self).ndim == 0:
-        return self.data
-
     if (<np.ndarray>self).ndim > 1 and isinstance(item, INTEGER_TYPES):
         return self.data[item]
 
diff --git a/astropy/table/pprint.py b/astropy/table/pprint.py
index 25d990ec0c..e78f099d35 100644
--- a/astropy/table/pprint.py
+++ b/astropy/table/pprint.py
@@ -424,6 +424,7 @@ class TableFormatter:
         """
         max_lines, _ = self._get_pprint_size(max_lines, -1)
         dtype = getattr(col, "dtype", None)
+        is_onedim = getattr(col, "ndim", 1) == 1
         multidims = getattr(col, "shape", [0])[1:]
         if multidims:
             multidim0 = tuple(0 for n in multidims)
@@ -524,8 +525,11 @@ class TableFormatter:
                     left = format_func(col_format, col[(idx,) + multidim0])
                     right = format_func(col_format, col[(idx,) + multidim1])
                     return f"{left} .. {right}"
-            else:
+            elif is_onedim:
                 return format_func(col_format, col[idx])
+            else:
+                # Scalar column
+                return format_func(col_format, col)
 
         # Add formatted values if within bounds allowed by max_lines
         for idx in indices:
```

neutrinoceros: > I didn't try, but I think that code would mean that `c[250] == 0` for the above column would be True instead of raising IndexError.

I confirm that's exactly what happens, nice catch ! I've added this check to the test to make sure this behaviour never gets checked in. I took your patch in and rebase the whole branch too; thanks a lot for your help
neutrinoceros: rebased to refresh CI. ping @hamogu and @taldcroft for second review
github-actions[bot]:  Hi humans :wave: - this pull request hasn't had any new commits for approximately 4 months. **I plan to close this in 30 days if the pull request doesn't have any new commits by then.**

In lieu of a stalled pull request, please consider closing this and open an issue instead if a reminder is needed to revisit in the future. Maintainers may also choose to add **keep-open** label to keep this PR open but it is discouraged unless absolutely necessary.

If this PR still needs to be reviewed, as an author, you can rebase it to reset the clock.

*If you believe I commented on this pull request incorrectly, please report this [here](https://github.com/pllim/action-astropy-stalebot/issues).*

Patch:
diff --git a/astropy/table/pprint.py b/astropy/table/pprint.py
index 5648571776..a4512fcca7 100644
--- a/astropy/table/pprint.py
+++ b/astropy/table/pprint.py
@@ -463,7 +463,13 @@ class TableFormatter:
 
         max_lines -= n_header
         n_print2 = max_lines // 2
-        n_rows = len(col)
+        try:
+            n_rows = len(col)
+        except TypeError:
+            is_scalar = True
+            n_rows = 1
+        else:
+            is_scalar = False
 
         # This block of code is responsible for producing the function that
         # will format values for this column.  The ``format_func`` function
@@ -501,17 +507,15 @@ class TableFormatter:
         auto_format_func = get_auto_format_func(col, pssf)
         format_func = col.info._format_funcs.get(col_format, auto_format_func)
 
-        if len(col) > max_lines:
+        if n_rows > max_lines:
             if show_length is None:
                 show_length = True
             i0 = n_print2 - (1 if show_length else 0)
             i1 = n_rows - n_print2 - max_lines % 2
-            indices = np.concatenate(
-                [np.arange(0, i0 + 1), np.arange(i1 + 1, len(col))]
-            )
+            indices = np.concatenate([np.arange(0, i0 + 1), np.arange(i1 + 1, n_rows)])
         else:
             i0 = -1
-            indices = np.arange(len(col))
+            indices = np.arange(n_rows)
 
         def format_col_str(idx):
             if multidims:
@@ -527,6 +531,8 @@ class TableFormatter:
                     left = format_func(col_format, col[(idx,) + multidim0])
                     right = format_func(col_format, col[(idx,) + multidim1])
                     return f"{left} .. {right}"
+            elif is_scalar:
+                return format_func(col_format, col)
             else:
                 return format_func(col_format, col[idx])
 
diff --git a/docs/changes/table/15749.bugfix.rst b/docs/changes/table/15749.bugfix.rst
new file mode 100644
index 0000000000..6f27285609
--- /dev/null
+++ b/docs/changes/table/15749.bugfix.rst
@@ -0,0 +1,1 @@
+Fix a crash when calling ``Column.pprint`` on a scalar column.


Test patch:
diff --git a/astropy/table/tests/test_column.py b/astropy/table/tests/test_column.py
index 654c716d1f..7f6cb8a698 100644
--- a/astropy/table/tests/test_column.py
+++ b/astropy/table/tests/test_column.py
@@ -464,6 +464,18 @@ class TestColumn:
         with pytest.raises(AttributeError):
             t["a"].mask = [True, False]
 
+    @pytest.mark.parametrize("scalar", [1, u.Quantity(0.6, "eV")])
+    def test_access_scalar(self, scalar):
+        # see https://github.com/astropy/astropy/pull/15749#issuecomment-1867561072
+        c = table.Column(scalar)
+        if isinstance(scalar, u.Quantity):
+            assert c.item() == scalar.value
+        else:
+            assert c.item() == scalar
+
+        with pytest.raises(IndexError):
+            c[0]
+
 
 @pytest.mark.parametrize(
     "data",
diff --git a/astropy/table/tests/test_pprint.py b/astropy/table/tests/test_pprint.py
index 1f352eec6e..ce76ab3881 100644
--- a/astropy/table/tests/test_pprint.py
+++ b/astropy/table/tests/test_pprint.py
@@ -10,7 +10,7 @@ import pytest
 from astropy import conf, table
 from astropy import units as u
 from astropy.io import ascii
-from astropy.table import QTable, Table
+from astropy.table import Column, QTable, Table
 from astropy.table.table_helpers import simple_table
 
 BIG_WIDE_ARR = np.arange(2000, dtype=np.float64).reshape(100, 20)
@@ -316,6 +316,40 @@ class TestPprint:
         assert len(out.splitlines()) == BIG_WIDE_ARR.shape[0] + 3
 
 
+class TestPprintColumn:
+    @pytest.mark.parametrize(
+        "scalar, exp",
+        [
+            (
+                1,
+                [
+                    "None",
+                    "----",
+                    "   1",
+                ],
+            ),
+            (
+                u.Quantity(0.6, "eV"),
+                [
+                    "None",
+                    "----",
+                    " 0.6",
+                ],
+            ),
+        ],
+    )
+    def test_pprint_scalar(self, scalar, exp):
+        # see https://github.com/astropy/astropy/issues/12584
+        c = Column(scalar)
+
+        # Make sure pprint() does not raise an exception
+        c.pprint()
+
+        # Check actual output
+        out = c.pformat()
+        assert out == exp
+
+
 @pytest.mark.usefixtures("table_type")
 class TestFormat:
     def test_column_format(self, table_type):