diff --git a/server/galaxyls/server.py b/server/galaxyls/server.py index 369b79f..9d58e25 100644 --- a/server/galaxyls/server.py +++ b/server/galaxyls/server.py @@ -31,10 +31,13 @@ TextDocumentPositionParams, TextEdit, ) +from pygls.workspace import Document from .config import CompletionMode, GalaxyToolsConfiguration from .features import AUTO_CLOSE_TAGS, CMD_GENERATE_TEST from .services.language import GalaxyToolLanguageService +from .services.xml.document import XmlDocument +from .services.xml.parser import XmlDocumentParser from .types import AutoCloseTagResult, GeneratedTestResult SERVER_NAME = "Galaxy Tools LS" @@ -89,7 +92,10 @@ def completions(server: GalaxyToolsLanguageServer, params: CompletionParams) -> if server.configuration.completion_mode == CompletionMode.DISABLED: return None document = server.workspace.get_document(params.textDocument.uri) - return server.service.get_completion(document, params, server.configuration.completion_mode) + if not _is_document_supported(document): + return None + xml_document = _get_xml_document(document) + return server.service.get_completion(xml_document, params, server.configuration.completion_mode) @language_server.feature(AUTO_CLOSE_TAGS) @@ -97,20 +103,28 @@ def auto_close_tag(server: GalaxyToolsLanguageServer, params: TextDocumentPositi """Responds to a close tag request to close the currently opened node.""" if server.configuration.auto_close_tags: document = server.workspace.get_document(params.textDocument.uri) - return server.service.get_auto_close_tag(document, params) + if not _is_document_supported(document): + return None + xml_document = _get_xml_document(document) + return server.service.get_auto_close_tag(xml_document, params) @language_server.feature(HOVER) def hover(server: GalaxyToolsLanguageServer, params: TextDocumentPositionParams) -> Optional[Hover]: """Displays Markdown documentation for the element under the cursor.""" document = server.workspace.get_document(params.textDocument.uri) - return server.service.get_documentation(document, params.position) + if not _is_document_supported(document): + return None + xml_document = _get_xml_document(document) + return server.service.get_documentation(xml_document, params.position) @language_server.feature(FORMATTING) -def formatting(server: GalaxyToolsLanguageServer, params: DocumentFormattingParams) -> List[TextEdit]: +def formatting(server: GalaxyToolsLanguageServer, params: DocumentFormattingParams) -> Optional[List[TextEdit]]: """Formats the whole document using the provided parameters""" document = server.workspace.get_document(params.textDocument.uri) + if not _is_document_supported(document): + return None content = document.source return server.service.format_document(content, params) @@ -137,6 +151,7 @@ def did_close(server: GalaxyToolsLanguageServer, params: DidCloseTextDocumentPar async def cmd_generate_test( server: GalaxyToolsLanguageServer, params: TextDocumentIdentifier ) -> Optional[GeneratedTestResult]: + """Generates some test snippets based on the inputs and outputs of the document.""" document = server.workspace.get_document(params.uri) return server.service.generate_test(document) @@ -144,5 +159,18 @@ async def cmd_generate_test( def _validate(server: GalaxyToolsLanguageServer, params) -> None: """Validates the Galaxy tool and reports any problem found.""" document = server.workspace.get_document(params.textDocument.uri) - diagnostics = server.service.get_diagnostics(document) - server.publish_diagnostics(document.uri, diagnostics) + if _is_document_supported(document): + xml_document = _get_xml_document(document) + diagnostics = server.service.get_diagnostics(xml_document) + server.publish_diagnostics(document.uri, diagnostics) + + +def _get_xml_document(document: Document) -> XmlDocument: + """Parses the input Document and returns an XmlDocument.""" + xml_document = XmlDocumentParser().parse(document) + return xml_document + + +def _is_document_supported(document: Document) -> bool: + """Returns True if the given document is supported by the server.""" + return XmlDocument.has_valid_root(document) diff --git a/server/galaxyls/services/context.py b/server/galaxyls/services/context.py index 16afec7..29d5111 100644 --- a/server/galaxyls/services/context.py +++ b/server/galaxyls/services/context.py @@ -3,11 +3,11 @@ from typing import List, Optional from pygls.types import Range -from pygls.workspace import Document, Position +from pygls.workspace import Position from .xml.constants import UNDEFINED_OFFSET +from .xml.document import XmlDocument from .xml.nodes import XmlSyntaxNode -from .xml.parser import XmlDocumentParser from .xml.types import NodeType from .xml.utils import convert_document_offsets_to_range from .xsd.types import XsdNode, XsdTree @@ -136,7 +136,7 @@ class XmlContextService: def __init__(self, xsd_tree: XsdTree): self.xsd_tree = xsd_tree - def get_xml_context(self, document: Document, position: Position) -> XmlContext: + def get_xml_context(self, xml_document: XmlDocument, position: Position) -> XmlContext: """Gets the XML context at a given position inside the document. Args: @@ -148,15 +148,13 @@ def get_xml_context(self, document: Document, position: Position) -> XmlContext: definition and other information. If the context can not be determined, the default context with no information is returned. """ - offset = document.offset_at_position(position) + offset = xml_document.document.offset_at_position(position) - parser = XmlDocumentParser() - xml_document = parser.parse(document) if xml_document.is_empty: return XmlContext(self.xsd_tree.root, node=None) node = xml_document.get_node_at(offset) xsd_node = self.find_matching_xsd_element(node, self.xsd_tree) - line_text = document.lines[position.line] + line_text = xml_document.document.lines[position.line] context = XmlContext(xsd_node, node, line_text, position, offset) return context @@ -177,6 +175,6 @@ def find_matching_xsd_element(self, node: Optional[XmlSyntaxNode], xsd_tree: Xsd return xsd_node return xsd_tree.root - def get_range_for_context(self, document: Document, context: XmlContext) -> Range: + def get_range_for_context(self, xml_document: XmlDocument, context: XmlContext) -> Range: start_offset, end_offset = context.token.get_offsets(context.offset) - return convert_document_offsets_to_range(document, start_offset, end_offset) + return convert_document_offsets_to_range(xml_document.document, start_offset, end_offset) diff --git a/server/galaxyls/services/language.py b/server/galaxyls/services/language.py index 5d85321..f71e67e 100644 --- a/server/galaxyls/services/language.py +++ b/server/galaxyls/services/language.py @@ -18,6 +18,7 @@ from .context import XmlContextService from .format import GalaxyToolFormatService from .tools import GalaxyToolTestSnippetGenerator, GalaxyToolXmlDocument +from .xml.document import XmlDocument from .xsd.service import GalaxyToolXsdService @@ -35,18 +36,18 @@ def __init__(self, server_name: str): self.completion_service = XmlCompletionService(tree) self.xml_context_service = XmlContextService(tree) - def get_diagnostics(self, document: Document) -> List[Diagnostic]: - """Validates the Galaxy tool and returns a list + def get_diagnostics(self, xml_document: XmlDocument) -> List[Diagnostic]: + """Validates the Galaxy tool XML document and returns a list of diagnotics if there are any problems. """ - return self.xsd_service.validate_document(document) + return self.xsd_service.validate_document(xml_document) - def get_documentation(self, document: Document, position: Position) -> Optional[Hover]: + def get_documentation(self, xml_document: XmlDocument, position: Position) -> Optional[Hover]: """Gets the documentation about the element at the given position.""" - context = self.xml_context_service.get_xml_context(document, position) + context = self.xml_context_service.get_xml_context(xml_document, position) if context.token and (context.is_tag or context.is_attribute_key): documentation = self.xsd_service.get_documentation_for(context) - context_range = self.xml_context_service.get_range_for_context(document, context) + context_range = self.xml_context_service.get_range_for_context(xml_document, context) return Hover(documentation, context_range) return None @@ -56,16 +57,18 @@ def format_document(self, content: str, params: DocumentFormattingParams) -> Lis """ return self.format_service.format(content, params) - def get_completion(self, document: Document, params: CompletionParams, mode: CompletionMode) -> CompletionList: + def get_completion(self, xml_document: XmlDocument, params: CompletionParams, mode: CompletionMode) -> CompletionList: """Gets completion items depending on the current document context.""" - context = self.xml_context_service.get_xml_context(document, params.position) + context = self.xml_context_service.get_xml_context(xml_document, params.position) return self.completion_service.get_completion_at_context(context, params.context, mode) - def get_auto_close_tag(self, document: Document, params: TextDocumentPositionParams) -> Optional[AutoCloseTagResult]: + def get_auto_close_tag( + self, xml_document: XmlDocument, params: TextDocumentPositionParams + ) -> Optional[AutoCloseTagResult]: """Gets the closing result for the currently opened tag in context.""" - trigger_character = document.lines[params.position.line][params.position.character - 1] + trigger_character = xml_document.document.lines[params.position.line][params.position.character - 1] position_before_trigger = Position(params.position.line, params.position.character - 1) - context = self.xml_context_service.get_xml_context(document, position_before_trigger) + context = self.xml_context_service.get_xml_context(xml_document, position_before_trigger) return self.completion_service.get_auto_close_tag(context, trigger_character) def generate_test(self, document: Document) -> Optional[GeneratedTestResult]: diff --git a/server/galaxyls/services/xml/document.py b/server/galaxyls/services/xml/document.py index 29317d0..6474296 100644 --- a/server/galaxyls/services/xml/document.py +++ b/server/galaxyls/services/xml/document.py @@ -1,6 +1,7 @@ from typing import Dict, Optional -from anytree.search import findall +from anytree.search import findall +from lxml import etree from pygls.types import Position, Range from pygls.workspace import Document @@ -66,6 +67,16 @@ def document_type(self) -> DocumentType: return self.supported_document_types.get(self.root.name, DocumentType.UNKNOWN) return DocumentType.UNKNOWN + @property + def is_unknown(self) -> bool: + """Indicates if the document is of unknown type.""" + return self.document_type == DocumentType.UNKNOWN + + @property + def is_macros_file(self) -> bool: + """Indicates if the document is a macro definition file.""" + return self.document_type == DocumentType.MACROS + def get_node_at(self, offset: int) -> Optional[XmlSyntaxNode]: """Gets the syntax node a the given offset.""" return self.root.find_node_at(offset) @@ -107,3 +118,17 @@ def get_position_after(self, element: XmlElement) -> Position: if element.is_self_closed: return convert_document_offset_to_position(self.document, element.end) return convert_document_offset_to_position(self.document, element.end_tag_close_offset) + + @staticmethod + def has_valid_root(document: Document) -> bool: + """Checks if the document's root element matches one of the supported types.""" + try: + xml = etree.parse(str(document.path)) + root = xml.getroot() + if root and root.tag: + root_tag = root.tag.upper() + supported = [e.name for e in DocumentType if e != DocumentType.UNKNOWN] + return root_tag in supported + return False + except BaseException: + return False diff --git a/server/galaxyls/services/xml/nodes.py b/server/galaxyls/services/xml/nodes.py index b967e61..ee0ada1 100644 --- a/server/galaxyls/services/xml/nodes.py +++ b/server/galaxyls/services/xml/nodes.py @@ -2,7 +2,6 @@ from typing import Dict, List, Optional, Tuple, cast from anytree import NodeMixin -from anytree.search import findall from .constants import UNDEFINED_OFFSET from .types import NodeType diff --git a/server/galaxyls/services/xsd/service.py b/server/galaxyls/services/xsd/service.py index d1d33d9..0446848 100644 --- a/server/galaxyls/services/xsd/service.py +++ b/server/galaxyls/services/xsd/service.py @@ -3,15 +3,15 @@ """ from typing import List -from lxml import etree -from pygls.workspace import Document +from lxml import etree from pygls.types import Diagnostic, MarkupContent, MarkupKind -from .constants import TOOL_XSD_FILE, MSG_NO_DOCUMENTATION_AVAILABLE +from ..context import XmlContext +from ..xml.document import XmlDocument +from .constants import MSG_NO_DOCUMENTATION_AVAILABLE, TOOL_XSD_FILE from .parser import GalaxyToolXsdParser from .validation import GalaxyToolValidationService -from ..context import XmlContext NO_DOC_MARKUP = MarkupContent(MarkupKind.Markdown, MSG_NO_DOCUMENTATION_AVAILABLE) @@ -31,11 +31,11 @@ def __init__(self, server_name: str) -> None: self.xsd_parser = GalaxyToolXsdParser(self.xsd_doc.getroot()) self.validator = GalaxyToolValidationService(server_name, self.xsd_schema) - def validate_document(self, document: Document) -> List[Diagnostic]: + def validate_document(self, xml_document: XmlDocument) -> List[Diagnostic]: """Validates the Galaxy tool xml using the XSD schema and returns a list of diagnotics if there are any problems. """ - return self.validator.validate_document(document) + return self.validator.validate_document(xml_document) def get_documentation_for(self, context: XmlContext) -> MarkupContent: """Gets the documentation annotated in the XSD about the diff --git a/server/galaxyls/services/xsd/validation.py b/server/galaxyls/services/xsd/validation.py index 7c967c5..9efd8a9 100644 --- a/server/galaxyls/services/xsd/validation.py +++ b/server/galaxyls/services/xsd/validation.py @@ -2,17 +2,13 @@ """ from typing import List, Optional -from lxml import etree -from galaxy.util import xml_macros +from galaxy.util import xml_macros +from lxml import etree +from pygls.types import Diagnostic, Position, Range from pygls.workspace import Document -from pygls.types import ( - Diagnostic, - Position, - Range, -) -MACROS_TAG = "" +from ..xml.document import XmlDocument class GalaxyToolValidationService: @@ -23,25 +19,25 @@ def __init__(self, server_name: str, xsd_schema: etree.XMLSchema): self.server_name = server_name self.xsd_schema = xsd_schema - def validate_document(self, document: Document) -> List[Diagnostic]: + def validate_document(self, xml_document: XmlDocument) -> List[Diagnostic]: """Validates the XML document and returns a list of diagnotics if there are any problems. Args: - document (Document): The XML document. Can be a tool wrapper or macro + xml_document (XmlDocument): The XML document. Can be a tool wrapper or macro definition file. Returns: List[Diagnostic]: The list of issues found in the document. """ try: - if self._is_macro_definition_file(document): - return self._check_syntax(document) + if xml_document.is_macros_file: + return self._check_syntax(xml_document.document) - xml_tree = etree.fromstring(document.source) + xml_tree = etree.fromstring(xml_document.document.source) return self._validate_tree(xml_tree) except ExpandMacrosFoundException as e: - result = self._validate_tree_with_macros(document, e.xml_tree) + result = self._validate_tree_with_macros(xml_document.document, e.xml_tree) return result except etree.XMLSyntaxError as e: return self._build_diagnostics_from_syntax_error(e) @@ -64,21 +60,10 @@ def _get_macros_range(self, document: Document, xml_tree: etree.ElementTree) -> filename = import_element.text start = document.lines[line_number].find(filename) end = start + len(filename) - return Range(Position(line_number, start), Position(line_number, end),) + return Range(Position(line_number, start), Position(line_number, end)) except BaseException: return None - def _is_macro_definition_file(self, document: Document) -> bool: - """Determines if a XML document is a macro definition file. - - Args: - document (Document): The XML document. - - Returns: - bool: True if it is a macro definition file or False otherwise. - """ - return document.source.lstrip().startswith(MACROS_TAG) - def _check_syntax(self, document: Document) -> List[Diagnostic]: """Check if the XML document contains any syntax error and returns it in a list. @@ -114,13 +99,13 @@ def _validate_tree_with_macros(self, document: Document, xml_tree: etree.Element return [] except etree.DocumentInvalid as e: diagnostics = [ - Diagnostic(error_range, f"Validation error on macro: {error.message}", source=self.server_name,) + Diagnostic(error_range, f"Validation error on macro: {error.message}", source=self.server_name) for error in e.error_log.filter_from_errors() ] return diagnostics except etree.XMLSyntaxError as e: - result = Diagnostic(error_range, f"Syntax error on macro: {e.msg}", source=self.server_name,) + result = Diagnostic(error_range, f"Syntax error on macro: {e.msg}", source=self.server_name) return [result] def _validate_tree(self, xml_tree: etree.ElementTree) -> List[Diagnostic]: @@ -178,14 +163,14 @@ def _build_diagnostics( raise ExpandMacrosFoundException(xml_tree) result = Diagnostic( - Range(Position(error.line - 1, error.column), Position(error.line - 1, error.column),), + Range(Position(error.line - 1, error.column), Position(error.line - 1, error.column)), error.message, source=self.server_name, ) diagnostics.append(result) return diagnostics - def _build_diagnostics_from_syntax_error(self, error: etree.XMLSyntaxError) -> Diagnostic: + def _build_diagnostics_from_syntax_error(self, error: etree.XMLSyntaxError) -> List[Diagnostic]: """Builds a Diagnostic element from a XMLSyntaxError. Args: @@ -195,7 +180,7 @@ def _build_diagnostics_from_syntax_error(self, error: etree.XMLSyntaxError) -> D Diagnostic: The converted Diagnostic item. """ result = Diagnostic( - Range(Position(error.lineno - 1, error.position[0] - 1), Position(error.lineno - 1, error.position[1] - 1),), + Range(Position(error.lineno - 1, error.position[0] - 1), Position(error.lineno - 1, error.position[1] - 1)), error.msg, source=self.server_name, ) diff --git a/server/galaxyls/tests/unit/test_context.py b/server/galaxyls/tests/unit/test_context.py index 51d699a..5d8f63b 100644 --- a/server/galaxyls/tests/unit/test_context.py +++ b/server/galaxyls/tests/unit/test_context.py @@ -78,7 +78,7 @@ def test_get_xml_context_returns_empty_document_context(self, mocker: MockerFixt xsd_tree_mock = mocker.Mock() service = XmlContextService(xsd_tree_mock) - context = service.get_xml_context(TestUtils.to_document(empty_xml_content), position) + context = service.get_xml_context(TestUtils.from_source_to_xml_document(empty_xml_content), position) assert context.is_empty @@ -128,12 +128,12 @@ def test_get_xml_context_returns_context_with_expected_node( ) -> None: service = XmlContextService(fake_xsd_tree) position, source = TestUtils.extract_mark_from_source("^", source_with_mark) - document = TestUtils.to_document(source) + xml_document = TestUtils.from_source_to_xml_document(source) print(fake_xsd_tree.render()) print(f"Test context at position [line={position.line}, char={position.character}]") - print(f"Document:\n{document.source}") + print(f"Document:\n{xml_document.document.source}") - context = service.get_xml_context(document, position) + context = service.get_xml_context(xml_document, position) assert context assert context.token diff --git a/server/galaxyls/tests/unit/test_validation.py b/server/galaxyls/tests/unit/test_validation.py index 7e58f74..b406be8 100644 --- a/server/galaxyls/tests/unit/test_validation.py +++ b/server/galaxyls/tests/unit/test_validation.py @@ -1,16 +1,16 @@ import pytest - from lxml import etree + from ...services.xsd.constants import TOOL_XSD_FILE from ...services.xsd.validation import GalaxyToolValidationService from .sample_data import ( - TEST_TOOL_01_DOCUMENT, - TEST_MACRO_01_DOCUMENT, TEST_INVALID_TOOL_01_DOCUMENT, - TEST_SYNTAX_ERROR_TOOL_01_DOCUMENT, + TEST_MACRO_01_DOCUMENT, TEST_SYNTAX_ERROR_MACRO_01_DOCUMENT, + TEST_SYNTAX_ERROR_TOOL_01_DOCUMENT, + TEST_TOOL_01_DOCUMENT, ) - +from .utils import TestUtils TEST_SERVER_NAME = "Test Server" @@ -25,35 +25,40 @@ def xsd_schema() -> etree.XMLSchema: class TestGalaxyToolValidationServiceClass: def test_validate_document_returns_empty_diagnostics_when_valid(self, xsd_schema: etree.XMLSchema) -> None: service = GalaxyToolValidationService(TEST_SERVER_NAME, xsd_schema) + xml_document = TestUtils.from_document_to_xml_document(TEST_TOOL_01_DOCUMENT) - actual = service.validate_document(TEST_TOOL_01_DOCUMENT) + actual = service.validate_document(xml_document) assert actual == [] def test_validate_macro_file_returns_empty_diagnostics_when_valid(self, xsd_schema: etree.XMLSchema) -> None: service = GalaxyToolValidationService(TEST_SERVER_NAME, xsd_schema) + xml_document = TestUtils.from_document_to_xml_document(TEST_MACRO_01_DOCUMENT) - actual = service.validate_document(TEST_MACRO_01_DOCUMENT) + actual = service.validate_document(xml_document) assert actual == [] def test_validate_document_returns_diagnostics_when_invalid(self, xsd_schema: etree.XMLSchema) -> None: service = GalaxyToolValidationService(TEST_SERVER_NAME, xsd_schema) + xml_document = TestUtils.from_document_to_xml_document(TEST_INVALID_TOOL_01_DOCUMENT) - actual = service.validate_document(TEST_INVALID_TOOL_01_DOCUMENT) + actual = service.validate_document(xml_document) assert len(actual) > 0 def test_validate_document_returns_diagnostics_when_syntax_error(self, xsd_schema: etree.XMLSchema) -> None: service = GalaxyToolValidationService(TEST_SERVER_NAME, xsd_schema) + xml_document = TestUtils.from_document_to_xml_document(TEST_SYNTAX_ERROR_TOOL_01_DOCUMENT) - actual = service.validate_document(TEST_SYNTAX_ERROR_TOOL_01_DOCUMENT) + actual = service.validate_document(xml_document) assert len(actual) == 1 def test_validate_macro_file_returns_diagnostics_when_syntax_error(self, xsd_schema: etree.XMLSchema) -> None: service = GalaxyToolValidationService(TEST_SERVER_NAME, xsd_schema) + xml_document = TestUtils.from_document_to_xml_document(TEST_SYNTAX_ERROR_MACRO_01_DOCUMENT) - actual = service.validate_document(TEST_SYNTAX_ERROR_MACRO_01_DOCUMENT) + actual = service.validate_document(xml_document) assert len(actual) == 1 diff --git a/server/galaxyls/tests/unit/utils.py b/server/galaxyls/tests/unit/utils.py index c0b7751..0e20ef2 100644 --- a/server/galaxyls/tests/unit/utils.py +++ b/server/galaxyls/tests/unit/utils.py @@ -1,9 +1,12 @@ -from typing import Tuple from pathlib import Path +from typing import Tuple + from pygls.types import Position from pygls.workspace import Document from ...services.xml.constants import NEW_LINE +from ...services.xml.document import XmlDocument +from ...services.xml.parser import XmlDocumentParser class TestUtils: @@ -21,6 +24,34 @@ def to_document(source: str, uri: str = "file://fake_doc.xml", version: int = 0) """ return Document(uri, source, version) + @staticmethod + def from_source_to_xml_document(source: str, uri: str = "file://fake_doc.xml", version: int = 0) -> XmlDocument: + """Converts the given string into a parsed XML document. + + Args: + - source (str): The input string to be converted to XmlDocument. + - uri (str, optional): The uri of the document. Defaults to "file://fake_doc.xml". + - version (int, optional): The version of the document. Defaults to 0. + + Returns: + XmlDocument: The resulting XML document. + """ + document = Document(uri, source, version) + return TestUtils.from_document_to_xml_document(document) + + @staticmethod + def from_document_to_xml_document(document: Document) -> XmlDocument: + """Converts the given document into a parsed XML document. + + Args: + - document (Document): The input string to be converted to XmlDocument. + + Returns: + XmlDocument: The resulting XML document. + """ + xml_document = XmlDocumentParser().parse(document) + return xml_document + @staticmethod def extract_mark_from_source(mark: str, source_with_mark: str) -> Tuple[Position, str]: """Gets a tuple with the position of the mark inside the text and the source text without the mark. diff --git a/server/galaxyls/tests/unit/xml/test_parser.py b/server/galaxyls/tests/unit/xml/test_parser.py index 0a72c41..4e4de87 100644 --- a/server/galaxyls/tests/unit/xml/test_parser.py +++ b/server/galaxyls/tests/unit/xml/test_parser.py @@ -214,3 +214,58 @@ def test_get_node_at_returns_expected_type(self, document: Document, position: P actual = xml_document.get_node_at(offset) assert actual.node_type == expected + + @pytest.mark.parametrize( + "source, expected", + [ + ("", True), + (" ", True), + ("\n", True), + (" \n ", True), + (" \n text", True), + ('', True), + ("<", True), + ("", False), + ("", False), + ('', False), + ('', False), + ], + ) + def test_parse_document_returns_expected_is_unknown(self, source: str, expected: bool) -> None: + parser = XmlDocumentParser() + document = TestUtils.to_document(source) + xml_document = parser.parse(document) + + assert xml_document.is_unknown == expected + + @pytest.mark.parametrize( + "source, expected", + [ + ("", False), + (" ", False), + ("\n", False), + (" \n ", False), + (" \n text", False), + ('', False), + ("<", False), + ("", False), + ("", True), + ('', False), + ('', True), + ], + ) + def test_parse_document_returns_expected_is_macros_file(self, source: str, expected: bool) -> None: + parser = XmlDocumentParser() + document = TestUtils.to_document(source) + xml_document = parser.parse(document) + + assert xml_document.is_macros_file == expected diff --git a/server/requirements-dev.txt b/server/requirements-dev.txt index 5050065..a52f73a 100644 --- a/server/requirements-dev.txt +++ b/server/requirements-dev.txt @@ -8,3 +8,4 @@ pytest-mock flake8 flake8-bugbear black +lxml-stubs \ No newline at end of file