From da57932b9f480e5b0f36edbe7054c498d18ed773 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 12 Aug 2026 12:04:03 +0100 Subject: [PATCH 1/4] :sparkles: Add an optional strict mode --- src/gophermap/__init__.py | 2 ++ src/gophermap/exceptions.py | 9 +++++++++ src/gophermap/gopher_map.py | 19 ++++++++++++++----- src/gophermap/item.py | 13 ++++++++++++- tests/test_gopher_map.py | 21 ++++++++++++++++++++- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/gophermap/exceptions.py diff --git a/src/gophermap/__init__.py b/src/gophermap/__init__.py index 967250f..776644d 100644 --- a/src/gophermap/__init__.py +++ b/src/gophermap/__init__.py @@ -16,6 +16,7 @@ ############################################################################## # Local imports. +from .exceptions import GopherMapError from .gopher_map import GopherMap from .item import GopherItem from .item_type import ItemType @@ -25,6 +26,7 @@ __all__ = [ "GopherItem", "GopherMap", + "GopherMapError", "ItemType", ] diff --git a/src/gophermap/exceptions.py b/src/gophermap/exceptions.py new file mode 100644 index 0000000..09d4c7e --- /dev/null +++ b/src/gophermap/exceptions.py @@ -0,0 +1,9 @@ +"""Provides exceptions for the Gopher map.""" + + +############################################################################## +class GopherMapError(Exception): + """Base class for Gopher map errors.""" + + +### exceptions.py ends here diff --git a/src/gophermap/gopher_map.py b/src/gophermap/gopher_map.py index 2502904..0650bf2 100644 --- a/src/gophermap/gopher_map.py +++ b/src/gophermap/gopher_map.py @@ -8,6 +8,7 @@ ############################################################################## # Local imports. +from .exceptions import GopherMapError from .item import GopherItem ############################################################################## @@ -19,17 +20,19 @@ class GopherMap: """A class for parsing and holding a Gopher map.""" - def __init__(self, map_text: str) -> None: + def __init__(self, map_text: str, strict: bool = False) -> None: """Initialise the Gopher map. Args: map_text: The text of the Gopher map. + strict: Whether to be strict about parsing the Gopher map. """ self._raw = map_text """The raw text of the Gopher map.""" + self._strict = strict + """Whether to be strict about parsing the Gopher map.""" - @staticmethod - def _parse_map(map_text: str) -> Iterator[GopherItem]: + def _parse_map(self, map_text: str) -> Iterator[GopherItem]: """Parse the Gopher map text into a list of Gopher items. Args: @@ -38,10 +41,12 @@ def _parse_map(map_text: str) -> Iterator[GopherItem]: Yields: Gopher items. """ + if self._strict and not map_text: + raise GopherMapError("Gopher map is empty") for line in map_text.splitlines(): if line == EOF: break - yield GopherItem(line) + yield GopherItem(line, self._strict) @property def raw(self) -> str: @@ -50,7 +55,11 @@ def raw(self) -> str: @cached_property def items(self) -> tuple[GopherItem, ...]: - """The list of Gopher items in the map.""" + """The list of Gopher items in the map. + + Raises: + GopherMapError: If the map is in strict mode and issues are found. + """ return tuple(self._parse_map(self._raw)) diff --git a/src/gophermap/item.py b/src/gophermap/item.py index b677b50..814d86f 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -2,6 +2,7 @@ ############################################################################## # Local imports. +from .exceptions import GopherMapError from .item_type import ItemType @@ -9,11 +10,15 @@ class GopherItem: """A class for holding an item in the Gopher map.""" - def __init__(self, line: str) -> None: + def __init__(self, line: str, strict: bool = False) -> None: """Initialise the Gopher item. Args: line: The line of text from the Gopher map. + strict: Whether to be strict about parsing the Gopher item. + + Raises: + GopherMapError: If the map is in strict mode and issues are found. """ self._raw = line """The raw text of the Gopher item.""" @@ -29,6 +34,12 @@ def __init__(self, line: str) -> None: """The host of the Gopher item.""" self._port = int(fields[3]) if len(fields) > 3 and fields[3].isdigit() else 70 """The port of the Gopher item.""" + # If we're in strict mode, let's do some harsh checks. + if strict: + if "\t" not in line: + raise GopherMapError(f"Line is missing a tab character: {line!r}") + if self._type is ItemType.UNKNOWN: + raise GopherMapError(f"Unknown item type: {self._type!r}") @property def raw(self) -> str: diff --git a/tests/test_gopher_map.py b/tests/test_gopher_map.py index 8d04fd6..1741cfd 100644 --- a/tests/test_gopher_map.py +++ b/tests/test_gopher_map.py @@ -1,8 +1,12 @@ """Tests for the GopherMap class.""" +############################################################################## +# Pytest imports. +from pytest import mark, raises + ############################################################################## # Local imports. -from gophermap import GopherMap +from gophermap import GopherMap, GopherMapError from gophermap.item_type import ItemType @@ -67,4 +71,19 @@ def test_allow_lines_without_tabs() -> None: assert gopher_map.items[0].port == 70 +############################################################################## +@mark.parametrize( + "test_map", + [ + "", + "Test\r\n.\r\n", + "!Hello\tworld\tlocalhost\r\n.\r\n", + ], +) +def test_strict_on_bad_map(test_map: str) -> None: + """Test that strict mode raises an error on a bad map.""" + with raises(GopherMapError): + _ = GopherMap(test_map, strict=True).items + + ### test_gopher_map.py ends here From 808e1ca953843fcc8a3d0cdf3c56177789283869 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 12 Aug 2026 12:09:40 +0100 Subject: [PATCH 2/4] :books: Update the ChangeLog --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3743479..eaa36b8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,8 @@ - Removed the exceptions from the library. ([#10](https://github.com/davep/gophermap/pull/10)) +- Added an optional strict mode (and, in doing so, added an exception back). + ([#12](https://github.com/davep/gophermap/pull/12)) ## v0.2.0 From 0b04102e51ba2ca1caf43fee1155d1b13bee826f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 12 Aug 2026 12:12:41 +0100 Subject: [PATCH 3/4] :hammer: Make the exceptions a wee bit more granular --- src/gophermap/exceptions.py | 15 +++++++++++++++ src/gophermap/gopher_map.py | 6 +++--- src/gophermap/item.py | 9 +++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/gophermap/exceptions.py b/src/gophermap/exceptions.py index 09d4c7e..7b7970c 100644 --- a/src/gophermap/exceptions.py +++ b/src/gophermap/exceptions.py @@ -6,4 +6,19 @@ class GopherMapError(Exception): """Base class for Gopher map errors.""" +############################################################################## +class EmptyMap(GopherMapError): + """Raised when a Gopher map is empty.""" + + +############################################################################## +class NoFields(GopherMapError): + """Raised when a Gopher item has no fields.""" + + +############################################################################## +class UnknownItemType(GopherMapError): + """Raised when a Gopher item has an unknown type.""" + + ### exceptions.py ends here diff --git a/src/gophermap/gopher_map.py b/src/gophermap/gopher_map.py index 0650bf2..d2841ec 100644 --- a/src/gophermap/gopher_map.py +++ b/src/gophermap/gopher_map.py @@ -8,7 +8,7 @@ ############################################################################## # Local imports. -from .exceptions import GopherMapError +from .exceptions import EmptyMap from .item import GopherItem ############################################################################## @@ -42,7 +42,7 @@ def _parse_map(self, map_text: str) -> Iterator[GopherItem]: Gopher items. """ if self._strict and not map_text: - raise GopherMapError("Gopher map is empty") + raise EmptyMap("Gopher map is empty") for line in map_text.splitlines(): if line == EOF: break @@ -58,7 +58,7 @@ def items(self) -> tuple[GopherItem, ...]: """The list of Gopher items in the map. Raises: - GopherMapError: If the map is in strict mode and issues are found. + EmptyMap: If in strict mode and the map is empty. """ return tuple(self._parse_map(self._raw)) diff --git a/src/gophermap/item.py b/src/gophermap/item.py index 814d86f..6fc9a46 100644 --- a/src/gophermap/item.py +++ b/src/gophermap/item.py @@ -2,7 +2,7 @@ ############################################################################## # Local imports. -from .exceptions import GopherMapError +from .exceptions import NoFields, UnknownItemType from .item_type import ItemType @@ -18,7 +18,8 @@ def __init__(self, line: str, strict: bool = False) -> None: strict: Whether to be strict about parsing the Gopher item. Raises: - GopherMapError: If the map is in strict mode and issues are found. + NoFields: If the line is missing a tab character and strict mode is enabled. + UnknownItemType: If the item type is unknown and strict mode is enabled. """ self._raw = line """The raw text of the Gopher item.""" @@ -37,9 +38,9 @@ def __init__(self, line: str, strict: bool = False) -> None: # If we're in strict mode, let's do some harsh checks. if strict: if "\t" not in line: - raise GopherMapError(f"Line is missing a tab character: {line!r}") + raise NoFields(f"Line is missing a tab character: {line!r}") if self._type is ItemType.UNKNOWN: - raise GopherMapError(f"Unknown item type: {self._type!r}") + raise UnknownItemType(f"Unknown item type: {self._type!r}") @property def raw(self) -> str: From 00096603162c053ca1689c8bf9aabf4ed3e64f14 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 12 Aug 2026 13:35:03 +0100 Subject: [PATCH 4/4] :books: Improve a docstring --- src/gophermap/gopher_map.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gophermap/gopher_map.py b/src/gophermap/gopher_map.py index d2841ec..3e64ab7 100644 --- a/src/gophermap/gopher_map.py +++ b/src/gophermap/gopher_map.py @@ -58,7 +58,9 @@ def items(self) -> tuple[GopherItem, ...]: """The list of Gopher items in the map. Raises: - EmptyMap: If in strict mode and the map is empty. + EmptyMap: If the map is empty and strict mode is enabled. + NoFields: If the line is missing a tab character and strict mode is enabled. + UnknownItemType: If the item type is unknown and strict mode is enabled. """ return tuple(self._parse_map(self._raw))