From 72f19832d79e45af649909f642c60cb03f0f4102 Mon Sep 17 00:00:00 2001 From: fleonce Date: Mon, 18 May 2026 07:34:48 +0200 Subject: [PATCH 1/5] Start work on an improved experience with dataclasses / attrs classes --- test/test_argparse2.py | 19 +++ with_argparse/configure_argparse.py | 244 +++++++++++++++++++++------- with_argparse/impl.py | 26 +-- with_argparse/py.typed | 0 with_argparse/with_argparse.pyi | 25 +++ 5 files changed, 240 insertions(+), 74 deletions(-) create mode 100644 test/test_argparse2.py create mode 100644 with_argparse/py.typed create mode 100644 with_argparse/with_argparse.pyi diff --git a/test/test_argparse2.py b/test/test_argparse2.py new file mode 100644 index 0000000..5bda402 --- /dev/null +++ b/test/test_argparse2.py @@ -0,0 +1,19 @@ +import dataclasses +import unittest + +from tools import sys_args +from with_argparse import with_dataclass + + +class ArgParseTestCase(unittest.TestCase): + def test_basic(self): + @dataclasses.dataclass + class A: + field1: int + + @with_dataclass + def func(args: A): + return args + + with sys_args(): + func() diff --git a/with_argparse/configure_argparse.py b/with_argparse/configure_argparse.py index 1615369..e75af7b 100644 --- a/with_argparse/configure_argparse.py +++ b/with_argparse/configure_argparse.py @@ -1,10 +1,13 @@ +import argparse import dataclasses import inspect import logging +import sys import typing import warnings from argparse import ArgumentParser -from dataclasses import dataclass, MISSING +from collections import defaultdict, OrderedDict +from dataclasses import dataclass, MISSING, is_dataclass from functools import partial from pathlib import Path from types import NoneType, UnionType @@ -28,6 +31,13 @@ _NO_DEFAULT = None +def _help_called(): + parser = ArgumentParser(add_help=False) + parser.add_argument("-h", "--help", action="store_true", default=False) + parsed = parser.parse_known_args()[0] + return parsed.help == True + + def first(iterable: Iterable[_T], default: Optional[_T] = None) -> _T: try: return next(iter(iterable)) @@ -51,8 +61,6 @@ class _Argument: @dataclass class DataclassConfig: func: Callable - positional_dataclasses: tuple[type[DataclassInstance], ...] - keyword_dataclasses: dict[str, type[DataclassInstance]] class WithArgparse: ignore_rename_sequences: set[str] @@ -67,11 +75,14 @@ class WithArgparse: remaining_args: list[str] func: Callable - dataclass: Optional[DataclassConfig] + func_type: Literal["attrs", "dataclass", "plain"] + strict: bool def __init__( self, - func_or_dataclass: Union[Callable, DataclassConfig], + func: Callable, + func_type: Literal["attrs", "dataclass", "plain"], + strict: bool, aliases: Optional[Mapping[str, Sequence[str]]] = None, ignore_rename: Optional[set[str]] = None, ignore_keys: Optional[set[str]] = None, @@ -99,18 +110,14 @@ def __init__( self.remaining_args = [] self.partial_parse_pass_remaining_args = partial_parse_pass_remaining_args or False self.on_help = on_help + self.add_help = add_help - if isinstance(func_or_dataclass, DataclassConfig): - self.dataclass = func_or_dataclass - self.func = func_or_dataclass.func - else: - self.func = func_or_dataclass - self.dataclass = None + self.func = func + self.func_type = func_type + self.strict = strict + self._help_caught = False - self.argparse = ArgumentParser( - add_help=add_help, - conflict_handler="resolve" if on_help is not None else "error" - ) + self._reset_argparse() if on_help is not None: self.argparse.add_argument("-h", "--help", action="store_true", dest="_help") @@ -130,6 +137,17 @@ def _register_post_parse_type_conversion(self, key: str, func: Callable[[Any], A logger.debug(f"Registering post parse type conversion for {key}: {func.__name__} ({func})") self.post_parse_type_conversions[key].append(func) + def _parser_parse( + self, + parser: argparse.ArgumentParser, + args: Optional[Sequence[str]] = None, + partial_parse: bool = False, + ) -> tuple[argparse.Namespace, list[str]]: + if partial_parse: + return parser.parse_known_args(args) + else: + return parser.parse_args(args), [] + def _argparse_parse(self): if self.partial_parse: namespace, self.remaining_args = self.argparse.parse_known_args() @@ -140,32 +158,108 @@ def _argparse_parse(self): self.on_help(self) return namespace + def _print_usage(self, parser: argparse.ArgumentParser | OrderedDict[str, argparse.ArgumentParser], short: bool = False): + if isinstance(parser, argparse.ArgumentParser): + if short: + parser.print_usage() + else: + parser.print_help() + else: + if len(parser) == 1: + self._print_usage(first(parser.values()), short=short) + return + usage = "" + for field_name in parser.keys(): + formatted_usage_or_help = parser[field_name].format_usage() if short else parser[field_name].format_help() + usage += "--- help for field name %s ---\n %s\n\n" % (field_name, formatted_usage_or_help) + first(parser.values()).exit(2, usage) + def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): - if args: - raise ValueError("Positional argument overrides are not supported, yet") - if self.dataclass is None: - raise ValueError("self.dataclass cannot be None") - - positional_dataclasses = self.dataclass.positional_dataclasses or tuple() - keyword_dataclasses = self.dataclass.keyword_dataclasses or dict() - dataclasses_to_process = [*positional_dataclasses, *keyword_dataclasses.values()] - - for klass in dataclasses_to_process: - field_hints = typing.get_type_hints(klass) - for field in dataclasses.fields(klass): + """ + This function identifies the dataclass function arguments inside this function, + parses these and calls the configured function with the parsed argument dataclasses + + Args: + args: Positional arguments to call the function with + kwargs: Keyword-only arguments to call the function with + + """ + signature = inspect.getfullargspec(self.func) + if signature.varargs: + raise TypeError("Variational positional arguments are not supported at the moment") + if signature.varkw and self.strict: + raise TypeError("Variational keyword arguments are not supported in strict mode") + if self.strict and len(args) > 0: + raise TypeError("In strict mode, providing positional arguments is unsupported") + if self.strict and len(kwargs) > 0: + raise TypeError("In strict mode, providing keyword arguments is unsupported") + + if len(args) > len(signature.args): + raise TypeError(f"Received more positional arguments ({len(args)}) to call {self.func!r} than this function receives: {len(signature.args)}") + + help_called = _help_called() + call_dataclasses = OrderedDict() + call_args = () + call_kw_only_args = dict(kwargs) + call_order = () + + if len(args) > 0: + call_args += tuple(args) + + for i, arg_name in enumerate(signature.args): + if i < len(call_args): + # this arg was provided as a positional argument, no need to get its type signature or + continue + if arg_name in call_kw_only_args: + # this arg was provided as a keyword argument, similar logic applies + continue + if arg_name not in signature.annotations: + raise TypeError( + f"Function {self.func!r} must be strongly typed, " + f"however has no no type annotation for field {arg_name!r}" + ) + arg_type = signature.annotations[arg_name] + if isinstance(arg_type, str): + arg_type = typing.get_type_hints(self.func)[arg_name] + if not is_dataclass(arg_type): + raise TypeError(f"Field {arg_name!r} must be a dataclass instance, but is {arg_type!r}") + call_dataclasses[arg_name] = (arg_type, "positional") + call_order += (arg_name,) + + for arg_name in signature.kwonlyargs: + if arg_name in call_kw_only_args: + continue + + if arg_name not in signature.annotations: + raise TypeError( + f"Function {self.func!r} must be strongly typed, " + f"however has no no type annotation for field {arg_name!r}" + ) + arg_type = signature.annotations[arg_name] + if isinstance(arg_type, str): + arg_type = typing.get_type_hints(self.func)[arg_name] + if not is_dataclass(arg_type): + raise TypeError(f"Field {arg_name!r} must be a dataclass instance, but is {arg_type!r}") + + call_dataclasses[arg_name] = (arg_type, "keyword-only") + call_order += (arg_name,) + + known_parser_args = defaultdict(set) + + call_argparse = OrderedDict() + for arg_name, (arg_type, fn_arg_type) in call_dataclasses.items(): + self.reset() + field_hints = typing.get_type_hints(arg_type) + for field in dataclasses.fields(arg_type): field_required = field.default is MISSING field_default = field.default if not field_required else None field_type = field.type field_help = None if isinstance(field_type, str): - field_type = typing.cast(type, field_hints.get(field.name)) + field_type = field_hints.get(field.name) if field.metadata is not None and "help" in field.metadata: field_help = str(field.metadata["help"]) - # known_types = {type, Literal, GenericAlias, UnionType} - # if type(field_type) not in known_types and typing.get_origin(field_type) not in known_types: - # raise ValueError(f"Cannot determine type of {field.name}, got {field_type} {type(field_type)}") - # raises on typing.Optional[typing.Literal['epsilon', 'v_prediction']] self._setup_argument( field.name, field_type, @@ -174,38 +268,66 @@ def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): field_help, ) - parsed_args = self._argparse_parse() - args_dict = self._apply_name_mapping(parsed_args.__dict__, None) - args_dict = self._apply_post_parse_conversions(args_dict, dict()) - - pos: tuple[Any, ...] = tuple() - keywords: MutableMapping[str, Any] = dict() - for i, klass in enumerate(positional_dataclasses): - klass_args = dict() - for field in dataclasses.fields(klass): - klass_args[field.name] = args_dict[field.name] - - pos = pos + (klass(**klass_args),) - - for name, klass in keyword_dataclasses.items(): - klass_args = dict() - for field in dataclasses.fields(klass): - klass_args[field.name] = args_dict[field.name] + known_parser_args[field.name].add(arg_type.__name__) + call_argparse[arg_name] = self.argparse + + invalid_field_names = [] + for field_name, arg_names in known_parser_args.items(): + if len(arg_names) > 1: + invalid_field_names.append((field_name, arg_names)) + + if len(invalid_field_names) > 0: + msg = "Function arguments of %s define overriding argument values: \n" % (self.func.__name__,) + for (field_name, arg_names) in invalid_field_names: + msg += (" - %s defined in %s\n" % (", ".join(arg_names), "\n")) + raise ValueError(msg) + + if _help_called(): + self._print_usage(call_argparse, False) + sys.exit(2) + + argv = sys.argv[1:] + call_values = {} + for arg_name in call_order: + parser = call_argparse[arg_name] + + try: + namespace, argv = parser.parse_known_args(argv) + except argparse.ArgumentError as err: + if err.message.startswith("the following arguments are required:"): + # todo: the user missed arguments, print our own summary + self._print_usage(call_argparse, short=True) + sys.exit(2) + else: + parser.error(err.message) + args_dict = self._apply_name_mapping(namespace.__dict__, None) + args_dict = self._apply_post_parse_conversions(args_dict, dict()) + arg_type, fn_arg_type = call_dataclasses[arg_name] + arg_value = arg_type(**args_dict) + + if fn_arg_type == "positional": + call_args += (arg_value,) + else: + call_kw_only_args[arg_name] = arg_value - keywords[name] = klass(**klass_args) + if not self.partial_parse and len(argv) > 0: + print("the following arguments are unknown: " + " ".join(argv)) + sys.exit(2) - if self.partial_parse_pass_remaining_args: - keywords["_args"] = self.remaining_args + if self.partial_parse_pass_remaining_args is not None: + raise NotImplementedError(f"Cannot pass the remaining args to field {self.partial_parse_pass_remaining_args}") - return self.func(*pos, **keywords, **kwargs) + return self.func(*call_args, **call_kw_only_args) def call(self, args: Sequence[Any], kwargs: Mapping[str, Any]): - if self.dataclass is not None: - return self._call_dataclass(args, kwargs) - elif self.func is not None: + if self.func_type == "plain": return self._call_func(args, kwargs) + elif self.func_type == "dataclass": + return self._call_dataclass(args, kwargs) + elif self.func_type == "attrs": + raise NotImplementedError("Calling attrs-defined functions is not supported, yet") else: - raise ValueError("self.dataclass and self.func cannot both be None") + raise ValueError(f"Invalid function type {self.func_type!r}") def _call_func(self, args: Sequence[Any], kwargs: Mapping[str, Any]): @@ -351,6 +473,14 @@ def _apply_post_parse_conversions( out[key] = value return out + def reset(self): + self._reset_argparse() + self.argument_mapping.clear() + self.post_parse_type_conversions.clear() + + def _reset_argparse(self): + self.argparse = ArgumentParser(add_help=False, exit_on_error=False) + def _setup_argument( self, arg_name: str, diff --git a/with_argparse/impl.py b/with_argparse/impl.py index 9c495cb..2246851 100644 --- a/with_argparse/impl.py +++ b/with_argparse/impl.py @@ -49,25 +49,16 @@ def set_enabled(enabled: bool): def set_config(key: str, state: bool): config[key] = state - def with_dataclass( - *pos: type[DataclassInstance], + func: Callable = None, + *, allow_glob: Optional[set[str]] = None, partial_parse: Optional[bool] = None, add_help: Optional[bool] = None, on_help: Optional[Callable[[WithArgparse], Any]] = None, partial_parse_pass_remaining_args: Optional[bool] = None, - **kwargs: type[DataclassInstance], + strict: bool = True, ): - if not pos: - pos = tuple() - else: - pass - # raise NotImplementedError( - # f"In the future, I would entertain the idea to get the dataclass instances from the " - # f"method spec instead of specifying it again via positional or keyword arguments" - # ) - def wrapper(fn): @functools.wraps(fn) def inner(*inner_args, **inner_kwargs): @@ -75,11 +66,9 @@ def inner(*inner_args, **inner_kwargs): return fn(*inner_args, **inner_kwargs) parser = WithArgparse( - DataclassConfig( - fn, - pos, - kwargs, - ), + fn, + "dataclass", + strict=strict, allow_glob=allow_glob, partial_parse=partial_parse, partial_parse_pass_remaining_args=partial_parse_pass_remaining_args, @@ -88,6 +77,9 @@ def inner(*inner_args, **inner_kwargs): ) return parser.call(inner_args, inner_kwargs) return inner + + if func is not None: + return wrapper(func) return wrapper diff --git a/with_argparse/py.typed b/with_argparse/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/with_argparse/with_argparse.pyi b/with_argparse/with_argparse.pyi new file mode 100644 index 0000000..34aeba1 --- /dev/null +++ b/with_argparse/with_argparse.pyi @@ -0,0 +1,25 @@ +from typing import Callable, Protocol, Any, TypeVar, Literal, Optional, overload + +from with_argparse.configure_argparse import WithArgparse + +T = TypeVar("T") + +class FunctionWithSpecificReturnType(Protocol[T]): + def __call__(self, *args, **kwargs) -> T: ... + +class StrictArgParsedFunction(Protocol[T]): + def __call__(self) -> T: ... + +@overload +def with_dataclass(func: FunctionWithSpecificReturnType[T], /) -> StrictArgParsedFunction[T]: + ... + +@overload +def with_dataclass( + allow_glob: Optional[set[str]] = None, + partial_parse: Optional[bool] = None, + add_help: Optional[bool] = None, + on_help: Optional[Callable[[WithArgparse], Any]] = None, + partial_parse_pass_remaining_args: Optional[bool] = None, + strict: Literal[True] = True, +) -> StrictArgParsedFunction[T]: ... \ No newline at end of file From 9c59a9f1e7b65549c5786172e81470d06f0f9eff Mon Sep 17 00:00:00 2001 From: fleonce Date: Tue, 21 Jul 2026 22:15:45 +0200 Subject: [PATCH 2/5] To be continued, support for attrs! --- pyproject.toml | 15 +- with_argparse/configure_argparse.py | 311 ++++++++++++++++---- with_argparse/impl.py | 3 +- with_argparse/main.py | 122 ++++++++ with_argparse/{types.py => typing_utils.py} | 0 with_argparse/with_argparse.pyi | 16 +- 6 files changed, 400 insertions(+), 67 deletions(-) create mode 100644 with_argparse/main.py rename with_argparse/{types.py => typing_utils.py} (100%) diff --git a/pyproject.toml b/pyproject.toml index 9203abe..99a2f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,11 @@ version = "1.0.5rc4" license = {text = "Apache 2.0"} readme = "README.md" -dependencies = ["typing-extensions>=4.15.0"] -requires-python = ">= 3.8" +dependencies = [ + "attrs>=26.1.0", + "typing-extensions>=4.15.0", +] +requires-python = ">= 3.12" authors = [ { name = "Moritz Hennen", email = "mail@fleance.de" } @@ -33,3 +36,11 @@ repository = "https://github.com/fleonce/with-argparse/" [tool.setuptools] packages = ["with_argparse"] + +[tool.uv] +package = true + +[dependency-groups] +dev = [ + "mypy>=2.3.0", +] diff --git a/with_argparse/configure_argparse.py b/with_argparse/configure_argparse.py index e75af7b..96a0785 100644 --- a/with_argparse/configure_argparse.py +++ b/with_argparse/configure_argparse.py @@ -15,9 +15,11 @@ Any, Set, List, get_origin, get_args, Union, Literal, Optional, Sequence, TypeVar, Iterable, Callable, MutableMapping, Mapping, ) + +import attrs from typing_extensions import Self -from with_argparse.types import DataclassInstance +from with_argparse.main import _internal_global_state, ParseArgs from with_argparse.utils import flatten, glob_to_paths from with_argparse.setup import config @@ -31,6 +33,11 @@ _NO_DEFAULT = None +class MissingArgument: + __slots__ = () + +MISSING_ARG = MissingArgument() + def _help_called(): parser = ArgumentParser(add_help=False) parser.add_argument("-h", "--help", action="store_true", default=False) @@ -47,7 +54,7 @@ def first(iterable: Iterable[_T], default: Optional[_T] = None) -> _T: raise -@dataclass +@attrs.define class _Argument: name: str type: type | Callable[[str], Any] @@ -58,6 +65,25 @@ class _Argument: action: Optional[str] = None +def _infer_func_type(func: Callable, parse_args: ParseArgs) -> Literal["plain", "attrs", "dataclass"]: + signature = inspect.getfullargspec(func) + + for arg in signature.args + signature.kwonlyargs: + if arg in parse_args.ignore: + continue + + arg_type = signature.annotations[arg] + if isinstance(arg_type, str): + arg_type = typing.get_type_hints(func)[arg] + + if attrs.has(arg_type): + return "attrs" + elif is_dataclass(arg_type): + return "dataclass" + + return "plain" + + @dataclass class DataclassConfig: func: Callable @@ -81,8 +107,8 @@ class WithArgparse: def __init__( self, func: Callable, - func_type: Literal["attrs", "dataclass", "plain"], - strict: bool, + func_type: Literal["attrs", "dataclass", "plain", "infer"] = "infer", + strict: bool = True, aliases: Optional[Mapping[str, Sequence[str]]] = None, ignore_rename: Optional[set[str]] = None, ignore_keys: Optional[set[str]] = None, @@ -91,16 +117,22 @@ def __init__( partial_parse: Optional[bool] = None, partial_parse_pass_remaining_args: Optional[bool] = None, add_help: Optional[bool] = None, - on_help: Optional[Callable[[Self], Any]] = None + on_help: Optional[Callable[[Self], Any]] = None, + parse_args: ParseArgs | None = None, ): super().__init__() + if parse_args is None: + parse_args = ParseArgs(set()) + + if func_type == "infer": + func_type = _infer_func_type(func, parse_args) + if add_help is None: add_help = config["add_help"] self.ignore_rename_sequences = ignore_rename or set() self.ignore_arg_keys = ignore_keys or set() - self.argument_mapping = dict() self.argument_aliases = dict(aliases or dict()) self.post_parse_type_conversions = dict() self.allow_glob = allow_glob or set() @@ -117,6 +149,7 @@ def __init__( self.strict = strict self._help_caught = False + self.parse_args = parse_args self._reset_argparse() if on_help is not None: @@ -174,6 +207,199 @@ def _print_usage(self, parser: argparse.ArgumentParser | OrderedDict[str, argpar usage += "--- help for field name %s ---\n %s\n\n" % (field_name, formatted_usage_or_help) first(parser.values()).exit(2, usage) + def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): + """ + This function identifies the attrs function arguments inside this function, + parses these and calls the configured function with the parsed argument attrs instances + + Args: + args: Positional arguments to call the function with + kwargs: Keyword-only arguments to call the function with + + """ + orig_args = args + orig_kwargs = kwargs + + signature = inspect.getfullargspec(self.func) + if signature.varargs: + raise TypeError("Variational positional arguments are not supported") + if signature.varkw and self.strict: + raise TypeError("Variational keyword arguments are not supported") + if self.strict and len(args) > 0: + raise TypeError("In strict mode, providing positional arguments is unsupported") + if self.strict and len(kwargs) > 0: + raise TypeError("In strict mode, providing keyword arguments is unsupported") + + if len(args) > len(signature.args): + raise TypeError(f"Received more positional arguments ({len(args)}) to call {self.func!r} than this function receives: {len(signature.args)}") + + args_to_parse = OrderedDict() + + call_args = {} + registered_args = {} + registering_fields = defaultdict(set) + + for pos, name in enumerate(signature.args + signature.kwonlyargs): + if pos < len(orig_args): + # this arg is provided as an argument already + call_args[name] = orig_args[pos] + continue + elif name in orig_kwargs: + # this arg is provided as an argument already + call_args[name] = orig_kwargs[name] + continue + + if name not in signature.annotations: + raise TypeError( + f"Function {self.func!r} must be strongly typed. " + f"The non-provided argument {name!r} is missing a type signature." + ) + + typ = signature.annotations[name] + if isinstance(typ, str): + typ = typing.get_type_hints(self.func)[name] + + if self.func_type == "plain": + if attrs.has(typ) or is_dataclass(typ): + raise TypeError( + f"In plain mode, arguments cannot be dataclasses. " + f"Field {name!r} of function {self.func!r} is an attrs or dataclass type: {typ!r}." + ) + + elif self.func_type == "dataclass": + if not is_dataclass(typ): + raise TypeError( + f"Field {name!r} of function {self.func!r} must be a dataclass instance, got {typ}" + ) + elif self.func_type == "attrs": + if not attrs.has(typ): + raise TypeError( + f"Field {name!r} of function {self.func!r} must be an attrs instance, got {typ}" + ) + args_to_parse[name] = typ + + if self.func_type == "plain": + raise NotImplementedError("plain mode") + else: + for arg, typ in args_to_parse.items(): + # at this point, typ is a dataclass or attrs instance, depending on self.func_type + fields = dataclasses.fields(typ) if self.func_type == "dataclass" else attrs.fields(typ) + field_hints = typing.get_type_hints(typ) + missing_obj = dataclasses.MISSING if self.func_type == "dataclass" else attrs.NOTHING + for field in fields: + field_required = field.default is missing_obj + field_default = field.default if not field_required else MISSING_ARG + field_type = field.type + if isinstance(field_type, str): + field_type = field_hints.get(field.name) + + field_help = None + if field.metadata is not None and "help" in field.metadata: + field_help = str(field.metadata["help"]) + + field_aliases = None + if field.metadata is not None and "aliases" in field.metadata: + field_aliases = field.metadata["aliases"] + + field_args = ( + field.name, + field_type, + field_default, + field_required, + field_help, + field_aliases, + field.kw_only, + ) + if field.name in registered_args and registered_args[field.name] != field_args: + previous_registrations = registering_fields[field.name] + previous_registrations = list(sorted(previous_registrations, key=str)) + + other_args = registered_args[field.name] + mismatch_reasons = [] + if field_args[1] != other_args[1]: + mismatch_reasons.append(f"type mismatch: {field_args[1]!r} vs {other_args[1]!r}") + if field_args[2] != other_args[2]: + mismatch_reasons.append(f"default value mismatch: {field_args[2]!r} vs {other_args[2]!r}") + if field_args[3] != field_args[3]: + mismatch_reasons.append(f"required value mismatch: {field_args[3]!r} vs {other_args[3]!r}") + if field_args[4] != field_args[4]: + mismatch_reasons.append(f"help mismatch: {field_args[4]!r} vs {other_args[4]!r}") + if field_args[5] != field_args[5]: + mismatch_reasons.append(f"aliases mismatch: {field_args[5]!r} vs {other_args[5]!r}") + + if len(mismatch_reasons) == 0: + mismatch_reasons = ["unknown reason"] + mismatch_reason = ",".join(mismatch_reasons) + + raise TypeError( + f"Mismatch in overlapping argument {field.name!r}: " + f"Previous instances ({previous_registrations}) have the following differences: " + f"{mismatch_reason}" + ) + + self._setup_argument( + field.name, + field_type, + field_default, + field_required, + field_help, + field_aliases, + ) + + registered_args[field.name] = field_args + registering_fields[field.name].add(typ) + + try: + namespace, remaining = self.argparse.parse_known_args() + if namespace.help: # help was called + self._handle_help_call() + + # todo: use copy of internal state within this object + if len(remaining) > 0 and not _internal_global_state().partial: + remaining_str = " ".join(map(repr, remaining)) + raise argparse.ArgumentError( + argument=None, + message=f"failed to parse the following args: {remaining_str}" + ) + except argparse.ArgumentError as err: + if err.message.startswith("the following arguments are required:"): + self._print_usage(self.argparse, short=True) + sys.exit(2) + else: + self.argparse.error(err.message) + + args_dict = self._apply_post_parse_conversions(namespace.__dict__, dict()) + + if self.func_type in {"attrs", "dataclass"}: + registering_types = defaultdict(set) + for field_name, field_types in registering_fields.items(): + for field_type in field_types: + registering_types[field_type].add(field_name) + + call_args = {} + for arg, typ in args_to_parse.items(): + field_kwargs = {} + for field_name in registering_types[typ]: + field_value = args_dict.get(field_name) + if field_value is not MISSING_ARG: + field_kwargs[field_name] = field_value + + # instantiate the attrs/dataclass type with its keyword arguments + typ_value = typ(**field_kwargs) + + call_args[arg] = typ_value + + positional_args = () + kwonly_args = {} + for arg in signature.args: + positional_args += (call_args.pop(arg),) + for arg in signature.kwonlyargs: + kwonly_args[arg] = call_args.pop(arg) + + return self.func(*positional_args, **kwonly_args) + raise NotImplementedError(self.func_type) + + def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): """ This function identifies the dataclass function arguments inside this function, @@ -300,7 +526,6 @@ def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): sys.exit(2) else: parser.error(err.message) - args_dict = self._apply_name_mapping(namespace.__dict__, None) args_dict = self._apply_post_parse_conversions(args_dict, dict()) arg_type, fn_arg_type = call_dataclasses[arg_name] arg_value = arg_type(**args_dict) @@ -325,7 +550,7 @@ def call(self, args: Sequence[Any], kwargs: Mapping[str, Any]): elif self.func_type == "dataclass": return self._call_dataclass(args, kwargs) elif self.func_type == "attrs": - raise NotImplementedError("Calling attrs-defined functions is not supported, yet") + return self._call_attrs(args, kwargs) else: raise ValueError(f"Invalid function type {self.func_type!r}") @@ -425,8 +650,7 @@ def _call_func(self, args: Sequence[Any], kwargs: Mapping[str, Any]): ) overriden_kwargs[field_name] = kwargs[field_name] - args_dict = self._apply_name_mapping(parsed_args.__dict__, args_dict) - args_dict = self._apply_post_parse_conversions(args_dict, args_dict) + args_dict = self._apply_post_parse_conversions(parsed_args.__dict__, args_dict) for key, value in overriden_kwargs.items(): if key in args_dict: @@ -441,19 +665,6 @@ def _call_func(self, args: Sequence[Any], kwargs: Mapping[str, Any]): return self.func(**args_dict) - def _apply_name_mapping( - self, - parsed_args: Mapping[str, Any], - out: MutableMapping[str, Any] | None - ) -> MutableMapping[str, Any]: - out = out or dict() - for key, value in parsed_args.items(): - if key in self.argument_mapping: - out[self.argument_mapping[key]] = value - else: - out[key] = value - return out - def _apply_post_parse_conversions( self, parsed_args: Mapping[str, Any], @@ -475,11 +686,20 @@ def _apply_post_parse_conversions( def reset(self): self._reset_argparse() - self.argument_mapping.clear() self.post_parse_type_conversions.clear() def _reset_argparse(self): self.argparse = ArgumentParser(add_help=False, exit_on_error=False) + self.argparse.add_argument( + "--help", "-h", + action="store_true", default=False, required=False, help="show this help message and exit" + ) + + def _handle_help_call(self): + if self.parse_args.help_strategy != "silent": + self._print_usage(self.argparse, False) + if self.parse_args.help_strategy == "print-and-exit": + sys.exit(2) def _setup_argument( self, @@ -488,9 +708,10 @@ def _setup_argument( arg_default: Any, arg_required: bool, arg_help: Optional[str], + arg_aliases: Optional[list[str]], ): - if arg_name.startswith("_"): - return + if not arg_aliases: + arg_aliases = [] args = self._dispatch_argparse_key_type( arg_name, @@ -517,33 +738,12 @@ def _setup_argument( if arg_help: argparse_kwargs["help"] = arg_help - aliases = self.argument_aliases.get(arg_name, list()) self.argparse.add_argument( "--" + args.name, - *aliases, + *arg_aliases, **argparse_kwargs ) - - def _register_substitution( - self, - arg_name: str, - replacement: str, - ) -> str: - """ - :param arg_name: - :param replacement: - :return: replacement - """ - logger.debug(f"Registering substitution for {arg_name} with {replacement}") - self.argument_mapping[replacement] = arg_name - return replacement - - def _resolve_orig_arg_name(self, arg_name: str) -> str: - while arg_name in self.argument_mapping: - arg_name = self.argument_mapping[arg_name] - return arg_name - def _dispatch_argparse_key_type( self, arg_name: str, @@ -592,21 +792,11 @@ def _dispatch_argparse_key_type( return inner origin_arg_type = get_origin(arg_type) - if ( - origin_arg_type - and origin_arg_type in SEQUENCE_TYPES - and arg_name not in self.ignore_rename_sequences - and arg_name.endswith("s") - ): - arg_name = self._register_substitution(arg_name, arg_name[:-1]) - if arg_type == bool: if arg_default is not None and not isinstance(arg_default, bool): raise ValueError(f"Default value for {arg_name} is of type {type(arg_default)}, but should be bool") arg_default = arg_default if arg_default is not None else False - if arg_default: - arg_name = self._register_substitution(arg_name, "no_" + arg_name) store_action = "store_true" if not arg_default else "store_false" return _Argument( @@ -630,8 +820,7 @@ def _dispatch_argparse_key_type( ) if origin_arg_type is not list: - orig_arg_name = self._resolve_orig_arg_name(arg_name) - self._register_post_parse_type_conversion(orig_arg_name, origin_arg_type) + self._register_post_parse_type_conversion(arg_name, origin_arg_type) return _Argument( inner.name, @@ -737,7 +926,7 @@ def first_working_inner_type(inp): "with inner types " + str(inner_arg_types) ) else: - orig_arg_name = self._resolve_orig_arg_name(arg_name) + orig_arg_name = arg_name if ( arg_type in {Path, str} and orig_arg_name in self.allow_glob diff --git a/with_argparse/impl.py b/with_argparse/impl.py index 2246851..236a6ad 100644 --- a/with_argparse/impl.py +++ b/with_argparse/impl.py @@ -4,8 +4,7 @@ from typing import Callable, Union, ParamSpec, TypeVar, overload, Optional, Mapping, _SpecialForm, Any import warnings -from with_argparse.configure_argparse import WithArgparse, DataclassConfig -from with_argparse.types import DataclassInstance +from with_argparse.configure_argparse import WithArgparse from with_argparse.setup import config try: diff --git a/with_argparse/main.py b/with_argparse/main.py new file mode 100644 index 0000000..4da52e8 --- /dev/null +++ b/with_argparse/main.py @@ -0,0 +1,122 @@ +import argparse +import functools +import inspect +from argparse import Namespace +from typing import Callable, TypeVar, overload, Literal, Self + +import attrs + + +@attrs.define +class GlobalState: + disabled: bool = False + partial: bool = False + + parse_hooks: list[Callable[[Namespace, list[str]], None]] = attrs.field(factory=list) + +_global_state = GlobalState() + + +def _internal_global_state(): + return _global_state + + +@attrs.define +class ParseArgs: + ignore: set[str] + + help_strategy: Literal["print-and-exit", "print-and-continue", "silent"] = "exit" + + +@attrs.define +class partial_argparse: # noqa + state: bool = attrs.field(init=False) + remaining: list[str] = attrs.field(init=False) + + def __enter__(self) -> Self: + self.state = _global_state.partial + _global_state.partial = True + _global_state.parse_hooks.append(self) + return self + + def __call__(self, parsed: Namespace, remaining: list[str]): + # after hooks have been called, the remaining args are available from the + # context manager variable + self.remaining = remaining + _global_state.parse_hooks.remove(self) + + def __exit__(self, exc_type, exc_val, exc_tb): + _global_state.partial = self.state + _global_state.parse_hooks.remove(self) + + # todo: add __copy__ that does a shallow copy of this class + # and store those copies in the WithArgparse instances + + +@attrs.define +class no_argparse: + state: bool = attrs.field(init=False) + + def __enter__(self): + self.state = _global_state.disabled + _global_state.disabled = True + + def __exit__(self, exc_type, exc_val, exc_tb): + _global_state.partial = self.state + + +@overload +def with_attrs[T](func: Callable[..., T], /) -> Callable[[], T]: + ... + +@overload +def with_attrs[T]( + *, + parse_args: ParseArgs | None = None, + strict: Literal[True] = True, +) -> Callable[[Callable[..., T]], Callable[[], T]]: + ... + +@overload +def with_attrs[T]( + *, + parse_args: ParseArgs | None = None, + strict: bool = False, +) -> Callable[[Callable[..., T]], Callable[..., T]]: + ... + + +def with_attrs(func: Callable | None = None, *, parse_args: ParseArgs | None = None, strict: bool = True): + # decorator-decorator path + + def decorator(decorated_func: Callable): + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + from with_argparse.configure_argparse import WithArgparse + + if strict and (len(args) > 0 or len(kwargs) > 0): + raise TypeError("In strict mode, arguments cannot be passed to the decorated dataclass function") + + wa = WithArgparse(decorated_func, "attrs", strict=strict) + return wa.call((), {}) + return wrapper + + if func is None: + return decorator + else: + return decorator(func) + +@attrs.define +class A: + value: int + +@attrs.define +class B: + ovalue: int + +@with_attrs +def func(a: A, b: B): + return a.value * b.ovalue + +if __name__ == "__main__": + print(func()) \ No newline at end of file diff --git a/with_argparse/types.py b/with_argparse/typing_utils.py similarity index 100% rename from with_argparse/types.py rename to with_argparse/typing_utils.py diff --git a/with_argparse/with_argparse.pyi b/with_argparse/with_argparse.pyi index 34aeba1..794d1a0 100644 --- a/with_argparse/with_argparse.pyi +++ b/with_argparse/with_argparse.pyi @@ -1,6 +1,6 @@ from typing import Callable, Protocol, Any, TypeVar, Literal, Optional, overload -from with_argparse.configure_argparse import WithArgparse +from with_argparse.configure_argparse import WithArgparse, ParseArgs T = TypeVar("T") @@ -14,6 +14,18 @@ class StrictArgParsedFunction(Protocol[T]): def with_dataclass(func: FunctionWithSpecificReturnType[T], /) -> StrictArgParsedFunction[T]: ... +@overload +def with_attrs(func: FunctionWithSpecificReturnType[T], /) -> StrictArgParsedFunction[T]: + ... + +@overload +def with_attrs( + *, + parse_args: ParseArgs | None = None, + strict: Literal[True] = True, +) -> Callable[[FunctionWithSpecificReturnType[T]], StrictArgParsedFunction[T]]: + ... + @overload def with_dataclass( allow_glob: Optional[set[str]] = None, @@ -22,4 +34,4 @@ def with_dataclass( on_help: Optional[Callable[[WithArgparse], Any]] = None, partial_parse_pass_remaining_args: Optional[bool] = None, strict: Literal[True] = True, -) -> StrictArgParsedFunction[T]: ... \ No newline at end of file +) -> Callable[[FunctionWithSpecificReturnType[T]], StrictArgParsedFunction[T]]: ... From 217bb02264bae814f75c7d33825640d017e3b2fb Mon Sep 17 00:00:00 2001 From: fleonce Date: Mon, 27 Jul 2026 21:07:52 +0200 Subject: [PATCH 3/5] Finished support for attrs --- pyproject.toml | 4 +- test/test_annotations.py | 8 +- test/test_argparse.py | 11 +- test/test_argparse2.py | 8 +- test/test_custom_parse.py | 60 -------- test/test_dataclass.py | 4 +- test/test_glob.py | 28 ---- test/test_ignore.py | 15 +- uv.lock | 214 ++++++++++++++++++++++++++++ with_argparse/__init__.py | 7 +- with_argparse/configure_argparse.py | 103 +++++++++---- with_argparse/impl.py | 1 + with_argparse/main.py | 46 +++--- 13 files changed, 344 insertions(+), 165 deletions(-) delete mode 100644 test/test_custom_parse.py delete mode 100644 test/test_glob.py create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 99a2f8f..43ac994 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "with-argparse" -description = "A simple but handy Python library to generate a `argparse.ArgumentParser` object from a type-annotated method " -version = "1.0.5rc4" +description = "A simple but handy Python library to parse a type-annotated method as a CLI interface with support for dataclasses and attrs instances" +version = "1.0.6rc1" license = {text = "Apache 2.0"} readme = "README.md" diff --git a/test/test_annotations.py b/test/test_annotations.py index a82faaf..a6bc6c1 100644 --- a/test/test_annotations.py +++ b/test/test_annotations.py @@ -10,8 +10,8 @@ def wrapper(arg): return arg with self.assertRaisesRegex( - ValueError, - "Argument [a-z]+ must have a type annotation in order to be viable for argparse" + TypeError, + "Function .+ must be strongly typed, however has no no type annotation for field '[a-z]+'" ): wrapper() @@ -21,7 +21,7 @@ def func(*, arg): return arg with self.assertRaisesRegex( - ValueError, - "Argument [a-z]+ must have a type annotation in order to be viable for argparse" + TypeError, + "Function int: + def wrapper(value: int | None = default) -> int | None: return value with sys_args(value=expect): @@ -112,10 +111,10 @@ def func(arg: str) -> str: func() def test_duplicate_inputs(self): - @with_argparse + @with_argparse(strict=False) def func(arg: str) -> str: return arg - with sys_args(arg="456"): + with sys_args(arg="456"), self.assertRaises(SystemExit): self.assertEqual(func(arg="123"), "456") self.assertEqual(func("123"), "456") diff --git a/test/test_argparse2.py b/test/test_argparse2.py index 5bda402..7878469 100644 --- a/test/test_argparse2.py +++ b/test/test_argparse2.py @@ -13,7 +13,11 @@ class A: @with_dataclass def func(args: A): - return args + return args.field1 with sys_args(): - func() + with self.assertRaises(SystemExit): + func() + + with sys_args(field1="42"): + self.assertEqual(42, func()) diff --git a/test/test_custom_parse.py b/test/test_custom_parse.py deleted file mode 100644 index 247c0e5..0000000 --- a/test/test_custom_parse.py +++ /dev/null @@ -1,60 +0,0 @@ -import logging -import unittest -from pathlib import Path -from typing import Optional - -from tools import sys_args -from with_argparse import with_argparse - -logging.basicConfig( - level="DEBUG" -) - - -class CustomParseTest(unittest.TestCase): - def test_parse_from_list_of_paths(self): - class TestClass: - @classmethod - def from_string(cls, inp: list[Path]): - return TestClass(inp) - def __init__(self, *args): - self.args = args - def values(self): - return self.args - @with_argparse(use_custom={"value": TestClass.from_string}) - def wrapper(value: TestClass): - return value - - with sys_args(value=[".", "."]): - path = Path(".") - self.assertEqual(wrapper().values()[0], [path, path]) - - def test_parse_class_from_str(self): - class TestClass: - @classmethod - def from_string(cls, inp: str) -> "TestClass": - return TestClass(inp, "") - - def __init__(self, inp: str, _: str): - self.inp = inp - - def value(self) -> str: - return self.inp - - @with_argparse(use_custom={"a": TestClass.from_string}) - def wrapper(a: TestClass): - return a - - with sys_args(a="abc"): - self.assertEqual(wrapper().value(), "abc") - - def test_parse_kwarg(self): - def wow_fn(inp: Optional[str]) -> int: - return 42 - - @with_argparse(value=wow_fn) - def wrapper(value: int): - return value - - with sys_args(value="any"): - self.assertEqual(wrapper(), 42) \ No newline at end of file diff --git a/test/test_dataclass.py b/test/test_dataclass.py index e0a9ff5..d900f5b 100644 --- a/test/test_dataclass.py +++ b/test/test_dataclass.py @@ -18,7 +18,7 @@ class Test: param: Literal['a', 'b'] items: list[str] - @with_dataclass(args=Test) + @with_dataclass def func(args: Test): return args.param @@ -33,7 +33,7 @@ class A: class B: number: int - @with_dataclass(A, B) + @with_dataclass def func(arg1: A, arg2: B): return len(arg1.param) + arg2.number diff --git a/test/test_glob.py b/test/test_glob.py deleted file mode 100644 index 9c453a3..0000000 --- a/test/test_glob.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest -from pathlib import Path - -from with_argparse import with_argparse -from tools import sys_args - - -class GlobTestCase(unittest.TestCase): - def test_glob_setting(self): - @with_argparse(use_glob={"paths"}) - def wrapper(paths: list[Path]): - return paths - with sys_args(path="*"): - out = wrapper() - self.assertGreater(len(out), 1) - - def test_glob_set(self): - @with_argparse(use_glob={"paths"}) - def wrapper(paths: set[Path]): - return paths - with sys_args(path="*"): - out = wrapper() - self.assertIsInstance(out, set) - self.assertGreater(len(out), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/test_ignore.py b/test/test_ignore.py index 33b575d..df76386 100644 --- a/test/test_ignore.py +++ b/test/test_ignore.py @@ -1,3 +1,4 @@ +import argparse import logging import unittest @@ -11,14 +12,15 @@ class IgnoreTest(unittest.TestCase): def test_ignore_single_param(self): - @with_argparse(ignore_keys={"arg"}) + @with_argparse(strict=False) def wrapper(arg: str) -> str: return arg - self.assertEqual("abc", wrapper("abc")) + with sys_args(): + self.assertEqual("abc", wrapper("abc")) def test_ignore_multi_param(self): - @with_argparse(ignore_keys={"arg"}) + @with_argparse(strict=False) def wrapper(arg: str, inp: str) -> str: return inp + arg @@ -32,16 +34,17 @@ def wrapper(arg: str, inp: str) -> str: self.assertEqual("42abc", wrapper("abc")) def test_ignore_kwarg_multi_param(self): - @with_argparse(ignore_keys={"arg"}) + @with_argparse(strict=False) def wrapper(inp: str, arg: str) -> str: return arg with sys_args(inp="42"): - self.assertEqual("value", wrapper("value")) + with self.assertRaises(SystemExit): + self.assertEqual("value", wrapper("value")) self.assertEqual("value", wrapper(arg="value")) def test_kwonly_kwarg_multi_param(self): - @with_argparse(ignore_keys={"arg"}) + @with_argparse(strict=False) def wrapper(*, inp: str, arg: str) -> str: return inp + arg diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0e46ab5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,214 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "with-argparse" +version = "1.0.6rc1" +source = { editable = "." } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs", specifier = ">=26.1.0" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "mypy", specifier = ">=2.3.0" }] diff --git a/with_argparse/__init__.py b/with_argparse/__init__.py index 83a7dea..6fcaf42 100644 --- a/with_argparse/__init__.py +++ b/with_argparse/__init__.py @@ -1,4 +1,7 @@ -from .impl import with_argparse, with_dataclass, script_argparse from .disable_argparse import no_argparse +from .main import with_attrs, no_argparse, partial_argparse, script_argparse -__all__ = ["with_argparse", "no_argparse", "with_dataclass", "script_argparse"] +with_argparse = with_attrs +with_dataclass = with_attrs + +__all__ = ["with_argparse", "no_argparse", "with_dataclass", "script_argparse", "with_attrs", "partial_argparse"] diff --git a/with_argparse/configure_argparse.py b/with_argparse/configure_argparse.py index 96a0785..6bd2524 100644 --- a/with_argparse/configure_argparse.py +++ b/with_argparse/configure_argparse.py @@ -1,3 +1,4 @@ +from types import GenericAlias import argparse import dataclasses import inspect @@ -15,6 +16,7 @@ Any, Set, List, get_origin, get_args, Union, Literal, Optional, Sequence, TypeVar, Iterable, Callable, MutableMapping, Mapping, ) +from zipfile import sizeCentralDir import attrs from typing_extensions import Self @@ -72,6 +74,11 @@ def _infer_func_type(func: Callable, parse_args: ParseArgs) -> Literal["plain", if arg in parse_args.ignore: continue + if arg not in signature.annotations: + raise TypeError( + f"Function {func!r} must be strongly typed, " + f"however has no no type annotation for field {arg!r}" + ) arg_type = signature.annotations[arg] if isinstance(arg_type, str): arg_type = typing.get_type_hints(func)[arg] @@ -152,8 +159,6 @@ def __init__( self.parse_args = parse_args self._reset_argparse() - if on_help is not None: - self.argparse.add_argument("-h", "--help", action="store_true", dest="_help") def _register_mapping(self): ... @@ -279,7 +284,37 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): args_to_parse[name] = typ if self.func_type == "plain": - raise NotImplementedError("plain mode") + positional_defaults = signature.defaults or () + non_default_positional_args = len(signature.args) - len(positional_defaults) + positional_defaults = (MISSING_ARG,) * non_default_positional_args + positional_defaults + + arg_defaults = { + arg: positional_defaults[i] + for i, arg in enumerate(signature.args) + } + arg_defaults = arg_defaults | (signature.kwonlydefaults or {}) + + for arg, typ in args_to_parse.items(): + arg_required = arg not in arg_defaults or arg_defaults[arg] is MISSING_ARG + arg_default = arg_defaults.get(arg, MISSING_ARG) + + if arg_default is not MISSING_ARG and ( + (not isinstance(typ, UnionType) and not isinstance(typ, GenericAlias)) + or (isinstance(typ, UnionType) and all(not isinstance(typ_arg, GenericAlias) for typ_arg in typ.__args__)) + ) and not isinstance(arg_default, typ): + raise TypeError( + f"Invalid default value for argument {arg!r}: " + f"got {arg_default!r} ({type(arg_default)!r}) for type {typ!r}" + ) + + self._setup_argument( + arg, + typ, + arg_default, + arg_required, + None, + self.parse_args.aliases.get(arg, None) + ) else: for arg, typ in args_to_parse.items(): # at this point, typ is a dataclass or attrs instance, depending on self.func_type @@ -292,6 +327,11 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): field_type = field.type if isinstance(field_type, str): field_type = field_hints.get(field.name) + if field_type is None: + raise TypeError( + f"Invalid field type {type(field_type)!r} " + f"for function argument {arg!r} and field name {field.name!r}" + ) field_help = None if field.metadata is not None and "help" in field.metadata: @@ -350,9 +390,9 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): registering_fields[field.name].add(typ) try: - namespace, remaining = self.argparse.parse_known_args() - if namespace.help: # help was called + if _help_called(): self._handle_help_call() + namespace, remaining = self.argparse.parse_known_args() # todo: use copy of internal state within this object if len(remaining) > 0 and not _internal_global_state().partial: @@ -362,11 +402,9 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): message=f"failed to parse the following args: {remaining_str}" ) except argparse.ArgumentError as err: - if err.message.startswith("the following arguments are required:"): - self._print_usage(self.argparse, short=True) - sys.exit(2) - else: - self.argparse.error(err.message) + self._print_usage(self.argparse, short=False) + print("error:", err.message, file=sys.stderr) + sys.exit(2) args_dict = self._apply_post_parse_conversions(namespace.__dict__, dict()) @@ -376,7 +414,6 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): for field_type in field_types: registering_types[field_type].add(field_name) - call_args = {} for arg, typ in args_to_parse.items(): field_kwargs = {} for field_name in registering_types[typ]: @@ -388,17 +425,25 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): typ_value = typ(**field_kwargs) call_args[arg] = typ_value + else: + for arg, value in args_dict.items(): + if arg in call_args: + raise ValueError(f"Illegal state, argument {arg!r} is already registered as a call arg: {call_args!r}") - positional_args = () - kwonly_args = {} - for arg in signature.args: - positional_args += (call_args.pop(arg),) - for arg in signature.kwonlyargs: - kwonly_args[arg] = call_args.pop(arg) + call_args[arg] = value - return self.func(*positional_args, **kwonly_args) - raise NotImplementedError(self.func_type) + positional_args = () + kwonly_args = {} + for arg, val in call_args.items(): + if isinstance(val, MissingArgument): + raise TypeError("Invalid state") + for arg in signature.args: + positional_args += (call_args.pop(arg),) + for arg in signature.kwonlyargs: + kwonly_args[arg] = call_args.pop(arg) + + return self.func(*positional_args, **kwonly_args) def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): """ @@ -545,15 +590,7 @@ def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): return self.func(*call_args, **call_kw_only_args) def call(self, args: Sequence[Any], kwargs: Mapping[str, Any]): - if self.func_type == "plain": - return self._call_func(args, kwargs) - elif self.func_type == "dataclass": - return self._call_dataclass(args, kwargs) - elif self.func_type == "attrs": - return self._call_attrs(args, kwargs) - else: - raise ValueError(f"Invalid function type {self.func_type!r}") - + return self._call_attrs(args, kwargs) def _call_func(self, args: Sequence[Any], kwargs: Mapping[str, Any]): info = inspect.getfullargspec(self.func) @@ -738,6 +775,13 @@ def _setup_argument( if arg_help: argparse_kwargs["help"] = arg_help + if "action" not in argparse_kwargs: + argparse_kwargs["metavar"] = ( + arg_type.__name__ + if hasattr(arg_type, "__name__") + else repr(arg_type) + ) + self.argparse.add_argument( "--" + args.name, *arg_aliases, @@ -793,7 +837,7 @@ def _dispatch_argparse_key_type( origin_arg_type = get_origin(arg_type) if arg_type == bool: - if arg_default is not None and not isinstance(arg_default, bool): + if arg_default is not MISSING_ARG and not isinstance(arg_default, bool): raise ValueError(f"Default value for {arg_name} is of type {type(arg_default)}, but should be bool") arg_default = arg_default if arg_default is not None else False @@ -899,7 +943,6 @@ def _dispatch_argparse_key_type( if len(inner_arg_types) < 2: raise ValueError() - warnings.warn(f"Using type unions in with_argparse is early beta and subject to change in the future", stacklevel=2) inner_types = [inner.type for inner in inner_arg_types] def first_working_inner_type(inp): for inner_type in inner_types: diff --git a/with_argparse/impl.py b/with_argparse/impl.py index 236a6ad..55e3ea7 100644 --- a/with_argparse/impl.py +++ b/with_argparse/impl.py @@ -6,6 +6,7 @@ from with_argparse.configure_argparse import WithArgparse from with_argparse.setup import config +from with_argparse.main import with_attrs try: from pyrootutils import setup_root as setup_root_fn diff --git a/with_argparse/main.py b/with_argparse/main.py index 4da52e8..a66c2fa 100644 --- a/with_argparse/main.py +++ b/with_argparse/main.py @@ -1,8 +1,10 @@ +from __future__ import annotations import argparse import functools import inspect +import pathlib from argparse import Namespace -from typing import Callable, TypeVar, overload, Literal, Self +from typing import Callable, TypeVar, overload, Literal, Self, ParamSpec, Any import attrs @@ -23,9 +25,18 @@ def _internal_global_state(): @attrs.define class ParseArgs: - ignore: set[str] + ignore: set[str] = attrs.field(factory=set) + aliases: dict[str, list[str]] = attrs.field(factory=dict) + parse_functions: dict[str, Callable[[str], Any]] = attrs.field(factory=dict) - help_strategy: Literal["print-and-exit", "print-and-continue", "silent"] = "exit" + help_strategy: Literal["print-and-exit", "print-and-continue", "silent"] = "print-and-exit" + + def __attrs_post_init__(self): + if self.parse_functions: + raise TypeError( + f"Parsing functions for the following arguments were specified, " + f"however parse support has been removed. Please use attrs.Converter as an alternative." + ) @attrs.define @@ -66,23 +77,23 @@ def __exit__(self, exc_type, exc_val, exc_tb): @overload -def with_attrs[T](func: Callable[..., T], /) -> Callable[[], T]: +def with_attrs[**P, T](func: Callable[P, T], /) -> Callable[[], T]: ... @overload -def with_attrs[T]( +def with_attrs[**P, T]( *, parse_args: ParseArgs | None = None, strict: Literal[True] = True, -) -> Callable[[Callable[..., T]], Callable[[], T]]: +) -> Callable[[Callable[P, T]], Callable[[], T]]: ... @overload -def with_attrs[T]( +def with_attrs[**P, T]( *, parse_args: ParseArgs | None = None, strict: bool = False, -) -> Callable[[Callable[..., T]], Callable[..., T]]: +) -> Callable[[Callable[P, T]], Callable[..., T]]: ... @@ -97,8 +108,8 @@ def wrapper(*args, **kwargs): if strict and (len(args) > 0 or len(kwargs) > 0): raise TypeError("In strict mode, arguments cannot be passed to the decorated dataclass function") - wa = WithArgparse(decorated_func, "attrs", strict=strict) - return wa.call((), {}) + wa = WithArgparse(decorated_func, "infer", strict=strict, parse_args=parse_args) + return wa.call(args, kwargs) return wrapper if func is None: @@ -106,17 +117,6 @@ def wrapper(*args, **kwargs): else: return decorator(func) -@attrs.define -class A: - value: int - -@attrs.define -class B: - ovalue: int - -@with_attrs -def func(a: A, b: B): - return a.value * b.ovalue -if __name__ == "__main__": - print(func()) \ No newline at end of file +def script_argparse(): + raise NotImplementedError \ No newline at end of file From 84ed0c8aac16388e299a81b61e2b8d8e94c62911 Mon Sep 17 00:00:00 2001 From: fleonce Date: Mon, 27 Jul 2026 21:31:00 +0200 Subject: [PATCH 4/5] Rename the _call_attrs method, make partial parsing more robust, add script_argparse again --- test/test_argparse2.py | 28 +++- with_argparse/__init__.py | 13 +- with_argparse/configure_argparse.py | 7 +- with_argparse/disable_argparse.py | 13 -- with_argparse/impl.py | 218 ---------------------------- with_argparse/main.py | 94 +++++++++++- 6 files changed, 128 insertions(+), 245 deletions(-) delete mode 100644 with_argparse/disable_argparse.py delete mode 100644 with_argparse/impl.py diff --git a/test/test_argparse2.py b/test/test_argparse2.py index 7878469..c0d3a02 100644 --- a/test/test_argparse2.py +++ b/test/test_argparse2.py @@ -1,8 +1,11 @@ +from with_argparse import with_attrs import dataclasses import unittest +import attrs + from tools import sys_args -from with_argparse import with_dataclass +from with_argparse import with_dataclass, partial_argparse, no_argparse class ArgParseTestCase(unittest.TestCase): @@ -21,3 +24,26 @@ def func(args: A): with sys_args(field1="42"): self.assertEqual(42, func()) + + def test_attrs(self): + @attrs.define + class A: + field1: int + + @with_attrs(strict=False) + def func(args: A): + return args.field1 + + with sys_args(): + with self.assertRaises(SystemExit): + func() + + with sys_args(field1="42"): + self.assertEqual(42, func()) + + with sys_args(field1="42", test="test"), partial_argparse() as partial: + func() + self.assertEqual(["--test", "test"], partial.remainder) + + with sys_args(), no_argparse(): + self.assertEqual(42, func(A(42))) diff --git a/with_argparse/__init__.py b/with_argparse/__init__.py index 6fcaf42..82aa9ca 100644 --- a/with_argparse/__init__.py +++ b/with_argparse/__init__.py @@ -1,7 +1,10 @@ -from .disable_argparse import no_argparse -from .main import with_attrs, no_argparse, partial_argparse, script_argparse - -with_argparse = with_attrs -with_dataclass = with_attrs +from .main import ( + no_argparse, + partial_argparse, + script_argparse, + with_argparse, + with_attrs, + with_dataclass, +) __all__ = ["with_argparse", "no_argparse", "with_dataclass", "script_argparse", "with_attrs", "partial_argparse"] diff --git a/with_argparse/configure_argparse.py b/with_argparse/configure_argparse.py index 6bd2524..a8ad267 100644 --- a/with_argparse/configure_argparse.py +++ b/with_argparse/configure_argparse.py @@ -16,7 +16,6 @@ Any, Set, List, get_origin, get_args, Union, Literal, Optional, Sequence, TypeVar, Iterable, Callable, MutableMapping, Mapping, ) -from zipfile import sizeCentralDir import attrs from typing_extensions import Self @@ -212,7 +211,7 @@ def _print_usage(self, parser: argparse.ArgumentParser | OrderedDict[str, argpar usage += "--- help for field name %s ---\n %s\n\n" % (field_name, formatted_usage_or_help) first(parser.values()).exit(2, usage) - def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): + def _call_any(self, args: Sequence[Any], kwargs: Mapping[str, Any]): """ This function identifies the attrs function arguments inside this function, parses these and calls the configured function with the parsed argument attrs instances @@ -393,6 +392,8 @@ def _call_attrs(self, args: Sequence[Any], kwargs: Mapping[str, Any]): if _help_called(): self._handle_help_call() namespace, remaining = self.argparse.parse_known_args() + for hook in _internal_global_state().parse_hooks: + hook(namespace, remaining) # todo: use copy of internal state within this object if len(remaining) > 0 and not _internal_global_state().partial: @@ -590,7 +591,7 @@ def _call_dataclass(self, args: Sequence[Any], kwargs: Mapping[str, Any]): return self.func(*call_args, **call_kw_only_args) def call(self, args: Sequence[Any], kwargs: Mapping[str, Any]): - return self._call_attrs(args, kwargs) + return self._call_any(args, kwargs) def _call_func(self, args: Sequence[Any], kwargs: Mapping[str, Any]): info = inspect.getfullargspec(self.func) diff --git a/with_argparse/disable_argparse.py b/with_argparse/disable_argparse.py deleted file mode 100644 index 5604e62..0000000 --- a/with_argparse/disable_argparse.py +++ /dev/null @@ -1,13 +0,0 @@ -from .impl import is_enabled, set_config, set_enabled - - -class no_argparse: # noqa - def __init__(self): - self.previous_state = None - - def __enter__(self): - self.previous_state = is_enabled() - set_enabled(False) - - def __exit__(self, exc_type, exc_val, exc_tb): - set_enabled(self.previous_state) diff --git a/with_argparse/impl.py b/with_argparse/impl.py deleted file mode 100644 index 55e3ea7..0000000 --- a/with_argparse/impl.py +++ /dev/null @@ -1,218 +0,0 @@ -import functools -import sys -import typing -from typing import Callable, Union, ParamSpec, TypeVar, overload, Optional, Mapping, _SpecialForm, Any -import warnings - -from with_argparse.configure_argparse import WithArgparse -from with_argparse.setup import config -from with_argparse.main import with_attrs - -try: - from pyrootutils import setup_root as setup_root_fn - - setup_root: Union[Callable, None] = setup_root_fn -except ImportError: - setup_root_fn = None - -P = ParamSpec('P') -T = TypeVar('T') - -LITERAL_TYPES: set[type | _SpecialForm] = { - typing.Literal, -} -if sys.version_info[:2] >= (3, 11): - LITERAL_TYPES |= { - typing.LiteralString, - } - -SET_TYPES = {set, typing.Set} -LIST_TYPES = {list, typing.List} -SEQUENCE_TYPES = SET_TYPES | LIST_TYPES - -ORIGIN_TYPES = { - list, - set, - typing.Union, - typing.Optional, -} - - -def is_enabled(): - return config.get("argparse_enabled", True) - - -def set_enabled(enabled: bool): - set_config("argparse_enabled", enabled) - - -def set_config(key: str, state: bool): - config[key] = state - -def with_dataclass( - func: Callable = None, - *, - allow_glob: Optional[set[str]] = None, - partial_parse: Optional[bool] = None, - add_help: Optional[bool] = None, - on_help: Optional[Callable[[WithArgparse], Any]] = None, - partial_parse_pass_remaining_args: Optional[bool] = None, - strict: bool = True, -): - def wrapper(fn): - @functools.wraps(fn) - def inner(*inner_args, **inner_kwargs): - if not is_enabled(): - return fn(*inner_args, **inner_kwargs) - - parser = WithArgparse( - fn, - "dataclass", - strict=strict, - allow_glob=allow_glob, - partial_parse=partial_parse, - partial_parse_pass_remaining_args=partial_parse_pass_remaining_args, - add_help=add_help, - on_help=on_help, - ) - return parser.call(inner_args, inner_kwargs) - return inner - - if func is not None: - return wrapper(func) - return wrapper - - -@overload -def with_argparse( - *, - ignore_keys: Optional[set[str]] = None, - ignore_mapping: Optional[set[str]] = None, - setup_cwd: Optional[bool] = None, - aliases: Optional[Mapping[str, list[str]]] = None, - use_glob: Optional[set[str]] = None, - use_custom: Optional[Mapping[str, Callable[[Any], Any]]] = None, - partial_parse: Optional[bool] = None, - partial_parse_pass_remaining_args: Optional[bool] = None, - add_help: Optional[bool] = None, - on_help: Optional[Callable[[WithArgparse], Any]] = None, - **kwargs: Callable[[Any], Any] -) -> Callable[[Callable[P, T]], Callable[[], T]]: ... - -@overload -def with_argparse(func: Callable[P, T], /) -> Callable[[], T]: ... - -def with_argparse( - func=None, - *, - ignore_keys: Optional[set[str]] = None, - ignore_mapping: Optional[set[str]] = None, - setup_cwd: Optional[bool] = None, - aliases: Optional[Mapping[str, list[str]]] = None, - use_glob: Optional[set[str]] = None, - use_custom: Optional[Mapping[str, Callable[[Any], Any]]] = None, - partial_parse: Optional[bool] = None, - partial_parse_pass_remaining_args: Optional[bool] = None, - add_help: Optional[bool] = None, - on_help: Optional[Callable[[WithArgparse], Any]] = None, - **kwargs: Callable[[Any], Any] -): - aliases = aliases or dict() - ignore_keys = ignore_keys or set() - ignore_mapping = ignore_mapping or set() - use_glob = use_glob or set() - setup_cwd = setup_cwd or False - use_custom = use_custom or dict() - use_custom = dict(use_custom, **kwargs) - partial_parse = partial_parse or False - - def wrapper(fn): - @functools.wraps(fn) - def inner(*inner_args, **inner_kwargs): - if not is_enabled(): - return fn(*inner_args, **inner_kwargs) - - if setup_cwd: - if setup_root is not None: - setup_root(search_from=__file__, cwd=True, pythonpath=True) - else: - warnings.warn( - "Could not import setup_root from pyrootutils. Using 'setup_cwd=True' requires installing" - "pyrootutils" - ) - - parser = WithArgparse( - fn, - aliases=aliases, - ignore_rename=ignore_mapping, - ignore_keys=ignore_keys, - allow_glob=use_glob, - allow_custom=use_custom, - partial_parse=partial_parse, - partial_parse_pass_remaining_args=partial_parse_pass_remaining_args, - add_help=add_help, - on_help=on_help, - ) - return parser.call(inner_args, inner_kwargs) - return inner - - if func is None: - return wrapper - - return wrapper(func) - - -@overload -def script_argparse( - *, - ignore_keys: Optional[set[str]] = None, - ignore_mapping: Optional[set[str]] = None, - setup_cwd: Optional[bool] = None, - aliases: Optional[Mapping[str, list[str]]] = None, - use_glob: Optional[set[str]] = None, - use_custom: Optional[Mapping[str, Callable[[Any], Any]]] = None, - partial_parse: Optional[bool] = None, - partial_parse_pass_remaining_args: Optional[bool] = None, - add_help: Optional[bool] = None, - on_help: Optional[Callable[[WithArgparse], Any]] = None, - **kwargs: Callable[[Any], Any] -) -> Callable[[Callable[P, T]], Callable[[], T]]: ... - -@overload -def script_argparse(func: Callable[P, T], /) -> Callable[[], T]: ... - -def script_argparse( - func=None, - *, - ignore_keys: Optional[set[str]] = None, - ignore_mapping: Optional[set[str]] = None, - setup_cwd: Optional[bool] = None, - aliases: Optional[Mapping[str, list[str]]] = None, - use_glob: Optional[set[str]] = None, - use_custom: Optional[Mapping[str, Callable[[Any], Any]]] = None, - partial_parse: Optional[bool] = None, - partial_parse_pass_remaining_args: Optional[bool] = None, - add_help: Optional[bool] = None, - on_help: Optional[Callable[[WithArgparse], Any]] = None, - **kwargs: Callable[[Any], Any] -): - decorator_func = with_argparse( - ignore_keys=ignore_keys, - ignore_mapping=ignore_mapping, - setup_cwd=setup_cwd, - aliases=aliases, - use_glob=use_glob, - use_custom=use_custom, - partial_parse=partial_parse, - partial_parse_pass_remaining_args=partial_parse_pass_remaining_args, - add_help=add_help, - on_help=on_help, - **kwargs, - ) - - argparse_func = decorator_func(func) - argparse_result = argparse_func() - - def producer_func(): - return argparse_result - return producer_func diff --git a/with_argparse/main.py b/with_argparse/main.py index a66c2fa..f8120a2 100644 --- a/with_argparse/main.py +++ b/with_argparse/main.py @@ -42,7 +42,7 @@ def __attrs_post_init__(self): @attrs.define class partial_argparse: # noqa state: bool = attrs.field(init=False) - remaining: list[str] = attrs.field(init=False) + remainder: list[str] = attrs.field(init=False, factory=list) def __enter__(self) -> Self: self.state = _global_state.partial @@ -53,12 +53,14 @@ def __enter__(self) -> Self: def __call__(self, parsed: Namespace, remaining: list[str]): # after hooks have been called, the remaining args are available from the # context manager variable - self.remaining = remaining - _global_state.parse_hooks.remove(self) + self.remainder = remaining + if self in _global_state.parse_hooks: + _global_state.parse_hooks.remove(self) def __exit__(self, exc_type, exc_val, exc_tb): _global_state.partial = self.state - _global_state.parse_hooks.remove(self) + if self in _global_state.parse_hooks: + _global_state.parse_hooks.remove(self) # todo: add __copy__ that does a shallow copy of this class # and store those copies in the WithArgparse instances @@ -98,17 +100,79 @@ def with_attrs[**P, T]( def with_attrs(func: Callable | None = None, *, parse_args: ParseArgs | None = None, strict: bool = True): + return _with_argparse(func, parse_args=parse_args, strict=strict, func_type="attrs") + +@overload +def with_dataclass[**P, T](func: Callable[P, T], /) -> Callable[[], T]: + ... + +@overload +def with_dataclass[**P, T]( + *, + parse_args: ParseArgs | None = None, + strict: Literal[True] = True, +) -> Callable[[Callable[P, T]], Callable[[], T]]: + ... + +@overload +def with_dataclass[**P, T]( + *, + parse_args: ParseArgs | None = None, + strict: bool = False, +) -> Callable[[Callable[P, T]], Callable[..., T]]: + ... + + +def with_dataclass(func: Callable | None = None, *, parse_args: ParseArgs | None = None, strict: bool = True): + return _with_argparse(func, parse_args=parse_args, strict=strict, func_type="dataclass") + + +@overload +def with_argparse[**P, T](func: Callable[P, T], /) -> Callable[[], T]: + ... + +@overload +def with_argparse[**P, T]( + *, + parse_args: ParseArgs | None = None, + strict: Literal[True] = True, +) -> Callable[[Callable[P, T]], Callable[[], T]]: + ... + +@overload +def with_argparse[**P, T]( + *, + parse_args: ParseArgs | None = None, + strict: bool = False, +) -> Callable[[Callable[P, T]], Callable[..., T]]: + ... + + +def with_argparse(func: Callable | None = None, *, parse_args: ParseArgs | None = None, strict: bool = True): + return _with_argparse(func, parse_args=parse_args, strict=strict, func_type="infer") + + +def _with_argparse( + func: Callable | None = None, + *, + parse_args: ParseArgs | None = None, + strict: bool = True, + func_type: Literal["attrs", "dataclass", "plain", "infer"] = "infer", +): # decorator-decorator path def decorator(decorated_func: Callable): @functools.wraps(decorated_func) def wrapper(*args, **kwargs): + if _internal_global_state().disabled: + return decorated_func(*args, **kwargs) + from with_argparse.configure_argparse import WithArgparse if strict and (len(args) > 0 or len(kwargs) > 0): raise TypeError("In strict mode, arguments cannot be passed to the decorated dataclass function") - wa = WithArgparse(decorated_func, "infer", strict=strict, parse_args=parse_args) + wa = WithArgparse(decorated_func, func_type, strict=strict, parse_args=parse_args) return wa.call(args, kwargs) return wrapper @@ -117,6 +181,26 @@ def wrapper(*args, **kwargs): else: return decorator(func) +@overload +def script_argparse[**P, T](func: Callable[P, T], /) -> T: + ... + +@overload +def script_argparse[**P, T]( + *, + parse_args: ParseArgs | None = None, +) -> Callable[[Callable[P, T]], T]: + ... + +def script_argparse(func: Callable | None = None, *, parse_args: ParseArgs | None = None): + if func is None: # parse_args was specified + def wrapper(decorated_func: Callable): + configured_argparse = with_argparse(parse_args=parse_args, strict=True) + return configured_argparse(decorated_func)() + return wrapper + else: + return with_argparse(func)() + def script_argparse(): raise NotImplementedError \ No newline at end of file From e7bc6a281fce243ef4957cd964c2ad8574c8ef96 Mon Sep 17 00:00:00 2001 From: fleonce Date: Mon, 27 Jul 2026 21:36:03 +0200 Subject: [PATCH 5/5] Fix script_argparse being overridden by a NotImplemented function --- with_argparse/__init__.py | 3 ++- with_argparse/main.py | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/with_argparse/__init__.py b/with_argparse/__init__.py index 82aa9ca..964202e 100644 --- a/with_argparse/__init__.py +++ b/with_argparse/__init__.py @@ -5,6 +5,7 @@ with_argparse, with_attrs, with_dataclass, + ParseArgs, ) -__all__ = ["with_argparse", "no_argparse", "with_dataclass", "script_argparse", "with_attrs", "partial_argparse"] +__all__ = ["with_argparse", "no_argparse", "with_dataclass", "script_argparse", "with_attrs", "partial_argparse", "ParseArgs"] diff --git a/with_argparse/main.py b/with_argparse/main.py index f8120a2..abbf8f2 100644 --- a/with_argparse/main.py +++ b/with_argparse/main.py @@ -181,6 +181,7 @@ def wrapper(*args, **kwargs): else: return decorator(func) + @overload def script_argparse[**P, T](func: Callable[P, T], /) -> T: ... @@ -200,7 +201,3 @@ def wrapper(decorated_func: Callable): return wrapper else: return with_argparse(func)() - - -def script_argparse(): - raise NotImplementedError \ No newline at end of file