diff --git a/.gitignore b/.gitignore index b74bbb4..4b2e5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,9 @@ build/ # Serena/Vibe working directories .serena/ .vibe/ + +# Playwright MCP screenshots +.playwright-mcp/ + +# Test reports +report.html diff --git a/src/dacli/api/content.py b/src/dacli/api/content.py index eadc989..c067b41 100644 --- a/src/dacli/api/content.py +++ b/src/dacli/api/content.py @@ -178,6 +178,7 @@ def get_elements( start_line=elem.source_location.line, end_line=elem.source_location.end_line or elem.source_location.line, ), + attributes=elem.attributes, # Issue #159: Include element attributes ) ) diff --git a/src/dacli/api/models.py b/src/dacli/api/models.py index 7a780d1..0a32d7b 100644 --- a/src/dacli/api/models.py +++ b/src/dacli/api/models.py @@ -4,7 +4,7 @@ and Content Access API as specified in 02_api_specification.adoc. """ -from typing import Literal +from typing import Any, Literal from pydantic import BaseModel, Field @@ -123,6 +123,10 @@ class ElementItem(BaseModel): path: str = Field(description="Section path containing this element") index: int = Field(description="Index of element within its section") location: ElementLocation + attributes: dict[str, Any] = Field( + default_factory=dict, + description="Element-specific attributes and content (Issue #159)", + ) # Note: preview field removed in Issue #142 as redundant diff --git a/src/dacli/asciidoc_parser.py b/src/dacli/asciidoc_parser.py index 2889173..242c94b 100644 --- a/src/dacli/asciidoc_parser.py +++ b/src/dacli/asciidoc_parser.py @@ -667,6 +667,13 @@ def _parse_elements( current_list_element: Element | None = None # Track list for end_line (#136) # Track ALL open blocks for end_line (Issue #157: use stack instead of single element) open_blocks: list[Element] = [] + # Content tracking for Issue #159 + code_block_content: list[str] = [] + plantuml_content: list[str] = [] + mermaid_content: list[str] = [] + ditaa_content: list[str] = [] + table_content: list[str] = [] + list_content: list[str] = [] for line_text, source_file, line_num, resolved_from in lines: # Track current section for parent_section @@ -723,6 +730,7 @@ def _sub_attr(match: re.Match) -> str: if pending_plantuml_info is not None: # Start of plantuml block in_plantuml_block = True + plantuml_content = [] # Initialize content tracking (Issue #159) name, fmt = pending_plantuml_info element = self._create_diagram_element( "plantuml", name, fmt, source_file, line_num, @@ -734,6 +742,7 @@ def _sub_attr(match: re.Match) -> str: elif pending_mermaid_info is not None: # Start of mermaid block in_mermaid_block = True + mermaid_content = [] # Initialize content tracking (Issue #159) name, fmt = pending_mermaid_info element = self._create_diagram_element( "mermaid", name, fmt, source_file, line_num, @@ -745,6 +754,7 @@ def _sub_attr(match: re.Match) -> str: elif pending_ditaa_info is not None: # Start of ditaa block in_ditaa_block = True + ditaa_content = [] # Initialize content tracking (Issue #159) name, fmt = pending_ditaa_info element = self._create_diagram_element( "ditaa", name, fmt, source_file, line_num, @@ -756,6 +766,7 @@ def _sub_attr(match: re.Match) -> str: elif pending_code_language is not None: # Start of code block in_code_block = True + code_block_content = [] # Initialize content tracking (Issue #159) source_location = SourceLocation( file=source_file, line=line_num, @@ -771,19 +782,27 @@ def _sub_attr(match: re.Match) -> str: open_blocks.append(element) pending_code_language = None elif in_code_block: - # End of code block + # End of code block - save content (Issue #159) + if open_blocks: + open_blocks[-1].attributes["content"] = "\n".join(code_block_content) in_code_block = False self._close_open_block(open_blocks, line_num) elif in_plantuml_block: - # End of plantuml block + # End of plantuml block - save content (Issue #159) + if open_blocks: + open_blocks[-1].attributes["content"] = "\n".join(plantuml_content) in_plantuml_block = False self._close_open_block(open_blocks, line_num) elif in_mermaid_block: - # End of mermaid block + # End of mermaid block - save content (Issue #159) + if open_blocks: + open_blocks[-1].attributes["content"] = "\n".join(mermaid_content) in_mermaid_block = False self._close_open_block(open_blocks, line_num) elif in_ditaa_block: - # End of ditaa block + # End of ditaa block - save content (Issue #159) + if open_blocks: + open_blocks[-1].attributes["content"] = "\n".join(ditaa_content) in_ditaa_block = False self._close_open_block(open_blocks, line_num) continue @@ -793,6 +812,7 @@ def _sub_attr(match: re.Match) -> str: if not in_table: # Start of table in_table = True + table_content = [] # Initialize content tracking (Issue #159) source_location = SourceLocation( file=source_file, line=line_num, @@ -807,7 +827,9 @@ def _sub_attr(match: re.Match) -> str: elements.append(element) open_blocks.append(element) else: - # End of table + # End of table - save content (Issue #159) + if open_blocks: + open_blocks[-1].attributes["content"] = "\n".join(table_content) in_table = False self._close_open_block(open_blocks, line_num) continue @@ -852,21 +874,31 @@ def _sub_attr(match: re.Match) -> str: parent_section=current_section_path, ) ) + # Save list content before reset (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = None # Reset list tracking + current_list_element = None continue # Detect lists (unordered, ordered, description) # Check for unordered list (* item) if UNORDERED_LIST_PATTERN.match(line_text): if current_list_type != "unordered": + # Save previous list content if any (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) # Start of a new unordered list current_list_type = "unordered" + list_content = [] # Initialize content tracking (Issue #159) current_list_element = self._create_list_element( "unordered", source_file, line_num, resolved_from, current_section_path, ) elements.append(current_list_element) - elif current_list_element is not None: + # Collect list item content (Issue #159) + list_content.append(line_text) + if current_list_element is not None: # Continue list - update end_line (Issue #136) current_list_element.source_location.end_line = line_num continue @@ -874,14 +906,20 @@ def _sub_attr(match: re.Match) -> str: # Check for ordered list (. item) if ORDERED_LIST_PATTERN.match(line_text): if current_list_type != "ordered": + # Save previous list content if any (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) # Start of a new ordered list current_list_type = "ordered" + list_content = [] # Initialize content tracking (Issue #159) current_list_element = self._create_list_element( "ordered", source_file, line_num, resolved_from, current_section_path, ) elements.append(current_list_element) - elif current_list_element is not None: + # Collect list item content (Issue #159) + list_content.append(line_text) + if current_list_element is not None: # Continue list - update end_line (Issue #136) current_list_element.source_location.end_line = line_num continue @@ -903,9 +941,24 @@ def _sub_attr(match: re.Match) -> str: # If line is not a list item, reset list tracking if line_text.strip(): + # Save list content before reset (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = None current_list_element = None + # Collect content for open blocks (Issue #159) + if in_code_block: + code_block_content.append(line_text) + elif in_plantuml_block: + plantuml_content.append(line_text) + elif in_mermaid_block: + mermaid_content.append(line_text) + elif in_ditaa_block: + ditaa_content.append(line_text) + elif in_table: + table_content.append(line_text) + # Handle ALL unclosed blocks - set end_line to last line of their source file # Issue #146: unclosed code blocks should have proper end_line # Issue #157: ALL unclosed blocks should be detected, not just the last one @@ -938,6 +991,10 @@ def _sub_attr(match: re.Match) -> str: message=warning_msg, )) + # Save list content if list is still open at end of file (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) + return elements, warnings def _find_section_path(self, sections: list[Section], title: str) -> str: diff --git a/src/dacli/cli.py b/src/dacli/cli.py index a0e1c6c..8777328 100644 --- a/src/dacli/cli.py +++ b/src/dacli/cli.py @@ -480,21 +480,25 @@ def search(ctx: CliContext, query: str, scope: str | None, max_results: int): @cli.command(epilog=""" Examples: - dacli elements # All elements - dacli elements --type code # Only code blocks - dacli elements --type table # Only tables - dacli elements api # Elements in 'api' section - dacli elements api --recursive # Elements in 'api' and all subsections - dacli --format json el --type image # JSON output using alias + dacli elements # All elements (metadata only) + dacli elements --type code # Only code blocks + dacli elements --include-content # Include element content + dacli elements --include-content --content-limit 10 # First 10 lines only + dacli elements api --recursive # Elements in 'api' and subsections + dacli --format json el --type image # JSON output using alias """) @click.argument("section_path", required=False, default=None) @click.option("--type", "element_type", default=None, help="Element type: code, table, image, diagram, list") @click.option("--recursive", is_flag=True, default=False, help="Include elements from child sections") +@click.option("--include-content", is_flag=True, default=False, + help="Include element content (Issue #159)") +@click.option("--content-limit", type=int, default=None, + help="Limit content to first N lines (requires --include-content)") @pass_context def elements(ctx: CliContext, section_path: str | None, element_type: str | None, - recursive: bool): + recursive: bool, include_content: bool, content_limit: int | None): """Get elements (code blocks, tables, images) from documentation.""" elems = ctx.index.get_elements( element_type=element_type, @@ -502,22 +506,37 @@ def elements(ctx: CliContext, section_path: str | None, element_type: str | None recursive=recursive, ) - # Note: preview field removed in Issue #142 as redundant - # The type field is sufficient to identify element kind + # Build element dicts with optional content (Issue #159) + element_dicts = [] + for e in elems: + elem_dict = { + "type": e.type, + "parent_section": e.parent_section, + "location": { + "file": str(e.source_location.file), + "start_line": e.source_location.line, + "end_line": e.source_location.end_line, + }, + } + + # Include attributes if requested (Issue #159) + if include_content: + attributes = dict(e.attributes) # Copy attributes + + # Apply content limit if specified + if content_limit is not None and "content" in attributes: + content = attributes["content"] + lines = content.split("\n") + if len(lines) > content_limit: + attributes["content"] = "\n".join(lines[:content_limit]) + + elem_dict["attributes"] = attributes + + element_dicts.append(elem_dict) + result = { - "elements": [ - { - "type": e.type, - "parent_section": e.parent_section, - "location": { - "file": str(e.source_location.file), - "start_line": e.source_location.line, - "end_line": e.source_location.end_line, - }, - } - for e in elems - ], - "count": len(elems), + "elements": element_dicts, + "count": len(element_dicts), } click.echo(format_output(ctx, result)) diff --git a/src/dacli/markdown_parser.py b/src/dacli/markdown_parser.py index 3d2e3e0..d57e0b4 100644 --- a/src/dacli/markdown_parser.py +++ b/src/dacli/markdown_parser.py @@ -488,6 +488,10 @@ def _parse_elements( current_list_type: str | None = None current_list_element: Element | None = None + # Content tracking for Issue #159 + table_content: list[str] = [] + list_content: list[str] = [] + for line_num, line in enumerate(lines, start=1 + line_offset): # Track current section heading_match = HEADING_PATTERN.match(line) @@ -506,6 +510,7 @@ def _parse_elements( attributes={ "columns": table_columns, "rows": table_rows, + "content": "\n".join(table_content), # Issue #159 }, parent_section=current_section_path, ) @@ -566,6 +571,7 @@ def _parse_elements( # Start of table in_table = True table_start_line = line_num + table_content = [] # Initialize content tracking (Issue #159) # Count columns from header row cells = table_row_match.group(1).split("|") table_columns = len(cells) @@ -576,6 +582,8 @@ def _parse_elements( elif has_separator: # Data row after separator table_rows += 1 + # Collect table line (Issue #159) + table_content.append(line) continue elif in_table: # End of table (non-table line) @@ -591,6 +599,7 @@ def _parse_elements( attributes={ "columns": table_columns, "rows": table_rows, + "content": "\n".join(table_content), # Issue #159 }, parent_section=current_section_path, ) @@ -618,6 +627,9 @@ def _parse_elements( parent_section=current_section_path, ) ) + # Save list content before reset (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = None # Reset list tracking current_list_element = None continue @@ -627,7 +639,11 @@ def _parse_elements( # Check for unordered list (*, -, +) if UNORDERED_LIST_PATTERN.match(line): if current_list_type != "unordered": + # Save previous list content if any (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = "unordered" + list_content = [] # Initialize content tracking (Issue #159) element = Element( type="list", source_location=SourceLocation( @@ -638,16 +654,21 @@ def _parse_elements( ) elements.append(element) current_list_element = element - else: - # Update end_line for each list item - if current_list_element is not None: - current_list_element.source_location.end_line = line_num + # Collect list item content (Issue #159) + list_content.append(line) + # Update end_line for each list item + if current_list_element is not None: + current_list_element.source_location.end_line = line_num continue # Check for ordered list (1., 2., etc.) if ORDERED_LIST_PATTERN.match(line): if current_list_type != "ordered": + # Save previous list content if any (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = "ordered" + list_content = [] # Initialize content tracking (Issue #159) element = Element( type="list", source_location=SourceLocation( @@ -658,14 +679,18 @@ def _parse_elements( ) elements.append(element) current_list_element = element - else: - # Update end_line for each list item - if current_list_element is not None: - current_list_element.source_location.end_line = line_num + # Collect list item content (Issue #159) + list_content.append(line) + # Update end_line for each list item + if current_list_element is not None: + current_list_element.source_location.end_line = line_num continue # If non-empty, non-list line, reset list tracking if line.strip(): + # Save list content before reset (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) current_list_type = None current_list_element = None @@ -690,11 +715,16 @@ def _parse_elements( attributes={ "columns": table_columns, "rows": table_rows, + "content": "\n".join(table_content), # Issue #159 }, parent_section=current_section_path, ) ) + # Save list content if list is still open at end of file (Issue #159) + if current_list_element is not None and list_content: + current_list_element.attributes["content"] = "\n".join(list_content) + return elements def _collect_markdown_files(self, folder_path: Path) -> list[Path]: diff --git a/src/dacli/mcp_app.py b/src/dacli/mcp_app.py index 497e626..c36cc42 100644 --- a/src/dacli/mcp_app.py +++ b/src/dacli/mcp_app.py @@ -274,6 +274,8 @@ def get_elements( element_type: str | None = None, section_path: str | None = None, recursive: bool = False, + include_content: bool = False, + content_limit: int | None = None, ) -> dict: """Get elements (code blocks, tables, images) from the documentation. @@ -286,10 +288,13 @@ def get_elements( section_path: Filter by section path (e.g., '/architecture'). recursive: If True, include elements from child sections. If False (default), only exact section matches. + include_content: If True, include element content and attributes (Issue #159). + If False (default), only return metadata. + content_limit: Limit content to first N lines (requires include_content=True). Returns: - Dictionary with 'elements' (list of elements with type, location) - and 'count'. + Dictionary with 'elements' (list of elements with type, location, and + optionally attributes/content) and 'count'. """ elements = index.get_elements( element_type=element_type, @@ -297,22 +302,37 @@ def get_elements( recursive=recursive, ) - # Note: preview field removed in Issue #142 as redundant - # The type field is sufficient to identify element kind + # Build element dicts with optional content (Issue #159) + element_dicts = [] + for e in elements: + elem_dict = { + "type": e.type, + "parent_section": e.parent_section, + "location": { + "file": str(e.source_location.file), + "start_line": e.source_location.line, + "end_line": e.source_location.end_line, + }, + } + + # Include attributes if requested (Issue #159) + if include_content: + attributes = dict(e.attributes) # Copy attributes + + # Apply content limit if specified + if content_limit is not None and "content" in attributes: + content = attributes["content"] + lines = content.split("\n") + if len(lines) > content_limit: + attributes["content"] = "\n".join(lines[:content_limit]) + + elem_dict["attributes"] = attributes + + element_dicts.append(elem_dict) + return { - "elements": [ - { - "type": e.type, - "parent_section": e.parent_section, - "location": { - "file": str(e.source_location.file), - "start_line": e.source_location.line, - "end_line": e.source_location.end_line, - }, - } - for e in elements - ], - "count": len(elements), + "elements": element_dicts, + "count": len(element_dicts), } @mcp.tool() diff --git a/src/docs/spec/02_api_specification.adoc b/src/docs/spec/02_api_specification.adoc index 14695c0..99f0e1f 100644 --- a/src/docs/spec/02_api_specification.adoc +++ b/src/docs/spec/02_api_specification.adoc @@ -295,23 +295,36 @@ Retrieves elements of a specific type. [source,json] ---- { - "type": "diagram", + "type": "code", "elements": [ { - "type": "diagram", - "path": "context.technical", + "type": "code", + "path": "architecture.decisions", "index": 0, "location": { - "file": "chapters/03_context.adoc", + "file": "chapters/09_decisions.adoc", "start_line": 25, - "end_line": 45 + "end_line": 35 + }, + "attributes": { + "language": "python", + "content": "def example():\n return 'hello world'" } } ], - "count": 8 + "count": 1 } ---- +NOTE: The `attributes` field contains element-specific data including content (Issue #159). +For `code` elements, it includes `language` and `content` fields. +For `table` elements, it includes `columns`, `rows`, and `content` (raw table text). +For `list` elements, it includes `list_type` and `content` (all list items). +For `image` elements, it includes `alt`, `src`/`target`, and optionally `title`. +For `plantuml`/diagram elements, it includes `format`, optionally `name`, and `content` (diagram source). +For `admonition` elements, it includes `admonition_type` and `content`. +---- + **Response 400:** [source,json] ---- diff --git a/src/docs/spec/06_cli_specification.adoc b/src/docs/spec/06_cli_specification.adoc index 4059b60..b513510 100644 --- a/src/docs/spec/06_cli_specification.adoc +++ b/src/docs/spec/06_cli_specification.adoc @@ -181,7 +181,7 @@ Lists elements (code blocks, tables, etc.). [source,bash] ---- -dacli elements [SECTION_PATH] [--type TYPE] [--recursive] +dacli elements [SECTION_PATH] [--type TYPE] [--recursive] [--include-content] [--content-limit N] ---- **Arguments:** @@ -192,6 +192,25 @@ dacli elements [SECTION_PATH] [--type TYPE] [--recursive] * `--type TYPE`: Element type filter - `code`, `table`, `image`, `diagram`, `list`, `admonition` * `--recursive`: Include elements from child sections (default: exact match only) +* `--include-content`: Include element content and attributes (Issue #159) +* `--content-limit N`: Limit content to first N lines (requires `--include-content`) + +**Examples:** + +[source,bash] +---- +# Get code blocks without content (metadata only) +$ dacli elements --type code + +# Get code blocks with full content +$ dacli elements --type code --include-content + +# Get code blocks with first 5 lines only +$ dacli elements --type code --include-content --content-limit 5 + +# Get all elements in a section with content +$ dacli elements api --recursive --include-content +---- == Meta-Information Commands diff --git a/tests/test_element_content.py b/tests/test_element_content.py new file mode 100644 index 0000000..28000da --- /dev/null +++ b/tests/test_element_content.py @@ -0,0 +1,279 @@ +"""Tests for Issue #159: Element Content Capture. + +Tests parser content capture, API attributes inclusion, CLI flags, +and MCP tool parameters for element content. +""" + +import tempfile +from pathlib import Path + +import pytest + +from dacli.asciidoc_parser import AsciidocStructureParser +from dacli.markdown_parser import MarkdownStructureParser + + +class TestAsciidocContentCapture: + """Test content capture in AsciiDoc parser.""" + + def test_code_block_captures_content(self): + """Code blocks should capture their content.""" + content = """= Document + +== Section + +[source,python] +---- +def hello(): + print("world") +---- +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".adoc", delete=False) as f: + f.write(content) + f.flush() + parser = AsciidocStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + code_elem = doc.elements[0] + assert code_elem.type == "code" + assert "content" in code_elem.attributes + assert 'def hello():' in code_elem.attributes["content"] + assert 'print("world")' in code_elem.attributes["content"] + + def test_table_captures_content(self): + """Tables should capture their content.""" + content = """= Document + +== Section + +|=== +| Header 1 | Header 2 +| Cell 1 | Cell 2 +|=== +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".adoc", delete=False) as f: + f.write(content) + f.flush() + parser = AsciidocStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + table_elem = doc.elements[0] + assert table_elem.type == "table" + assert "content" in table_elem.attributes + assert "Header 1" in table_elem.attributes["content"] + assert "Cell 1" in table_elem.attributes["content"] + + def test_plantuml_captures_content(self): + """PlantUML diagrams should capture their source.""" + content = """= Document + +== Section + +[plantuml,diagram,svg] +---- +@startuml +Alice -> Bob: Hello +@enduml +---- +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".adoc", delete=False) as f: + f.write(content) + f.flush() + parser = AsciidocStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + diagram = doc.elements[0] + assert diagram.type == "plantuml" + assert "content" in diagram.attributes + assert "@startuml" in diagram.attributes["content"] + assert "Alice -> Bob" in diagram.attributes["content"] + + def test_list_captures_content(self): + """Lists should capture their items.""" + content = """= Document + +== Section + +* Item 1 +* Item 2 +* Item 3 +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".adoc", delete=False) as f: + f.write(content) + f.flush() + parser = AsciidocStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + list_elem = doc.elements[0] + assert list_elem.type == "list" + assert "content" in list_elem.attributes + assert "* Item 1" in list_elem.attributes["content"] + assert "* Item 2" in list_elem.attributes["content"] + + +class TestMarkdownContentCapture: + """Test content capture in Markdown parser.""" + + def test_code_block_captures_content(self): + """Markdown code blocks should capture content.""" + content = """# Document + +## Section + +```python +def hello(): + print("world") +``` +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: + f.write(content) + f.flush() + parser = MarkdownStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + code_elem = doc.elements[0] + assert code_elem.type == "code" + assert "content" in code_elem.attributes + assert 'def hello():' in code_elem.attributes["content"] + + def test_table_captures_content(self): + """Markdown tables should capture content.""" + content = """# Document + +## Section + +| Header 1 | Header 2 | +|----------|----------| +| Cell 1 | Cell 2 | +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: + f.write(content) + f.flush() + parser = MarkdownStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + table_elem = doc.elements[0] + assert table_elem.type == "table" + assert "content" in table_elem.attributes + assert "Header 1" in table_elem.attributes["content"] + + def test_list_captures_content(self): + """Markdown lists should capture content.""" + content = """# Document + +## Section + +- Item 1 +- Item 2 +- Item 3 +""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: + f.write(content) + f.flush() + parser = MarkdownStructureParser(Path(f.name).parent) + doc = parser.parse_file(Path(f.name)) + + assert len(doc.elements) == 1 + list_elem = doc.elements[0] + assert list_elem.type == "list" + assert "content" in list_elem.attributes + assert "- Item 1" in list_elem.attributes["content"] + + +class TestCliContentFlags: + """Test CLI flags for content control.""" + + def test_elements_without_include_content_has_no_attributes(self, cli_runner, temp_docs_dir): + """Default: elements should not include attributes.""" + (temp_docs_dir / "test.adoc").write_text("""= Test + +== Section + +[source,bash] +---- +echo "test" +---- +""") + result = cli_runner.invoke( + ["--docs-root", str(temp_docs_dir), "elements", "--type", "code"] + ) + assert result.exit_code == 0 + assert "attributes" not in result.output + + def test_elements_with_include_content_has_attributes(self, cli_runner, temp_docs_dir): + """With --include-content: elements should include attributes.""" + (temp_docs_dir / "test.adoc").write_text("""= Test + +== Section + +[source,bash] +---- +echo "test" +---- +""") + result = cli_runner.invoke([ + "--docs-root", str(temp_docs_dir), + "elements", "--type", "code", "--include-content" + ]) + assert result.exit_code == 0 + assert "attributes" in result.output + assert "content" in result.output + assert 'echo "test"' in result.output + + def test_content_limit_truncates_content(self, cli_runner, temp_docs_dir): + """With --content-limit: content should be truncated.""" + (temp_docs_dir / "test.adoc").write_text("""= Test + +== Section + +[source,bash] +---- +line 1 +line 2 +line 3 +line 4 +line 5 +---- +""") + result = cli_runner.invoke([ + "--docs-root", str(temp_docs_dir), + "elements", "--type", "code", "--include-content", "--content-limit", "2" + ]) + assert result.exit_code == 0 + assert "line 1" in result.output + assert "line 2" in result.output + assert "line 3" not in result.output + assert "line 4" not in result.output + + +@pytest.fixture +def cli_runner(): + """Create a CLI runner for testing.""" + from click.testing import CliRunner + + from dacli.cli import cli + + runner = CliRunner() + + class Runner: + def invoke(self, args): + return runner.invoke(cli, args) + + return Runner() + + +@pytest.fixture +def temp_docs_dir(): + """Create a temporary directory for test documents.""" + import shutil + import tempfile + + temp_dir = Path(tempfile.mkdtemp()) + yield temp_dir + shutil.rmtree(temp_dir) diff --git a/uv.lock b/uv.lock index 4388e65..95d41d5 100644 --- a/uv.lock +++ b/uv.lock @@ -225,6 +225,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + [[package]] name = "cryptography" version = "46.0.3" @@ -298,7 +372,7 @@ wheels = [ [[package]] name = "dacli" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "click" }, @@ -314,6 +388,7 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, { name = "pytest-html" }, { name = "ruff" }, ] @@ -327,6 +402,7 @@ requires-dist = [ { name = "pydocket", specifier = "<0.17" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, { name = "pytest-html", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, @@ -1197,6 +1273,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "pytest-html" version = "4.2.0"