diff --git a/AUTHORS.rst b/AUTHORS.rst index f4de7b7e..b32563a1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -40,5 +40,4 @@ Patches and ideas * `Jeff Byrnes `_ * `Denis Belavin `_ * `Mickaƫl Schoentgen `_ - - +* `Rohit Sehgal `_ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 28b4be0c..56c15b3b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ This project adheres to `Semantic Versioning `_. ------------------------- * Added ``--raw`` to allow specifying the raw request body without extra processing as an alternative to ``stdin``. (`#534`_) +* Added support for XML formatting. (`#1129`_) * Fixed ``--continue --download`` with a single byte to be downloaded left. (`#1032`_) * Fixed ``--verbose`` HTTP 307 redirects with streamed request body. (`#1088`_) * Fixed handling of session files with ``Cookie:`` followed by other headers. (`#1126`_) @@ -507,3 +508,4 @@ This project adheres to `Semantic Versioning `_. .. _#1088: https://github.com/httpie/httpie/issues/1088 .. _#1094: https://github.com/httpie/httpie/issues/1094 .. _#1126: https://github.com/httpie/httpie/issues/1126 +.. _#1129: https://github.com/httpie/httpie/issues/1129 diff --git a/README.rst b/README.rst index be572c98..6da3c33e 100644 --- a/README.rst +++ b/README.rst @@ -1580,6 +1580,10 @@ The following options are available: +--------------------+----------+---------------+------------------------------+ | ``json.sort_keys`` | ``true`` | ``--sorted``, ``--unsorted`` | +--------------------+----------+---------------+------------------------------+ +| ``xml.format`` | ``true`` | N/A | ++-------------------------------+---------------+------------------------------+ +| ``xml.indent`` | ``4`` | N/A | ++--------------------+----------+---------------+------------------------------+ For example, this is how you would disable the default header and JSON key sorting, and specify a custom JSON indent size: diff --git a/extras/brew-deps.py b/extras/brew-deps.py index a26487fe..422e5a17 100755 --- a/extras/brew-deps.py +++ b/extras/brew-deps.py @@ -29,6 +29,7 @@ PACKAGES = [ 'idna', 'chardet', 'PySocks', + 'defusedxml', ] diff --git a/httpie/cli/constants.py b/httpie/cli/constants.py index e0690e02..ef6bf2c3 100644 --- a/httpie/cli/constants.py +++ b/httpie/cli/constants.py @@ -90,6 +90,8 @@ DEFAULT_FORMAT_OPTIONS = [ 'json.format:true', 'json.indent:4', 'json.sort_keys:true', + 'xml.format:true', + 'xml.indent:4', ] SORTED_FORMAT_OPTIONS = [ 'headers.sort:true', diff --git a/httpie/output/formatters/xml.py b/httpie/output/formatters/xml.py new file mode 100644 index 00000000..be2cd248 --- /dev/null +++ b/httpie/output/formatters/xml.py @@ -0,0 +1,59 @@ +import sys +from typing import TYPE_CHECKING, Optional + +from ...constants import UTF8 +from ...plugins import FormatterPlugin + +if TYPE_CHECKING: + from xml.dom.minidom import Document + + +def parse_xml(data: str) -> 'Document': + """Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object.""" + from defusedxml.minidom import parseString + return parseString(data) + + +def pretty_xml(document: 'Document', + encoding: Optional[str] = UTF8, + indent: int = 4, + standalone: Optional[bool] = None) -> str: + """Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string.""" + kwargs = { + 'encoding': encoding or UTF8, + 'indent': ' ' * indent, + } + if standalone is not None and sys.version_info >= (3, 9): + kwargs['standalone'] = standalone + body = document.toprettyxml(**kwargs).decode() + + # Remove blank lines automatically added by `toprettyxml()`. + return '\n'.join(line for line in body.splitlines() if line.strip()) + + +class XMLFormatter(FormatterPlugin): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.enabled = self.format_options['xml']['format'] + + def format_body(self, body: str, mime: str): + if 'xml' not in mime: + return body + + from xml.parsers.expat import ExpatError + from defusedxml.common import DefusedXmlException + + try: + parsed_body = parse_xml(body) + except ExpatError: + pass # Invalid XML, ignore. + except DefusedXmlException: + pass # Unsafe XML, ignore. + else: + body = pretty_xml(parsed_body, + encoding=parsed_body.encoding, + indent=self.format_options['xml']['indent'], + standalone=parsed_body.standalone) + + return body diff --git a/httpie/plugins/registry.py b/httpie/plugins/registry.py index 29dafffd..e03c70aa 100644 --- a/httpie/plugins/registry.py +++ b/httpie/plugins/registry.py @@ -2,6 +2,7 @@ from .manager import PluginManager from .builtin import BasicAuthPlugin, DigestAuthPlugin from ..output.formatters.headers import HeadersFormatter from ..output.formatters.json import JSONFormatter +from ..output.formatters.xml import XMLFormatter from ..output.formatters.colors import ColorFormatter @@ -14,5 +15,6 @@ plugin_manager.register( DigestAuthPlugin, HeadersFormatter, JSONFormatter, + XMLFormatter, ColorFormatter, ) diff --git a/setup.py b/setup.py index 3d668090..dca1d727 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ dev_require = [ 'wheel', ] install_requires = [ + 'defusedxml>=0.6.0', 'requests[socks]>=2.22.0', 'Pygments>=2.5.2', 'requests-toolbelt>=0.9.1', diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 240326c7..ca1f0337 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -17,6 +17,9 @@ FIXTURES_ROOT = Path(__file__).parent FILE_PATH = FIXTURES_ROOT / 'test.txt' JSON_FILE_PATH = FIXTURES_ROOT / 'test.json' BIN_FILE_PATH = FIXTURES_ROOT / 'test.bin' +XML_FILES_PATH = FIXTURES_ROOT / 'xmldata' +XML_FILES_VALID = list((XML_FILES_PATH / 'valid').glob('*_raw.xml')) +XML_FILES_INVALID = list((XML_FILES_PATH / 'invalid').glob('*.xml')) FILE_PATH_ARG = patharg(FILE_PATH) BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH) diff --git a/tests/fixtures/xmldata/invalid/cyclic.xml b/tests/fixtures/xmldata/invalid/cyclic.xml new file mode 100644 index 00000000..eec080d3 --- /dev/null +++ b/tests/fixtures/xmldata/invalid/cyclic.xml @@ -0,0 +1,5 @@ + + +]> +&a; diff --git a/tests/fixtures/xmldata/invalid/external.xml b/tests/fixtures/xmldata/invalid/external.xml new file mode 100644 index 00000000..611fa23e --- /dev/null +++ b/tests/fixtures/xmldata/invalid/external.xml @@ -0,0 +1,4 @@ + +]> + diff --git a/tests/fixtures/xmldata/invalid/external_file.xml b/tests/fixtures/xmldata/invalid/external_file.xml new file mode 100644 index 00000000..33fd9e46 --- /dev/null +++ b/tests/fixtures/xmldata/invalid/external_file.xml @@ -0,0 +1,5 @@ + +]> + + diff --git a/tests/fixtures/xmldata/invalid/not-xml.xml b/tests/fixtures/xmldata/invalid/not-xml.xml new file mode 100644 index 00000000..1d7f2dfe --- /dev/null +++ b/tests/fixtures/xmldata/invalid/not-xml.xml @@ -0,0 +1 @@ +some string \ No newline at end of file diff --git a/tests/fixtures/xmldata/invalid/quadratic.xml b/tests/fixtures/xmldata/invalid/quadratic.xml new file mode 100644 index 00000000..2ebce678 --- /dev/null +++ b/tests/fixtures/xmldata/invalid/quadratic.xml @@ -0,0 +1,4 @@ + +]> +&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a; diff --git a/tests/fixtures/xmldata/invalid/xalan_exec.xsl b/tests/fixtures/xmldata/invalid/xalan_exec.xsl new file mode 100644 index 00000000..b06c59a5 --- /dev/null +++ b/tests/fixtures/xmldata/invalid/xalan_exec.xsl @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/tests/fixtures/xmldata/invalid/xalan_write.xsl b/tests/fixtures/xmldata/invalid/xalan_write.xsl new file mode 100644 index 00000000..56d35b9e --- /dev/null +++ b/tests/fixtures/xmldata/invalid/xalan_write.xsl @@ -0,0 +1,18 @@ + + + + + + Something bad happens here! + + + + diff --git a/tests/fixtures/xmldata/invalid/xmlbomb.xml b/tests/fixtures/xmldata/invalid/xmlbomb.xml new file mode 100644 index 00000000..f3b338ec --- /dev/null +++ b/tests/fixtures/xmldata/invalid/xmlbomb.xml @@ -0,0 +1,7 @@ + + + + +]> +&c; diff --git a/tests/fixtures/xmldata/invalid/xmlbomb2.xml b/tests/fixtures/xmldata/invalid/xmlbomb2.xml new file mode 100644 index 00000000..d5d20ea3 --- /dev/null +++ b/tests/fixtures/xmldata/invalid/xmlbomb2.xml @@ -0,0 +1,4 @@ + +]> +text&a; diff --git a/tests/fixtures/xmldata/valid/dtd_formatted.xml b/tests/fixtures/xmldata/valid/dtd_formatted.xml new file mode 100644 index 00000000..c196becb --- /dev/null +++ b/tests/fixtures/xmldata/valid/dtd_formatted.xml @@ -0,0 +1,8 @@ + + + + + text + diff --git a/tests/fixtures/xmldata/valid/dtd_raw.xml b/tests/fixtures/xmldata/valid/dtd_raw.xml new file mode 100644 index 00000000..64f16a56 --- /dev/null +++ b/tests/fixtures/xmldata/valid/dtd_raw.xml @@ -0,0 +1,2 @@ + +text diff --git a/tests/fixtures/xmldata/valid/simple-ns_formatted.xml b/tests/fixtures/xmldata/valid/simple-ns_formatted.xml new file mode 100644 index 00000000..607f2c74 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-ns_formatted.xml @@ -0,0 +1,9 @@ + + + + + text + text + tail + + diff --git a/tests/fixtures/xmldata/valid/simple-ns_raw.xml b/tests/fixtures/xmldata/valid/simple-ns_raw.xml new file mode 100644 index 00000000..d96c4b2d --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-ns_raw.xml @@ -0,0 +1 @@ +texttexttail diff --git a/tests/fixtures/xmldata/valid/simple-standalone-no_formatted.xml b/tests/fixtures/xmldata/valid/simple-standalone-no_formatted.xml new file mode 100644 index 00000000..f3c84fc4 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-standalone-no_formatted.xml @@ -0,0 +1,3 @@ + + +........ diff --git a/tests/fixtures/xmldata/valid/simple-standalone-no_raw.xml b/tests/fixtures/xmldata/valid/simple-standalone-no_raw.xml new file mode 100644 index 00000000..ea45d264 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-standalone-no_raw.xml @@ -0,0 +1,2 @@ + +........ diff --git a/tests/fixtures/xmldata/valid/simple-standalone-yes_formatted.xml b/tests/fixtures/xmldata/valid/simple-standalone-yes_formatted.xml new file mode 100644 index 00000000..c1dd7a28 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-standalone-yes_formatted.xml @@ -0,0 +1,3 @@ + + +........ diff --git a/tests/fixtures/xmldata/valid/simple-standalone-yes_raw.xml b/tests/fixtures/xmldata/valid/simple-standalone-yes_raw.xml new file mode 100644 index 00000000..d9be4c82 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple-standalone-yes_raw.xml @@ -0,0 +1,2 @@ + +........ diff --git a/tests/fixtures/xmldata/valid/simple_formatted.xml b/tests/fixtures/xmldata/valid/simple_formatted.xml new file mode 100644 index 00000000..31973fa6 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple_formatted.xml @@ -0,0 +1,8 @@ + + + + text + text + tail + + diff --git a/tests/fixtures/xmldata/valid/simple_raw.xml b/tests/fixtures/xmldata/valid/simple_raw.xml new file mode 100644 index 00000000..d14932b9 --- /dev/null +++ b/tests/fixtures/xmldata/valid/simple_raw.xml @@ -0,0 +1 @@ +texttexttail diff --git a/tests/fixtures/xmldata/xhtml/xhtml_formatted.xml b/tests/fixtures/xmldata/xhtml/xhtml_formatted.xml new file mode 100644 index 00000000..67bbdb33 --- /dev/null +++ b/tests/fixtures/xmldata/xhtml/xhtml_formatted.xml @@ -0,0 +1,29 @@ + + + + + XHTML 1.0 Strict Example + + + +

