Repository: astropy/astropy

Issue:
TypeError when using astropy.io.votable.validate( ... , xmllint=True) 
When xmllint = True, 
validation goes through **astropy.io.votable.table.py** line 306 
```
 if xmllint and os.path.exists(filename):
        from ...utils.xml import validate

        if votable is None:
            version = "1.1"
        else:
            version = votable.version
        success, stdout, stderr = validate.validate_schema(
            filename, version)
```
Where **astropy.utils.xml.validate.py** validate_schema is called with schema_file as the votable version - like "1.1" for example
`validate_schema(filename, schema_file):`
```
    base, ext = os.path.splitext(schema_file)
    if ext == '.xsd':
        schema_part = '--schema ' + schema_file
    elif ext == '.dtd':
        schema_part = '--dtdvalid ' + schema_file
    else:
        raise TypeError("schema_file must be a path to an XML Schema or DTD")
```
Thing being that xml.validate.py validate() is expecting a ".xsd" or a ".dtd" and throws a TypeError exception line 42 otherwise. 

table.py should be calling **astropy.io.votable.xmlutil.py** validate_schema() instead. 
`validate_schema(filename, version='1.1'): ...`

Discussion:
astropy-bot[bot]: Hi there @mmebsout :wave: - thanks for the pull request! I'm just a friendly :robot: that checks for issues related to the changelog and making sure that this pull request is milestoned and labeled correctly. This is mainly intended for the maintainers, so if you are not a maintainer you can ignore this, and a maintainer will let you know if any action is required on your part :smiley:.

I noticed the following issues with this pull request:

* The milestone has not been set (this can only be set by a maintainer)
* Changelog entry not present (or pull request number missing) and neither the **Affects-dev** nor the **no-changelog-entry-needed** label are set

Would it be possible to fix these? Thanks!

*If there are any issues with this message, please report them [here](https://github.com/astropy/astropy-bot/issues).*
pllim: Please add a change log under "bug fix" for 3.0.1. I will do a thorough review next week. Thanks!
pllim: This has slipped to 3.0.2. I was going to move the change log for you, but that section has not been added in `master` yet. I'll come back later this week to rebase and move the change log, unless you get to it first. After that, I'll review. Thank you for your patience!
mmebsout: @pllim No worries :) I'll try to do it tonight .
mmebsout: @pllim Sorry I did a merge instead of rebase. I'll fix it today. 
mmebsout: Re-wrote the commits in a clean branch 
Fixes #7255 
pllim: Would you mind squashing it, or me doing it for you? Just wondering.
mmebsout: @pllim I would appreciate if you do it. Thank you :) 
pllim: Okay, no problem. I'll take care of this today. 

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 0f832f100c..1c7022eb7c 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -290,6 +290,8 @@ astropy.io.registry
 astropy.io.votable
 ^^^^^^^^^^^^^^^^^^
 
+- Fix validate with xmllint=True. [#7255, #7283]
+
 astropy.modeling
 ^^^^^^^^^^^^^^^^
 
diff --git a/astropy/io/votable/table.py b/astropy/io/votable/table.py
index ee8d447411..14ea53e6b0 100644
--- a/astropy/io/votable/table.py
+++ b/astropy/io/votable/table.py
@@ -5,8 +5,6 @@ This file contains a contains the high-level functions to read a
 VOTable file.
 """
 
-
-
 # STDLIB
 import io
 import os
@@ -133,8 +131,8 @@ def parse(source, columns=None, invalid='exception', pedantic=None,
         config['filename'] = source
 
     with iterparser.get_xml_iterator(
-        source,
-        _debug_python_based_parser=_debug_python_based_parser) as iterator:
+            source,
+            _debug_python_based_parser=_debug_python_based_parser) as iterator:
         return tree.VOTableFile(
             config=config, pos=(1, 1)).parse(iterator, config)
 
@@ -294,19 +292,19 @@ def validate(source, output=None, xmllint=False, filename=None):
 
     success = 0
     if xmllint and os.path.exists(filename):
-        from ...utils.xml import validate
+        from . import xmlutil
 
         if votable is None:
             version = "1.1"
         else:
             version = votable.version
-        success, stdout, stderr = validate.validate_schema(
+        success, stdout, stderr = xmlutil.validate_schema(
             filename, version)
 
         if success != 0:
             output.write(
                 'xmllint schema violations:\n\n')
-            output.write(stderr)
+            output.write(stderr.decode('utf-8'))
         else:
             output.write('xmllint passed\n')
 
@@ -352,12 +350,12 @@ def is_votable(source):
     """
     try:
         with iterparser.get_xml_iterator(source) as iterator:
-            for start, tag, data, pos in iterator:
+            for start, tag, d, pos in iterator:
                 if tag != 'xml':
                     return False
                 break
 
-            for start, tag, data, pos in iterator:
+            for start, tag, d, pos in iterator:
                 if tag != 'VOTABLE':
                     return False
                 break


Test patch:
diff --git a/astropy/io/votable/tests/table_test.py b/astropy/io/votable/tests/table_test.py
index d5ec8714de..183593e58b 100644
--- a/astropy/io/votable/tests/table_test.py
+++ b/astropy/io/votable/tests/table_test.py
@@ -2,8 +2,6 @@
 """
 Test the conversion to/from astropy.table
 """
-
-
 import io
 import os
 
@@ -176,4 +174,4 @@ def test_empty_table():
         get_pkg_data_filename('data/empty_table.xml'),
         pedantic=False)
     table = votable.get_first_table()
-    astropy_table = table.to_table()
+    astropy_table = table.to_table()  # noqa
diff --git a/astropy/io/votable/tests/vo_test.py b/astropy/io/votable/tests/vo_test.py
index 3018eceba6..ec15473e8f 100644
--- a/astropy/io/votable/tests/vo_test.py
+++ b/astropy/io/votable/tests/vo_test.py
@@ -13,6 +13,7 @@ import io
 import pathlib
 import sys
 import gzip
+from unittest import mock
 
 # THIRD-PARTY
 import pytest
@@ -154,7 +155,7 @@ def _test_regression(tmpdir, _python_based=False, binary_mode=1):
             'rt', encoding='utf-8') as fd:
         truth = fd.readlines()
     with open(str(tmpdir.join("regression.bin.tabledata.xml")),
-                 'rt', encoding='utf-8') as fd:
+              'rt', encoding='utf-8') as fd:
         output = fd.readlines()
 
     # If the lines happen to be different, print a diff
@@ -816,6 +817,18 @@ def test_validate(test_path_object=False):
     assert truth == output
 
 
+@mock.patch('subprocess.Popen')
+def test_validate_xmllint_true(mock_subproc_popen):
+    process_mock = mock.Mock()
+    attrs = {'communicate.return_value': ('ok', 'ko'),
+             'returncode': 0}
+    process_mock.configure_mock(**attrs)
+    mock_subproc_popen.return_value = process_mock
+
+    assert validate(get_pkg_data_filename('data/empty_table.xml'),
+                    xmllint=True)
+
+
 def test_validate_path_object():
     """
     Validating when source is passed as path object. (#4412)