Skip to content

Commit 57dfcac

Browse files
authored
feat!: adopt Python 3.13 as minimum supported version (#61)
* feat!: adopt Python 3.13 as minimum supported version - 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) * chore: loosen .python-version to 3.13 (latest 3.13.x)
1 parent 4b745a3 commit 57dfcac

5 files changed

Lines changed: 54 additions & 17 deletions

File tree

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
This provides decorators and collecting of decorated code, formatting it and writing to yaml file.
1313

14+
## Requirements
15+
16+
- Python >= 3.13
17+
1418
## Installation
1519

1620
The package name is `reqstool-python-decorators`.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dynamic = ["version"]
1313
authors = [{ name = "reqstool" }]
1414
description = "Reqstool Python Decorators"
1515
readme = "README.md"
16-
requires-python = ">=3.10"
16+
requires-python = ">=3.13"
1717
classifiers = [
1818
"License :: OSI Approved :: MIT License",
1919
"Programming Language :: Python :: 3",
@@ -48,7 +48,7 @@ dependencies = [
4848

4949
[tool.black]
5050
line-length = 120
51-
target-version = ['py310']
51+
target-version = ['py313']
5252

5353
[tool.flake8]
5454
ignore = ["W503"]
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
# Copyright © LFV
22

3+
from collections.abc import Callable
34

4-
def Requirements(*requirements):
5-
def decorator(func):
6-
func.requirements = requirements
5+
6+
def Requirements[T: Callable](*requirements: str) -> Callable[[T], T]:
7+
def decorator(func: T) -> T:
8+
func.requirements = requirements # type: ignore[attr-defined]
79
return func
810

911
return decorator
1012

1113

12-
def SVCs(*svc_ids):
13-
def decorator(func):
14-
func.svc_ids = svc_ids
14+
def SVCs[T: Callable](*svc_ids: str) -> Callable[[T], T]:
15+
def decorator(func: T) -> T:
16+
func.svc_ids = svc_ids # type: ignore[attr-defined]
1517
return func
1618

1719
return decorator

src/reqstool_python_decorators/processors/decorator_processor.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from enum import Enum, unique
44
import os
5+
from typing import ReadOnly, TypedDict
56
from ruamel.yaml import YAML
67
import ast
78

@@ -19,6 +20,35 @@ def get_from_to(self):
1920
return f"from: {self.from_value}, to: {self.to_value}"
2021

2122

23+
class DecoratorInfo(TypedDict):
24+
name: ReadOnly[str]
25+
args: ReadOnly[list[str]]
26+
27+
28+
class ElementResult(TypedDict):
29+
fullyQualifiedName: ReadOnly[str]
30+
elementKind: ReadOnly[str]
31+
name: ReadOnly[str]
32+
decorators: ReadOnly[list[DecoratorInfo]]
33+
34+
35+
class FormattedEntry(TypedDict):
36+
elementKind: str
37+
fullyQualifiedName: str
38+
39+
40+
class RequirementAnnotations(TypedDict):
41+
implementations: dict[str, list[FormattedEntry]]
42+
tests: dict[str, list[FormattedEntry]]
43+
44+
45+
class FormattedData(TypedDict):
46+
requirement_annotations: RequirementAnnotations
47+
48+
49+
type Results = list[ElementResult]
50+
51+
2252
class DecoratorProcessor:
2353
"""
2454
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):
4777
4878
"""
4979
super().__init__(*args, **kwargs)
50-
self.req_svc_results = []
80+
self.req_svc_results: Results = []
5181

5282
def find_python_files(self, directory: str | os.PathLike) -> list[str]:
5383
"""
@@ -86,7 +116,7 @@ def get_functions_and_classes(self, file_path: str | os.PathLike, decorator_name
86116

87117
for node in ast.walk(tree):
88118
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
89-
decorators_info = []
119+
decorators_info: list[DecoratorInfo] = []
90120
for decorator_node in getattr(node, "decorator_list", []):
91121
if isinstance(decorator_node, ast.Call) and isinstance(decorator_node.func, ast.Name):
92122
decorator_name = decorator_node.func.id
@@ -132,25 +162,25 @@ def map_type(self, input_str) -> str:
132162
mapping = {item.from_value: item.to_value for item in DECORATOR_TYPES}
133163
return mapping.get(input_str, input_str)
134164

135-
def format_results(self, results) -> dict:
165+
def format_results(self, results: Results) -> FormattedData:
136166
"""
137167
Format the collected results into a structured data format for YAML.
138168
139169
Parameters:
140170
- `results` (list): List of dictionaries containing information about functions and classes.
141171
142172
Returns:
143-
- `formatted_data` (dict): Formatted data in a structured `yaml_language_server` compatible format.
173+
- `formatted_data` (FormattedData): Formatted data in a structured `yaml_language_server` compatible format.
144174
145175
This function formats a list of decorated data into the structure required by the `yaml_language_server`.
146176
It includes version information, requirement annotations, and relevant element information.
147177
"""
148178

149-
formatted_data = {}
150-
implementations = {}
151-
tests = {}
152-
requirement_annotations = {"implementations": implementations, "tests": tests}
153-
formatted_data["requirement_annotations"] = requirement_annotations
179+
implementations: dict[str, list[FormattedEntry]] = {}
180+
tests: dict[str, list[FormattedEntry]] = {}
181+
formatted_data: FormattedData = {
182+
"requirement_annotations": {"implementations": implementations, "tests": tests}
183+
}
154184

155185
for result in results:
156186
for decorator_info in result["decorators"]:

0 commit comments

Comments
 (0)