From fda3f13ab4845fa004bad2a0f61e21830b827acf Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 13:39:01 +0200 Subject: [PATCH 01/70] Fix marker definition tests --- tests/test_marker_definitions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 5112b9a..572d003 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -29,14 +29,17 @@ def test_render_underline_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert UnderlineMarkerDefinition.render(node, 'u', block) == f'{text}' + assert ( + UnderlineMarkerDefinition.render(node, 'u', block) + == f'{text}' + ) def test_render_strikethrough_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert StrikeThroughMarkerDefinition.render(node, 'strike', block) == f'{text}' + assert StrikeThroughMarkerDefinition.render(node, 'strike', block) == f'{text}' def test_render_link_marker_success(): From c831850f89d624bd8f5f54a4b536fb347bb50ee4 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 13:49:19 +0200 Subject: [PATCH 02/70] tests: Mark tests for unsupported features as unsupported --- pyproject.toml | 1 + tests/test_upstream_suite.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd4b3c1..e46d64a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ line_length = 120 [tool.pytest.ini_options] addopts = ['--cov=sanity_html','--cov-report', 'term-missing'] +markers = ["unsupported"] [tool.coverage.run] source = ['sanity_html/*'] diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 82ddf6b..2a33893 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -102,6 +102,7 @@ def test_011_basic_numbered_list(): assert output == expected_output +@pytest.mark.unsupported def test_012_image_support(): fixture_data = get_fixture('fixtures/upstream/012-image-support.json') input_blocks = fixture_data['input'] @@ -110,6 +111,7 @@ def test_012_image_support(): assert output == expected_output +@pytest.mark.unsupported def test_013_materialized_image_support(): fixture_data = get_fixture('fixtures/upstream/013-materialized-image-support.json') input_blocks = fixture_data['input'] @@ -150,7 +152,7 @@ def test_017_all_default_block_style(): assert output == expected_output -@pytest.mark.skip('Requires custom definitions') +@pytest.mark.unsupported def test_018_marks_all_the_way_dow(): fixture_data = get_fixture('fixtures/upstream/018-marks-all-the-way-down.json') input_blocks = fixture_data['input'] @@ -199,6 +201,7 @@ def test_023_hard_break(): assert output == expected_output +@pytest.mark.unsupported def test_024_inline_image(): fixture_data = get_fixture('fixtures/upstream/024-inline-images.json') input_blocks = fixture_data['input'] @@ -207,6 +210,7 @@ def test_024_inline_image(): assert output == expected_output +@pytest.mark.unsupported def test_025_image_with_hotspot(): fixture_data = get_fixture('fixtures/upstream/025-image-with-hotspot.json') input_blocks = fixture_data['input'] @@ -231,6 +235,7 @@ def test_027_styled_list_item(): assert output == expected_output +@pytest.mark.unsupported def test_050_custom_block_type(): fixture_data = get_fixture('fixtures/upstream/050-custom-block-type.json') input_blocks = fixture_data['input'] @@ -239,6 +244,7 @@ def test_050_custom_block_type(): assert output == expected_output +@pytest.mark.unsupported def test_051_override_default(): fixture_data = get_fixture('fixtures/upstream/051-override-defaults.json') input_blocks = fixture_data['input'] @@ -247,6 +253,7 @@ def test_051_override_default(): assert output == expected_output +@pytest.mark.unsupported def test_052_custom_mark(): fixture_data = get_fixture('fixtures/upstream/052-custom-marks.json') input_blocks = fixture_data['input'] @@ -255,6 +262,7 @@ def test_052_custom_mark(): assert output == expected_output +@pytest.mark.unsupported def test_053_override_default_mark(): fixture_data = get_fixture('fixtures/upstream/053-override-default-marks.json') input_blocks = fixture_data['input'] From 76c0eda57d7600e1e44ff050c8bcc2b434cb8770 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 13:53:05 +0200 Subject: [PATCH 03/70] Use for missing mark rendering --- sanity_html/marker_definitions.py | 6 ++++++ sanity_html/renderer.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_definitions.py index fa7a83f..1a87bd3 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_definitions.py @@ -41,6 +41,12 @@ def render(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) # Decorators +class DefaultMarkerDefinition(MarkerDefinition): + """Marker used for unknown definitions.""" + + tag = 'span' + + class EmphasisMarkerDefinition(MarkerDefinition): """Marker definition for rendering.""" diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index d1b1a27..ba060b7 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -5,6 +5,7 @@ from sanity_html.constants import STYLE_MAP from sanity_html.dataclasses import Block, Span +from sanity_html.marker_definitions import DefaultMarkerDefinition from sanity_html.utils import get_list_tags, is_block, is_list, is_span if TYPE_CHECKING: @@ -106,7 +107,7 @@ def _render_span(self, span: Span, block: Block) -> str: for mark in sorted_marks: if mark in prev_marks: continue - marker_callable = block.marker_definitions[mark]() + marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_prefix(span, mark, block) result += html.escape(span.text) @@ -114,8 +115,7 @@ def _render_span(self, span: Span, block: Block) -> str: for mark in reversed(sorted_marks): if mark in next_marks: continue - - marker_callable = block.marker_definitions[mark]() + marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_suffix(span, mark, block) return result From 12f90d10ee99ab67d7459fa3c1cc54d95a97a595 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 13:55:24 +0200 Subject: [PATCH 04/70] Do not run tests for unsupported features on CI --- .github/workflows/codecov.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index e641e78..2cf3ca0 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -27,7 +27,7 @@ jobs: - run: poetry install --no-interaction --no-root if: steps.cache-venv.outputs.cache-hit != 'true' - run: poetry install --no-interaction - - run: poetry run pytest --cov-report=xml + - run: poetry run pytest -m "not unsupported" --cov-report=xml - uses: codecov/codecov-action@v1 with: file: ./coverage.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67e2635..397a32d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,4 +58,4 @@ jobs: - name: Run tests run: | source .venv/bin/activate - poetry run pytest + poetry run pytest -m "not unsupported" From afeda8bfc9a63d6ac38881f1cef189b8f9883885 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 14:20:53 +0200 Subject: [PATCH 05/70] Only wrap content in div if list of block has more than one block --- sanity_html/renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index ba060b7..00fdf1b 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -26,7 +26,7 @@ def __init__(self, blocks: Union[list[dict], dict]) -> None: self._blocks = [blocks] elif isinstance(blocks, list): self._blocks = blocks - self._wrapper_element = 'div' + self._wrapper_element = 'div' if len(blocks) > 1 else '' def render(self) -> str: """Render HTML from self._blocks.""" From b7fc2b82bd7ca349e4af37fdc7ab924e84070319 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 14:21:42 +0200 Subject: [PATCH 06/70] Support injecting hard breaks - fixes upstream test 023 --- sanity_html/renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 00fdf1b..7c4c498 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -110,7 +110,7 @@ def _render_span(self, span: Span, block: Block) -> str: marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_prefix(span, mark, block) - result += html.escape(span.text) + result += html.escape(span.text).replace('\n', '
') for mark in reversed(sorted_marks): if mark in next_marks: From f9ddea7f5ed0acf398a2734d6de24d8da66b03cb Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 21:59:28 +0200 Subject: [PATCH 07/70] Allow passing custom serializers and marker definitions --- sanity_html/dataclasses.py | 8 +++++--- sanity_html/renderer.py | 18 ++++++++++++++---- sanity_html/utils.py | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/sanity_html/dataclasses.py b/sanity_html/dataclasses.py index 0ebe8f5..24cf457 100644 --- a/sanity_html/dataclasses.py +++ b/sanity_html/dataclasses.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, cast -from sanity_html.utils import get_marker_definitions +from sanity_html.utils import get_default_marker_definitions if TYPE_CHECKING: from typing import Literal, Optional, Tuple, Type, Union @@ -44,7 +44,7 @@ class Block: listItem: Optional[Literal['bullet', 'number', 'square']] = None children: list[dict] = field(default_factory=list) markDefs: list[dict] = field(default_factory=list) - marker_definitions: dict[str, Type[MarkerDefinition]] = field(init=False) + marker_definitions: dict[str, Type[MarkerDefinition]] = field(default_factory=dict) marker_frequencies: dict[str, int] = field(init=False) def __post_init__(self) -> None: @@ -54,7 +54,9 @@ def __post_init__(self) -> None: To make handling of span `marks` simpler, we define marker_definitions as a dict, from which we can directly look up both annotation marks or decorator marks. """ - self.marker_definitions = get_marker_definitions(self.markDefs) + marker_definitions = get_default_marker_definitions(self.markDefs) + marker_definitions.update(self.marker_definitions) + self.marker_definitions = marker_definitions self.marker_frequencies = self._compute_marker_frequencies() def _compute_marker_frequencies(self) -> dict[str, int]: diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 7c4c498..6f3d760 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -9,7 +9,9 @@ from sanity_html.utils import get_list_tags, is_block, is_list, is_span if TYPE_CHECKING: - from typing import Dict, List, Optional, Union + from typing import Callable, Dict, List, Optional, Type, Union + + from sanity_html.marker_definitions import MarkerDefinition # TODO: Let user pass custom code block definitions/plugins @@ -19,8 +21,15 @@ class SanityBlockRenderer: """HTML renderer for Sanity block content.""" - def __init__(self, blocks: Union[list[dict], dict]) -> None: + def __init__( + self, + blocks: Union[list[dict], dict], + custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, + custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, + ) -> None: self._wrapper_element: Optional[str] = None + self._custom_marker_definitions = custom_marker_definitions or {} + self._custom_serializers = custom_serializers or {} if isinstance(blocks, dict): self._blocks = [blocks] @@ -65,7 +74,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b :param list_item: Whether we are handling a list upstream (impacts block handling). """ if is_block(node): - block = Block(**node) + block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_block(block, list_item=list_item) elif is_span(node): @@ -78,7 +87,8 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b assert context # this should be a cast return self._render_span(span, block=context) # context is span's outer block - + elif custom_serializer := self._custom_serializers.get(node.get('_type', '')): + return custom_serializer(node, context, list_item) else: print('Unexpected code path 👺') # noqa: T001 # TODO: Remove after thorough testing return '' diff --git a/sanity_html/utils.py b/sanity_html/utils.py index d3c6d5e..c483d37 100644 --- a/sanity_html/utils.py +++ b/sanity_html/utils.py @@ -10,7 +10,7 @@ from sanity_html.marker_definitions import MarkerDefinition -def get_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerDefinition]]: +def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerDefinition]]: """ Convert JSON definitions to a map of marker definition renderers. From 4ff35f293aa4907e6ed7864e869c2d6ac233ef50 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 21:59:48 +0200 Subject: [PATCH 08/70] Enable upstream image tests using custom serializer --- tests/test_upstream_suite.py | 60 ++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 2a33893..9bb9ee0 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -1,9 +1,50 @@ import json +import re from pathlib import Path +from typing import Optional import pytest from sanity_html import render +from sanity_html.dataclasses import Block +from sanity_html.marker_definitions import MarkerDefinition +from sanity_html.renderer import SanityBlockRenderer + + +def fake_image_serializer(node: dict, context: Optional[Block], list_item: bool): + assert node['_type'] == 'image' + if 'url' in node['asset']: + image_url = node['asset']['url'] + else: + project_id = '3do82whm' + dataset = 'production' + asset_ref: str = node['asset']['_ref'] + image_path = asset_ref[6:].replace('-jpg', '.jpg').replace('-png', '.png') + image_url = f'https://cdn.sanity.io/images/{project_id}/{dataset}/{image_path}' + + if 'crop' in node and 'hotspot' in node: + crop = node['crop'] + hotspot = node['hotspot'] + size_match = re.match(r'.*-(\d+)x(\d+)\..*', image_url) + if size_match: + orig_width, orig_height = [int(x) for x in size_match.groups()] + rect_x1 = round((orig_width * hotspot['x']) - ((orig_width * hotspot['width']) / 2)) + rect_y1 = round((orig_height * hotspot['y']) - ((orig_height * hotspot['height']) / 2)) + rect_x2 = round(orig_width - (orig_width * crop['left']) - (orig_width * crop['right'])) + rect_y2 = round(orig_height - (orig_height * crop['top']) - (orig_height * crop['bottom'])) + # These are passed as "imageOptions" upstream. + # It's up the the implementor of the serializer to fix this. + # We might provide one for images that does something like this, but for now + # let's just make the test suite pass + width = 320 + height = 240 + + image_url += f'?rect={rect_x1},{rect_y1},{rect_x2},{rect_y2}&w={width}&h={height}' + + image = f'' + if context: + return image + return f'
{image}
' def get_fixture(rel_path) -> dict: @@ -102,21 +143,21 @@ def test_011_basic_numbered_list(): assert output == expected_output -@pytest.mark.unsupported def test_012_image_support(): fixture_data = get_fixture('fixtures/upstream/012-image-support.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + output = sbr.render() assert output == expected_output -@pytest.mark.unsupported def test_013_materialized_image_support(): fixture_data = get_fixture('fixtures/upstream/013-materialized-image-support.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + output = sbr.render() assert output == expected_output @@ -189,7 +230,8 @@ def test_022_inline_node(): fixture_data = get_fixture('fixtures/upstream/022-inline-nodes.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + output = sbr.render() assert output == expected_output @@ -201,21 +243,21 @@ def test_023_hard_break(): assert output == expected_output -@pytest.mark.unsupported def test_024_inline_image(): fixture_data = get_fixture('fixtures/upstream/024-inline-images.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + output = sbr.render() assert output == expected_output -@pytest.mark.unsupported def test_025_image_with_hotspot(): fixture_data = get_fixture('fixtures/upstream/025-image-with-hotspot.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + output = sbr.render() assert output == expected_output From 9266b1869cd66e40c71e710cbaa88725ed31d9c6 Mon Sep 17 00:00:00 2001 From: Kristian Klette Date: Thu, 15 Apr 2021 22:49:07 +0200 Subject: [PATCH 09/70] Enable upstream test 53 - override default mark --- tests/test_upstream_suite.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 9bb9ee0..de16467 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -7,7 +7,7 @@ from sanity_html import render from sanity_html.dataclasses import Block -from sanity_html.marker_definitions import MarkerDefinition +from sanity_html.marker_definitions import LinkMarkerDefinition, MarkerDefinition from sanity_html.renderer import SanityBlockRenderer @@ -304,12 +304,19 @@ def test_052_custom_mark(): assert output == expected_output -@pytest.mark.unsupported def test_053_override_default_mark(): fixture_data = get_fixture('fixtures/upstream/053-override-default-marks.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + + class CustomLinkMark(LinkMarkerDefinition): + @classmethod + def render_prefix(cls, span, marker, context) -> str: + result = super().render_prefix(span, marker, context) + return result.replace(' Date: Sun, 18 Apr 2021 01:30:01 +0200 Subject: [PATCH 10/70] Improve list rendering with nesting support --- sanity_html/renderer.py | 106 ++++++++++++++++++++++++++++++---------- sanity_html/utils.py | 2 +- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 6f3d760..55b88d3 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -47,7 +47,8 @@ def render(self) -> str: for node in self._blocks: if list_nodes and not is_list(node): - result += self._render_list(list_nodes) + tree = self._normalize_list_tree(list_nodes, Block(**node)) + result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) list_nodes = [] # reset list_nodes if is_list(node): @@ -57,7 +58,8 @@ def render(self) -> str: result += self._render_node(node) # render non-list nodes immediately if list_nodes: - result += self._render_list(list_nodes) + tree = self._normalize_list_tree(list_nodes, Block(**node)) + result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) result = result.strip() @@ -73,7 +75,10 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b :param context: Optional context. Spans are passed with a Block instance as context for mark lookups. :param list_item: Whether we are handling a list upstream (impacts block handling). """ - if is_block(node): + if is_list(node): + block = Block(**node, marker_definitions=self._custom_marker_definitions) + return self._render_list(block, context) + elif is_block(node): block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_block(block, list_item=list_item) @@ -130,40 +135,87 @@ def _render_span(self, span: Span, block: Block) -> str: return result - def _render_list(self, nodes: list) -> str: - result, tag_dict = '', {} - for index, node in enumerate(nodes): - - current_level = node['level'] # 1 - prev_level = nodes[index - 1]['level'] if index > 0 else 0 # default triggers first condition below + def _render_list(self, node: Block, context: Optional[Block]) -> str: + assert node.listItem + head, tail = get_list_tags(node.listItem) + result = head + for child in node.children: + result += f'
  • {self._render_block(Block(**child), True)}
  • ' + result += tail + return result - list_item = node.pop('listItem') # popping this attribute lets us call render_node for non-list handling - node_inner_html = '
  • ' + ''.join(list(self._render_node(node, list_item=True))) + '
  • ' + def _normalize_list_tree(self, nodes: list, block: Block) -> list[dict]: + tree = [] - if current_level > prev_level: - list_tags = get_list_tags(list_item) - result += list_tags[0] - result += node_inner_html - tag_dict[current_level] = list_tags[1] + current_list = None + for node in nodes: + if not is_block(node): + tree.append(node) + current_list = None continue - elif current_level == prev_level: - result += node_inner_html + if current_list is None: + current_list = self._list_from_block(node) + tree.append(current_list) continue - elif current_level < prev_level: - result += node_inner_html - result += tag_dict.pop(prev_level) + if node.get('level') == current_list['level'] and node.get('listItem') == current_list['listItem']: + current_list['children'].append(node) continue - else: - print('Unexpected code path 🕵🏻‍') # noqa: T001 # TODO: Remove or alter when done testing + if node.get('level') > current_list['level']: + new_list = self._list_from_block(node) + current_list['children'][-1]['children'].append(new_list) + current_list = new_list + continue - # there should be one or more tag in the dict for us to close off - for value in tag_dict.values(): - result += value + if node.get('level') < current_list['level']: + parent = self._find_list(tree[-1], level=node.get('level'), list_item=node.get('listItem')) + if parent: + current_list = parent + current_list['children'].append(node) + continue + current_list = self._list_from_block(node) + tree.append(current_list) + continue - return result + if node.get('listItem') != current_list['listItem']: + match = self._find_list(tree[-1], level=node.get('level')) + if match and match['listItem'] == node.get('listItem'): + current_list = match + current_list.children.append(node) + continue + current_list = self._list_from_block(node) + tree.append(current_list) + continue + # TODO: Warn + tree.append(node) + + return tree + + def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: + filter_on_type = isinstance(list_item, str) + if ( + root_node.get('_type') == 'list' + and root_node.get('level') == level + and (filter_on_type and root_node.get('listItem') == list_item) + ): + return root_node + + children = root_node.get('children') + if children: + return self._find_list(children[-1], level, list_item) + + return None + + def _list_from_block(self, block: dict) -> dict: + return { + '_type': 'list', + '_key': f'${block["_key"]}-parent', + 'level': block.get('level'), + 'listItem': block['listItem'], + 'children': [block], + } def render(blocks: List[Dict]) -> str: diff --git a/sanity_html/utils.py b/sanity_html/utils.py index c483d37..1c75293 100644 --- a/sanity_html/utils.py +++ b/sanity_html/utils.py @@ -38,7 +38,7 @@ def is_span(node: dict) -> bool: def is_block(node: dict) -> bool: """Check whether a node is a block node.""" - return node.get('_type') == 'block' and 'listItem' not in node + return node.get('_type') == 'block' def get_list_tags(list_item: str) -> tuple[str, str]: From 5eeaad52468f9be715402bdde775621fd16dd5fc Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Sat, 17 Apr 2021 23:30:34 +0000 Subject: [PATCH 11/70] 'Refactored by Sourcery' --- sanity_html/renderer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 55b88d3..bfdfd8e 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -59,7 +59,10 @@ def render(self) -> str: if list_nodes: tree = self._normalize_list_tree(list_nodes, Block(**node)) - result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) + result += ''.join( + self._render_node(n, Block(**node), list_item=True) for n in tree + ) + result = result.strip() From 13191cdc9fa260c78a8a21bf4cb287f6c4453c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 19 Apr 2021 12:31:22 +0200 Subject: [PATCH 12/70] Add default href to match Sanity studio behavior (href is omitted if given as empty string) --- sanity_html/marker_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_definitions.py index 1a87bd3..5947160 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_definitions.py @@ -100,7 +100,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: marker_defintion = next((md for md in context.markDefs if md['_key'] == marker), None) if not marker_defintion: raise ValueError(f'Marker definition for key: {marker} not found in parent block context') - href = marker_defintion['href'] + href = marker_defintion.get('href', '') return f'
    ' From a7e96ca751b1e21300edc3799d28b140f3179404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 19 Apr 2021 12:31:58 +0200 Subject: [PATCH 13/70] Bump version to v0.0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e46d64a..50d3536 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-sanity-html" -version = "0.0.3" +version = "0.0.4" description = "HTML renderer for Sanity's Portable Text format" homepage = "https://github.com/otovo/python-sanity-html" repository = "https://github.com/otovo/python-sanity-html" From a3a96047514a70ecd6a31f46bd7a7ba357a1bc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 19 Apr 2021 17:51:43 +0200 Subject: [PATCH 14/70] linting and cleanup --- sanity_html/dataclasses.py | 15 ++++++++------- sanity_html/marker_definitions.py | 6 +++--- sanity_html/renderer.py | 22 ++++++++-------------- sanity_html/types.py | 8 -------- 4 files changed, 19 insertions(+), 32 deletions(-) delete mode 100644 sanity_html/types.py diff --git a/sanity_html/dataclasses.py b/sanity_html/dataclasses.py index 24cf457..f893656 100644 --- a/sanity_html/dataclasses.py +++ b/sanity_html/dataclasses.py @@ -9,7 +9,6 @@ from typing import Literal, Optional, Tuple, Type, Union from sanity_html.marker_definitions import MarkerDefinition - from sanity_html.types import SanityIdType @dataclass(frozen=True) @@ -22,7 +21,7 @@ class Span: _type: Literal['span'] text: str - _key: SanityIdType = None + _key: Optional[str] = None marks: list[str] = field(default_factory=list) # keys that correspond with block.mark_definitions style: Literal['normal'] = 'normal' @@ -38,7 +37,7 @@ class Block: _type: Literal['block'] - _key: SanityIdType = None + _key: Optional[str] = None style: Literal['h1', 'h2', 'h3', 'h4', 'normal'] = 'normal' level: Optional[int] = None listItem: Optional[Literal['bullet', 'number', 'square']] = None @@ -72,16 +71,18 @@ def _compute_marker_frequencies(self) -> dict[str, int]: def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Optional[dict]]: """Return the sibling nodes (prev, next) to the given node.""" if not self.children: - return (None, None) + return None, None try: - if type(node) == dict: + if not isinstance(node, (dict, Span)): + raise ValueError(f'Expected dict or Span but received {type(node)}') + elif type(node) == dict: node = cast(dict, node) node_idx = self.children.index(node) elif type(node) == Span: node = cast(Span, node) node_idx = self.children.index(next((c for c in self.children if c.get('_key') == node._key), {})) except ValueError: - return (None, None) + return None, None prev_node = None next_node = None @@ -91,4 +92,4 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op if node_idx < len(self.children) - 2: next_node = self.children[node_idx + 1] - return (prev_node, next_node) + return prev_node, next_node diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_definitions.py index 5947160..91618a2 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_definitions.py @@ -97,10 +97,10 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: The href attribute is fetched from the provided block context using the provided marker key. """ - marker_defintion = next((md for md in context.markDefs if md['_key'] == marker), None) - if not marker_defintion: + marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) + if not marker_definition: raise ValueError(f'Marker definition for key: {marker} not found in parent block context') - href = marker_defintion.get('href', '') + href = marker_definition.get('href', '') return f'' diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index bfdfd8e..bc4d876 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -14,10 +14,6 @@ from sanity_html.marker_definitions import MarkerDefinition -# TODO: Let user pass custom code block definitions/plugins -# to represent custom types (see children definition in portable text spec) - - class SanityBlockRenderer: """HTML renderer for Sanity block content.""" @@ -44,10 +40,11 @@ def render(self) -> str: result = '' list_nodes: List[Dict] = [] + for node in self._blocks: if list_nodes and not is_list(node): - tree = self._normalize_list_tree(list_nodes, Block(**node)) + tree = self._normalize_list_tree(list_nodes) result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) list_nodes = [] # reset list_nodes @@ -58,11 +55,8 @@ def render(self) -> str: result += self._render_node(node) # render non-list nodes immediately if list_nodes: - tree = self._normalize_list_tree(list_nodes, Block(**node)) - result += ''.join( - self._render_node(n, Block(**node), list_item=True) for n in tree - ) - + tree = self._normalize_list_tree(list_nodes) + result += ''.join(self._render_node(n, Block(**node), list_item=True) for n in tree) result = result.strip() @@ -95,8 +89,8 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b assert context # this should be a cast return self._render_span(span, block=context) # context is span's outer block - elif custom_serializer := self._custom_serializers.get(node.get('_type', '')): - return custom_serializer(node, context, list_item) + elif self._custom_serializers.get(node.get('_type', '')): + return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore else: print('Unexpected code path 👺') # noqa: T001 # TODO: Remove after thorough testing return '' @@ -147,7 +141,7 @@ def _render_list(self, node: Block, context: Optional[Block]) -> str: result += tail return result - def _normalize_list_tree(self, nodes: list, block: Block) -> list[dict]: + def _normalize_list_tree(self, nodes: list) -> list[dict]: tree = [] current_list = None @@ -186,7 +180,7 @@ def _normalize_list_tree(self, nodes: list, block: Block) -> list[dict]: match = self._find_list(tree[-1], level=node.get('level')) if match and match['listItem'] == node.get('listItem'): current_list = match - current_list.children.append(node) + current_list['children'].append(node) continue current_list = self._list_from_block(node) tree.append(current_list) diff --git a/sanity_html/types.py b/sanity_html/types.py deleted file mode 100644 index 35e3c1f..0000000 --- a/sanity_html/types.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional - - SanityIdType = Optional[str] # represents a [:13] uuid hex From 023ef9256405d1f17fe24b7241a9966e18eacdd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 19 Apr 2021 23:42:16 +0200 Subject: [PATCH 15/70] Fix test 52 --- sanity_html/marker_definitions.py | 2 +- sanity_html/renderer.py | 23 +++++++++--------- sanity_html/{dataclasses.py => types.py} | 2 +- sanity_html/utils.py | 5 ++-- setup.cfg | 2 ++ tests/fixtures/upstream/052-custom-marks.json | 24 ++++++++++++++++++- tests/test_marker_definitions.py | 2 +- tests/test_upstream_suite.py | 14 ++++++++--- 8 files changed, 54 insertions(+), 20 deletions(-) rename sanity_html/{dataclasses.py => types.py} (97%) diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_definitions.py index 91618a2..9bd3153 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_definitions.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: from typing import Type - from sanity_html.dataclasses import Block, Span + from sanity_html.types import Block, Span class MarkerDefinition: diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index bc4d876..2bd9805 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING from sanity_html.constants import STYLE_MAP -from sanity_html.dataclasses import Block, Span from sanity_html.marker_definitions import DefaultMarkerDefinition +from sanity_html.types import Block, Span from sanity_html.utils import get_list_tags, is_block, is_list, is_span if TYPE_CHECKING: @@ -96,17 +96,17 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b return '' def _render_block(self, block: Block, list_item: bool = False) -> str: - text = '' - if not list_item: - tag = STYLE_MAP[block.style] + text, tag = '', STYLE_MAP[block.style] + + if not list_item or tag != 'p': text += f'<{tag}>' - for child_node in block.children: - text += self._render_node(child_node, context=block) + for child_node in block.children: + text += self._render_node(child_node, context=block) + + if not list_item or tag != 'p': text += f'' - else: - for child_node in block.children: - text += self._render_node(child_node, context=block) + return text def _render_span(self, span: Span, block: Block) -> str: @@ -127,6 +127,7 @@ def _render_span(self, span: Span, block: Block) -> str: for mark in reversed(sorted_marks): if mark in next_marks: continue + marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_suffix(span, mark, block) @@ -215,7 +216,7 @@ def _list_from_block(self, block: dict) -> dict: } -def render(blocks: List[Dict]) -> str: +def render(blocks: List[Dict], *args, **kwargs) -> str: """Shortcut function inspired by Sanity's own blocksToHtml.h callable.""" - renderer = SanityBlockRenderer(blocks) + renderer = SanityBlockRenderer(blocks, *args, **kwargs) return renderer.render() diff --git a/sanity_html/dataclasses.py b/sanity_html/types.py similarity index 97% rename from sanity_html/dataclasses.py rename to sanity_html/types.py index f893656..929594e 100644 --- a/sanity_html/dataclasses.py +++ b/sanity_html/types.py @@ -38,7 +38,7 @@ class Block: _type: Literal['block'] _key: Optional[str] = None - style: Literal['h1', 'h2', 'h3', 'h4', 'normal'] = 'normal' + style: Literal['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'normal'] = 'normal' level: Optional[int] = None listItem: Optional[Literal['bullet', 'number', 'square']] = None children: list[dict] = field(default_factory=list) diff --git a/sanity_html/utils.py b/sanity_html/utils.py index 1c75293..d0afec7 100644 --- a/sanity_html/utils.py +++ b/sanity_html/utils.py @@ -20,8 +20,9 @@ def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[Mark marker_definitions = {} for definition in mark_defs: - marker = ANNOTATION_MARKER_DEFINITIONS[definition['_type']] - marker_definitions[definition['_key']] = marker + if definition['_type'] in ANNOTATION_MARKER_DEFINITIONS: + marker = ANNOTATION_MARKER_DEFINITIONS[definition['_type']] + marker_definitions[definition['_key']] = marker return {**marker_definitions, **DECORATOR_MARKER_DEFINITIONS} diff --git a/setup.cfg b/setup.cfg index 54a9a23..24e2313 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,6 +10,8 @@ ignore= ANN101, # W503 line break before binary operator W503, + # ANN002 and ANN003 missing type annotation for *args and **kwargs + ANN002, ANN003 select = E, F, N, W diff --git a/tests/fixtures/upstream/052-custom-marks.json b/tests/fixtures/upstream/052-custom-marks.json index e263ad4..a6de602 100644 --- a/tests/fixtures/upstream/052-custom-marks.json +++ b/tests/fixtures/upstream/052-custom-marks.json @@ -1 +1,23 @@ -{"input":{"_type":"block","children":[{"_key":"a1ph4","_type":"span","marks":["mark1"],"text":"Sanity"}],"markDefs":[{"_key":"mark1","_type":"highlight","thickness":5}]},"output":"

    Sanity

    "} +{ + "input": { + "_type": "block", + "children": [ + { + "_key": "a1ph4", + "_type": "span", + "marks": [ + "mark1" + ], + "text": "Sanity" + } + ], + "markDefs": [ + { + "_key": "mark1", + "_type": "highlight", + "thickness": 5 + } + ] + }, + "output": "

    Sanity

    " +} diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 572d003..cbbccf3 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,4 +1,3 @@ -from sanity_html.dataclasses import Block, Span from sanity_html.marker_definitions import ( CommentMarkerDefinition, EmphasisMarkerDefinition, @@ -7,6 +6,7 @@ StrongMarkerDefinition, UnderlineMarkerDefinition, ) +from sanity_html.types import Block, Span sample_texts = ['test', None, 1, 2.2, '!"#$%&/()'] diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index de16467..340e9e3 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -1,14 +1,14 @@ import json import re from pathlib import Path -from typing import Optional +from typing import Optional, Type import pytest from sanity_html import render -from sanity_html.dataclasses import Block from sanity_html.marker_definitions import LinkMarkerDefinition, MarkerDefinition from sanity_html.renderer import SanityBlockRenderer +from sanity_html.types import Block, Span def fake_image_serializer(node: dict, context: Optional[Block], list_item: bool): @@ -300,7 +300,15 @@ def test_052_custom_mark(): fixture_data = get_fixture('fixtures/upstream/052-custom-marks.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + + class CustomMarkerSerializer(MarkerDefinition): + tag = 'span' + + @classmethod + def render_prefix(cls, span: Span, marker: str, context: Block) -> str: + return '' + + output = render(input_blocks, custom_marker_definitions={'mark1': CustomMarkerSerializer}) assert output == expected_output From 5a03ad85390309566fd38690a3f8ad3e49ae6dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Thu, 3 Jun 2021 11:57:43 +0200 Subject: [PATCH 16/70] Add debug logging --- sanity_html/logger.py | 12 + sanity_html/marker_definitions.py | 4 + sanity_html/renderer.py | 15 +- sanity_html/types.py | 4 +- setup.cfg | 2 + .../upstream/021-list-without-level.json | 285 +++++++++++++++++- 6 files changed, 313 insertions(+), 9 deletions(-) create mode 100644 sanity_html/logger.py diff --git a/sanity_html/logger.py b/sanity_html/logger.py new file mode 100644 index 0000000..8b2fe02 --- /dev/null +++ b/sanity_html/logger.py @@ -0,0 +1,12 @@ +import logging +import sys + +logger = logging.getLogger('sanity_html') + +# Make logger output to stdout +logger.setLevel(logging.DEBUG) +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_definitions.py index 9bd3153..7a7bb05 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_definitions.py @@ -2,6 +2,8 @@ from typing import TYPE_CHECKING +from sanity_html.logger import logger + if TYPE_CHECKING: from typing import Type @@ -19,6 +21,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Usually this this the opening of the HTML tag. """ + logger.debug('Rendering %s prefix', cls.tag) return f'<{cls.tag}>' @classmethod @@ -27,6 +30,7 @@ def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Usually this this the closing of the HTML tag. """ + logger.debug('Rendering %s suffix', cls.tag) return f'' @classmethod diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 2bd9805..224c8c4 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from sanity_html.constants import STYLE_MAP +from sanity_html.logger import logger from sanity_html.marker_definitions import DefaultMarkerDefinition from sanity_html.types import Block, Span from sanity_html.utils import get_list_tags, is_block, is_list, is_span @@ -23,6 +24,7 @@ def __init__( custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, ) -> None: + logger.debug('Initializing block renderer') self._wrapper_element: Optional[str] = None self._custom_marker_definitions = custom_marker_definitions or {} self._custom_serializers = custom_serializers or {} @@ -35,6 +37,7 @@ def __init__( def render(self) -> str: """Render HTML from self._blocks.""" + logger.debug('Rendering HTML') if not self._blocks: return '' @@ -73,19 +76,17 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b :param list_item: Whether we are handling a list upstream (impacts block handling). """ if is_list(node): + logger.debug('Rendering node as list') block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_list(block, context) elif is_block(node): + logger.debug('Rendering node as block') block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_block(block, list_item=list_item) elif is_span(node): - if isinstance(node, str): - # TODO: Remove if we there's no coverage for this after we've fixed tests - # not convinced this code path is possible - put it in because the sanity lib checks for it - span = Span(**{'text': node}) - else: - span = Span(**node) + logger.debug('Rendering node as span') + span = Span(**node) assert context # this should be a cast return self._render_span(span, block=context) # context is span's outer block @@ -110,8 +111,10 @@ def _render_block(self, block: Block, list_item: bool = False) -> str: return text def _render_span(self, span: Span, block: Block) -> str: + logger.debug('Rendering span') result: str = '' prev_node, next_node = block.get_node_siblings(span) + prev_marks = prev_node.get('marks', []) if prev_node else [] next_marks = next_node.get('marks', []) if next_node else [] diff --git a/sanity_html/types.py b/sanity_html/types.py index 929594e..4c8c0e0 100644 --- a/sanity_html/types.py +++ b/sanity_html/types.py @@ -87,9 +87,9 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op prev_node = None next_node = None - if node_idx >= 1: + if node_idx != 0: prev_node = self.children[node_idx - 1] - if node_idx < len(self.children) - 2: + if node_idx != len(self.children) - 1: next_node = self.children[node_idx + 1] return prev_node, next_node diff --git a/setup.cfg b/setup.cfg index 24e2313..72028f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,8 @@ ignore= W503, # ANN002 and ANN003 missing type annotation for *args and **kwargs ANN002, ANN003 + # SM106 - Handle error cases first + SIM106 select = E, F, N, W diff --git a/tests/fixtures/upstream/021-list-without-level.json b/tests/fixtures/upstream/021-list-without-level.json index ee1146d..5ff95af 100644 --- a/tests/fixtures/upstream/021-list-without-level.json +++ b/tests/fixtures/upstream/021-list-without-level.json @@ -1 +1,284 @@ -{"input":[{"_key":"e3ac53b5b339","_type":"block","children":[{"_type":"span","marks":[],"text":"In-person access: Research appointments"}],"markDefs":[],"style":"h2"},{"_key":"a25f0be55c47","_type":"block","children":[{"_type":"span","marks":[],"text":"The collection may be examined by arranging a research appointment "},{"_type":"span","marks":["strong"],"text":"in advance"},{"_type":"span","marks":[],"text":" by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings. "}],"markDefs":[],"style":"normal"},{"_key":"9490a3085498","_type":"block","children":[{"_type":"span","marks":[],"text":"The collection space is located at:\n20 Ames Street\nBuilding E15-235\nCambridge, Massachusetts 02139"}],"markDefs":[],"style":"normal"},{"_key":"4c37f3bc1d71","_type":"block","children":[{"_type":"span","marks":[],"text":"In-person access: Space policies"}],"markDefs":[],"style":"h2"},{"_key":"a77cf4905e83","_type":"block","children":[{"_type":"span","marks":[],"text":"The Archivist or an authorized ACT staff member must attend researchers at all times."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"9a039c533554","_type":"block","children":[{"_type":"span","marks":[],"text":"No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"beeee9405136","_type":"block","children":[{"_type":"span","marks":[],"text":"Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"8b78daa65d60","_type":"block","children":[{"_type":"span","marks":[],"text":"No food or beverages are permitted in the collection space."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"d0188e00a887","_type":"block","children":[{"_type":"span","marks":[],"text":"Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"06486dd9e1c6","_type":"block","children":[{"_type":"span","marks":[],"text":"Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"e6f6f5255fb6","_type":"block","children":[{"_type":"span","marks":[],"text":"Patrons may only browse materials that have been made available for access."}],"listItem":"bullet","markDefs":[],"style":"normal"},{"_key":"99b3e265fa02","_type":"block","children":[{"_type":"span","marks":[],"text":"Remote access: Reference requests"}],"markDefs":[],"style":"h2"},{"_key":"ea13459d9e46","_type":"block","children":[{"_type":"span","marks":[],"text":"For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received."}],"markDefs":[],"style":"normal"},{"_key":"100958e35c94","_type":"block","children":[{"_type":"span","marks":["strong"],"text":"Use of patron information"}],"markDefs":[],"style":"h2"},{"_key":"2e0dde67b7df","_type":"block","children":[{"_type":"span","marks":[],"text":"Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections."}],"markDefs":[],"style":"normal"},{"_key":"8f39a1ec6366","_type":"block","children":[{"_type":"span","marks":["strong"],"text":"Fees"}],"markDefs":[],"style":"h2"},{"_key":"090062c9e8ce","_type":"block","children":[{"_type":"span","marks":[],"text":"ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees."}],"markDefs":[],"style":"normal"},{"_key":"e2b58e246069","_type":"block","children":[{"_type":"span","marks":["strong"],"text":"Use of MIT-owned materials by patrons"}],"markDefs":[],"style":"h2"},{"_key":"7cedb6800dc6","_type":"block","children":[{"_type":"span","marks":[],"text":"Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. "},{"_type":"span","marks":["strong"],"text":"When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted."}],"markDefs":[],"style":"normal"}],"output":"

    In-person access: Research appointments

    The collection may be examined by arranging a research appointment in advance by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings.

    The collection space is located at:
    20 Ames Street
    Building E15-235
    Cambridge, Massachusetts 02139

    In-person access: Space policies

    • The Archivist or an authorized ACT staff member must attend researchers at all times.
    • No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request.
    • Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist.
    • No food or beverages are permitted in the collection space.
    • Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only.
    • Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist.
    • Patrons may only browse materials that have been made available for access.

    Remote access: Reference requests

    For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.

    Use of patron information

    Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.

    Fees

    ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.

    Use of MIT-owned materials by patrons

    Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.

    "} +{ + "input": [ + { + "_key": "e3ac53b5b339", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "In-person access: Research appointments" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "a25f0be55c47", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "The collection may be examined by arranging a research appointment " + }, + { + "_type": "span", + "marks": [ + "strong" + ], + "text": "in advance" + }, + { + "_type": "span", + "marks": [], + "text": " by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings. " + } + ], + "markDefs": [], + "style": "normal" + }, + { + "_key": "9490a3085498", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "The collection space is located at:\n20 Ames Street\nBuilding E15-235\nCambridge, Massachusetts 02139" + } + ], + "markDefs": [], + "style": "normal" + }, + { + "_key": "4c37f3bc1d71", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "In-person access: Space policies" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "a77cf4905e83", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "The Archivist or an authorized ACT staff member must attend researchers at all times." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "9a039c533554", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "beeee9405136", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "8b78daa65d60", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "No food or beverages are permitted in the collection space." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "d0188e00a887", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "06486dd9e1c6", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "e6f6f5255fb6", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Patrons may only browse materials that have been made available for access." + } + ], + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "99b3e265fa02", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Remote access: Reference requests" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "ea13459d9e46", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received." + } + ], + "markDefs": [], + "style": "normal" + }, + { + "_key": "100958e35c94", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [ + "strong" + ], + "text": "Use of patron information" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "2e0dde67b7df", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections." + } + ], + "markDefs": [], + "style": "normal" + }, + { + "_key": "8f39a1ec6366", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [ + "strong" + ], + "text": "Fees" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "090062c9e8ce", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees." + } + ], + "markDefs": [], + "style": "normal" + }, + { + "_key": "e2b58e246069", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [ + "strong" + ], + "text": "Use of MIT-owned materials by patrons" + } + ], + "markDefs": [], + "style": "h2" + }, + { + "_key": "7cedb6800dc6", + "_type": "block", + "children": [ + { + "_type": "span", + "marks": [], + "text": "Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. " + }, + { + "_type": "span", + "marks": [ + "strong" + ], + "text": "When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted." + } + ], + "markDefs": [], + "style": "normal" + } + ], + "output": "

    In-person access: Research appointments

    The collection may be examined by arranging a research appointment in advance by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings.

    The collection space is located at:
    20 Ames Street
    Building E15-235
    Cambridge, Massachusetts 02139

    In-person access: Space policies

    • The Archivist or an authorized ACT staff member must attend researchers at all times.
    • No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request.
    • Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist.
    • No food or beverages are permitted in the collection space.
    • Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only.
    • Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist.
    • Patrons may only browse materials that have been made available for access.

    Remote access: Reference requests

    For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.

    Use of patron information

    Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.

    Fees

    ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.

    Use of MIT-owned materials by patrons

    Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.

    " +} From 5c31d6d7f10a223e71bc5c960a73b32bfa2d6fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Thu, 3 Jun 2021 11:57:57 +0200 Subject: [PATCH 17/70] Correct index logic to resolve test 21 --- sanity_html/types.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sanity_html/types.py b/sanity_html/types.py index 4c8c0e0..c24f165 100644 --- a/sanity_html/types.py +++ b/sanity_html/types.py @@ -73,14 +73,18 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op if not self.children: return None, None try: - if not isinstance(node, (dict, Span)): - raise ValueError(f'Expected dict or Span but received {type(node)}') - elif type(node) == dict: + if type(node) == dict: node = cast(dict, node) node_idx = self.children.index(node) elif type(node) == Span: node = cast(Span, node) - node_idx = self.children.index(next((c for c in self.children if c.get('_key') == node._key), {})) + for index, item in enumerate(self.children): + if 'text' in item and node.text == item['text']: + # Is it possible to handle several identical texts? + node_idx = index + break + else: + raise ValueError(f'Expected dict or Span but received {type(node)}') except ValueError: return None, None From 772c7013ccd0097c82e229f165581c303f05d303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 4 Jun 2021 17:36:12 +0200 Subject: [PATCH 18/70] Add custom errors for when we cannot handle a node --- sanity_html/renderer.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 224c8c4..da9a975 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -15,14 +15,31 @@ from sanity_html.marker_definitions import MarkerDefinition +class UnhandledNodeError(Exception): + """ + Raised when we receive a node that we cannot parse. + """ + pass + + +class MissingSerializerError(UnhandledNodeError): + """ + Raised when an unrecognized node _type value is found. + + This usually means that you need to pass a custom serializer + to handle the custom type. + """ + pass + + class SanityBlockRenderer: """HTML renderer for Sanity block content.""" def __init__( - self, - blocks: Union[list[dict], dict], - custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, - custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, + self, + blocks: Union[list[dict], dict], + custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, + custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, ) -> None: logger.debug('Initializing block renderer') self._wrapper_element: Optional[str] = None @@ -93,8 +110,13 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b elif self._custom_serializers.get(node.get('_type', '')): return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore else: - print('Unexpected code path 👺') # noqa: T001 # TODO: Remove after thorough testing - return '' + if hasattr(node, '_type'): + raise MissingSerializerError( + f'Found unhandled node type: {node._type}. ' + 'Most likely this requires a custom serializer.' + ) + else: + raise UnhandledNodeError(f'Received node that we cannot handle: {node.__dict__}') def _render_block(self, block: Block, list_item: bool = False) -> str: text, tag = '', STYLE_MAP[block.style] @@ -197,9 +219,9 @@ def _normalize_list_tree(self, nodes: list) -> list[dict]: def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: filter_on_type = isinstance(list_item, str) if ( - root_node.get('_type') == 'list' - and root_node.get('level') == level - and (filter_on_type and root_node.get('listItem') == list_item) + root_node.get('_type') == 'list' + and root_node.get('level') == level + and (filter_on_type and root_node.get('listItem') == list_item) ): return root_node From bb93d22c3e6e752b0cebbae60f7a6d8b3b9c5e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 4 Jun 2021 17:37:25 +0200 Subject: [PATCH 19/70] Fix test 26 --- sanity_html/renderer.py | 23 ++++++++--------- .../upstream/026-inline-block-with-text.json | 25 ++++++++++++++++++- tests/test_upstream_suite.py | 7 +++++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index da9a975..f61c447 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -16,9 +16,8 @@ class UnhandledNodeError(Exception): - """ - Raised when we receive a node that we cannot parse. - """ + """Raised when we receive a node that we cannot parse.""" + pass @@ -29,6 +28,7 @@ class MissingSerializerError(UnhandledNodeError): This usually means that you need to pass a custom serializer to handle the custom type. """ + pass @@ -36,10 +36,10 @@ class SanityBlockRenderer: """HTML renderer for Sanity block content.""" def __init__( - self, - blocks: Union[list[dict], dict], - custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, - custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, + self, + blocks: Union[list[dict], dict], + custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, + custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, ) -> None: logger.debug('Initializing block renderer') self._wrapper_element: Optional[str] = None @@ -112,8 +112,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b else: if hasattr(node, '_type'): raise MissingSerializerError( - f'Found unhandled node type: {node._type}. ' - 'Most likely this requires a custom serializer.' + f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.' ) else: raise UnhandledNodeError(f'Received node that we cannot handle: {node.__dict__}') @@ -219,9 +218,9 @@ def _normalize_list_tree(self, nodes: list) -> list[dict]: def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: filter_on_type = isinstance(list_item, str) if ( - root_node.get('_type') == 'list' - and root_node.get('level') == level - and (filter_on_type and root_node.get('listItem') == list_item) + root_node.get('_type') == 'list' + and root_node.get('level') == level + and (filter_on_type and root_node.get('listItem') == list_item) ): return root_node diff --git a/tests/fixtures/upstream/026-inline-block-with-text.json b/tests/fixtures/upstream/026-inline-block-with-text.json index 7de2d7d..164f1c1 100644 --- a/tests/fixtures/upstream/026-inline-block-with-text.json +++ b/tests/fixtures/upstream/026-inline-block-with-text.json @@ -1 +1,24 @@ -{"input":[{"_type":"block","_key":"foo","style":"normal","children":[{"_type":"span","text":"Men, "},{"_type":"button","text":"bli med du også"},{"_type":"span","text":", da!"}]}],"output":"

    Men, , da!

    "} +{ + "input": [ + { + "_type": "block", + "_key": "foo", + "style": "normal", + "children": [ + { + "_type": "span", + "text": "Men, " + }, + { + "_type": "button", + "text": "bli med du ogsĂĄ" + }, + { + "_type": "span", + "text": ", da!" + } + ] + } + ], + "output": "

    Men, , da!

    " +} diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 340e9e3..82c0088 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -261,11 +261,16 @@ def test_025_image_with_hotspot(): assert output == expected_output +def button_serializer(node: dict, context: Optional[Block], list_item: bool): + return f'' + + def test_026_inline_block_with_text(): fixture_data = get_fixture('fixtures/upstream/026-inline-block-with-text.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'button': button_serializer}) + output = sbr.render() assert output == expected_output From 1503ff943eb20bc05ee448f6de179ad2b0e51a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:09:38 +0200 Subject: [PATCH 20/70] Convert assert to cast --- sanity_html/renderer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index f61c447..4519b0e 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -1,7 +1,7 @@ from __future__ import annotations import html -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from sanity_html.constants import STYLE_MAP from sanity_html.logger import logger @@ -96,6 +96,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b logger.debug('Rendering node as list') block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_list(block, context) + elif is_block(node): logger.debug('Rendering node as block') block = Block(**node, marker_definitions=self._custom_marker_definitions) @@ -104,11 +105,12 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b elif is_span(node): logger.debug('Rendering node as span') span = Span(**node) + context = cast(Block, context) # context should always be a Block here + return self._render_span(span, block=context) - assert context # this should be a cast - return self._render_span(span, block=context) # context is span's outer block elif self._custom_serializers.get(node.get('_type', '')): return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore + else: if hasattr(node, '_type'): raise MissingSerializerError( @@ -143,6 +145,7 @@ def _render_span(self, span: Span, block: Block) -> str: for mark in sorted_marks: if mark in prev_marks: continue + marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_prefix(span, mark, block) From 4496fe82bd6d89086382ed8944e857ed7750b46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:12:03 +0200 Subject: [PATCH 21/70] Add python 3.10 to the test matrix and simplify workflows --- .github/workflows/codecov.yml | 6 +++--- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 29 ++++++++++------------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2cf3ca0..1693966 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,15 +15,15 @@ jobs: with: python-version: 3.9 - name: Install poetry - uses: snok/install-poetry@v1.1.2 + uses: snok/install-poetry@v1.1.6 with: - virtualenvs-create: true + version: 1.2.0a1 virtualenvs-in-project: true - uses: actions/cache@v2 id: cache-venv with: path: .venv - key: ${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-0 + key: ${{ hashFiles('**/poetry.lock') }}-0 - run: poetry install --no-interaction --no-root if: steps.cache-venv.outputs.cache-hit != 'true' - run: poetry install --no-interaction diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7bf7c2f..fd2ef12 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-python@v2 with: python-version: 3.9 - - uses: snok/install-poetry@v1.1.2 + - uses: snok/install-poetry@v1.1.6 - name: Publish to test-pypi run: | poetry config repositories.test https://test.pypi.org/legacy/ @@ -28,7 +28,7 @@ jobs: - uses: actions/setup-python@v2 with: python-version: 3.9 - - uses: snok/install-poetry@v1.1.2 + - uses: snok/install-poetry@v1.1.6 - name: Publish to pypi run: | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 397a32d..bf455d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,31 +31,22 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9"] + python-version: [ "3.7", "3.8", "3.9", "3.10.0-beta.2" ] steps: - - name: Check out repository - uses: actions/checkout@v2 - - name: Set up python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install poetry - uses: snok/install-poetry@v1.1.2 + - uses: snok/install-poetry@v1.1.6 with: - virtualenvs-create: true + version: 1.2.0a1 virtualenvs-in-project: true - - name: Load cached venv - uses: actions/cache@v2 + - uses: actions/cache@v2 id: cache-venv with: path: .venv - key: ${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-3 - - name: Install dependencies - run: poetry install --no-interaction --no-root + key: ${{ hashFiles('**/poetry.lock') }}-3 + - run: poetry install --no-interaction --no-root if: steps.cache-venv.outputs.cache-hit != 'true' - - name: Install package - run: poetry install --no-interaction - - name: Run tests - run: | - source .venv/bin/activate - poetry run pytest -m "not unsupported" + - run: poetry install --no-interaction + - run: poetry run pytest -m "not unsupported" From 29a4d9774d89fd1b8347e859c1a5cbb1e1e44701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:20:31 +0200 Subject: [PATCH 22/70] Update pyproject.toml and dependencies --- poetry.lock | 95 +++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 38 ++++++++++---------- 2 files changed, 94 insertions(+), 39 deletions(-) diff --git a/poetry.lock b/poetry.lock index 53f8891..00b8fcb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,17 +8,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.3.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "colorama" @@ -41,17 +41,34 @@ toml = ["toml"] [[package]] name = "flake8" -version = "3.9.0" +version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "importlib-metadata" +version = "4.5.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -87,6 +104,9 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + [package.extras] dev = ["pre-commit", "tox"] @@ -124,7 +144,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.2.3" +version = "6.2.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -134,6 +154,7 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<1.0.0a1" @@ -145,7 +166,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.11.1" +version = "2.12.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -154,9 +175,10 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" +toml = "*" [package.extras] -testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] [[package]] name = "toml" @@ -166,10 +188,30 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "031b6c95c5355805b7c59e9aef1a159db3e8339edd40c50292ae2ae81e7bf191" +python-versions = "^3.7" +content-hash = "c641d950bccb6ffac52cf3fcd3571b51f5e31d4864c03e763fe2748919bf855b" [metadata.files] atomicwrites = [ @@ -177,8 +219,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -239,8 +281,12 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] flake8 = [ - {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, - {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -275,14 +321,23 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, - {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] pytest-cov = [ - {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, - {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +zipp = [ + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, +] diff --git a/pyproject.toml b/pyproject.toml index 50d3536..dda2cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [tool.poetry] -name = "python-sanity-html" -version = "0.0.4" -description = "HTML renderer for Sanity's Portable Text format" -homepage = "https://github.com/otovo/python-sanity-html" -repository = "https://github.com/otovo/python-sanity-html" -authors = ["Kristian Klette "] -maintainers = ["Sondre Lillebø Gundersen "] -license = "Apache2" -readme = "README.md" -keywords = ["Sanity", "Portable text", "HTML", "Parsing"] +name = 'python-sanity-html' +version = '0.0.4' +description = 'HTML renderer for Sanity's Portable Text format' +homepage = 'https://github.com/otovo/python-sanity-html' +repository = 'https://github.com/otovo/python-sanity-html' +authors = ['Kristian Klette '] +maintainers = ['Sondre Lillebø Gundersen '] +license = 'Apache2' +readme = 'README.md' +keywords = ['Sanity', 'Portable text', 'HTML', 'Parsing'] include = ['CHANGELOG.md'] packages = [{ include = 'sanity_html' }] classifiers = [ @@ -27,20 +27,20 @@ classifiers = [ ] [tool.poetry.urls] -"Changelog" = "https://github.com/otovo/python-sanity-html/blob/main/CHANGELOG.md" +'Changelog' = 'https://github.com/otovo/python-sanity-html/blob/main/CHANGELOG.md' [tool.poetry.dependencies] -python = "^3.9" +python = '^3.7' [tool.poetry.dev-dependencies] -pytest = "^6.2.3" -flake8 = "^3.9.0" -pytest-cov = "^2.11.1" -coverage = "^5.5" +pytest = '^6.2.3' +flake8 = '^3.9.0' +pytest-cov = '^2.11.1' +coverage = '^5.5' [build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ['poetry-core>=1.0.0'] +build-backend = 'poetry.core.masonry.api' [tool.black] line-length = 120 @@ -55,7 +55,7 @@ line_length = 120 [tool.pytest.ini_options] addopts = ['--cov=sanity_html','--cov-report', 'term-missing'] -markers = ["unsupported"] +markers = ['unsupported'] [tool.coverage.run] source = ['sanity_html/*'] From 7d2ccfc0294aab7539615db9553ad87821252b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:23:25 +0200 Subject: [PATCH 23/70] Remove .txt extension for license --- LICENSE.txt => LICENSE | 0 pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename LICENSE.txt => LICENSE (100%) diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/pyproject.toml b/pyproject.toml index dda2cca..99cea36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = 'python-sanity-html' version = '0.0.4' -description = 'HTML renderer for Sanity's Portable Text format' +description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' authors = ['Kristian Klette '] From deb1ca267a7114dd4a0e80cc554f565a6353520c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:24:58 +0200 Subject: [PATCH 24/70] Update pre-commit config --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83ec612..0d9ca91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/ambv/black - rev: 20.8b1 + rev: 21.5b2 hooks: - id: black args: [ "--quiet" ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: check-ast - id: check-merge-conflict @@ -19,7 +19,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [ @@ -35,7 +35,7 @@ repos: 'flake8-type-checking', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.12.0 + rev: v2.19.1 hooks: - id: pyupgrade args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ] From daf63fcc7bce03a0ee765895425e531fe5aedcf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 11:26:27 +0200 Subject: [PATCH 25/70] Run pre-commit on project --- tests/test_upstream_suite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 82c0088..01241bf 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -27,7 +27,7 @@ def fake_image_serializer(node: dict, context: Optional[Block], list_item: bool) hotspot = node['hotspot'] size_match = re.match(r'.*-(\d+)x(\d+)\..*', image_url) if size_match: - orig_width, orig_height = [int(x) for x in size_match.groups()] + orig_width, orig_height = (int(x) for x in size_match.groups()) rect_x1 = round((orig_width * hotspot['x']) - ((orig_width * hotspot['width']) / 2)) rect_y1 = round((orig_height * hotspot['y']) - ((orig_height * hotspot['height']) / 2)) rect_x2 = round(orig_width - (orig_width * crop['left']) - (orig_width * crop['right'])) From 498ca85e55c312d4dbd1738edc567fb135dcce70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 4 Jun 2021 17:37:25 +0200 Subject: [PATCH 26/70] Fix test 26 --- sanity_html/renderer.py | 23 ++++++++--------- .../upstream/026-inline-block-with-text.json | 25 ++++++++++++++++++- tests/test_upstream_suite.py | 7 +++++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index da9a975..f61c447 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -16,9 +16,8 @@ class UnhandledNodeError(Exception): - """ - Raised when we receive a node that we cannot parse. - """ + """Raised when we receive a node that we cannot parse.""" + pass @@ -29,6 +28,7 @@ class MissingSerializerError(UnhandledNodeError): This usually means that you need to pass a custom serializer to handle the custom type. """ + pass @@ -36,10 +36,10 @@ class SanityBlockRenderer: """HTML renderer for Sanity block content.""" def __init__( - self, - blocks: Union[list[dict], dict], - custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, - custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, + self, + blocks: Union[list[dict], dict], + custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, + custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, ) -> None: logger.debug('Initializing block renderer') self._wrapper_element: Optional[str] = None @@ -112,8 +112,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b else: if hasattr(node, '_type'): raise MissingSerializerError( - f'Found unhandled node type: {node._type}. ' - 'Most likely this requires a custom serializer.' + f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.' ) else: raise UnhandledNodeError(f'Received node that we cannot handle: {node.__dict__}') @@ -219,9 +218,9 @@ def _normalize_list_tree(self, nodes: list) -> list[dict]: def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: filter_on_type = isinstance(list_item, str) if ( - root_node.get('_type') == 'list' - and root_node.get('level') == level - and (filter_on_type and root_node.get('listItem') == list_item) + root_node.get('_type') == 'list' + and root_node.get('level') == level + and (filter_on_type and root_node.get('listItem') == list_item) ): return root_node diff --git a/tests/fixtures/upstream/026-inline-block-with-text.json b/tests/fixtures/upstream/026-inline-block-with-text.json index 7de2d7d..164f1c1 100644 --- a/tests/fixtures/upstream/026-inline-block-with-text.json +++ b/tests/fixtures/upstream/026-inline-block-with-text.json @@ -1 +1,24 @@ -{"input":[{"_type":"block","_key":"foo","style":"normal","children":[{"_type":"span","text":"Men, "},{"_type":"button","text":"bli med du også"},{"_type":"span","text":", da!"}]}],"output":"

    Men, , da!

    "} +{ + "input": [ + { + "_type": "block", + "_key": "foo", + "style": "normal", + "children": [ + { + "_type": "span", + "text": "Men, " + }, + { + "_type": "button", + "text": "bli med du ogsĂĄ" + }, + { + "_type": "span", + "text": ", da!" + } + ] + } + ], + "output": "

    Men, , da!

    " +} diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 340e9e3..82c0088 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -261,11 +261,16 @@ def test_025_image_with_hotspot(): assert output == expected_output +def button_serializer(node: dict, context: Optional[Block], list_item: bool): + return f'' + + def test_026_inline_block_with_text(): fixture_data = get_fixture('fixtures/upstream/026-inline-block-with-text.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'button': button_serializer}) + output = sbr.render() assert output == expected_output From b44f85f6f02595e6dd13a762cff6fe0b9a9422f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 7 Jun 2021 17:01:56 +0200 Subject: [PATCH 27/70] Fetch Python versions from official source --- .github/scripts/get_python_versions.py | 22 ++++++++++++++++++++++ .github/workflows/test.yml | 12 ++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/get_python_versions.py diff --git a/.github/scripts/get_python_versions.py b/.github/scripts/get_python_versions.py new file mode 100644 index 0000000..8a39145 --- /dev/null +++ b/.github/scripts/get_python_versions.py @@ -0,0 +1,22 @@ +if __name__ == '__main__': + import json + + import requests + from packaging import version as semver + + stable_versions = requests.get( + 'https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json' + ).json() + + min_version = semver.parse('3.7') + versions = {} + + for version_object in stable_versions: + + version = version_object['version'] + major_and_minor_version = semver.parse('.'.join(version.split('.')[:2])) + + if major_and_minor_version not in versions and major_and_minor_version >= min_version: + versions[major_and_minor_version] = version + + print(json.dumps(list(versions.values()))) # noqa diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf455d3..a0f602f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,13 +25,21 @@ jobs: ${{ runner.os }}- - run: python -m pip install pre-commit - run: pre-commit run --all-files + get-python-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - id: set-matrix + run: echo "::set-output name=matrix::$(python .github/scripts/get_python_versions.py)" + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} test: - needs: linting + needs: [ linting, get-python-versions ] runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10.0-beta.2" ] + python-version: ${{ fromJson(needs.get-python-versions.outputs.matrix) }} steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 From 49f7ff42209b2c963c0d79df19d345d9ebd8709e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Sat, 5 Jun 2021 12:21:43 +0200 Subject: [PATCH 28/70] Add a section on serializers --- README.md | 202 ++++++++++++++++-- sanity_html/renderer.py | 3 +- ...05-basic-mark-multiple-adjacent-spans.json | 34 ++- .../fixtures/upstream/012-image-support.json | 27 ++- .../upstream/027-styled-list-items.json | 64 +++++- tests/test_marker_definitions.py | 22 ++ 6 files changed, 334 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bb9e9a7..56f36fe 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,6 @@
    Code coverage - - Test status - Supported Python versions @@ -14,18 +11,13 @@ Checked with mypy -# Python Sanity HTML Renderer - -> Repo is currently a work in progress. Not ready to be used. +# Sanity HTML Renderer -HTML renderer for [Sanity's](https://www.sanity.io/) [Portable Text](https://github.com/portabletext/portabletext) format. +> Repo is currently a work in progress. Not production ready. -Written as a python alternative to [Sanity's](https://www.sanity.io/) [block-content-to-html](https://www.npmjs.com/package/%40sanity/block-content-to-html) npm package, -for when you don't have access to a JavaScript runtime. +This package generates HTML from [Portable Text](https://github.com/portabletext/portabletext). -### TODO - -- [ ] Add support for image type +For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-content-to-html](https://www.npmjs.com/package/%40sanity/block-content-to-html) NPM library. ## Installation @@ -35,16 +27,198 @@ pip install python-sanity-html ## Usage -To parse your block content as HTML, simply instantiate the parser like this +Instantiate the `SanityBlockRenderer` class with your content and call the `render` method. + +The following content ```python from sanity_html import SanityBlockRenderer +renderer = SanityBlockRenderer({ + "_key": "R5FvMrjo", + "_type": "block", + "children": [ + {"_key": "cZUQGmh4", "_type": "span", "marks": ["strong"], "text": "A word of"}, + {"_key": "toaiCqIK", "_type": "span", "marks": ["strong"], "text": " warning;"}, + {"_key": "gaZingsA", "_type": "span", "marks": [], "text": " Sanity is addictive."} + ], + "markDefs": [], + "style": "normal" +}) +renderer.render() +``` + +Generates this HTML +```html +

    A word of warning; Sanity is addictive.

    +``` + +### Supported types -renderer = SanityBlockRenderer(block_content) +The `block` and `span` types are supported out of the box. + +### Custom types + +Beyond the built-in types, you have the freedom to provide +your own serializers to render any custom `_type` the way you +would like to. + +To illustrate, if you passed this data to the renderer class: + +```python +from sanity_html import SanityBlockRenderer + +renderer = SanityBlockRenderer({ + "_type": "block", + "_key": "foo", + "style": "normal", + "children": [ + { + "_type": "span", + "text": "Press, " + }, + { + "_type": "button", + "text": "here" + }, + { + "_type": "span", + "text": ", now!" + } + ] +}) +renderer.render() +``` + +The renderer would actually throw an error here, since `button` +does not have a corresponding built-in type serializer by default. + +To render this text you must provide your own serializer, like this: + +```python +from sanity_html import SanityBlockRenderer + +def button_serializer(node: dict, context: Optional[Block], list_item: bool): + return f'' + +renderer = SanityBlockRenderer( + ..., + custom_serializers={'button': button_serializer} +) output = renderer.render() ``` +With the custom serializer provided, the renderer would now successfully +output the following HTML: + +```html +

    Press , now!

    +``` + +### Supported mark definitions + +The package provides several built-in marker definitions and styles: + +**decorator marker definitions** + +- `em` +- `strong` +- `code` +- `underline` +- `strike-through` + +**annotation marker definitions** + +- `link` +- `comment` + +### Custom mark definitions + +Like with custom type serializers, additional serializers for +marker definitions and styles can be passed in like this: + +```python +from sanity_html import SanityBlockRenderer + +renderer = SanityBlockRenderer( + ..., + custom_marker_definitions={'em': ComicSansEmphasis} +) +renderer.render() +``` + +The primary difference between a type serializer and a mark definition serializer +is that the latter uses a class structure, and has three required methods. + +Here's an example of a custom style, adding an extra font +to the built-in equivalent serializer: + +```python +from sanity_html.marker_definitions import MarkerDefinition + + +class ComicSansEmphasis(MarkerDefinition): + tag = 'em' + + @classmethod + def render_prefix(cls, span: Span, marker: str, context: Block) -> str: + return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + + @classmethod + def render_suffix(cls, span: Span, marker: str, context: Block) -> str: + return f'' + + @classmethod + def render(cls, span: Span, marker: str, context: Block) -> str: + result = cls.render_prefix(span, marker, context) + result += str(span.text) + result += cls.render_suffix(span, marker, context) + return result +``` + +Since the `render_suffix` and `render` methods here are actually identical to the base class, +they do not need to be specified, and the whole example can be reduced to: + +```python +from sanity_html.marker_definitions import MarkerDefinition # base +from sanity_html import SanityBlockRenderer + +class ComicSansEmphasis(MarkerDefinition): + tag = 'em' + + @classmethod + def render_prefix(cls, span: Span, marker: str, context: Block) -> str: + return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + + +renderer = SanityBlockRenderer( + ..., + custom_marker_definitions={'em': ComicSansEmphasis} +) +renderer.render() +``` + + +### Supported styles + +Blocks can optionally define a `style` tag. These styles are supported: + +- `h1` +- `h2` +- `h3` +- `h4` +- `h5` +- `h6` +- `blockquote` +- `normal` + +## Missing features + +Despite being mentioned as a custom type in the +Portable Text [spec](https://github.com/portabletext/portabletext), +the `image` type serializer is something we hope to add a default serializer for +at some point. + ## Contributing Contributions are always appreciated 👏 diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 4519b0e..67c2577 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -55,6 +55,7 @@ def __init__( def render(self) -> str: """Render HTML from self._blocks.""" logger.debug('Rendering HTML') + if not self._blocks: return '' @@ -117,7 +118,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.' ) else: - raise UnhandledNodeError(f'Received node that we cannot handle: {node.__dict__}') + raise UnhandledNodeError(f'Received node that we cannot handle: {node}') def _render_block(self, block: Block, list_item: bool = False) -> str: text, tag = '', STYLE_MAP[block.style] diff --git a/tests/fixtures/upstream/005-basic-mark-multiple-adjacent-spans.json b/tests/fixtures/upstream/005-basic-mark-multiple-adjacent-spans.json index 091ecab..d1c2ef8 100644 --- a/tests/fixtures/upstream/005-basic-mark-multiple-adjacent-spans.json +++ b/tests/fixtures/upstream/005-basic-mark-multiple-adjacent-spans.json @@ -1 +1,33 @@ -{"input":{"_key":"R5FvMrjo","_type":"block","children":[{"_key":"cZUQGmh4","_type":"span","marks":["strong"],"text":"A word of"},{"_key":"toaiCqIK","_type":"span","marks":["strong"],"text":" warning;"},{"_key":"gaZingA","_type":"span","marks":[],"text":" Sanity is addictive."}],"markDefs":[],"style":"normal"},"output":"

    A word of warning; Sanity is addictive.

    "} +{ + "input": { + "_key": "R5FvMrjo", + "_type": "block", + "children": [ + { + "_key": "cZUQGmh4", + "_type": "span", + "marks": [ + "strong" + ], + "text": "A word of" + }, + { + "_key": "toaiCqIK", + "_type": "span", + "marks": [ + "strong" + ], + "text": " warning;" + }, + { + "_key": "gaZingA", + "_type": "span", + "marks": [], + "text": " Sanity is addictive." + } + ], + "markDefs": [], + "style": "normal" + }, + "output": "

    A word of warning; Sanity is addictive.

    " +} diff --git a/tests/fixtures/upstream/012-image-support.json b/tests/fixtures/upstream/012-image-support.json index 6197ecd..5d2d958 100644 --- a/tests/fixtures/upstream/012-image-support.json +++ b/tests/fixtures/upstream/012-image-support.json @@ -1 +1,26 @@ -{"input":[{"style":"normal","_type":"block","_key":"bd73ec5f61a1","markDefs":[],"children":[{"_type":"span","text":"Also, images are pretty common.","marks":[]}]},{"_type":"image","_key":"d234a4fa317a","asset":{"_type":"reference","_ref":"image-YiOKD0O6AdjKPaK24WtbOEv0-3456x2304-jpg"}}],"output":"

    Also, images are pretty common.

    "} +{ + "input": [ + { + "style": "normal", + "_type": "block", + "_key": "bd73ec5f61a1", + "markDefs": [], + "children": [ + { + "_type": "span", + "text": "Also, images are pretty common.", + "marks": [] + } + ] + }, + { + "_type": "image", + "_key": "d234a4fa317a", + "asset": { + "_type": "reference", + "_ref": "image-YiOKD0O6AdjKPaK24WtbOEv0-3456x2304-jpg" + } + } + ], + "output": "

    Also, images are pretty common.

    " +} diff --git a/tests/fixtures/upstream/027-styled-list-items.json b/tests/fixtures/upstream/027-styled-list-items.json index 2ffdfc6..7b8d93b 100644 --- a/tests/fixtures/upstream/027-styled-list-items.json +++ b/tests/fixtures/upstream/027-styled-list-items.json @@ -1 +1,63 @@ -{"input":[{"style":"normal","_type":"block","_key":"f94596b05b41","markDefs":[],"children":[{"_type":"span","text":"Let's test some of these lists!","marks":[]}]},{"listItem":"bullet","style":"normal","level":1,"_type":"block","_key":"937effb1cd06","markDefs":[],"children":[{"_type":"span","text":"Bullet 1","marks":[]}]},{"listItem":"bullet","style":"h1","level":1,"_type":"block","_key":"bd2d22278b88","markDefs":[],"children":[{"_type":"span","text":"Bullet 2","marks":[]}]},{"listItem":"bullet","style":"normal","level":1,"_type":"block","_key":"a97d32e9f747","markDefs":[],"children":[{"_type":"span","text":"Bullet 3","marks":[]}]}],"output":"

    Let's test some of these lists!

    • Bullet 1
    • Bullet 2

    • Bullet 3
    "} +{ + "input": [ + { + "style": "normal", + "_type": "block", + "_key": "f94596b05b41", + "markDefs": [], + "children": [ + { + "_type": "span", + "text": "Let's test some of these lists!", + "marks": [] + } + ] + }, + { + "listItem": "bullet", + "style": "normal", + "level": 1, + "_type": "block", + "_key": "937effb1cd06", + "markDefs": [], + "children": [ + { + "_type": "span", + "text": "Bullet 1", + "marks": [] + } + ] + }, + { + "listItem": "bullet", + "style": "h1", + "level": 1, + "_type": "block", + "_key": "bd2d22278b88", + "markDefs": [], + "children": [ + { + "_type": "span", + "text": "Bullet 2", + "marks": [] + } + ] + }, + { + "listItem": "bullet", + "style": "normal", + "level": 1, + "_type": "block", + "_key": "a97d32e9f747", + "markDefs": [], + "children": [ + { + "_type": "span", + "text": "Bullet 3", + "marks": [] + } + ] + } + ], + "output": "

    Let's test some of these lists!

    • Bullet 1
    • Bullet 2

    • Bullet 3
    " +} diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index cbbccf3..0a2577c 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,3 +1,4 @@ +from sanity_html import SanityBlockRenderer from sanity_html.marker_definitions import ( CommentMarkerDefinition, EmphasisMarkerDefinition, @@ -56,3 +57,24 @@ def test_render_comment_marker_success(): node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) assert CommentMarkerDefinition.render(node, 'comment', block) == f'' + + +def test_custom_marker_definition(): + from sanity_html.marker_definitions import MarkerDefinition + + class ComicSansEmphasis(MarkerDefinition): + tag = 'em' + + @classmethod + def render_prefix(cls, span, marker, context): + return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + + renderer = SanityBlockRenderer( + { + '_type': 'block', + 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['em'], 'text': 'Sanity'}], + 'markDefs': [], + }, + custom_marker_definitions={'em': ComicSansEmphasis}, + ) + assert renderer.render() == '

    Sanity

    ' From e6a984d832246e39e64195e599f803d8a19cb5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Wed, 9 Jun 2021 10:36:12 +0200 Subject: [PATCH 29/70] Rename package to sanity-html (#24) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 99cea36..fc9ae00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] -name = 'python-sanity-html' -version = '0.0.4' +name = 'sanity-html' +version = '0.1.0' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 453f5731bca6761a66eb439dc19f93b27f2eff82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Wed, 9 Jun 2021 10:41:21 +0200 Subject: [PATCH 30/70] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56f36fe..487ac4f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-cont ## Installation ``` -pip install python-sanity-html +pip install sanity-html ``` ## Usage From ddb90a0ea2f27c6d5aade4ce34d6f52cd79b59c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Wed, 9 Jun 2021 10:45:42 +0200 Subject: [PATCH 31/70] Update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc9ae00..3c2814b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'sanity-html' -version = '0.1.0' +version = '0.0.6' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From fb23360a72a625a9c66bf2cbf34af515320f87ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Wed, 9 Jun 2021 11:06:14 +0200 Subject: [PATCH 32/70] Update pypi badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 487ac4f..44c09e1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - - Package version + + Package version Code coverage From 9385667c9f2ea5939bd1e5a5369b3fed68b13b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Wed, 16 Jun 2021 14:31:44 +0200 Subject: [PATCH 33/70] Update package title --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 44c09e1..45cc5c0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Checked with mypy -# Sanity HTML Renderer +# Sanity HTML Renderer for Python > Repo is currently a work in progress. Not production ready. @@ -214,10 +214,8 @@ Blocks can optionally define a `style` tag. These styles are supported: ## Missing features -Despite being mentioned as a custom type in the -Portable Text [spec](https://github.com/portabletext/portabletext), -the `image` type serializer is something we hope to add a default serializer for -at some point. +We plan to implement a default built-in serializer for the `image` type. +In the meantime, you should be able to serialize image types by passing a custom serializer. ## Contributing From 97845186d160c12c978c17abbc01bf903fcc9ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 29 Jun 2021 14:06:23 +0200 Subject: [PATCH 34/70] Make logging opt-in --- sanity_html/logger.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sanity_html/logger.py b/sanity_html/logger.py index 8b2fe02..7d9ea67 100644 --- a/sanity_html/logger.py +++ b/sanity_html/logger.py @@ -1,12 +1,13 @@ +""" +Logging setup. + +The rest of the code gets the logger through this module rather than +`logging.getLogger` to make sure that it is configured. +""" import logging -import sys logger = logging.getLogger('sanity_html') -# Make logger output to stdout -logger.setLevel(logging.DEBUG) -handler = logging.StreamHandler(sys.stdout) -handler.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -handler.setFormatter(formatter) -logger.addHandler(handler) +if not logger.handlers: # pragma: no cover + logger.setLevel(logging.WARNING) + logger.addHandler(logging.NullHandler()) From 3c9aed1e3c2b7a84cc4588f43df1d5418ad601ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 29 Jun 2021 14:06:53 +0200 Subject: [PATCH 35/70] Upgrade to v0.0.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c2814b..3f3f32a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'sanity-html' -version = '0.0.6' +version = '0.0.7' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From a4828d8f79e51fff39367b2a9b870ac35a2314a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Thu, 25 Nov 2021 16:38:32 +0100 Subject: [PATCH 36/70] Prepare for v1 release (#25) * Remove unused changelog * Bump version to v1.0.0 and update metadata * Update dev dependencies * Update pre-commit config * Remove WIP banner * Update test workflow --- .github/workflows/codecov.yml | 34 ----------------- .github/workflows/test.yml | 72 +++++++++++++++++++++-------------- .pre-commit-config.yaml | 11 +++--- CHANGELOG.md | 0 README.md | 7 ++-- poetry.lock | 54 +++++++++++++------------- pyproject.toml | 9 ++--- 7 files changed, 84 insertions(+), 103 deletions(-) delete mode 100644 .github/workflows/codecov.yml delete mode 100644 CHANGELOG.md diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index 1693966..0000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: coverage - -on: - pull_request: - push: - branches: - - main - -jobs: - codecov: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install poetry - uses: snok/install-poetry@v1.1.6 - with: - version: 1.2.0a1 - virtualenvs-in-project: true - - uses: actions/cache@v2 - id: cache-venv - with: - path: .venv - key: ${{ hashFiles('**/poetry.lock') }}-0 - - run: poetry install --no-interaction --no-root - if: steps.cache-venv.outputs.cache-hit != 'true' - - run: poetry install --no-interaction - - run: poetry run pytest -m "not unsupported" --cov-report=xml - - uses: codecov/codecov-action@v1 - with: - file: ./coverage.xml - fail_ci_if_error: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0f602f..082066e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,48 +13,64 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - uses: actions/cache@v2 + id: cache-venv with: - path: | - ~/.cache/pip - ~/.cache/pre-commit - key: ${{ runner.os }}-pip-2 - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - run: python -m pip install pre-commit - - run: pre-commit run --all-files - get-python-versions: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - id: set-matrix - run: echo "::set-output name=matrix::$(python .github/scripts/get_python_versions.py)" - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} + path: .venv + key: venv-0 + - run: | + python -m venv .venv --upgrade-deps + source .venv/bin/activate + pip install pre-commit + if: steps.cache-venv.outputs.cache-hit != 'true' + - uses: actions/cache@v2 + id: pre-commit-cache + with: + path: ~/.cache/pre-commit + key: key-0 + - run: | + source .venv/bin/activate + pre-commit run --all-files + test: - needs: [ linting, get-python-versions ] runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ${{ fromJson(needs.get-python-versions.outputs.matrix) }} + python-version: [ "3.7", "3.8", "3.9", "3.10" ] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} - - uses: snok/install-poetry@v1.1.6 + python-version: "${{ matrix.python-version }}" + - uses: actions/cache@v2 + id: poetry-cache + with: + path: ~/.local + key: key-0 + - uses: snok/install-poetry@v1 with: - version: 1.2.0a1 - virtualenvs-in-project: true + virtualenvs-create: false + version: 1.2.0a2 - uses: actions/cache@v2 id: cache-venv with: path: .venv - key: ${{ hashFiles('**/poetry.lock') }}-3 - - run: poetry install --no-interaction --no-root + key: ${{ hashFiles('**/poetry.lock') }}-0 + - run: | + python -m venv .venv + source .venv/bin/activate + pip install -U pip + poetry install --no-interaction --no-root if: steps.cache-venv.outputs.cache-hit != 'true' - - run: poetry install --no-interaction - - run: poetry run pytest -m "not unsupported" + - name: Run tests + run: | + source .venv/bin/activate + pytest -m "not unsupported" --cov-report=xml + coverage report + - uses: codecov/codecov-action@v2 + with: + file: ./coverage.xml + fail_ci_if_error: true + if: matrix.python-version == '3.10' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d9ca91..10b4d9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/ambv/black - rev: 21.5b2 + rev: 21.10b0 hooks: - id: black args: [ "--quiet" ] @@ -35,16 +35,17 @@ repos: 'flake8-type-checking', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.19.1 + rev: v2.29.0 hooks: - id: pyupgrade args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ] - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.10.0 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.910-1 hooks: - id: mypy - additional_dependencies: [ pytest ] + additional_dependencies: + - types-requests diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index 45cc5c0..b026c61 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ # Sanity HTML Renderer for Python -> Repo is currently a work in progress. Not production ready. - This package generates HTML from [Portable Text](https://github.com/portabletext/portabletext). For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-content-to-html](https://www.npmjs.com/package/%40sanity/block-content-to-html) NPM library. @@ -214,8 +212,9 @@ Blocks can optionally define a `style` tag. These styles are supported: ## Missing features -We plan to implement a default built-in serializer for the `image` type. -In the meantime, you should be able to serialize image types by passing a custom serializer. +For anyone interested, we would be happy to see a +default built-in serializer for the `image` type added. +In the meantime, users should be able to serialize image types by passing a custom serializer. ## Contributing diff --git a/poetry.lock b/poetry.lock index 00b8fcb..15e5029 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,7 +55,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "importlib-metadata" -version = "4.5.0" +version = "4.8.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -67,7 +67,8 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -87,28 +88,29 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.9" +version = "21.2" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3" [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" @@ -144,7 +146,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.2.4" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -157,7 +159,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" @@ -190,7 +192,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "3.10.0.0" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "dev" optional = false @@ -198,7 +200,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.4.1" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -206,11 +208,11 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" -python-versions = "^3.7" +python-versions = '^3.7' content-hash = "c641d950bccb6ffac52cf3fcd3571b51f5e31d4864c03e763fe2748919bf855b" [metadata.files] @@ -285,8 +287,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, - {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -297,12 +299,12 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -321,8 +323,8 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -333,11 +335,11 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index 3f3f32a..15152a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'sanity-html' -version = '0.0.7' +version = '1.0.0' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' @@ -8,11 +8,11 @@ authors = ['Kristian Klette '] maintainers = ['Sondre Lillebø Gundersen '] license = 'Apache2' readme = 'README.md' -keywords = ['Sanity', 'Portable text', 'HTML', 'Parsing'] +keywords = ['sanity', 'portable', 'text', 'html', 'parsing'] include = ['CHANGELOG.md'] packages = [{ include = 'sanity_html' }] classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Environment :: Web Environment', 'Operating System :: OS Independent', @@ -26,9 +26,6 @@ classifiers = [ 'Typing :: Typed', ] -[tool.poetry.urls] -'Changelog' = 'https://github.com/otovo/python-sanity-html/blob/main/CHANGELOG.md' - [tool.poetry.dependencies] python = '^3.7' From 265b3938b661b6c64148543262c57778182c3f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 08:49:45 +0100 Subject: [PATCH 37/70] Rename package from sanity-html to portabletext-html --- README.md | 69 ++++++++++--------- portabletext_html/__init__.py | 3 + .../constants.py | 4 +- {sanity_html => portabletext_html}/logger.py | 2 +- .../marker_definitions.py | 4 +- {sanity_html => portabletext_html}/py.typed | 0 .../renderer.py | 18 ++--- {sanity_html => portabletext_html}/types.py | 4 +- {sanity_html => portabletext_html}/utils.py | 4 +- pyproject.toml | 8 +-- sanity_html/__init__.py | 5 -- tests/test_marker_definitions.py | 10 +-- tests/test_module_loading.py | 4 +- tests/test_rendering.py | 2 +- tests/test_upstream_suite.py | 22 +++--- 15 files changed, 80 insertions(+), 79 deletions(-) create mode 100644 portabletext_html/__init__.py rename {sanity_html => portabletext_html}/constants.py (88%) rename {sanity_html => portabletext_html}/logger.py (85%) rename {sanity_html => portabletext_html}/marker_definitions.py (97%) rename {sanity_html => portabletext_html}/py.typed (100%) rename {sanity_html => portabletext_html}/renderer.py (94%) rename {sanity_html => portabletext_html}/types.py (96%) rename {sanity_html => portabletext_html}/utils.py (90%) delete mode 100644 sanity_html/__init__.py diff --git a/README.md b/README.md index b026c61..dc43d25 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Checked with mypy -# Sanity HTML Renderer for Python +# Portable Text HTML Renderer for Python This package generates HTML from [Portable Text](https://github.com/portabletext/portabletext). @@ -20,19 +20,19 @@ For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-cont ## Installation ``` -pip install sanity-html +pip install portabletext-html ``` ## Usage -Instantiate the `SanityBlockRenderer` class with your content and call the `render` method. +Instantiate the `PortableTextRenderer` class with your content and call the `render` method. The following content ```python -from sanity_html import SanityBlockRenderer +from portabletext_html import PortableTextRenderer -renderer = SanityBlockRenderer({ +renderer = PortableTextRenderer({ "_key": "R5FvMrjo", "_type": "block", "children": [ @@ -64,26 +64,26 @@ would like to. To illustrate, if you passed this data to the renderer class: ```python -from sanity_html import SanityBlockRenderer - -renderer = SanityBlockRenderer({ - "_type": "block", - "_key": "foo", - "style": "normal", - "children": [ - { - "_type": "span", - "text": "Press, " - }, - { - "_type": "button", - "text": "here" - }, - { - "_type": "span", - "text": ", now!" - } - ] +from portabletext_html import PortableTextRenderer + +renderer = PortableTextRenderer({ + "_type": "block", + "_key": "foo", + "style": "normal", + "children": [ + { + "_type": "span", + "text": "Press, " + }, + { + "_type": "button", + "text": "here" + }, + { + "_type": "span", + "text": ", now!" + } + ] }) renderer.render() ``` @@ -94,12 +94,14 @@ does not have a corresponding built-in type serializer by default. To render this text you must provide your own serializer, like this: ```python -from sanity_html import SanityBlockRenderer +from portabletext_html import PortableTextRenderer + def button_serializer(node: dict, context: Optional[Block], list_item: bool): return f'' -renderer = SanityBlockRenderer( + +renderer = PortableTextRenderer( ..., custom_serializers={'button': button_serializer} ) @@ -136,9 +138,9 @@ Like with custom type serializers, additional serializers for marker definitions and styles can be passed in like this: ```python -from sanity_html import SanityBlockRenderer +from portabletext_html import PortableTextRenderer -renderer = SanityBlockRenderer( +renderer = PortableTextRenderer( ..., custom_marker_definitions={'em': ComicSansEmphasis} ) @@ -152,7 +154,7 @@ Here's an example of a custom style, adding an extra font to the built-in equivalent serializer: ```python -from sanity_html.marker_definitions import MarkerDefinition +from portabletext_html.marker_definitions import MarkerDefinition class ComicSansEmphasis(MarkerDefinition): @@ -178,8 +180,9 @@ Since the `render_suffix` and `render` methods here are actually identical to th they do not need to be specified, and the whole example can be reduced to: ```python -from sanity_html.marker_definitions import MarkerDefinition # base -from sanity_html import SanityBlockRenderer +from portabletext_html.marker_definitions import MarkerDefinition # base +from portabletext_html import PortableTextRenderer + class ComicSansEmphasis(MarkerDefinition): tag = 'em' @@ -189,7 +192,7 @@ class ComicSansEmphasis(MarkerDefinition): return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' -renderer = SanityBlockRenderer( +renderer = PortableTextRenderer( ..., custom_marker_definitions={'em': ComicSansEmphasis} ) diff --git a/portabletext_html/__init__.py b/portabletext_html/__init__.py new file mode 100644 index 0000000..bd2f0d5 --- /dev/null +++ b/portabletext_html/__init__.py @@ -0,0 +1,3 @@ +from portabletext_html.renderer import PortableTextRenderer, render + +__all__ = ['PortableTextRenderer', 'render'] diff --git a/sanity_html/constants.py b/portabletext_html/constants.py similarity index 88% rename from sanity_html/constants.py rename to portabletext_html/constants.py index bc73e2d..f5ead44 100644 --- a/sanity_html/constants.py +++ b/portabletext_html/constants.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from sanity_html.marker_definitions import ( +from portabletext_html.marker_definitions import ( CodeMarkerDefinition, CommentMarkerDefinition, EmphasisMarkerDefinition, @@ -15,7 +15,7 @@ if TYPE_CHECKING: from typing import Dict, Type - from sanity_html.marker_definitions import MarkerDefinition + from portabletext_html.marker_definitions import MarkerDefinition STYLE_MAP = { 'h1': 'h1', diff --git a/sanity_html/logger.py b/portabletext_html/logger.py similarity index 85% rename from sanity_html/logger.py rename to portabletext_html/logger.py index 7d9ea67..18122b3 100644 --- a/sanity_html/logger.py +++ b/portabletext_html/logger.py @@ -6,7 +6,7 @@ """ import logging -logger = logging.getLogger('sanity_html') +logger = logging.getLogger('portabletext_html') if not logger.handlers: # pragma: no cover logger.setLevel(logging.WARNING) diff --git a/sanity_html/marker_definitions.py b/portabletext_html/marker_definitions.py similarity index 97% rename from sanity_html/marker_definitions.py rename to portabletext_html/marker_definitions.py index 7a7bb05..72cc2d2 100644 --- a/sanity_html/marker_definitions.py +++ b/portabletext_html/marker_definitions.py @@ -2,12 +2,12 @@ from typing import TYPE_CHECKING -from sanity_html.logger import logger +from portabletext_html.logger import logger if TYPE_CHECKING: from typing import Type - from sanity_html.types import Block, Span + from portabletext_html.types import Block, Span class MarkerDefinition: diff --git a/sanity_html/py.typed b/portabletext_html/py.typed similarity index 100% rename from sanity_html/py.typed rename to portabletext_html/py.typed diff --git a/sanity_html/renderer.py b/portabletext_html/renderer.py similarity index 94% rename from sanity_html/renderer.py rename to portabletext_html/renderer.py index 67c2577..614b177 100644 --- a/sanity_html/renderer.py +++ b/portabletext_html/renderer.py @@ -3,16 +3,16 @@ import html from typing import TYPE_CHECKING, cast -from sanity_html.constants import STYLE_MAP -from sanity_html.logger import logger -from sanity_html.marker_definitions import DefaultMarkerDefinition -from sanity_html.types import Block, Span -from sanity_html.utils import get_list_tags, is_block, is_list, is_span +from portabletext_html.constants import STYLE_MAP +from portabletext_html.logger import logger +from portabletext_html.marker_definitions import DefaultMarkerDefinition +from portabletext_html.types import Block, Span +from portabletext_html.utils import get_list_tags, is_block, is_list, is_span if TYPE_CHECKING: from typing import Callable, Dict, List, Optional, Type, Union - from sanity_html.marker_definitions import MarkerDefinition + from portabletext_html.marker_definitions import MarkerDefinition class UnhandledNodeError(Exception): @@ -32,8 +32,8 @@ class MissingSerializerError(UnhandledNodeError): pass -class SanityBlockRenderer: - """HTML renderer for Sanity block content.""" +class PortableTextRenderer: + """HTML renderer for Sanity's portable text format.""" def __init__( self, @@ -246,5 +246,5 @@ def _list_from_block(self, block: dict) -> dict: def render(blocks: List[Dict], *args, **kwargs) -> str: """Shortcut function inspired by Sanity's own blocksToHtml.h callable.""" - renderer = SanityBlockRenderer(blocks, *args, **kwargs) + renderer = PortableTextRenderer(blocks, *args, **kwargs) return renderer.render() diff --git a/sanity_html/types.py b/portabletext_html/types.py similarity index 96% rename from sanity_html/types.py rename to portabletext_html/types.py index c24f165..68638cd 100644 --- a/sanity_html/types.py +++ b/portabletext_html/types.py @@ -3,12 +3,12 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, cast -from sanity_html.utils import get_default_marker_definitions +from portabletext_html.utils import get_default_marker_definitions if TYPE_CHECKING: from typing import Literal, Optional, Tuple, Type, Union - from sanity_html.marker_definitions import MarkerDefinition + from portabletext_html.marker_definitions import MarkerDefinition @dataclass(frozen=True) diff --git a/sanity_html/utils.py b/portabletext_html/utils.py similarity index 90% rename from sanity_html/utils.py rename to portabletext_html/utils.py index d0afec7..3977d81 100644 --- a/sanity_html/utils.py +++ b/portabletext_html/utils.py @@ -2,12 +2,12 @@ from typing import TYPE_CHECKING -from sanity_html.constants import ANNOTATION_MARKER_DEFINITIONS, DECORATOR_MARKER_DEFINITIONS +from portabletext_html.constants import ANNOTATION_MARKER_DEFINITIONS, DECORATOR_MARKER_DEFINITIONS if TYPE_CHECKING: from typing import Type - from sanity_html.marker_definitions import MarkerDefinition + from portabletext_html.marker_definitions import MarkerDefinition def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerDefinition]]: diff --git a/pyproject.toml b/pyproject.toml index 15152a2..70fba75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = 'sanity-html' +name = 'portabletext-html' version = '1.0.0' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' @@ -10,7 +10,7 @@ license = 'Apache2' readme = 'README.md' keywords = ['sanity', 'portable', 'text', 'html', 'parsing'] include = ['CHANGELOG.md'] -packages = [{ include = 'sanity_html' }] +packages = [{ include = 'portabletext_html' }] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -51,11 +51,11 @@ include_trailing_comma = true line_length = 120 [tool.pytest.ini_options] -addopts = ['--cov=sanity_html','--cov-report', 'term-missing'] +addopts = ['--cov=portabletext_html','--cov-report', 'term-missing'] markers = ['unsupported'] [tool.coverage.run] -source = ['sanity_html/*'] +source = ['portabletext_html/*'] omit = [] branch = true diff --git a/sanity_html/__init__.py b/sanity_html/__init__.py deleted file mode 100644 index 879ae73..0000000 --- a/sanity_html/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Python Sanity HTML Renderer.""" - -from sanity_html.renderer import SanityBlockRenderer, render - -__all__ = ['SanityBlockRenderer', 'render'] diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 0a2577c..6011426 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,5 +1,5 @@ -from sanity_html import SanityBlockRenderer -from sanity_html.marker_definitions import ( +from portabletext_html import PortableTextRenderer +from portabletext_html.marker_definitions import ( CommentMarkerDefinition, EmphasisMarkerDefinition, LinkMarkerDefinition, @@ -7,7 +7,7 @@ StrongMarkerDefinition, UnderlineMarkerDefinition, ) -from sanity_html.types import Block, Span +from portabletext_html.types import Block, Span sample_texts = ['test', None, 1, 2.2, '!"#$%&/()'] @@ -60,7 +60,7 @@ def test_render_comment_marker_success(): def test_custom_marker_definition(): - from sanity_html.marker_definitions import MarkerDefinition + from portabletext_html.marker_definitions import MarkerDefinition class ComicSansEmphasis(MarkerDefinition): tag = 'em' @@ -69,7 +69,7 @@ class ComicSansEmphasis(MarkerDefinition): def render_prefix(cls, span, marker, context): return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' - renderer = SanityBlockRenderer( + renderer = PortableTextRenderer( { '_type': 'block', 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['em'], 'text': 'Sanity'}], diff --git a/tests/test_module_loading.py b/tests/test_module_loading.py index d1a6523..03fe074 100644 --- a/tests/test_module_loading.py +++ b/tests/test_module_loading.py @@ -6,6 +6,6 @@ def test_module_should_be_importable(): This catches any compilation issue we might have. """ - from sanity_html import SanityBlockRenderer + from portabletext_html import PortableTextRenderer - assert SanityBlockRenderer + assert PortableTextRenderer diff --git a/tests/test_rendering.py b/tests/test_rendering.py index a9eaf8d..3894862 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -2,7 +2,7 @@ import json from pathlib import Path -from sanity_html.renderer import render +from portabletext_html.renderer import render def load_fixture(fixture_name) -> dict: diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 01241bf..08efedf 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -5,10 +5,10 @@ import pytest -from sanity_html import render -from sanity_html.marker_definitions import LinkMarkerDefinition, MarkerDefinition -from sanity_html.renderer import SanityBlockRenderer -from sanity_html.types import Block, Span +from portabletext_html import render +from portabletext_html.marker_definitions import LinkMarkerDefinition, MarkerDefinition +from portabletext_html.renderer import PortableTextRenderer +from portabletext_html.types import Block, Span def fake_image_serializer(node: dict, context: Optional[Block], list_item: bool): @@ -147,7 +147,7 @@ def test_012_image_support(): fixture_data = get_fixture('fixtures/upstream/012-image-support.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) output = sbr.render() assert output == expected_output @@ -156,7 +156,7 @@ def test_013_materialized_image_support(): fixture_data = get_fixture('fixtures/upstream/013-materialized-image-support.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) output = sbr.render() assert output == expected_output @@ -230,7 +230,7 @@ def test_022_inline_node(): fixture_data = get_fixture('fixtures/upstream/022-inline-nodes.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) output = sbr.render() assert output == expected_output @@ -247,7 +247,7 @@ def test_024_inline_image(): fixture_data = get_fixture('fixtures/upstream/024-inline-images.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) output = sbr.render() assert output == expected_output @@ -256,7 +256,7 @@ def test_025_image_with_hotspot(): fixture_data = get_fixture('fixtures/upstream/025-image-with-hotspot.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'image': fake_image_serializer}) output = sbr.render() assert output == expected_output @@ -269,7 +269,7 @@ def test_026_inline_block_with_text(): fixture_data = get_fixture('fixtures/upstream/026-inline-block-with-text.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - sbr = SanityBlockRenderer(input_blocks, custom_serializers={'button': button_serializer}) + sbr = PortableTextRenderer(input_blocks, custom_serializers={'button': button_serializer}) output = sbr.render() assert output == expected_output @@ -328,7 +328,7 @@ def render_prefix(cls, span, marker, context) -> str: result = super().render_prefix(span, marker, context) return result.replace(' Date: Mon, 29 Nov 2021 08:50:13 +0100 Subject: [PATCH 38/70] Update version from v1 to v1.0.0b --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70fba75..957cd8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.0.0' +version = '1.0.0b1' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 4e8cda9a3819d5fa47f4978a504a66cfcd705e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 08:50:35 +0100 Subject: [PATCH 39/70] Update lockfile --- poetry.lock | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/poetry.lock b/poetry.lock index 15e5029..268a8e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,7 +55,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "importlib-metadata" -version = "4.8.1" +version = "4.8.2" description = "Read metadata from Python packages" category = "dev" optional = false @@ -68,7 +68,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -88,14 +88,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.2" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2,<3" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pluggy" @@ -114,11 +114,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" @@ -138,11 +138,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -192,11 +195,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.0" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "zipp" @@ -287,8 +290,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, - {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -299,16 +302,16 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] packaging = [ - {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, - {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, @@ -319,8 +322,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -335,9 +338,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, + {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, ] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, From 18e0aa1602efbc61a38ad770a20a0a66460c02cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 08:51:04 +0100 Subject: [PATCH 40/70] Update pre-commit hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10b4d9f..3062b26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/ambv/black - rev: 21.10b0 + rev: 21.11b1 hooks: - id: black args: [ "--quiet" ] @@ -35,12 +35,12 @@ repos: 'flake8-type-checking', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ] - repo: https://github.com/pycqa/isort - rev: 5.10.0 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy From 4f4fad50dae7a5ee2ddfca5398f60e83658ee1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 09:00:43 +0100 Subject: [PATCH 41/70] Update package metadata to support python 3.7+ --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 957cd8d..cc7ffeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ classifiers = [ 'Topic :: Text Processing :: Markup', 'Topic :: Text Processing :: Markup :: HTML', 'Programming Language :: Python', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Typing :: Typed', From ccb34329d3377e562f82ba41b9823403b0b1c6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 09:03:33 +0100 Subject: [PATCH 42/70] Bump version from 1.0.0b1 to v1.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cc7ffeb..8d8340c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.0.0b1' +version = '1.0.0' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 5bb6edf97242e4415d579e3aa12a61625be45808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 09:10:16 +0100 Subject: [PATCH 43/70] Remove no longer used ci-script and update poetry version --- .github/scripts/get_python_versions.py | 22 ---------------------- .github/workflows/test.yml | 5 ++--- 2 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 .github/scripts/get_python_versions.py diff --git a/.github/scripts/get_python_versions.py b/.github/scripts/get_python_versions.py deleted file mode 100644 index 8a39145..0000000 --- a/.github/scripts/get_python_versions.py +++ /dev/null @@ -1,22 +0,0 @@ -if __name__ == '__main__': - import json - - import requests - from packaging import version as semver - - stable_versions = requests.get( - 'https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json' - ).json() - - min_version = semver.parse('3.7') - versions = {} - - for version_object in stable_versions: - - version = version_object['version'] - major_and_minor_version = semver.parse('.'.join(version.split('.')[:2])) - - if major_and_minor_version not in versions and major_and_minor_version >= min_version: - versions[major_and_minor_version] = version - - print(json.dumps(list(versions.values()))) # noqa diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 082066e..a9fc2e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,11 +48,10 @@ jobs: id: poetry-cache with: path: ~/.local - key: key-0 + key: key-1 - uses: snok/install-poetry@v1 with: virtualenvs-create: false - version: 1.2.0a2 - uses: actions/cache@v2 id: cache-venv with: @@ -61,7 +60,7 @@ jobs: - run: | python -m venv .venv source .venv/bin/activate - pip install -U pip + pip install -U pip wheel poetry install --no-interaction --no-root if: steps.cache-venv.outputs.cache-hit != 'true' - name: Run tests From da2282657658c1675e48f3a4bd19e02d84ffc4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 10:27:46 +0100 Subject: [PATCH 44/70] Update readme badges --- README.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dc43d25..f3a2d9a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,7 @@ - - Package version - - - Code coverage - - - Supported Python versions - - - Checked with mypy - +[![pypi](https://img.shields.io/pypi/v/portabletext-html.svg)](https://pypi.org/project/portabletext-html/) +[![test](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml) +[![code coverage](https://codecov.io/gh/otovo/python-portabletext-html/branch/main/graph/badge.svg)](https://codecov.io/gh/otovo/python-portabletext-html) +[![supported python versions](https://img.shields.io/badge/python-3.7%2B-blue)](https://pypi.org/project/python-portabletext-html/) # Portable Text HTML Renderer for Python @@ -223,4 +215,4 @@ In the meantime, users should be able to serialize image types by passing a cust Contributions are always appreciated 👏 -For details, see the [CONTRIBUTING.md](https://github.com/otovo/python-sanity-html/blob/main/CONTRIBUTING.md). +For details, see the [CONTRIBUTING.md](https://github.com/otovo/python-portabletext-html/blob/main/CONTRIBUTING.md). From 310a0ec14c5b594aece554c33b09854ecd6cfd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 10:30:51 +0100 Subject: [PATCH 45/70] Reset test workflow caches --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9fc2e7..45b4dc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: id: cache-venv with: path: .venv - key: venv-0 + key: venv-1 - run: | python -m venv .venv --upgrade-deps source .venv/bin/activate @@ -28,7 +28,7 @@ jobs: id: pre-commit-cache with: path: ~/.cache/pre-commit - key: key-0 + key: key-1 - run: | source .venv/bin/activate pre-commit run --all-files @@ -48,7 +48,7 @@ jobs: id: poetry-cache with: path: ~/.local - key: key-1 + key: key-2 - uses: snok/install-poetry@v1 with: virtualenvs-create: false @@ -56,7 +56,7 @@ jobs: id: cache-venv with: path: .venv - key: ${{ hashFiles('**/poetry.lock') }}-0 + key: ${{ hashFiles('**/poetry.lock') }}-1 - run: | python -m venv .venv source .venv/bin/activate From e445dd9d0f05f8cfe424112e568f37c36e663096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Mon, 29 Nov 2021 10:35:04 +0100 Subject: [PATCH 46/70] Update test badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3a2d9a..58abe31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![pypi](https://img.shields.io/pypi/v/portabletext-html.svg)](https://pypi.org/project/portabletext-html/) -[![test](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml) +[![test](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml/badge.svg)](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml) [![code coverage](https://codecov.io/gh/otovo/python-portabletext-html/branch/main/graph/badge.svg)](https://codecov.io/gh/otovo/python-portabletext-html) [![supported python versions](https://img.shields.io/badge/python-3.7%2B-blue)](https://pypi.org/project/python-portabletext-html/) From b70e66de7f1935cb38133f8522b4c72ff63f1e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 18 Jan 2022 12:38:55 +0100 Subject: [PATCH 47/70] chore: Add stricter mypy config --- setup.cfg | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/setup.cfg b/setup.cfg index 72028f4..44e5250 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,3 +47,21 @@ exclude = max-complexity = 15 max-line-length = 120 + +[mypy] +show_error_codes = True +warn_unused_ignores = True +strict_optional = True +incremental = True +ignore_missing_imports = True +warn_redundant_casts = True +warn_unused_configs = True +disallow_untyped_defs = True +disallow_untyped_calls = True +local_partial_types = True +show_traceback = True +exclude = + .venv/ + +[mypy-tests.*] +ignore_errors = True From 050a4d9f22e476f44bb08e8f1f67786209967dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 18 Jan 2022 12:39:14 +0100 Subject: [PATCH 48/70] chore: Remove redundant casts --- portabletext_html/renderer.py | 4 ++-- portabletext_html/types.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py index 614b177..3804c85 100644 --- a/portabletext_html/renderer.py +++ b/portabletext_html/renderer.py @@ -10,7 +10,7 @@ from portabletext_html.utils import get_list_tags, is_block, is_list, is_span if TYPE_CHECKING: - from typing import Callable, Dict, List, Optional, Type, Union + from typing import Any, Callable, Dict, List, Optional, Type, Union from portabletext_html.marker_definitions import MarkerDefinition @@ -244,7 +244,7 @@ def _list_from_block(self, block: dict) -> dict: } -def render(blocks: List[Dict], *args, **kwargs) -> str: +def render(blocks: List[Dict], *args: Any, **kwargs: Any) -> str: """Shortcut function inspired by Sanity's own blocksToHtml.h callable.""" renderer = PortableTextRenderer(blocks, *args, **kwargs) return renderer.render() diff --git a/portabletext_html/types.py b/portabletext_html/types.py index 68638cd..1cd38d3 100644 --- a/portabletext_html/types.py +++ b/portabletext_html/types.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from portabletext_html.utils import get_default_marker_definitions @@ -74,10 +74,8 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op return None, None try: if type(node) == dict: - node = cast(dict, node) node_idx = self.children.index(node) elif type(node) == Span: - node = cast(Span, node) for index, item in enumerate(self.children): if 'text' in item and node.text == item['text']: # Is it possible to handle several identical texts? From 352ce6a3eb794c78fe77627fef172661eeaeaa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 18 Jan 2022 12:40:32 +0100 Subject: [PATCH 49/70] chore: Update pre-commit config --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3062b26..0968ba6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/ambv/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: [ "--quiet" ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: check-ast - id: check-merge-conflict @@ -35,7 +35,7 @@ repos: 'flake8-type-checking', ] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ] @@ -44,7 +44,7 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.931 hooks: - id: mypy additional_dependencies: From 655a6fcaee339f6f2bf6adc7e691c7cfdcaf25fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Tue, 18 Jan 2022 12:40:52 +0100 Subject: [PATCH 50/70] chore: Update lockfile --- poetry.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 268a8e6..30cd47f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,17 +8,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "colorama" @@ -55,11 +55,11 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "importlib-metadata" -version = "4.8.2" +version = "4.10.1" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -68,7 +68,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -195,7 +195,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "4.0.0" +version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false @@ -203,15 +203,15 @@ python-versions = ">=3.6" [[package]] name = "zipp" -version = "3.6.0" +version = "3.7.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" @@ -224,8 +224,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -290,8 +290,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, - {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, + {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, + {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -338,10 +338,10 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, - {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, + {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, ] From 22dc82d6e2abfa195377014086f74fbb6421b940 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Tue, 1 Feb 2022 11:26:17 +0100 Subject: [PATCH 51/70] custom marks: add test for conditional custom mark --- tests/test_marker_definitions.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 6011426..f67f913 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,3 +1,5 @@ +from typing import Type + from portabletext_html import PortableTextRenderer from portabletext_html.marker_definitions import ( CommentMarkerDefinition, @@ -9,7 +11,7 @@ ) from portabletext_html.types import Block, Span -sample_texts = ['test', None, 1, 2.2, '!"#$%&/()'] +sample_texts = ['test', None, 1, 2.2, '!"#$%/()'] def test_render_emphasis_marker_success(): @@ -62,19 +64,26 @@ def test_render_comment_marker_success(): def test_custom_marker_definition(): from portabletext_html.marker_definitions import MarkerDefinition - class ComicSansEmphasis(MarkerDefinition): + class ConditionalMarkerDefinition(MarkerDefinition): tag = 'em' @classmethod - def render_prefix(cls, span, marker, context): - return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) + condition = marker_definition.get('cloudCondition', '') + if not condition: + style = "display: none" + return f'<{cls.tag} style=\"{style}\">' + else: + return super().render_prefix(span, marker, context) renderer = PortableTextRenderer( { '_type': 'block', - 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['em'], 'text': 'Sanity'}], - 'markDefs': [], + 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['some_id'], 'text': 'Sanity'}], + 'markDefs': [{"_key": "some_id", "_type": "contractConditional", "cloudCondition": False}], }, - custom_marker_definitions={'em': ComicSansEmphasis}, + custom_marker_definitions={'contractConditional': ConditionalMarkerDefinition}, ) - assert renderer.render() == '

    Sanity

    ' + result = renderer.render() + assert result == '

    Sanity

    ' From 9b7393e5b9ce8fc09fdff0ac2ca22b74c8726ad7 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Mon, 31 Jan 2022 08:56:11 +0100 Subject: [PATCH 52/70] Bugfix: user-defined custom marks can be passed to Blocks again When I was trying to create my own custom mark definition, I noticed that the information from 'markDefs' property is not passed down to the renderer. `get_default_marker_definitions` method graps the context only from the default_annotations dictionary. `add_custom_marker_definitions` solves the issue by using the same logic as `get_default_marker_definitions`, but looping over user-defined annotations (self.marker_definitions). --- portabletext_html/types.py | 14 +++++++++++--- tests/test_marker_definitions.py | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/portabletext_html/types.py b/portabletext_html/types.py index 1cd38d3..0d1c015 100644 --- a/portabletext_html/types.py +++ b/portabletext_html/types.py @@ -53,9 +53,7 @@ def __post_init__(self) -> None: To make handling of span `marks` simpler, we define marker_definitions as a dict, from which we can directly look up both annotation marks or decorator marks. """ - marker_definitions = get_default_marker_definitions(self.markDefs) - marker_definitions.update(self.marker_definitions) - self.marker_definitions = marker_definitions + self.marker_definitions = self._add_custom_marker_definitions() self.marker_frequencies = self._compute_marker_frequencies() def _compute_marker_frequencies(self) -> dict[str, int]: @@ -68,6 +66,16 @@ def _compute_marker_frequencies(self) -> dict[str, int]: counts[mark] = 0 return counts + def _add_custom_marker_definitions(self) -> dict[str, Type[MarkerDefinition]]: + marker_definitions = get_default_marker_definitions(self.markDefs) + marker_definitions.update(self.marker_definitions) + for definition in self.markDefs: + if definition['_type'] in self.marker_definitions: + marker = self.marker_definitions[definition['_type']] + marker_definitions[definition['_key']] = marker + del marker_definitions[definition['_type']] + return marker_definitions + def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Optional[dict]]: """Return the sibling nodes (prev, next) to the given node.""" if not self.children: diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index f67f913..e357eea 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -72,7 +72,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) condition = marker_definition.get('cloudCondition', '') if not condition: - style = "display: none" + style = 'display: none' return f'<{cls.tag} style=\"{style}\">' else: return super().render_prefix(span, marker, context) @@ -81,7 +81,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: { '_type': 'block', 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['some_id'], 'text': 'Sanity'}], - 'markDefs': [{"_key": "some_id", "_type": "contractConditional", "cloudCondition": False}], + 'markDefs': [{'_key': 'some_id', '_type': 'contractConditional', 'cloudCondition': False}], }, custom_marker_definitions={'contractConditional': ConditionalMarkerDefinition}, ) From 87938c8a7bd45a4872439dbbda531dfd002f5d8d Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Tue, 1 Feb 2022 12:32:11 +0000 Subject: [PATCH 53/70] 'Refactored by Sourcery' --- portabletext_html/types.py | 4 +--- tests/test_marker_definitions.py | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/portabletext_html/types.py b/portabletext_html/types.py index 0d1c015..ab5236b 100644 --- a/portabletext_html/types.py +++ b/portabletext_html/types.py @@ -94,11 +94,9 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op except ValueError: return None, None - prev_node = None next_node = None - if node_idx != 0: - prev_node = self.children[node_idx - 1] + prev_node = self.children[node_idx - 1] if node_idx != 0 else None if node_idx != len(self.children) - 1: next_node = self.children[node_idx + 1] diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index e357eea..9a3463a 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -70,12 +70,10 @@ class ConditionalMarkerDefinition(MarkerDefinition): @classmethod def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) - condition = marker_definition.get('cloudCondition', '') - if not condition: - style = 'display: none' - return f'<{cls.tag} style=\"{style}\">' - else: + if condition := marker_definition.get('cloudCondition', ''): return super().render_prefix(span, marker, context) + else: + return f'<{cls.tag} style="display: none">' renderer = PortableTextRenderer( { From 4a7524241b1f016efbce6dcb18f9327a00623460 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Tue, 1 Feb 2022 13:55:09 +0100 Subject: [PATCH 54/70] remove walrus operator: pytest tests python 3.7, where the operator is not available yet --- tests/test_marker_definitions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 9cffcc6..4f5c24d 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -70,10 +70,12 @@ class ConditionalMarkerDefinition(MarkerDefinition): @classmethod def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) - if condition := marker_definition.get('cloudCondition', ''): - return super().render_prefix(span, marker, context) + condition = marker_definition.get('cloudCondition', '') + if not condition: + style = 'display: none' + return f'<{cls.tag} style=\"{style}\">' else: - return f'<{cls.tag} style="display: none">' + return super().render_prefix(span, marker, context) renderer = PortableTextRenderer( { From ebe606c9809a5fd863b4269bf8e6898936e822ec Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Tue, 1 Feb 2022 14:13:24 +0100 Subject: [PATCH 55/70] release 1.0.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8d8340c..99aeb7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.0.0' +version = '1.0.1' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 35803be148a18c9d7ccde24bbe881d78e361bfe1 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Wed, 9 Feb 2022 14:49:31 +0100 Subject: [PATCH 56/70] add render_text method for custom marker definition class. This method gives a library user more flexibility when using custom marks with complex logic (conditionals, dynamic content substitution" --- portabletext_html/marker_definitions.py | 7 ++++++- tests/test_marker_definitions.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/portabletext_html/marker_definitions.py b/portabletext_html/marker_definitions.py index 72cc2d2..396f4e1 100644 --- a/portabletext_html/marker_definitions.py +++ b/portabletext_html/marker_definitions.py @@ -37,10 +37,15 @@ def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context: def render(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: """Render the marked span directly with prefix and suffix.""" result = cls.render_prefix(span, marker, context) - result += str(span.text) + result += cls.render_text(span, marker, context) result += cls.render_suffix(span, marker, context) return result + @classmethod + def render_text(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + """Render the content part for a marked span.""" + return str(span.text) + # Decorators diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 4f5c24d..0236cbb 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,3 +1,4 @@ +# pylint: skip-file from typing import Type from portabletext_html import PortableTextRenderer @@ -18,6 +19,7 @@ def test_render_emphasis_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) + assert EmphasisMarkerDefinition.render_text(node, 'em', block) == f'{text}' assert EmphasisMarkerDefinition.render(node, 'em', block) == f'{text}' @@ -25,6 +27,7 @@ def test_render_strong_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) + assert StrongMarkerDefinition.render_text(node, 'strong', block) == f'{text}' assert StrongMarkerDefinition.render(node, 'strong', block) == f'{text}' @@ -32,6 +35,7 @@ def test_render_underline_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) + assert UnderlineMarkerDefinition.render_text(node, 'u', block) == f'{text}' assert ( UnderlineMarkerDefinition.render(node, 'u', block) == f'{text}' @@ -42,6 +46,7 @@ def test_render_strikethrough_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) + assert StrikeThroughMarkerDefinition.render_text(node, 'strike', block) == f'{text}' assert StrikeThroughMarkerDefinition.render(node, 'strike', block) == f'{text}' @@ -51,6 +56,7 @@ def test_render_link_marker_success(): block = Block( _type='block', children=[node.__dict__], markDefs=[{'_type': 'link', '_key': 'linkId', 'href': text}] ) + assert LinkMarkerDefinition.render_text(node, 'linkId', block) == f'{text}' assert LinkMarkerDefinition.render(node, 'linkId', block) == f'{text}' From b136e2fa48399290f002be6a6f70cbaf3636dac9 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Thu, 10 Feb 2022 09:21:36 +0100 Subject: [PATCH 57/70] update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 58abe31..fd610a8 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,11 @@ class ComicSansEmphasis(MarkerDefinition): def render_suffix(cls, span: Span, marker: str, context: Block) -> str: return f'' + @classmethod + def render_text(cls, span: Span, marker: str, context: Block) -> str: + # custom rendering logic can be placed here + return str(span.text) + @classmethod def render(cls, span: Span, marker: str, context: Block) -> str: result = cls.render_prefix(span, marker, context) From 5bf33f9b7fe45cd1bb94f93ef2e888111130eee5 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Thu, 10 Feb 2022 09:25:43 +0100 Subject: [PATCH 58/70] release 1.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99aeb7e..dd2b721 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.0.1' +version = '1.1.0' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From e3ca1b111e91417dbde0fab5c80a0da2af9cabe3 Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Sat, 12 Mar 2022 14:33:36 +0100 Subject: [PATCH 59/70] bugfix: call custom mark render_text() on render This commit fixes _render_span method: - if any of a span's marks has overriden render_text() method from the base class, we call it. Note that if a span has multiple mark definitions with render_text() method, only the first one will be processed. This is done to avoid text duplication inside an HTML tag. - otherwise, we append span.text to the resulting HTML string --- portabletext_html/renderer.py | 14 +++++++++++++- portabletext_html/types.py | 2 +- tests/test_marker_definitions.py | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py index 3804c85..56fbfba 100644 --- a/portabletext_html/renderer.py +++ b/portabletext_html/renderer.py @@ -150,7 +150,19 @@ def _render_span(self, span: Span, block: Block) -> str: marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() result += marker_callable.render_prefix(span, mark, block) - result += html.escape(span.text).replace('\n', '
    ') + # to avoid rendering the text multiple times, + # only the first custom mark will be used + custom_mark_text_rendered = False + if sorted_marks: + for mark in sorted_marks: + if custom_mark_text_rendered or mark in prev_marks: + continue + marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() + result += marker_callable.render_text(span, mark, block) + custom_mark_text_rendered = True + + if not custom_mark_text_rendered: + result += html.escape(span.text).replace('\n', '
    ') for mark in reversed(sorted_marks): if mark in next_marks: diff --git a/portabletext_html/types.py b/portabletext_html/types.py index ab5236b..898d61e 100644 --- a/portabletext_html/types.py +++ b/portabletext_html/types.py @@ -73,7 +73,7 @@ def _add_custom_marker_definitions(self) -> dict[str, Type[MarkerDefinition]]: if definition['_type'] in self.marker_definitions: marker = self.marker_definitions[definition['_type']] marker_definitions[definition['_key']] = marker - del marker_definitions[definition['_type']] + # del marker_definitions[definition['_type']] return marker_definitions def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Optional[dict]]: diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 0236cbb..2677218 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -83,8 +83,14 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: else: return super().render_prefix(span, marker, context) + @classmethod + def render_text(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) + condition = marker_definition.get('cloudCondition', '') + return span.text if not condition else '' + renderer = PortableTextRenderer( - { + blocks={ '_type': 'block', 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['some_id'], 'text': 'Sanity'}], 'markDefs': [{'_key': 'some_id', '_type': 'contractConditional', 'cloudCondition': False}], From f8ff2f2121d478c8179bea94b0f82b465db1b31b Mon Sep 17 00:00:00 2001 From: rootisenabled Date: Mon, 14 Mar 2022 08:54:40 +0100 Subject: [PATCH 60/70] relese 1.1.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd2b721..8a1bdbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.1.0' +version = '1.1.1' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 51e8f804b1a1ba6ae0b90279c8fd41b7ae9290ab Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Wed, 27 Apr 2022 09:36:54 +0200 Subject: [PATCH 61/70] tests: add tests for _render_node's error handling --- tests/fixtures/invalid_node.json | 13 +++++++++++++ tests/fixtures/invalid_type.json | 14 ++++++++++++++ tests/test_rendering.py | 16 +++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/invalid_node.json create mode 100644 tests/fixtures/invalid_type.json diff --git a/tests/fixtures/invalid_node.json b/tests/fixtures/invalid_node.json new file mode 100644 index 0000000..74f09bf --- /dev/null +++ b/tests/fixtures/invalid_node.json @@ -0,0 +1,13 @@ +{ + "_key": "73405dda68e7", + "children": [ + { + "_key": "25a09c61d80a", + "_type": "span", + "marks": [], + "text": "Otovo guarantee is good" + } + ], + "markDefs": [], + "style": "normal" +} diff --git a/tests/fixtures/invalid_type.json b/tests/fixtures/invalid_type.json new file mode 100644 index 0000000..745ac66 --- /dev/null +++ b/tests/fixtures/invalid_type.json @@ -0,0 +1,14 @@ +{ + "_key": "73405dda68e7", + "_type": "invalid_type", + "children": [ + { + "_key": "25a09c61d80a", + "_type": "span", + "marks": [], + "text": "Otovo guarantee is good" + } + ], + "markDefs": [], + "style": "normal" +} diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 3894862..b418e62 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -2,7 +2,9 @@ import json from pathlib import Path -from portabletext_html.renderer import render +import pytest + +from portabletext_html.renderer import MissingSerializerError, UnhandledNodeError, render def load_fixture(fixture_name) -> dict: @@ -45,3 +47,15 @@ def test_nested_marks(): fixture = load_fixture('nested_marks.json') output = render(fixture) assert output == '

    A word of warning; Sanity is addictive.

    ' + + +def test_missing_serializer(): + fixture = load_fixture('invalid_type.json') + with pytest.raises(MissingSerializerError): + render(fixture) + + +def test_invalid_node(): + fixture = load_fixture('invalid_node.json') + with pytest.raises(UnhandledNodeError): + render(fixture) From 058b755309ca515dc90c05b7048ddb825994c34d Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Wed, 27 Apr 2022 09:40:43 +0200 Subject: [PATCH 62/70] bugfix: fix _render_node's error handling A bug caused the MissingSerializerError to not be raised when it was supposed to --- .pre-commit-config.yaml | 7 +++++-- portabletext_html/renderer.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0968ba6..5dadb48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/ambv/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black args: [ "--quiet" ] @@ -19,7 +19,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [ @@ -34,6 +34,9 @@ repos: 'flake8-printf-formatting', 'flake8-type-checking', ] + args: [ + '--allow-star-arg-any' + ] - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py index 56fbfba..e924c35 100644 --- a/portabletext_html/renderer.py +++ b/portabletext_html/renderer.py @@ -113,7 +113,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore else: - if hasattr(node, '_type'): + if '_type' in node: raise MissingSerializerError( f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.' ) From b79237f6cac1c06a5462b0516ac06f3ae34244be Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Wed, 27 Apr 2022 10:04:03 +0200 Subject: [PATCH 63/70] Make the 1.1.2 release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8a1bdbb..2aaec41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.1.1' +version = '1.1.2' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From 69fa4757adb0821e73ac5c6f5817e82f346e30a7 Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Mon, 20 Jun 2022 16:17:15 +0200 Subject: [PATCH 64/70] tests: add tests for custom serializer block + list bug --- .../custom_serializer_node_after_list.json | 20 +++++++++++++++++++ tests/test_rendering.py | 15 ++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/fixtures/custom_serializer_node_after_list.json diff --git a/tests/fixtures/custom_serializer_node_after_list.json b/tests/fixtures/custom_serializer_node_after_list.json new file mode 100644 index 0000000..39386cf --- /dev/null +++ b/tests/fixtures/custom_serializer_node_after_list.json @@ -0,0 +1,20 @@ +[ + { + "_key": "e5b6e416e6e9", + "_type": "block", + "children": [ + { "_key": "3bbbff0f158b", "_type": "span", "marks": [], "text": "resers" } + ], + "level": 1, + "listItem": "bullet", + "markDefs": [], + "style": "normal" + }, + { + "_key": "73405dda68e0", + "_type": "extraInfoBlock", + "extraInfo": "This informations is not supported by Block", + "markDefs": [], + "style": "normal" + } +] diff --git a/tests/test_rendering.py b/tests/test_rendering.py index b418e62..1c82309 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -1,10 +1,18 @@ import html import json from pathlib import Path +from typing import Optional import pytest from portabletext_html.renderer import MissingSerializerError, UnhandledNodeError, render +from portabletext_html.types import Block + + +def extraInfoSerializer(node: dict, context: Optional[Block], list_item: bool) -> str: + extraInfo = node.get('extraInfo') + + return f'

    {extraInfo}

    ' def load_fixture(fixture_name) -> dict: @@ -59,3 +67,10 @@ def test_invalid_node(): fixture = load_fixture('invalid_node.json') with pytest.raises(UnhandledNodeError): render(fixture) + + +def test_custom_serializer_node_after_list(): + fixture = load_fixture('custom_serializer_node_after_list.json') + output = render(fixture, custom_serializers={'extraInfoBlock': extraInfoSerializer}) + + assert output == '
    • resers

    This informations is not supported by Block

    ' From bf018e41477177080ec2bf1556b82574d116116f Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Mon, 20 Jun 2022 16:20:52 +0200 Subject: [PATCH 65/70] bugfix: fix a bug where custom serializer blocks can fail if after a list This commit fixes a bug where the rendering of a node can fail if it has fields not supported by Block and follows directly after a list item. The list item logic would pass in the first node after the list as context and cast it to a Block. This fails if the node has fields not supported by Block. Which is the case for custom serializer blocks. The context is actually not used, and removing it solves the bug. --- portabletext_html/renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py index e924c35..211125e 100644 --- a/portabletext_html/renderer.py +++ b/portabletext_html/renderer.py @@ -66,7 +66,7 @@ def render(self) -> str: if list_nodes and not is_list(node): tree = self._normalize_list_tree(list_nodes) - result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) + result += ''.join([self._render_node(n, list_item=True) for n in tree]) list_nodes = [] # reset list_nodes if is_list(node): From 8e895e36bf54b39a8e365d05d2934384ee04ef61 Mon Sep 17 00:00:00 2001 From: Espen Ogino Rathe Date: Tue, 21 Jun 2022 12:04:58 +0200 Subject: [PATCH 66/70] Increase patch version to 1.1.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2aaec41..d6e1665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'portabletext-html' -version = '1.1.2' +version = '1.1.3' description = "HTML renderer for Sanity's Portable Text format" homepage = 'https://github.com/otovo/python-sanity-html' repository = 'https://github.com/otovo/python-sanity-html' From aef47dcd03c491a7b618ad39ebcbd978524fd18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 18 Nov 2022 22:21:12 +0100 Subject: [PATCH 67/70] chore: Update pre-commit hooks and lockfile --- .pre-commit-config.yaml | 17 ++++----- poetry.lock | 71 +++++++++++++++++------------------ portabletext_html/renderer.py | 6 +-- 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5dadb48..6493166 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/ambv/black - rev: 22.3.0 + rev: 22.10.0 hooks: - id: black args: [ "--quiet" ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.3.0 hooks: - id: check-ast - id: check-merge-conflict @@ -18,8 +18,8 @@ repos: - id: trailing-whitespace - id: mixed-line-ending - id: trailing-whitespace - - repo: https://gitlab.com/pycqa/flake8 - rev: 4.0.1 + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [ @@ -34,11 +34,10 @@ repos: 'flake8-printf-formatting', 'flake8-type-checking', ] - args: [ - '--allow-star-arg-any' - ] + args: + - '--allow-star-arg-any' - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ] @@ -47,7 +46,7 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.991 hooks: - id: mypy additional_dependencies: diff --git a/poetry.lock b/poetry.lock index 30cd47f..b05bfd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -8,25 +8,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" @@ -55,7 +55,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "5.0.0" description = "Read metadata from Python packages" category = "dev" optional = false @@ -66,9 +66,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -138,11 +138,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "3.0.6" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] @@ -183,7 +183,7 @@ pytest = ">=4.6" toml = "*" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "toml" @@ -195,23 +195,23 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "zipp" -version = "3.7.0" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" @@ -220,16 +220,15 @@ content-hash = "c641d950bccb6ffac52cf3fcd3571b51f5e31d4864c03e763fe2748919bf855b [metadata.files] atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, @@ -290,8 +289,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, + {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, + {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -322,8 +321,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -338,10 +337,10 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py index 211125e..1ca2ce3 100644 --- a/portabletext_html/renderer.py +++ b/portabletext_html/renderer.py @@ -38,8 +38,8 @@ class PortableTextRenderer: def __init__( self, blocks: Union[list[dict], dict], - custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, - custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, + custom_marker_definitions: dict[str, Type[MarkerDefinition]] | None = None, + custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] | None = None, ) -> None: logger.debug('Initializing block renderer') self._wrapper_element: Optional[str] = None @@ -106,7 +106,7 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b elif is_span(node): logger.debug('Rendering node as span') span = Span(**node) - context = cast(Block, context) # context should always be a Block here + context = cast('Block', context) # context should always be a Block here return self._render_span(span, block=context) elif self._custom_serializers.get(node.get('_type', '')): From fcf1489bf01df025e6afce7f855a0ddba20ae0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 18 Nov 2022 22:22:20 +0100 Subject: [PATCH 68/70] chore: Add Python 3.11 to test suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45b4dc7..5692bbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10" ] + python-version: [ "3.7.14", "3.8.14", "3.9.15", "3.10.8", "3.11.0" ] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 From 4515b42662c13a10744ff8f24fd148888dda523d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 18 Nov 2022 22:22:37 +0100 Subject: [PATCH 69/70] chore: Add Python 3.11 package classifiers --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d6e1665..7dc039a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Typing :: Typed', ] From 7cd5d21abca77e61bebc3fd46b13ee37842de13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Lilleb=C3=B8=20Gundersen?= Date: Fri, 18 Nov 2022 22:23:09 +0100 Subject: [PATCH 70/70] chore: Add Python 3.12 alpha to test suite --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5692bbf..22b34ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.7.14", "3.8.14", "3.9.15", "3.10.8", "3.11.0" ] + python-version: [ "3.7.14", "3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12.0-alpha.1" ] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2