+ This is an example of an + XHTML + 1.0 Strict document. +
+ Valid XHTML 1.0 Strict +
+ + +

+ + diff --git a/tests/fixtures/xmldata/xhtml/xhtml_formatted_python_less_than_3.8.xml b/tests/fixtures/xmldata/xhtml/xhtml_formatted_python_less_than_3.8.xml new file mode 100644 index 00000000..7e6eced3 --- /dev/null +++ b/tests/fixtures/xmldata/xhtml/xhtml_formatted_python_less_than_3.8.xml @@ -0,0 +1,30 @@ + + + + + + XHTML 1.0 Strict Example + + + +

+ This is an example of an + XHTML + 1.0 Strict document. +
+ Valid XHTML 1.0 Strict +
+ + +

+ + diff --git a/tests/fixtures/xmldata/xhtml/xhtml_raw.xml b/tests/fixtures/xmldata/xhtml/xhtml_raw.xml new file mode 100644 index 00000000..78684272 --- /dev/null +++ b/tests/fixtures/xmldata/xhtml/xhtml_raw.xml @@ -0,0 +1,30 @@ + + + + + XHTML 1.0 Strict Example + + + +

This is an example of an + XHTML 1.0 Strict document.
+ Valid XHTML 1.0 Strict
+ + +

