From 9fd83bd132a9b73ab870ab39c712a34d9179e811 Mon Sep 17 00:00:00 2001 From: Kieran Ryan Date: Thu, 1 Jan 2026 20:02:35 +0000 Subject: [PATCH] Data model type hints - Type hints on common data models - Standardised internal types to string-based enums - Updated `ParameterMode.DEFAULT` from `None` to `"default"` - Updated `ALLURE_UNIQUE_LABELS` to use `LabelType` values - Introduced missing 'Stage' enum - Corrected `TestResult` to initialise with required UUID --- .../src/allure_commons/lifecycle.py | 3 +- .../src/allure_commons/logger.py | 17 ++- .../src/allure_commons/mapping.py | 24 ++-- .../src/allure_commons/model2.py | 105 ++++++++++-------- .../src/allure_commons/types.py | 17 +-- .../src/listener/allure_listener.py | 2 +- allure-robotframework/src/listener/utils.py | 11 +- 7 files changed, 102 insertions(+), 77 deletions(-) diff --git a/allure-python-commons/src/allure_commons/lifecycle.py b/allure-python-commons/src/allure_commons/lifecycle.py index 2e730e2e..41e5b4fa 100644 --- a/allure-python-commons/src/allure_commons/lifecycle.py +++ b/allure-python-commons/src/allure_commons/lifecycle.py @@ -35,8 +35,7 @@ def _last_item_uuid(self, item_type=None): @contextmanager def schedule_test_case(self, uuid=None): - test_result = TestResult() - test_result.uuid = uuid or uuid4() + test_result = TestResult(uuid=uuid or uuid4()) self._items[test_result.uuid] = test_result yield test_result diff --git a/allure-python-commons/src/allure_commons/logger.py b/allure-python-commons/src/allure_commons/logger.py index 55f956f2..c410f58b 100644 --- a/allure-python-commons/src/allure_commons/logger.py +++ b/allure-python-commons/src/allure_commons/logger.py @@ -4,12 +4,21 @@ import json import uuid import shutil -from attr import asdict +from typing import Any +from enum import Enum +from attr import asdict, Attribute from allure_commons import hookimpl INDENT = 4 +def _enum_value_serializer(inst: Any, field: Attribute, value: Any) -> Any: + """Convert enum values to their string representation for serialization.""" + if isinstance(value, Enum): + return value.value + return value + + class AllureFileLogger: def __init__(self, report_dir, clean=False): @@ -21,7 +30,7 @@ def __init__(self, report_dir, clean=False): def _report_item(self, item): indent = INDENT if os.environ.get("ALLURE_INDENT_OUTPUT") else None filename = item.file_pattern.format(prefix=uuid.uuid4()) - data = asdict(item, filter=lambda _, v: v or v is False) + data = asdict(item, filter=lambda _, v: v or v is False, value_serializer=_enum_value_serializer) with io.open(self._report_dir / filename, 'w', encoding='utf8') as json_file: json.dump(data, json_file, indent=indent, ensure_ascii=False) @@ -57,12 +66,12 @@ def __init__(self): @hookimpl def report_result(self, result): - data = asdict(result, filter=lambda _, v: v or v is False) + data = asdict(result, filter=lambda _, v: v or v is False, value_serializer=_enum_value_serializer) self.test_cases.append(data) @hookimpl def report_container(self, container): - data = asdict(container, filter=lambda _, v: v or v is False) + data = asdict(container, filter=lambda _, v: v or v is False, value_serializer=_enum_value_serializer) self.test_containers.append(data) @hookimpl diff --git a/allure-python-commons/src/allure_commons/mapping.py b/allure-python-commons/src/allure_commons/mapping.py index 737d3390..30a66c0c 100644 --- a/allure-python-commons/src/allure_commons/mapping.py +++ b/allure-python-commons/src/allure_commons/mapping.py @@ -26,28 +26,28 @@ def __is(kind, t): def parse_tag(tag, issue_pattern=None, link_pattern=None): """ >>> parse_tag("blocker") - Label(name='severity', value='blocker') + Label(name=, value='blocker') >>> parse_tag("allure.issue:http://example.com/BUG-42") - Link(type='issue', url='http://example.com/BUG-42', name='http://example.com/BUG-42') + Link(type=, url='http://example.com/BUG-42', name='http://example.com/BUG-42') >>> parse_tag("allure.link.home:http://qameta.io") - Link(type='link', url='http://qameta.io', name='home') + Link(type=, url='http://qameta.io', name='home') >>> parse_tag("allure.suite:mapping") - Label(name='suite', value='mapping') + Label(name=, value='mapping') >>> parse_tag("allure.suite:mapping") - Label(name='suite', value='mapping') + Label(name=, value='mapping') >>> parse_tag("allure.label.owner:me") Label(name='owner', value='me') >>> parse_tag("foo.label:1") - Label(name='tag', value='foo.label:1') + Label(name=, value='foo.label:1') >>> parse_tag("allure.foo:1") - Label(name='tag', value='allure.foo:1') + Label(name=, value='allure.foo:1') """ sep = allure_tag_sep(tag) schema, value = islice(chain(tag.split(sep, 1), [None]), 2) @@ -63,10 +63,10 @@ def parse_tag(tag, issue_pattern=None, link_pattern=None): value = issue_pattern.format(value) if link_pattern and kind == "link" and not value.startswith("http"): value = link_pattern.format(value) - return Link(type=kind, name=name or value, url=value) + return Link(type=LinkType(kind), name=name or value, url=value) if __is(kind, LabelType): - return Label(name=kind, value=value) + return Label(name=LabelType(kind), value=value) if kind == "id": return Label(name=LabelType.ID, value=value) @@ -82,7 +82,7 @@ def labels_set(labels): >>> labels_set([Label(name=LabelType.SEVERITY, value=Severity.NORMAL), ... Label(name=LabelType.SEVERITY, value=Severity.BLOCKER) ... ]) - [Label(name='severity', value=)] + [Label(name=, value=)] >>> labels_set([Label(name=LabelType.SEVERITY, value=Severity.NORMAL), ... Label(name='severity', value='minor') @@ -92,12 +92,12 @@ def labels_set(labels): >>> labels_set([Label(name=LabelType.EPIC, value="Epic"), ... Label(name=LabelType.EPIC, value="Epic") ... ]) - [Label(name='epic', value='Epic')] + [Label(name=, value='Epic')] >>> labels_set([Label(name=LabelType.EPIC, value="Epic1"), ... Label(name=LabelType.EPIC, value="Epic2") ... ]) - [Label(name='epic', value='Epic1'), Label(name='epic', value='Epic2')] + [Label(name=, value='Epic1'), Label(name=, value='Epic2')] """ class Wl: def __init__(self, label): diff --git a/allure-python-commons/src/allure_commons/model2.py b/allure-python-commons/src/allure_commons/model2.py index d8591598..d65d6d5e 100644 --- a/allure-python-commons/src/allure_commons/model2.py +++ b/allure-python-commons/src/allure_commons/model2.py @@ -1,6 +1,11 @@ +from __future__ import annotations + +from enum import Enum + from attr import attrs, attrib from attr import Factory +from allure_commons.types import AttachmentType, LabelType, LinkType, ParameterMode TEST_GROUP_PATTERN = "{prefix}-container.json" TEST_CASE_PATTERN = "{prefix}-result.json" @@ -12,49 +17,49 @@ class TestResultContainer: file_pattern = TEST_GROUP_PATTERN - uuid = attrib(default=None) - name = attrib(default=None) - children = attrib(default=Factory(list)) - description = attrib(default=None) - descriptionHtml = attrib(default=None) - befores = attrib(default=Factory(list)) - afters = attrib(default=Factory(list)) - links = attrib(default=Factory(list)) - start = attrib(default=None) - stop = attrib(default=None) + uuid: str = attrib(default=None) + name: str | None = attrib(default=None) + children: list[str] = attrib(default=Factory(list)) + description: str | None = attrib(default=None) + descriptionHtml: str | None = attrib(default=None) + befores: list[TestBeforeResult] = attrib(default=Factory(list)) + afters: list[TestAfterResult] = attrib(default=Factory(list)) + links: list[Link] = attrib(default=Factory(list)) + start: int | None = attrib(default=None) + stop: int | None = attrib(default=None) @attrs class ExecutableItem: - name = attrib(default=None) - status = attrib(default=None) - statusDetails = attrib(default=None) - stage = attrib(default=None) - description = attrib(default=None) - descriptionHtml = attrib(default=None) - steps = attrib(default=Factory(list)) - attachments = attrib(default=Factory(list)) - parameters = attrib(default=Factory(list)) - start = attrib(default=None) - stop = attrib(default=None) + name: str | None = attrib(default=None) + status: Status | None = attrib(default=None) + statusDetails: StatusDetails | None = attrib(default=None) + stage: Stage | None = attrib(default=None) + description: str | None = attrib(default=None) + descriptionHtml: str | None = attrib(default=None) + steps: list[TestStepResult] = attrib(default=Factory(list)) + attachments: list[Attachment] = attrib(default=Factory(list)) + parameters: list[Parameter] = attrib(default=Factory(list)) + start: int | None = attrib(default=None) + stop: int | None = attrib(default=None) @attrs class TestResult(ExecutableItem): file_pattern = TEST_CASE_PATTERN - uuid = attrib(default=None) - historyId = attrib(default=None) - testCaseId = attrib(default=None) - fullName = attrib(default=None) - labels = attrib(default=Factory(list)) - links = attrib(default=Factory(list)) - titlePath = attrib(default=Factory(list)) + uuid: str = attrib(default=None) + historyId: str | None = attrib(default=None) + testCaseId: str | None = attrib(default=None) + fullName: str | None = attrib(default=None) + labels: list[Label] = attrib(default=Factory(list)) + links: list[Link] = attrib(default=Factory(list)) + titlePath: list[str] = attrib(default=Factory(list)) @attrs class TestStepResult(ExecutableItem): - id = attrib(default=None) # noqa: A003 + id: str | None = attrib(default=None) # noqa: A003 @attrs @@ -69,43 +74,51 @@ class TestAfterResult(ExecutableItem): @attrs class Parameter: - name = attrib(default=None) - value = attrib(default=None) - excluded = attrib(default=None) - mode = attrib(default=None) + name: str = attrib(default=None) + value: str = attrib(default=None) + excluded: bool | None = attrib(default=None) + mode: ParameterMode | None = attrib(default=None) @attrs class Label: - name = attrib(default=None) - value = attrib(default=None) + name: LabelType | str = attrib(default=None) + value: str = attrib(default=None) @attrs class Link: - type = attrib(default=None) # noqa: A003 - url = attrib(default=None) - name = attrib(default=None) + type: LinkType | str | None = attrib(default=None) # noqa: A003 + url: str = attrib(default=None) + name: str | None = attrib(default=None) @attrs class StatusDetails: - known = attrib(default=None) - flaky = attrib(default=None) - message = attrib(default=None) - trace = attrib(default=None) + known: bool | None = attrib(default=None) + flaky: bool | None = attrib(default=None) + message: str | None = attrib(default=None) + trace: str | None = attrib(default=None) @attrs class Attachment: - name = attrib(default=None) - source = attrib(default=None) - type = attrib(default=None) # noqa: A003 + name: str = attrib(default=None) + source: str = attrib(default=None) + type: AttachmentType | str | None = attrib(default=None) # noqa: A003 -class Status: +class Status(str, Enum): FAILED = 'failed' BROKEN = 'broken' PASSED = 'passed' SKIPPED = 'skipped' UNKNOWN = 'unknown' + + +class Stage(str, Enum): + SCHEDULED = "scheduled" + RUNNING = "running" + FINISHED = "finished" + PENDING = "pending" + INTERRUPTED = "interrupted" diff --git a/allure-python-commons/src/allure_commons/types.py b/allure-python-commons/src/allure_commons/types.py index 1db7fd51..541cae2f 100644 --- a/allure-python-commons/src/allure_commons/types.py +++ b/allure-python-commons/src/allure_commons/types.py @@ -1,6 +1,6 @@ -from enum import Enum +from __future__ import annotations -ALLURE_UNIQUE_LABELS = ['severity', 'thread', 'host'] +from enum import Enum class Severity(str, Enum): @@ -11,13 +11,13 @@ class Severity(str, Enum): TRIVIAL = 'trivial' -class LinkType: +class LinkType(str, Enum): LINK = 'link' ISSUE = 'issue' TEST_CASE = 'tms' -class LabelType(str): +class LabelType(str, Enum): EPIC = 'epic' FEATURE = 'feature' STORY = 'story' @@ -34,9 +34,12 @@ class LabelType(str): MANUAL = 'ALLURE_MANUAL' +ALLURE_UNIQUE_LABELS = [LabelType.SEVERITY, LabelType.THREAD, LabelType.HOST] + + class AttachmentType(Enum): - def __init__(self, mime_type, extension): + def __init__(self, mime_type: str, extension: str) -> None: self.mime_type = mime_type self.extension = extension @@ -66,7 +69,7 @@ def __init__(self, mime_type, extension): PDF = ("application/pdf", "pdf") -class ParameterMode(Enum): +class ParameterMode(str, Enum): HIDDEN = 'hidden' MASKED = 'masked' - DEFAULT = None + DEFAULT = 'default' diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 29b4e0e3..55ec4a6b 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -163,7 +163,7 @@ def stop_test(self, _, attributes, messages): test_result.labels.append(Label(name=LabelType.SEVERITY, value=Severity.CRITICAL)) for link_type in (LinkType.ISSUE, LinkType.TEST_CASE, LinkType.LINK): - test_result.links.extend(allure_links(attributes, link_type)) + test_result.links.extend(allure_links(attributes, link_type.value)) self._current_tb, self._current_msg = None, None diff --git a/allure-robotframework/src/listener/utils.py b/allure-robotframework/src/listener/utils.py index 8b308d81..03778394 100644 --- a/allure-robotframework/src/listener/utils.py +++ b/allure-robotframework/src/listener/utils.py @@ -26,13 +26,14 @@ def get_allure_parameters(parameters): def get_allure_suites(longname): """ >>> get_allure_suites('Suite1.Test') - [Label(name='suite', value='Suite1')] + [Label(name=, value='Suite1')] >>> get_allure_suites('Suite1.Suite2.Test') # doctest: +NORMALIZE_WHITESPACE - [Label(name='suite', value='Suite1'), Label(name='subSuite', value='Suite2')] + [Label(name=, value='Suite1'), + Label(name=, value='Suite2')] >>> get_allure_suites('Suite1.Suite2.Suite3.Test') # doctest: +NORMALIZE_WHITESPACE - [Label(name='parentSuite', value='Suite1'), - Label(name='suite', value='Suite2'), - Label(name='subSuite', value='Suite3')] + [Label(name=, value='Suite1'), + Label(name=, value='Suite2'), + Label(name=, value='Suite3')] """ labels = [] suites = longname.split('.')