Repository: astropy/astropy

Issue:
UnicodeDecodeError during fits.Header.fromfile()
MWE:
```
import numpy as np
from astropy.io import fits

fn = 'test.fits'
data = np.random.randn(1, 1)
hdu = fits.HDUList(fits.PrimaryHDU(data))
hdu.writeto(fn, overwrite=True)
header = fits.Header.fromfile(fn)
```

produces:

```
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-2-32a656c8e40e> in <module>
      6 hdu = fits.HDUList(fits.PrimaryHDU(data))
      7 hdu.writeto(fn, overwrite=True)
----> 8 header = fits.Header.fromfile(fn)

/usr/local/lib/python3.6/dist-packages/astropy/io/fits/header.py in fromfile(cls, fileobj, sep, endcard, padding)
    445 
    446             return cls._from_blocks(block_iter, is_binary, sep, endcard,
--> 447                                     padding)[1]
    448         finally:
    449             if close_file:

/usr/local/lib/python3.6/dist-packages/astropy/io/fits/header.py in _from_blocks(cls, block_iter, is_binary, sep, endcard, padding)
    474         # Read the first header block.
    475         try:
--> 476             block = next(blocks)
    477         except StopIteration:
    478             raise EOFError()

/usr/local/lib/python3.6/dist-packages/astropy/io/fits/header.py in block_iter(nbytes)
    437             def block_iter(nbytes):
    438                 while True:
--> 439                     data = fileobj.read(nbytes)
    440 
    441                     if data:

/usr/lib/python3.6/codecs.py in decode(self, input, final)
    319         # decode input (taking the buffer into account)
    320         data = self.buffer + input
--> 321         (result, consumed) = self._buffer_decode(data, self.errors, final)
    322         # keep undecoded input until the next call
    323         self.buffer = data[consumed:]

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa4 in position 2883: invalid start byte
```

However, opening the file in binary mode seems to work:
```
with open(fn, 'rb') as fp:
    header = fits.Header.fromfile(fp)
```

Discussion:
pllim: Unfortunately, this seemingly simple change broke Appveyor and 32-bit CircleCI. The HTML doc broke, I think, because of perhaps you branched off an older version of `master`.
codecov[bot]: # [Codecov](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=h1) Report
> Merging [#8713](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=desc) into [master](https://codecov.io/gh/astropy/astropy/commit/eb20883bed9f815a0db177c4fd1474ab4077cfa1?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/astropy/astropy/pull/8713/graphs/tree.svg?width=650&token=eEhbJxwU0j&height=150&src=pr)](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #8713      +/-   ##
==========================================
- Coverage   86.95%   86.95%   -0.01%     
==========================================
  Files         399      399              
  Lines       59339    59339              
  Branches     1100     1100              
==========================================
- Hits        51597    51596       -1     
- Misses       7101     7102       +1     
  Partials      641      641
```


| [Impacted Files](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [astropy/io/fits/header.py](https://codecov.io/gh/astropy/astropy/pull/8713/diff?src=pr&el=tree#diff-YXN0cm9weS9pby9maXRzL2hlYWRlci5weQ==) | `95.89% <ø> (-0.12%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=footer). Last update [eb20883...c482a99](https://codecov.io/gh/astropy/astropy/pull/8713?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

embray: I'm poking at this since it's very closely related to #8707 .

Longer-term I still need to rework str/bytes handling for headers.  The current code still carries a lot of Python 2 legacy despite explicitly Python 2 support being removed.
embray: I think the suggestions I added in astromancer/astropy#1 will fix this.
embray: It would be good to add a regression test as well; perhaps more-or-less along the lines of the case you gave in #8711 would suffice.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 6a32d019f3..ce23900e33 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2254,6 +2254,8 @@ astropy.io.fits
 - Fix ``Header.update`` which was dropping the comments when passed
   a ``Header`` object. [#8840]
 
+- Fix ``Header.fromfile`` to work on FITS files. [#8713]
+
 astropy.io.misc
 ^^^^^^^^^^^^^^^
 
diff --git a/astropy/io/fits/header.py b/astropy/io/fits/header.py
index 0518aa5c0f..bc3367b7a9 100644
--- a/astropy/io/fits/header.py
+++ b/astropy/io/fits/header.py
@@ -8,7 +8,8 @@ import warnings
 
 from .card import Card, _pad, KEYWORD_LENGTH, UNDEFINED
 from .file import _File
-from .util import encode_ascii, decode_ascii, fileobj_closed, fileobj_is_binary
+from .util import (encode_ascii, decode_ascii, fileobj_closed,
+                   fileobj_is_binary, path_like)
 from ._utils import parse_header
 
 from astropy.utils import isiterable
@@ -481,11 +482,20 @@ class Header:
         """
 
         close_file = False
-        if isinstance(fileobj, str):
-            # Open in text mode by default to support newline handling; if a
-            # binary-mode file object is passed in, the user is on their own
-            # with respect to newline handling
-            fileobj = open(fileobj, 'r')
+
+        if isinstance(fileobj, path_like):
+            # If sep is non-empty we are trying to read a header printed to a
+            # text file, so open in text mode by default to support newline
+            # handling; if a binary-mode file object is passed in, the user is
+            # then on their own w.r.t. newline handling.
+            #
+            # Otherwise assume we are reading from an actual FITS file and open
+            # in binary mode.
+            if sep:
+                fileobj = open(fileobj, 'r', encoding='latin1')
+            else:
+                fileobj = open(fileobj, 'rb')
+
             close_file = True
 
         try:
diff --git a/astropy/io/fits/util.py b/astropy/io/fits/util.py
index 7c620b021b..c01d0d8067 100644
--- a/astropy/io/fits/util.py
+++ b/astropy/io/fits/util.py
@@ -26,6 +26,14 @@ import numpy as np
 
 from astropy.utils.exceptions import AstropyUserWarning
 
+try:
+    # Support the Python 3.6 PathLike ABC where possible
+    from os import PathLike
+    path_like = (str, PathLike)
+except ImportError:
+    path_like = (str,)
+
+
 cmp = lambda a, b: (a > b) - (a < b)
 
 all_integer_types = (int, np.integer)


Test patch:
diff --git a/astropy/io/fits/tests/test_header.py b/astropy/io/fits/tests/test_header.py
index 84f75651a3..564a1da2dc 100644
--- a/astropy/io/fits/tests/test_header.py
+++ b/astropy/io/fits/tests/test_header.py
@@ -1587,6 +1587,13 @@ class TestHeaderFunctions(FitsTestCase):
             assert len(hdul2) == 2
             assert 'MYKEY' in hdul2[1].header
 
+    def test_fromfile(self):
+        """Regression test for https://github.com/astropy/astropy/issues/8711
+        """
+        filename = self.data('scale.fits')
+        hdr = fits.Header.fromfile(filename)
+        assert hdr['DATASET'] == '2MASS'
+
     def test_header_fromtextfile(self):
         """Regression test for https://aeon.stsci.edu/ssb/trac/pyfits/ticket/122
 
diff --git a/astropy/units/tests/test_quantity.py b/astropy/units/tests/test_quantity.py
index be18e65c5b..9f353a096d 100644
--- a/astropy/units/tests/test_quantity.py
+++ b/astropy/units/tests/test_quantity.py
@@ -1397,7 +1397,7 @@ def test_quantity_from_table():
     This also generically checks the use of *anything* with a `unit` attribute
     passed into Quantity
     """
-    from astropy.table import Table
+    from... table import Table
 
     t = Table(data=[np.arange(5), np.arange(5)], names=['a', 'b'])
     t['a'].unit = u.kpc