From 712cea39f1ec670e41526d0cebe4843745303a5f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:09:54 +0100 Subject: [PATCH 01/10] :white_check_mark: Add test for https://github.com/davep/rogallo/discussions/241 Currently failing (on purpose). --- tests/test_gopher_map.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_gopher_map.py b/tests/test_gopher_map.py index 4097cf2..0a4f086 100644 --- a/tests/test_gopher_map.py +++ b/tests/test_gopher_map.py @@ -7,6 +7,7 @@ ############################################################################## # Local imports. from gophermap import GopherMap, NoFields +from gophermap.item_type import ItemType ############################################################################## @@ -51,4 +52,20 @@ def test_skip_empty_lines() -> None: assert len(gopher_map.items) == 0 +############################################################################## +def test_allow_lines_without_tabs() -> None: + """Test that lines without tabs are allowed. + + https://github.com/davep/rogallo/discussions/241 + """ + gopher_map = GopherMap(raw := "iHello\r\n.\r\n") + assert raw == gopher_map.raw + assert len(gopher_map.items) == 1 + assert gopher_map.items[0].type is ItemType.INFO + assert gopher_map.items[0].display_text == "Hello" + assert gopher_map.items[0].selector == "" + assert gopher_map.items[0].host == "" + assert gopher_map.items[0].port == 70 + + ### test_gopher_map.py ends here From 105c6bbc6ceac08d2c215c81b82b184112071236 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:10:26 +0100 Subject: [PATCH 02/10] :rotating_light: Correctly test a line type Driveby fix. --- tests/test_gopher_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gopher_map.py b/tests/test_gopher_map.py index 0a4f086..f440f45 100644 --- a/tests/test_gopher_map.py +++ b/tests/test_gopher_map.py @@ -37,7 +37,7 @@ def test_valid_map() -> None: gopher_map = GopherMap(raw := "iHello\tworld\tlocalhost\t70\r\n.\r\n") assert raw == gopher_map.raw assert len(gopher_map.items) == 1 - assert gopher_map.items[0].type.name == "INFO" + assert gopher_map.items[0].type is ItemType.INFO assert gopher_map.items[0].display_text == "Hello" assert gopher_map.items[0].selector == "world" assert gopher_map.items[0].host == "localhost" From 00300c0b5460d6ed5af502fc976fd74669e10c33 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:14:20 +0100 Subject: [PATCH 03/10] :hammer: Don't require tabs --- ChangeLog.md | 7 +++++++ src/gophermap/item.py | 2 -- tests/test_gopher_map.py | 13 +------------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index be2390e..6886e0b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,12 @@ # GopherMap ChangeLog +## Unreleased + +**Released: 2026-07-27** + +- Remove the "no tabs is a problem" approach. + ([#7](https://github.com/davep/gophermap/pull/7)) + ## v0.1.1 **Released: 2026-07-27** diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 6e0e5c4..354eb9b 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -18,8 +18,6 @@ def __init__(self, line: str) -> None: """ if not line: raise NoFields("The Gopher item line is empty.") - if "\t" not in line: - raise NoFields(f"The Gopher item line has no tab characters: {line!r}") self._raw = line """The raw text of the Gopher item.""" fields = line.rstrip("\r\n").split("\t") diff --git a/tests/test_gopher_map.py b/tests/test_gopher_map.py index f440f45..6cfedc7 100644 --- a/tests/test_gopher_map.py +++ b/tests/test_gopher_map.py @@ -1,12 +1,8 @@ """Tests for the GopherMap class.""" -############################################################################## -# Pytest imports. -from pytest import raises - ############################################################################## # Local imports. -from gophermap import GopherMap, NoFields +from gophermap import GopherMap from gophermap.item_type import ItemType @@ -24,13 +20,6 @@ def test_eof_only_map() -> None: assert gopher_map.items == () -############################################################################## -def test_no_fields() -> None: - """Test that a Gopher map with no fields raises NoFields.""" - with raises(NoFields): - _ = GopherMap("x").items - - ############################################################################## def test_valid_map() -> None: """Test that a valid Gopher map is parsed correctly.""" From 4c2753bc7ce62cf91cb9c57c279d4e463f1dbdce Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:35:52 +0100 Subject: [PATCH 04/10] :hammer: Improve the no-fields testing --- src/gophermap/item.py | 8 +++++--- tests/test_gopher_item.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 354eb9b..490458c 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -16,11 +16,13 @@ def __init__(self, line: str) -> None: Args: line: The line of text from the Gopher map. """ - if not line: - raise NoFields("The Gopher item line is empty.") self._raw = line """The raw text of the Gopher item.""" - fields = line.rstrip("\r\n").split("\t") + if not (line := line.strip("\r\n")): + raise NoFields("The Gopher item line is empty.") + fields = line.split("\t") + if not fields or not fields[0]: + raise NoFields(f"The Gopher item line has no fields: {line!r}") self._type = ItemType(fields[0][0] or ItemType.INFO) """The type of the Gopher item.""" self._display_text = fields[0][1:] if len(fields) > 0 else "" diff --git a/tests/test_gopher_item.py b/tests/test_gopher_item.py index 485de77..3ebf090 100644 --- a/tests/test_gopher_item.py +++ b/tests/test_gopher_item.py @@ -2,7 +2,7 @@ ############################################################################## # Pytest imports. -from pytest import raises +from pytest import mark, raises ############################################################################## # Local imports. @@ -10,10 +10,18 @@ ############################################################################## -def test_empty_line() -> None: - """Test that an empty line raises NoFields.""" +@mark.parametrize( + "line", + [ + "", + "\r\n", + "\t\r\n", + ], +) +def test_for_no_fields(line: str) -> None: + """Test that a GopherItem with no fields raises NoFields.""" with raises(NoFields): - _ = GopherItem("") + _ = GopherItem(line) ############################################################################## From 0e8ae46adccc0ef7503e54bfcb873eab024a5cb3 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:55:30 +0100 Subject: [PATCH 05/10] :hammer: Relax how empty lines are handled --- src/gophermap/gopher_map.py | 3 +-- src/gophermap/item.py | 11 ++++------- tests/test_gopher_item.py | 8 ++++---- tests/test_gopher_map.py | 16 +++++++++++++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/gophermap/gopher_map.py b/src/gophermap/gopher_map.py index 8b41d26..e76ca98 100644 --- a/src/gophermap/gopher_map.py +++ b/src/gophermap/gopher_map.py @@ -41,8 +41,7 @@ def _parse_map(map_text: str) -> Iterator[GopherItem]: for line in map_text.splitlines(keepends=True): if line.strip() == EOF: break - if line.strip(): - yield GopherItem(line) + yield GopherItem(line) @property def raw(self) -> str: diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 490458c..630556f 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -18,14 +18,11 @@ def __init__(self, line: str) -> None: """ self._raw = line """The raw text of the Gopher item.""" - if not (line := line.strip("\r\n")): - raise NoFields("The Gopher item line is empty.") - fields = line.split("\t") - if not fields or not fields[0]: - raise NoFields(f"The Gopher item line has no fields: {line!r}") - self._type = ItemType(fields[0][0] or ItemType.INFO) + if not (fields := line.strip("\r\n").split("\t"))[0]: + fields[0] = ItemType.INFO.value + self._type = ItemType(fields[0][0]) """The type of the Gopher item.""" - self._display_text = fields[0][1:] if len(fields) > 0 else "" + self._display_text = fields[0][1:] """The display text of the Gopher item.""" self._selector = fields[1] if len(fields) > 1 else "" """The selector of the Gopher item.""" diff --git a/tests/test_gopher_item.py b/tests/test_gopher_item.py index 3ebf090..e23a466 100644 --- a/tests/test_gopher_item.py +++ b/tests/test_gopher_item.py @@ -7,6 +7,7 @@ ############################################################################## # Local imports. from gophermap import GopherItem, NoFields +from gophermap.item_type import ItemType ############################################################################## @@ -18,10 +19,9 @@ "\t\r\n", ], ) -def test_for_no_fields(line: str) -> None: - """Test that a GopherItem with no fields raises NoFields.""" - with raises(NoFields): - _ = GopherItem(line) +def test_empty_lines_become_info(line: str) -> None: + """Test that empty lines become INFO items.""" + assert GopherItem(line).type is ItemType.INFO ############################################################################## diff --git a/tests/test_gopher_map.py b/tests/test_gopher_map.py index 6cfedc7..8d04fd6 100644 --- a/tests/test_gopher_map.py +++ b/tests/test_gopher_map.py @@ -34,11 +34,21 @@ def test_valid_map() -> None: ############################################################################## -def test_skip_empty_lines() -> None: - """Test that empty lines are skipped.""" +def test_empty_lines_become_info() -> None: + """Test that empty lines become INFO items.""" gopher_map = GopherMap(raw := "\r\n\r\n.\r\n") assert raw == gopher_map.raw - assert len(gopher_map.items) == 0 + assert len(gopher_map.items) == 2 + assert gopher_map.items[0].type is ItemType.INFO + assert gopher_map.items[0].display_text == "" + assert gopher_map.items[0].selector == "" + assert gopher_map.items[0].host == "" + assert gopher_map.items[0].port == 70 + assert gopher_map.items[1].type is ItemType.INFO + assert gopher_map.items[1].display_text == "" + assert gopher_map.items[1].selector == "" + assert gopher_map.items[1].host == "" + assert gopher_map.items[1].port == 70 ############################################################################## From 0a7ba91d7bccbe63efb55866798a57c14966f84e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:56:01 +0100 Subject: [PATCH 06/10] :fire: Remove unused imports --- src/gophermap/item.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 630556f..3b5a8f8 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -2,7 +2,6 @@ ############################################################################## # Local imports. -from .exceptions import NoFields from .item_type import ItemType From 42d477a0c21b7becc6618ed41ea09bc7a2a07eee Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 14:57:05 +0100 Subject: [PATCH 07/10] :fire: Remove unused imports --- tests/test_gopher_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gopher_item.py b/tests/test_gopher_item.py index e23a466..2c3722d 100644 --- a/tests/test_gopher_item.py +++ b/tests/test_gopher_item.py @@ -2,11 +2,11 @@ ############################################################################## # Pytest imports. -from pytest import mark, raises +from pytest import mark ############################################################################## # Local imports. -from gophermap import GopherItem, NoFields +from gophermap import GopherItem from gophermap.item_type import ItemType From f1d5cbd01aefe68db5a2986d048e03a1c189eedb Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 15:02:14 +0100 Subject: [PATCH 08/10] :hammer: Relax EOL handling --- src/gophermap/gopher_map.py | 4 ++-- src/gophermap/item.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gophermap/gopher_map.py b/src/gophermap/gopher_map.py index e76ca98..2502904 100644 --- a/src/gophermap/gopher_map.py +++ b/src/gophermap/gopher_map.py @@ -38,8 +38,8 @@ def _parse_map(map_text: str) -> Iterator[GopherItem]: Yields: Gopher items. """ - for line in map_text.splitlines(keepends=True): - if line.strip() == EOF: + for line in map_text.splitlines(): + if line == EOF: break yield GopherItem(line) diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 3b5a8f8..b677b50 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -17,7 +17,7 @@ def __init__(self, line: str) -> None: """ self._raw = line """The raw text of the Gopher item.""" - if not (fields := line.strip("\r\n").split("\t"))[0]: + if not (fields := line.rstrip("\r\n").split("\t"))[0]: fields[0] = ItemType.INFO.value self._type = ItemType(fields[0][0]) """The type of the Gopher item.""" From dea1433b9f0f2645b257b0ba0cf31d033bab21f4 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 15:04:32 +0100 Subject: [PATCH 09/10] :books: Update the ChangeLog --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 6886e0b..b0e7951 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,7 +4,7 @@ **Released: 2026-07-27** -- Remove the "no tabs is a problem" approach. +- Make parsing far more relaxed. Empty lines also become empty `i`nfo lines. ([#7](https://github.com/davep/gophermap/pull/7)) ## v0.1.1 From fd7ac6755d028c47657059360f3e7219895d45ef Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 28 Jul 2026 15:06:06 +0100 Subject: [PATCH 10/10] :books: Mark the exceptions as deprecated I could just remove them, but Rogallo is already using them. I'll be releasing this library first and then a new Rogallo which pins the lower version of this library. That means there's a window in which things would crash. So I'm going to release this, update Rogallo, and then remove the exceptions for the next release of this. --- src/gophermap/exceptions.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/gophermap/exceptions.py b/src/gophermap/exceptions.py index 31ab79a..5bc985e 100644 --- a/src/gophermap/exceptions.py +++ b/src/gophermap/exceptions.py @@ -1,14 +1,28 @@ -"""Exceptions for the library.""" +"""Exceptions for the library. + +Note: + This module is now deprecated and will be removed in a future release. +""" ############################################################################## class GopherMapError(Exception): - """Base exception for all errors raised by the GopherMap library.""" + """Base exception for all errors raised by the GopherMap library. + + Note: + This exception class is now deprecated and will be removed in a + future release. + """ ############################################################################## class NoFields(GopherMapError): - """Raised when a Gopher item has no fields.""" + """Raised when a Gopher item has no fields. + + Note: + This exception class is now deprecated and will be removed in a + future release. + """ ### exceptions.py ends here