From 29d67ea73cce6e4a183b6c99aceeb555a8c38503 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sat, 28 Feb 2026 21:29:24 +0100 Subject: [PATCH 1/2] feat!: adopt Python 3.13 as minimum supported version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump requires-python to >=3.13 (drops 3.10–3.12 support) - Add .python-version pinned to 3.13.11 via pyenv - Update black target-version to py313 - Add README Requirements section noting Python >= 3.13 - decorators.py: use generic function syntax [T: Callable] (PEP 695) so type checkers preserve the decorated callable's type - decorator_processor.py: add DecoratorInfo, ElementResult TypedDicts with ReadOnly fields (PEP 705, typing.ReadOnly new in 3.13); add FormattedEntry/RequirementAnnotations/FormattedData TypedDicts for format_results return type; add `type Results` alias (PEP 695) --- .python-version | 1 + README.md | 4 ++ pyproject.toml | 4 +- .../decorators/decorators.py | 14 +++--- .../processors/decorator_processor.py | 48 +++++++++++++++---- 5 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c45fe3 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.11 diff --git a/README.md b/README.md index 309cf19..cfc7b05 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ This provides decorators and collecting of decorated code, formatting it and writing to yaml file. +## Requirements + +- Python >= 3.13 + ## Installation The package name is `reqstool-python-decorators`. diff --git a/pyproject.toml b/pyproject.toml index 83060db..0c1ab4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dynamic = ["version"] authors = [{ name = "reqstool" }] description = "Reqstool Python Decorators" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.13" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", @@ -48,7 +48,7 @@ dependencies = [ [tool.black] line-length = 120 -target-version = ['py310'] +target-version = ['py313'] [tool.flake8] ignore = ["W503"] diff --git a/src/reqstool_python_decorators/decorators/decorators.py b/src/reqstool_python_decorators/decorators/decorators.py index 945aa09..7a9ce0c 100644 --- a/src/reqstool_python_decorators/decorators/decorators.py +++ b/src/reqstool_python_decorators/decorators/decorators.py @@ -1,17 +1,19 @@ # Copyright © LFV +from collections.abc import Callable -def Requirements(*requirements): - def decorator(func): - func.requirements = requirements + +def Requirements[T: Callable](*requirements: str) -> Callable[[T], T]: + def decorator(func: T) -> T: + func.requirements = requirements # type: ignore[attr-defined] return func return decorator -def SVCs(*svc_ids): - def decorator(func): - func.svc_ids = svc_ids +def SVCs[T: Callable](*svc_ids: str) -> Callable[[T], T]: + def decorator(func: T) -> T: + func.svc_ids = svc_ids # type: ignore[attr-defined] return func return decorator diff --git a/src/reqstool_python_decorators/processors/decorator_processor.py b/src/reqstool_python_decorators/processors/decorator_processor.py index 9f81d21..b53c3b5 100644 --- a/src/reqstool_python_decorators/processors/decorator_processor.py +++ b/src/reqstool_python_decorators/processors/decorator_processor.py @@ -2,6 +2,7 @@ from enum import Enum, unique import os +from typing import ReadOnly, TypedDict from ruamel.yaml import YAML import ast @@ -19,6 +20,35 @@ def get_from_to(self): return f"from: {self.from_value}, to: {self.to_value}" +class DecoratorInfo(TypedDict): + name: ReadOnly[str] + args: ReadOnly[list[str]] + + +class ElementResult(TypedDict): + fullyQualifiedName: ReadOnly[str] + elementKind: ReadOnly[str] + name: ReadOnly[str] + decorators: ReadOnly[list[DecoratorInfo]] + + +class FormattedEntry(TypedDict): + elementKind: str + fullyQualifiedName: str + + +class RequirementAnnotations(TypedDict): + implementations: dict[str, list[FormattedEntry]] + tests: dict[str, list[FormattedEntry]] + + +class FormattedData(TypedDict): + requirement_annotations: RequirementAnnotations + + +type Results = list[ElementResult] + + class DecoratorProcessor: """ A class for collecting and processing Requirements and SVCs annotations on functions and classes in a directory. @@ -47,7 +77,7 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) - self.req_svc_results = [] + self.req_svc_results: Results = [] def find_python_files(self, directory: str | os.PathLike) -> list[str]: """ @@ -86,7 +116,7 @@ def get_functions_and_classes(self, file_path: str | os.PathLike, decorator_name for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): - decorators_info = [] + decorators_info: list[DecoratorInfo] = [] for decorator_node in getattr(node, "decorator_list", []): if isinstance(decorator_node, ast.Call) and isinstance(decorator_node.func, ast.Name): decorator_name = decorator_node.func.id @@ -132,7 +162,7 @@ def map_type(self, input_str) -> str: mapping = {item.from_value: item.to_value for item in DECORATOR_TYPES} return mapping.get(input_str, input_str) - def format_results(self, results) -> dict: + def format_results(self, results: Results) -> FormattedData: """ Format the collected results into a structured data format for YAML. @@ -140,17 +170,17 @@ def format_results(self, results) -> dict: - `results` (list): List of dictionaries containing information about functions and classes. Returns: - - `formatted_data` (dict): Formatted data in a structured `yaml_language_server` compatible format. + - `formatted_data` (FormattedData): Formatted data in a structured `yaml_language_server` compatible format. This function formats a list of decorated data into the structure required by the `yaml_language_server`. It includes version information, requirement annotations, and relevant element information. """ - formatted_data = {} - implementations = {} - tests = {} - requirement_annotations = {"implementations": implementations, "tests": tests} - formatted_data["requirement_annotations"] = requirement_annotations + implementations: dict[str, list[FormattedEntry]] = {} + tests: dict[str, list[FormattedEntry]] = {} + formatted_data: FormattedData = { + "requirement_annotations": {"implementations": implementations, "tests": tests} + } for result in results: for decorator_info in result["decorators"]: From 69b3b8283a6f2f5b58b79f4070e639e60c47d653 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 1 Mar 2026 11:12:44 +0100 Subject: [PATCH 2/2] chore: loosen .python-version to 3.13 (latest 3.13.x) --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index 2c45fe3..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13.11 +3.13