From ca086e3c695938fcde5ff9c0409e2cc5938d45c0 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:13:47 -0400 Subject: [PATCH 01/10] Added deprecation text and made change to private method --- snakemd/elements.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/snakemd/elements.py b/snakemd/elements.py index 2a1d8d7..0118385 100644 --- a/snakemd/elements.py +++ b/snakemd/elements.py @@ -845,6 +845,9 @@ class MDList(Block): (i.e., :code:`- [x]`) - set to :code:`Iterable[bool]` to render the checked status of the top-level list elements directly + + .. deprecated:: 2.4 + Use :class:`snakemd.Checklist` template instead """ def __init__( @@ -964,7 +967,8 @@ def _process_items(items) -> list[Block]: processed.append(item) return processed - def _top_level_count(self) -> int: + @staticmethod + def _top_level_count(items) -> int: """ Given that MDList can accept a variety of blocks, we need to know how many items in the provided list @@ -972,11 +976,13 @@ def _top_level_count(self) -> int: We use this number to throw errors if this count does not match up with the checklist count. + :param items: + a list of items :return: a count of top-level elements """ count = 0 - for item in self._items: + for item in items: if not isinstance(item, MDList): count += 1 return count From 6b5ef4a3b6ac74f31f9389c3956ba4b8b6b23325 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:14:01 -0400 Subject: [PATCH 02/10] Started toying with Checklist object --- snakemd/templates.py | 68 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/snakemd/templates.py b/snakemd/templates.py index bef9736..a49b19e 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -84,14 +84,78 @@ class Kind(Enum): CAUTION = auto() def __init__(self, kind: Kind, message: str | Iterable[str | Inline | Block]) -> None: + super().__init__() self._kind = kind self._message = message + self._alert = Quote([f"[!{self._kind.name}]", self._message]) def __str__(self) -> str: - return str(Quote([f"[!{self._kind.name}]", self._message])) + """ + Renders self as a markdown ready string. See + :class:`snakemd.Quote` for more details. + + :return: + the Alert as a markdown string + """ + return str(self._alert) def __repr__(self) -> str: - return f"Alerts(kind={self._kind!r},message={self._message!r})" + """ + Renders self as an unambiguous string for development. + See :class:`snakemd.Quote` for more details. + + :return: + the Alert as a development string + """ + return repr(self._alert) + + +class Checklist(Template): + """ + Checklist is an MDList wrapper to provide support + for Markdown checklists, which are a Markdown + extension. Previously, this feature was baked + directly into MDList. However, because checklists + are not a vanilla Markdown feature, they were + moved here. + + .. versionadded:: 2.4 + Included for user convenience + + :raises ValueError: + when the checked argument is an Iterable[bool] that does not + match the number of top-level elements in the list + :param Iterable[str | Inline | Block] items: + a "list" of objects to be rendered as a list + :param bool | Iterable[bool] checked: + the checked state of the list + + - defaults to :code:`False` which renders a series of unchecked + boxes (i.e., :code:`- [ ]`) + - set to :code:`True` to render a series of checked boxes + (i.e., :code:`- [x]`) + - set to :code:`Iterable[bool]` to render the checked + status of the top-level list elements directly + """ + def __init__( + self, + items: Iterable[str | Inline | Block], + checked: bool | Iterable[bool] = False + ) -> None: + super.__init__() + self._items: list[Block] = MDList._process_items(items) + self._checked = bool | list[bool] = ( + checked if checked is None or isinstance(checked, bool) else list(checked) + ) + if isinstance(self._checked, list) and MDList._top_level_count() != len( + self._checked + ): + raise ValueError( + "Number of top-level elements in checklist does not " + "match number of booleans supplied by checked parameter: " + f"{self._checked}" + ) + class CSVTable(Template): From b4714a17f3a661443de1257aa29aee82f695d48a Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:29:58 -0400 Subject: [PATCH 03/10] Added an initial iteration of the string method --- snakemd/templates.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/snakemd/templates.py b/snakemd/templates.py index a49b19e..8ba2c9f 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -157,6 +157,25 @@ def __init__( ) + def __str__(self): + output = [] + i = 1 + for item in self._items: + + if isinstance(self._checked, bool): + checked_str = "X" if self._checked else " " + row = f"{row} [{checked_str}] {item}" + else: + checked_str = "X" if self._checked[i - 1] else " " + row = f"{row} [{checked_str}] {item}" + + output.append(row) + i += 1 + + checklist = "\n".join(output) + logger.debug("Rendered markdown list: %r", checklist) + return checklist + class CSVTable(Template): """ From fb7e71c8a75bec2b62528c1b43fd34c7b010f7d2 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:44:52 -0400 Subject: [PATCH 04/10] Got a prototype working --- snakemd/elements.py | 2 +- snakemd/templates.py | 36 ++++++++++++++++++++++++++----- tests/templates/test_checklist.py | 17 +++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 tests/templates/test_checklist.py diff --git a/snakemd/elements.py b/snakemd/elements.py index 0118385..a9dc270 100644 --- a/snakemd/elements.py +++ b/snakemd/elements.py @@ -862,7 +862,7 @@ def __init__( checked if checked is None or isinstance(checked, bool) else list(checked) ) self._space = "" - if isinstance(self._checked, list) and self._top_level_count() != len( + if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len( self._checked ): raise ValueError( diff --git a/snakemd/templates.py b/snakemd/templates.py index 8ba2c9f..1a65fca 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -112,7 +112,7 @@ def __repr__(self) -> str: class Checklist(Template): """ - Checklist is an MDList wrapper to provide support + Checklist is an MDList extension to provide support for Markdown checklists, which are a Markdown extension. Previously, this feature was baked directly into MDList. However, because checklists @@ -142,12 +142,13 @@ def __init__( items: Iterable[str | Inline | Block], checked: bool | Iterable[bool] = False ) -> None: - super.__init__() + super().__init__() self._items: list[Block] = MDList._process_items(items) - self._checked = bool | list[bool] = ( + self._checked: bool | list[bool] = ( checked if checked is None or isinstance(checked, bool) else list(checked) ) - if isinstance(self._checked, list) and MDList._top_level_count() != len( + self._space = "" + if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len( self._checked ): raise ValueError( @@ -156,11 +157,11 @@ def __init__( f"{self._checked}" ) - def __str__(self): output = [] i = 1 for item in self._items: + row = f"{self._space}-" if isinstance(self._checked, bool): checked_str = "X" if self._checked else " " @@ -175,6 +176,31 @@ def __str__(self): checklist = "\n".join(output) logger.debug("Rendered markdown list: %r", checklist) return checklist + + def __repr__(self) -> str: + """ + Renders self as an unambiguous string for development. + In this case, it displays in the style of a dataclass, + where instance variables are listed with their + values. Unlike many of the other templates, Checklists + aren't a direct wrapper of MDList, and therefore cannot + be represented as MDList alone. + + .. doctest:: checklist + + >>> checklist = Checklist(["Do Homework"], True) + >>> repr(checklist) + "Checklist(items=[Paragraph(...)], checked=True)" + + :return: + the Checklist object as a development string + """ + return ( + f"Checklist(" + f"items={self._items!r}, " + f"checked={self._checked!r}" + f")" + ) class CSVTable(Template): diff --git a/tests/templates/test_checklist.py b/tests/templates/test_checklist.py new file mode 100644 index 0000000..d71291b --- /dev/null +++ b/tests/templates/test_checklist.py @@ -0,0 +1,17 @@ +from snakemd.templates import Checklist + +def test_checklist_one_item_true(): + checklist = Checklist(["Write code"], True) + assert str(checklist) == "- [X] Write code" + +def test_checklist_one_item_false(): + checklist = Checklist(["Write code"], False) + assert str(checklist) == "- [ ] Write code" + +def test_checklist_one_item_explicit(): + checklist = Checklist(["Write code"], [False]) + assert str(checklist) == "- [ ] Write code" + +def test_checklist_many_items_true(): + checklist = Checklist(["Write code", "Do Laundry"], True) + assert str(checklist) == "- [X] Write code\n- [X] Do Laundry" From c48da6d00267066895f099f1529d4332a20c90c6 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:54:43 -0400 Subject: [PATCH 05/10] Got nested checklists working --- snakemd/elements.py | 7 ++++--- snakemd/templates.py | 22 +++++++++++++--------- tests/templates/test_checklist.py | 4 ++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/snakemd/elements.py b/snakemd/elements.py index a9dc270..5638a86 100644 --- a/snakemd/elements.py +++ b/snakemd/elements.py @@ -899,7 +899,7 @@ def __str__(self) -> str: i = 1 for item in self._items: if isinstance(item, MDList): - item._space = self._space + " " * self._get_indent_size(i) + item._space = self._space + " " * self._get_indent_size(self._ordered, i) output.append(str(item)) else: # Create the start of the row based on `order` parameter @@ -987,7 +987,8 @@ def _top_level_count(items) -> int: count += 1 return count - def _get_indent_size(self, item_index: int = -1) -> int: + @staticmethod + def _get_indent_size(ordered: bool, item_index: int = -1) -> int: """ Returns the number of spaces that any sublists should be indented. @@ -997,7 +998,7 @@ def _get_indent_size(self, item_index: int = -1) -> int: :return: the number of spaces """ - if not self._ordered: + if not ordered: return 2 # Ordered items vary in length, so we adjust the result based on the index return 2 + len(str(item_index)) diff --git a/snakemd/templates.py b/snakemd/templates.py index 1a65fca..06b09c1 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -161,16 +161,20 @@ def __str__(self): output = [] i = 1 for item in self._items: - row = f"{self._space}-" - - if isinstance(self._checked, bool): - checked_str = "X" if self._checked else " " - row = f"{row} [{checked_str}] {item}" + if isinstance(item, Checklist | MDList): + item._space = self._space + " " * 2 + output.append(str(item)) else: - checked_str = "X" if self._checked[i - 1] else " " - row = f"{row} [{checked_str}] {item}" - - output.append(row) + row = f"{self._space}-" + + if isinstance(self._checked, bool): + checked_str = "X" if self._checked else " " + row = f"{row} [{checked_str}] {item}" + else: + checked_str = "X" if self._checked[i - 1] else " " + row = f"{row} [{checked_str}] {item}" + + output.append(row) i += 1 checklist = "\n".join(output) diff --git a/tests/templates/test_checklist.py b/tests/templates/test_checklist.py index d71291b..2e9f5da 100644 --- a/tests/templates/test_checklist.py +++ b/tests/templates/test_checklist.py @@ -15,3 +15,7 @@ def test_checklist_one_item_explicit(): def test_checklist_many_items_true(): checklist = Checklist(["Write code", "Do Laundry"], True) assert str(checklist) == "- [X] Write code\n- [X] Do Laundry" + +def test_checklist_many_items_nested_true(): + checklist = Checklist(["Write code", Checklist(["Implement TODO"], True), "Do Laundry"], True) + assert str(checklist) == "- [X] Write code\n - [X] Implement TODO\n- [X] Do Laundry" From dd3095d695060a790ead68b45f0a9c13863e17c8 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:56:02 -0400 Subject: [PATCH 06/10] Nesting seems to work as well --- tests/templates/test_checklist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/templates/test_checklist.py b/tests/templates/test_checklist.py index 2e9f5da..450e290 100644 --- a/tests/templates/test_checklist.py +++ b/tests/templates/test_checklist.py @@ -1,3 +1,4 @@ +from snakemd.elements import MDList from snakemd.templates import Checklist def test_checklist_one_item_true(): @@ -19,3 +20,7 @@ def test_checklist_many_items_true(): def test_checklist_many_items_nested_true(): checklist = Checklist(["Write code", Checklist(["Implement TODO"], True), "Do Laundry"], True) assert str(checklist) == "- [X] Write code\n - [X] Implement TODO\n- [X] Do Laundry" + +def test_checklist_many_items_nested_mdlist_true(): + checklist = Checklist(["Write code", MDList(["Implement TODO"]), "Do Laundry"], True) + assert str(checklist) == "- [X] Write code\n - Implement TODO\n- [X] Do Laundry" From 20203fc3ee1d9a7948b110c4a6b8da434057c0ac Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:59:20 -0400 Subject: [PATCH 07/10] Cleaned up docs --- snakemd/templates.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/snakemd/templates.py b/snakemd/templates.py index 06b09c1..fb27af1 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -158,6 +158,20 @@ def __init__( ) def __str__(self): + """ + Renders the checklist as a markdown string. Checklists + function very similarly to unorded lists, but require + additional information about the status of each task + (i.e., whether it is checked or not). + + .. code-block:: markdown + + - [ ] Do reading + - [X] Do writing + + :return: + the list as a markdown string + """ output = [] i = 1 for item in self._items: @@ -178,7 +192,7 @@ def __str__(self): i += 1 checklist = "\n".join(output) - logger.debug("Rendered markdown list: %r", checklist) + logger.debug("Rendered checklist: %r", checklist) return checklist def __repr__(self) -> str: From 9b7950dbb23d06e23e4ff3b6f73f2ea433d51984 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:04:16 -0400 Subject: [PATCH 08/10] Added missing docs --- snakemd/elements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snakemd/elements.py b/snakemd/elements.py index 5638a86..c6a034c 100644 --- a/snakemd/elements.py +++ b/snakemd/elements.py @@ -992,6 +992,8 @@ def _get_indent_size(ordered: bool, item_index: int = -1) -> int: """ Returns the number of spaces that any sublists should be indented. + :param bool ordered: + the boolean value indicating if a list is ordered :param int item_index: the index of the item to check (only used for ordered lists); defaults to -1 From 6e92711d396832cc7d03361441637d8babf4d518 Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:07:18 -0400 Subject: [PATCH 09/10] Cleaned up trailing whitespace --- snakemd/templates.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/snakemd/templates.py b/snakemd/templates.py index fb27af1..69faebe 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -83,7 +83,11 @@ class Kind(Enum): WARNING = auto() CAUTION = auto() - def __init__(self, kind: Kind, message: str | Iterable[str | Inline | Block]) -> None: + def __init__( + self, + kind: Kind, + message: str | Iterable[str | Inline | Block] + ) -> None: super().__init__() self._kind = kind self._message = message @@ -108,8 +112,8 @@ def __repr__(self) -> str: the Alert as a development string """ return repr(self._alert) - - + + class Checklist(Template): """ Checklist is an MDList extension to provide support @@ -118,10 +122,10 @@ class Checklist(Template): directly into MDList. However, because checklists are not a vanilla Markdown feature, they were moved here. - + .. versionadded:: 2.4 Included for user convenience - + :raises ValueError: when the checked argument is an Iterable[bool] that does not match the number of top-level elements in the list @@ -137,15 +141,17 @@ class Checklist(Template): - set to :code:`Iterable[bool]` to render the checked status of the top-level list elements directly """ + def __init__( - self, - items: Iterable[str | Inline | Block], + self, + items: Iterable[str | Inline | Block], checked: bool | Iterable[bool] = False ) -> None: super().__init__() self._items: list[Block] = MDList._process_items(items) self._checked: bool | list[bool] = ( - checked if checked is None or isinstance(checked, bool) else list(checked) + checked if checked is None or isinstance( + checked, bool) else list(checked) ) self._space = "" if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len( @@ -156,7 +162,7 @@ def __init__( "match number of booleans supplied by checked parameter: " f"{self._checked}" ) - + def __str__(self): """ Renders the checklist as a markdown string. Checklists @@ -180,21 +186,21 @@ def __str__(self): output.append(str(item)) else: row = f"{self._space}-" - + if isinstance(self._checked, bool): checked_str = "X" if self._checked else " " row = f"{row} [{checked_str}] {item}" else: checked_str = "X" if self._checked[i - 1] else " " row = f"{row} [{checked_str}] {item}" - + output.append(row) i += 1 - + checklist = "\n".join(output) logger.debug("Rendered checklist: %r", checklist) return checklist - + def __repr__(self) -> str: """ Renders self as an unambiguous string for development. From 1e747f483d5141eeed0c0ea6b4d91e1cee3abf8b Mon Sep 17 00:00:00 2001 From: jrg94 <11050412+jrg94@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:09:23 -0400 Subject: [PATCH 10/10] Removed python 3.10 feature --- snakemd/templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snakemd/templates.py b/snakemd/templates.py index 69faebe..266a66b 100644 --- a/snakemd/templates.py +++ b/snakemd/templates.py @@ -181,7 +181,7 @@ def __str__(self): output = [] i = 1 for item in self._items: - if isinstance(item, Checklist | MDList): + if isinstance(item, (Checklist, MDList)): item._space = self._space + " " * 2 output.append(str(item)) else: