codecov-io: # [Codecov](https://codecov.io/gh/python-tap/tappy/pull/112?src=pr&el=h1) Report
> Merging [#112](https://codecov.io/gh/python-tap/tappy/pull/112?src=pr&el=desc) into [master](https://codecov.io/gh/python-tap/tappy/commit/015c21c1a98b88f23844a44e346604f2398b11d5&el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/python-tap/tappy/pull/112/graphs/tree.svg?width=650&height=150&src=pr&token=ULVfoGuR2p)](https://codecov.io/gh/python-tap/tappy/pull/112?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #112   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           12        12           
  Lines          599       600    +1     
=========================================
+ Hits           599       600    +1     
```


| [Impacted Files](https://codecov.io/gh/python-tap/tappy/pull/112?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tap/parser.py](https://codecov.io/gh/python-tap/tappy/pull/112/diff?src=pr&el=tree#diff-dGFwL3BhcnNlci5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-tap/tappy/pull/112?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/python-tap/tappy/pull/112?src=pr&el=footer). Last update [015c21c...05c6b82](https://codecov.io/gh/python-tap/tappy/pull/112?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/AUTHORS b/AUTHORS
index e87e3b8..3735496 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -3,6 +3,7 @@ tappy was originally created by Matt Layman.
 Contributors
 ------------
 
+* Adeodato Simó
 * Andrew McNamara
 * Chris Clarke
 * Erik Cederstrand
diff --git a/docs/releases.rst b/docs/releases.rst
index dd80f97..7f708c7 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -5,6 +5,7 @@ Version 3.1, To Be Released
 ---------------------------
 
 * Add support for Python 3.8.
+* Fix parsing of multi-line strings in YAML blocks (#111)
 
 Version 3.0, Released January 10, 2020
 --------------------------------------
diff --git a/tap/parser.py b/tap/parser.py
index 637436b..2f28bd8 100644
--- a/tap/parser.py
+++ b/tap/parser.py
@@ -181,7 +181,8 @@ WARNING: Optional imports not found, TAP 13 output will be
         try:
             next(fh)
             while indent_match.match(fh.peek()):
-                raw_yaml.append(next(fh).replace(indent, "", 1))
+                yaml_line = next(fh).replace(indent, "", 1)
+                raw_yaml.append(yaml_line.rstrip("\n"))
                 # check for the end and stop adding yaml if encountered
                 if self.yaml_block_end.match(fh.peek()):
                     next(fh)

Additional newlines added in YAML blocks for multi-line strings
Hello!

I've observed that `_extract_yaml_block()` performs `"\n".join(raw_yaml)` without having stripped the newline character in each possible line, resulting in unwanted empty lines when parsing multi-line strings in YAML blocks.

I'll give an example. With this file:

```python
#!/usr/bin/python3

import tap.parser

tap_input = """TAP version 13
1..2
ok 1 A passing test
   ---
   message: test
   severity: fail
   output: |+
     this is a multiline
     string but with tap
     somehow blank lines
     are added to it.
"""
parser = tap.parser.Parser()

for i, l in enumerate(parser.parse_text(tap_input)):
    if i == 2:
        print("{!r}".format(l.yaml_block["output"].splitlines()))
```

The output is:

```
['', 'this is a multiline', '', 'string but with tap', '', 'somehow blank lines', '', 'are added to it.']
```

Where as I expected:

```
['this is a multiline', 'string but with tap', 'somehow blank lines', 'are added to it.']
```

I will submit a PR shortly.
diff --git a/tap/tests/test_parser.py b/tap/tests/test_parser.py
index 431297a..ac8f3d3 100644
--- a/tap/tests/test_parser.py
+++ b/tap/tests/test_parser.py
@@ -348,7 +348,12 @@ class TestParser(unittest.TestCase):
                  got:
                    - foo
                  expect:
-                   - bar"""
+                   - bar
+               output: |-
+                 a multiline string
+                 must be handled properly
+                 even with | pipes
+                 | here > and: there"""
         )
         parser = Parser()
         lines = []
@@ -358,20 +363,21 @@ class TestParser(unittest.TestCase):
 
         if have_yaml:
             converted_yaml = yaml.safe_load(
-                u"""
+                u'''
                message: test
                severity: fail
                data:
                  got:
                    - foo
                  expect:
-                   - bar"""
+                   - bar
+               output: "a multiline string\\nmust be handled properly\\neven with | pipes\\n| here > and: there"'''
             )
             self.assertEqual(3, len(lines))
             self.assertEqual(13, lines[0].version)
             self.assertEqual(converted_yaml, lines[2].yaml_block)
         else:
-            self.assertEqual(11, len(lines))
+            self.assertEqual(16, len(lines))
             self.assertEqual(13, lines[0].version)
             for l in list(range(3, 11)):
                 self.assertEqual("unknown", lines[l].category)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.8
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
babel==2.17.0
backports.tarfile==1.2.0
black==24.8.0
cachetools==5.5.2
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
coverage==7.6.1
cryptography==44.0.2
distlib==0.3.9
docutils==0.20.1
exceptiongroup==1.2.2
filelock==3.16.1
flake8==7.1.2
id==1.5.0
idna==3.10
imagesize==1.4.1
importlib_metadata==8.5.0
importlib_resources==6.4.5
iniconfig==2.1.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.5.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mccabe==0.7.0
mdurl==0.1.2
more-itertools==10.5.0
mypy-extensions==1.0.0
nh3==0.2.21
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
pluggy==1.5.0
pycodestyle==2.12.1
pycparser==2.22
pyflakes==3.2.0
Pygments==2.19.1
pyproject-api==1.8.0
pytest==8.3.5
pytz==2025.2
PyYAML==6.0.2
readme_renderer==43.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
snowballstemmer==2.2.0
Sphinx==7.1.2
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
-e git+https://github.com/python-tap/tappy.git@015c21c1a98b88f23844a44e346604f2398b11d5#egg=tap.py
tomli==2.2.1
tox==4.25.0
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.2.3
virtualenv==20.29.3
zipp==3.20.2

name: tappy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - black==24.8.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.6.1
      - cryptography==44.0.2
      - distlib==0.3.9
      - docutils==0.20.1
      - exceptiongroup==1.2.2
      - filelock==3.16.1
      - flake8==7.1.2
      - id==1.5.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.5.0
      - importlib-resources==6.4.5
      - iniconfig==2.1.0
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.5.0
      - markdown-it-py==3.0.0
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mdurl==0.1.2
      - more-itertools==10.5.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.6
      - pluggy==1.5.0
      - pycodestyle==2.12.1
      - pycparser==2.22
      - pyflakes==3.2.0
      - pygments==2.19.1
      - pyproject-api==1.8.0
      - pytest==8.3.5
      - pytz==2025.2
      - pyyaml==6.0.2
      - readme-renderer==43.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - snowballstemmer==2.2.0
      - sphinx==7.1.2
      - sphinxcontrib-applehelp==1.0.4
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==2.2.1
      - tox==4.25.0
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.2.3
      - virtualenv==20.29.3
      - zipp==3.20.2
prefix: /opt/conda/envs/tappy

tap/tests/test_parser.py::TestParser::test_parses_yaml_more_complex
tap/tests/test_parser.py::TestParser::test_after_hash_is_not_description tap/tests/test_parser.py::TestParser::test_bail_out_line tap/tests/test_parser.py::TestParser::test_diagnostic_line tap/tests/test_parser.py::TestParser::test_errors_on_old_version tap/tests/test_parser.py::TestParser::test_finds_description tap/tests/test_parser.py::TestParser::test_finds_directive tap/tests/test_parser.py::TestParser::test_finds_not_ok tap/tests/test_parser.py::TestParser::test_finds_number tap/tests/test_parser.py::TestParser::test_finds_ok tap/tests/test_parser.py::TestParser::test_finds_plan tap/tests/test_parser.py::TestParser::test_finds_plan_with_skip tap/tests/test_parser.py::TestParser::test_finds_skip tap/tests/test_parser.py::TestParser::test_finds_todo tap/tests/test_parser.py::TestParser::test_finds_version tap/tests/test_parser.py::TestParser::test_ignores_plan_with_any_non_skip_directive tap/tests/test_parser.py::TestParser::test_malformed_yaml tap/tests/test_parser.py::TestParser::test_parse_empty_file tap/tests/test_parser.py::TestParser::test_parses_file tap/tests/test_parser.py::TestParser::test_parses_mixed tap/tests/test_parser.py::TestParser::test_parses_stdin tap/tests/test_parser.py::TestParser::test_parses_text tap/tests/test_parser.py::TestParser::test_parses_yaml tap/tests/test_parser.py::TestParser::test_parses_yaml_no_association tap/tests/test_parser.py::TestParser::test_parses_yaml_no_end tap/tests/test_parser.py::TestParser::test_parses_yaml_no_start tap/tests/test_parser.py::TestParser::test_unrecognizable_line
docs/releases.rst tap/parser.py AUTHORS
docs/releases.rst tap/parser.py AUTHORS