+ + diff --git a/tests/test_output.py b/tests/test_output.py index ee3a85c4..1a9d9dad 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -377,6 +377,10 @@ class TestFormatOptions: 'indent': 10, 'format': True }, + 'xml': { + 'format': True, + 'indent': 4, + }, } ), ( @@ -392,6 +396,10 @@ class TestFormatOptions: 'indent': 4, 'format': True }, + 'xml': { + 'format': True, + 'indent': 4, + }, } ), ( @@ -409,6 +417,10 @@ class TestFormatOptions: 'indent': 4, 'format': True }, + 'xml': { + 'format': True, + 'indent': 4, + }, } ), ( @@ -423,6 +435,8 @@ class TestFormatOptions: ( [ '--format-options=json.indent:2', + '--format-options=xml.format:false', + '--format-options=xml.indent:2', '--unsorted', '--no-unsorted', ], @@ -435,6 +449,10 @@ class TestFormatOptions: 'indent': 2, 'format': True }, + 'xml': { + 'format': False, + 'indent': 2, + }, } ), ( @@ -452,6 +470,10 @@ class TestFormatOptions: 'indent': 2, 'format': True }, + 'xml': { + 'format': True, + 'indent': 4, + }, } ), ( @@ -470,6 +492,10 @@ class TestFormatOptions: 'indent': 2, 'format': True }, + 'xml': { + 'format': True, + 'indent': 4, + }, } ), ], diff --git a/tests/test_xml.py b/tests/test_xml.py new file mode 100644 index 00000000..a91a583a --- /dev/null +++ b/tests/test_xml.py @@ -0,0 +1,90 @@ +import sys + +import pytest +import responses + +from httpie.constants import UTF8 +from httpie.output.formatters.xml import parse_xml, pretty_xml + +from .fixtures import XML_FILES_PATH, XML_FILES_VALID, XML_FILES_INVALID +from .utils import http + +SAMPLE_XML_DATA = 'text' + + +@pytest.mark.parametrize( + 'options, expected_xml', + [ + ('xml.format:false', SAMPLE_XML_DATA), + ('xml.indent:2', pretty_xml(parse_xml(SAMPLE_XML_DATA), indent=2)), + ('xml.indent:4', pretty_xml(parse_xml(SAMPLE_XML_DATA))), + ] +) +@responses.activate +def test_xml_format_options(options, expected_xml): + url = 'https://example.org' + responses.add(responses.GET, url, body=SAMPLE_XML_DATA, + content_type='application/xml') + + r = http('--format-options', options, url) + assert expected_xml in r + + +@pytest.mark.parametrize('file', XML_FILES_VALID) +@responses.activate +def test_valid_xml(file): + """Test XML formatter limits with data containing comments, doctypes + and other XML-specific subtles. + """ + if 'standalone' in file.stem and sys.version_info < (3, 9): + pytest.skip('Standalone XML requires Python 3.9+') + + url = 'https://example.org' + xml_data = file.read_text(encoding=UTF8) + expected_xml_file = file.with_name(file.name.replace('_raw', '_formatted')) + expected_xml_output = expected_xml_file.read_text(encoding=UTF8) + responses.add(responses.GET, url, body=xml_data, + content_type='application/xml') + + r = http(url) + assert expected_xml_output in r + + +@responses.activate +def test_xml_xhtml(): + """XHTML responses are handled by the XML formatter.""" + url = 'https://example.org' + file = XML_FILES_PATH / 'xhtml' / 'xhtml_raw.xml' + xml_data = file.read_text(encoding=UTF8) + + # Python < 3.8 was sorting attributes (https://bugs.python.org/issue34160) + # so we have 2 different output expected given the Python version. + expected_file_name = ( + 'xhtml_formatted_python_less_than_3.8.xml' + if sys.version_info < (3, 8) + else 'xhtml_formatted.xml' + ) + expected_xml_file = file.with_name(expected_file_name) + expected_xml_output = expected_xml_file.read_text(encoding=UTF8) + responses.add(responses.GET, url, body=xml_data, + content_type='application/xhtml+xml') + + r = http(url) + print(r) + assert expected_xml_output in r + + +@pytest.mark.parametrize('file', XML_FILES_INVALID) +@responses.activate +def test_invalid_xml(file): + """Testing several problematic XML files, none should be formatted + and none should make HTTPie to crash. + """ + url = 'https://example.org' + xml_data = file.read_text(encoding=UTF8) + responses.add(responses.GET, url, body=xml_data, + content_type='application/xml') + + # No formatting done, data is simply printed as-is + r = http(url) + assert xml_data in r