This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Added Bundle and Oreo packages

+4199 -1708
+4
oreo/__init__.py
··· 1 + from .functions import * 2 + from .variables import * 3 + from .oreo import hydrox 4 + from .confirm import Confirm, ConfirmOreo
+40
oreo/confirm.py
··· 1 + from rich.prompt import PromptBase, InvalidResponse 2 + from rich.text import Text 3 + 4 + 5 + # Adapted From: https://github.com/Textualize/rich/blob/master/rich/prompt.py#L322 6 + class Confirm(PromptBase[bool]): 7 + """A yes / no confirmation prompt. 8 + Example: 9 + >>> if Confirm.ask("Continue"): 10 + run_job() 11 + """ 12 + 13 + response_type = bool 14 + validate_error_message = "[prompt.invalid]Please enter Y/Yes or N/No" 15 + choices: list[str] = ["y", "yes", "n", "no"] 16 + 17 + def render_default(self, default) -> Text: 18 + """Render the default as (y) or (n) rather than True/False.""" 19 + y, yes, n, no = self.choices 20 + return Text( 21 + f"({y})/({yes})" if default else f"({n})/({no})", style="prompt.default" 22 + ) 23 + 24 + def process_response(self, value: str) -> bool: 25 + """Convert choices to a bool.""" 26 + value = value.strip().lower() 27 + if value not in self.choices: 28 + raise InvalidResponse(self.validate_error_message) 29 + return value == self.choices[0] 30 + 31 + 32 + class ConfirmOreo(Confirm): 33 + prompt_suffix = "" 34 + 35 + def __init__( 36 + self, *args, show_default: bool = False, show_choices: bool = False, **kwargs 37 + ): 38 + super().__init__( 39 + *args, show_default=show_default, show_choices=show_choices, **kwargs 40 + )
+23
oreo/functions.py
··· 1 + from itertools import product, chain 2 + from string import ascii_lowercase as lower 3 + 4 + 5 + def product_intersperse(a, *args, b="", **kwargs): 6 + for seq in product( 7 + args, 8 + (a, b, *(kwargs[c] for c in lower if c in kwargs)), 9 + repeat=len(args), 10 + ): 11 + if all( 12 + False 13 + for i, s in enumerate(item for item in seq if item in args) 14 + if args[i] != s 15 + ): 16 + yield [item for item in seq if item != ""] 17 + 18 + 19 + def powerset_intersperse(a, *args, **kwargs): 20 + return chain.from_iterable( 21 + product_intersperse(a, *args[: index + 1], **kwargs) 22 + for index in range(len(args)) 23 + )
+232
oreo/oreo.py
··· 1 + #!/usr/bin/env python3 2 + 3 + # Adapted From: 4 + # Answer: https://stackoverflow.com/a/50824001 5 + # User: https://stackoverflow.com/users/9931167/phrreakk 6 + 7 + import rich_click as click 8 + 9 + # Keeping this separate allows me to override the options and arguments 10 + # passed to the application, as importing `argv` directly sets the arguments 11 + # when the module is first initialized. This also happens when I put calls to `argv` 12 + # in the global namespace and not in a function. 13 + import sys 14 + 15 + from addict import Dict 16 + from bundle import bundle, Bundle 17 + from contextlib import contextmanager 18 + from functools import partial 19 + from hydrox.helpers import flatten 20 + from itertools import dropwhile 21 + from more_itertools import split_at 22 + from rich import print 23 + from thefuzz import process 24 + from unittest import mock 25 + from rich.pretty import pprint 26 + 27 + from .confirm import ConfirmOreo 28 + 29 + 30 + # Adapted From: https://realpython.com/inherit-python-list/#inheriting-from-pythons-built-in-list-class 31 + class Args(list): 32 + def __init__(self, args): 33 + super().__init__(map(flatten, args)) 34 + 35 + def __getitem__(self, index): 36 + try: 37 + return super().__getitem__(index) 38 + except IndexError: 39 + return [] 40 + 41 + def __setitem__(self, index, item): 42 + super().__setitem__(index, flatten(item)) 43 + 44 + def insert(self, index, item): 45 + super().insert(index, flatten(item)) 46 + 47 + def append(self, item): 48 + super().append(flatten(item)) 49 + 50 + def extend(self, other): 51 + if isinstance(other, self.__class__): 52 + return super().extend(other) 53 + return super().extend(flatten(item) for item in other) 54 + 55 + 56 + # Adapted From: https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases 57 + class FuzzedGroup(click.RichGroup): 58 + def get_command(self, ctx, cmd_name): 59 + commands = self.list_commands(ctx) 60 + return super().get_command(ctx, cmd_name) or super().get_command( 61 + ctx, process.extractOne(cmd_name, commands)[0] if commands else "" 62 + ) 63 + 64 + def resolve_command(self, ctx, args): 65 + _, cmd, args = super().resolve_command( 66 + ctx, 67 + list(dropwhile(lambda arg: arg.startswith("-"), args)), 68 + ) 69 + return cmd.name, cmd, args 70 + 71 + # def make_context( 72 + # self, 73 + # info_name: Optional[str], 74 + # args: list[str], 75 + # parent: Optional[click.Context] = None, 76 + # **extra: Any, 77 + # ) -> click.Context: 78 + # return super().make_context( 79 + # info_name, 80 + # list(dropwhile(lambda arg: arg.startswith("-"), args)), 81 + # parent, 82 + # **extra, 83 + # ) 84 + 85 + 86 + types = ("double-stuf", "mini", "thins") 87 + flavors = ("carrot-cake", "golden") 88 + click_kwargs = {"cls": FuzzedGroup, "no_args_is_help": False} 89 + 90 + # Adapted From: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams 91 + # And: https://github.com/pallets/click/issues/1370#issuecomment-522549260 92 + # echo "Hello!" | ./test.py 93 + # ./test.py <<< "Hello!" 94 + # ./test.py < <(echo "Hello!") 95 + # ./test.py < tmpfile 96 + 97 + 98 + def oreo(argv=tuple(), *args, **kwargs): 99 + argv = argv or sys.argv 100 + 101 + # Adapted From: 102 + # Answer: https://stackoverflow.com/a/53541456 103 + # User: https://stackoverflow.com/users/10553976/charles-landau 104 + # And: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams 105 + # And: https://github.com/pallets/click/issues/1370#issuecomment-522549260 106 + if not (argv[0] is None or sys.stdin.isatty()): 107 + argv = [argv[0], *sys.stdin.read().strip().split(), *argv[1:]] 108 + 109 + # Adapted From: 110 + # Answer 1: https://stackoverflow.com/a/27765993 111 + # User 1: https://stackoverflow.com/users/211734/jason-antman 112 + with mock.patch.object(sys, "argv", argv): 113 + opts = [ 114 + f"--{opt}" for opt in ("list", "password", "prompt", "stderr", "stdout") 115 + ] 116 + items = [] 117 + skip = False 118 + for item in argv: 119 + if skip: 120 + skip = False 121 + elif item == "--code": 122 + skip = True 123 + elif item not in opts: 124 + items.append(item) 125 + _argvs = Args(split_at(items[1:], lambda arg: not arg.startswith("-"))) 126 + if len(_argvs) > 2: 127 + argvs = Args(_argvs[:2]) 128 + argvs.append(_argvs[2:]) 129 + else: 130 + argvs = _argvs 131 + root = len(argvs) < 2 132 + flavored = len(argvs) == 2 133 + 134 + obj = Dict() 135 + 136 + def stdprint(*args, **kwargs): 137 + args = ("oreo", *args) 138 + if obj.password or obj.prompt: 139 + ConfirmOreo.ask( 140 + ("\n" if obj.list else " ").join(args), 141 + password=obj.password, 142 + show_choices=False, 143 + ) 144 + else: 145 + for std in ("out", "err"): 146 + if obj[std]: 147 + kwargs.pop("file", None) 148 + if std == "err": 149 + kwargs["file"] = getattr(sys, "stderr") 150 + fprint = partial(print, **kwargs) 151 + if obj.list: 152 + for item in args: 153 + fprint(item) 154 + else: 155 + fprint(*args) 156 + if obj.code is not None: 157 + exit(obj.code) 158 + 159 + @bundle(name="oreo", **click_kwargs) 160 + @bundle.up("args", disabled=not root) 161 + def f( 162 + code: int, 163 + _list: bool, 164 + prompt: bool, 165 + password: bool, 166 + stdout: bool, 167 + stderr: bool, 168 + *args, 169 + ): 170 + obj.code = code 171 + obj.list = _list 172 + obj.password = password 173 + obj.prompt = prompt 174 + obj.err = stderr 175 + obj.out = stdout or not (obj.err or stdout) 176 + if root: 177 + stdprint(*args) 178 + 179 + f.print = stdprint 180 + 181 + # Adapted From: 182 + # Answer: https://stackoverflow.com/a/49631500 183 + # User: https://stackoverflow.com/users/379311/yann-vernier 184 + def type_wrapper(flavor, type): 185 + @flavor.up(name=type, **click_kwargs) 186 + def f1(*args): 187 + flavor.print(type, *args) 188 + 189 + return f1 190 + 191 + def flavor_wrapper(flavor): 192 + fprint = partial(f.print, *argvs[0], flavor, *argvs[1]) 193 + 194 + @f.up(name=flavor, **click_kwargs) 195 + @bundle.up("args", disabled=not flavored) 196 + def f2(*args): 197 + if flavored: 198 + fprint() 199 + 200 + f2.print = fprint 201 + return f2 202 + 203 + globals().update( 204 + {type.replace("-", "_"): type_wrapper(f, type) for type in types} 205 + ) 206 + 207 + for flavor in flavors: 208 + f2 = flavor_wrapper(flavor) 209 + globals()[flavor.replace("-", "_")] = f2 210 + globals().update( 211 + { 212 + f"{flavor}-{type}".replace("-", "_"): type_wrapper(f2, type) 213 + for type in types 214 + } 215 + ) 216 + 217 + if argv[0] is None: 218 + return f.compile() 219 + return f.flip(*args, **kwargs) 220 + 221 + 222 + # Adapted From: 223 + # Answer: https://stackoverflow.com/a/27765993 224 + # User: https://stackoverflow.com/users/211734/jason-antman 225 + @contextmanager 226 + def hydrox(*args): 227 + with mock.patch.object(sys, "argv", (None, *args)): 228 + yield Bundle.runner.invoke(oreo(sys.argv), args) 229 + 230 + 231 + if __name__ == "__main__": 232 + oreo()
+124
oreo/tests/test_app.py
··· 1 + import os 2 + 3 + from addict import Dict 4 + from itertools import chain 5 + from oreo import powerset_intersperse, building, hydrox 6 + from parametrized import parametrized 7 + from pytest import mark 8 + 9 + from subprocess import run 10 + 11 + oreos = Dict( 12 + args=("c", "d"), 13 + flavor="carrot-cake", 14 + type="double-stuf", 15 + ) 16 + oreos.joint.args = (oreos.flavor, oreos.type) 17 + 18 + # NOTE: This is not a mistake; `powerset_intersperse` takes a `b` keyword argument 19 + oreos.kwargs.b = oreos.joint.kwargs.b = "-0" 20 + 21 + oreos.powerset = list(powerset_intersperse("", "oreo", *oreos.args, **oreos.kwargs)) 22 + oreos.superpowerset = list( 23 + chain( 24 + oreos.powerset, 25 + ((*o, "e") for o in oreos.powerset if "c" in o and "d" in o), 26 + ((*o, "e", "-0") for o in oreos.powerset if "c" in o and "d" in o), 27 + ((*o, "e", "-0", "f") for o in oreos.powerset if "c" in o and "d" in o), 28 + ) 29 + ) 30 + oreos.joint.powerset = oreos.noreo.joint.powerset = list( 31 + powerset_intersperse("", "oreo", *oreos.joint.args, **oreos.joint.kwargs) 32 + ) 33 + oreos.joint.superpowerset = oreos.noreo.joint.superpowerset = list( 34 + chain( 35 + oreos.joint.powerset, 36 + ( 37 + (*o, "e") 38 + for o in oreos.joint.powerset 39 + if oreos.flavor in o and oreos.type in o 40 + ), 41 + ( 42 + (*o, "e", "-0") 43 + for o in oreos.joint.powerset 44 + if oreos.flavor in o and oreos.type in o 45 + ), 46 + ( 47 + (*o, "e", "-0", "f") 48 + for o in oreos.joint.powerset 49 + if oreos.flavor in o and oreos.type in o 50 + ), 51 + ) 52 + ) 53 + oreos.noreo.powerset = [powerset[1:] for powerset in oreos.powerset] 54 + oreos.noreo.superpowerset = list( 55 + chain( 56 + oreos.noreo.powerset, 57 + ((*o, "e") for o in oreos.noreo.powerset if "c" in o and "d" in o), 58 + ((*o, "e", "-0") for o in oreos.noreo.powerset if "c" in o and "d" in o), 59 + ((*o, "e", "-0", "f") for o in oreos.noreo.powerset if "c" in o and "d" in o), 60 + ) 61 + ) 62 + 63 + # Adapted From: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams 64 + # And: https://github.com/pallets/click/issues/1370#issuecomment-522549260 65 + # echo "Jeet" | ./test.py 66 + # ./test.py <<< "Jeet" 67 + # ./test.py < <(echo "Jeet") 68 + # ./test.py < tmpfile 69 + 70 + 71 + @mark.oreo 72 + @mark.oreo_runner 73 + class TestRunner: 74 + @parametrized.zip 75 + def test_oreos( 76 + self, 77 + args=oreos.noreo.superpowerset, 78 + Oreo=[" ".join(o) for o in oreos.noreo.joint.superpowerset], 79 + ): 80 + with hydrox(*args) as oreo: 81 + assert oreo.output.strip() == Oreo, oreo.stderr.strip() 82 + 83 + @parametrized.zip 84 + def test_list( 85 + self, 86 + args=oreos.noreo.superpowerset, 87 + Oreo=["\n".join(o) for o in oreos.noreo.joint.superpowerset], 88 + ): 89 + with hydrox("--list", *args) as oreo: 90 + assert oreo.output.strip() == Oreo, oreo.stderr.strip() 91 + 92 + 93 + # Adapted From: https://github.com/dstathis/fastprocess/issues/5#issuecomment-938794618 94 + @mark.oreo 95 + @mark.oreo_subprocess 96 + @mark.skipif(building, reason="oreo app isn't available while building oreo") 97 + class TestSubprocess: 98 + @parametrized.zip 99 + def test_oreos( 100 + self, 101 + args=oreos.superpowerset, 102 + Oreo=[" ".join(o) for o in oreos.joint.superpowerset], 103 + ): 104 + output = run(args, capture_output=True) 105 + print(output.stdout) 106 + assert output.stdout.decode().strip() == Oreo, output.stderr.decode().strip() 107 + # r, w = os.pipe() 108 + # pid = FastProcess(args, stdout=os.fdopen(w)) 109 + # pid.wait() 110 + # assert os.fdopen(r).read().strip() == Oreo 111 + 112 + @parametrized.zip 113 + def test_list( 114 + self, 115 + args=oreos.superpowerset, 116 + Oreo=[os.linesep.join(o) for o in oreos.joint.superpowerset], 117 + ): 118 + output = run((args[0], "--list", *args[1:]), capture_output=True) 119 + print(output.stdout) 120 + assert output.stdout.decode().strip() == Oreo, output.stderr.decode().strip() 121 + # r, w = os.pipe() 122 + # pid = FastProcess((args[0], "--list", *args[1:]), stdout=os.fdopen(w)) 123 + # pid.wait() 124 + # assert os.fdopen(r).read().strip() == Oreo
+32
oreo/variables.py
··· 1 + from os import environ 2 + 3 + __all__ = ("building",) 4 + 5 + # Adapted From: 6 + # Answer: https://stackoverflow.com/a/68938778 7 + # User: https://stackoverflow.com/users/14030287/ariadne-paradis 8 + building = any( 9 + True 10 + for var in ( 11 + "NIX_BINTOOLS", 12 + "NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu", 13 + "NIX_BUILD_CORES", 14 + "NIX_BUILD_TOP", 15 + "NIX_CC", 16 + "NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu", 17 + "NIX_CFLAGS_COMPILE", 18 + "NIX_ENFORCE_NO_NATIVE", 19 + "NIX_ENFORCE_PURITY", 20 + "NIX_HARDENING_ENABLE", 21 + "NIX_INDENT_MAKE", 22 + "NIX_LDFLAGS", 23 + "NIX_LOG_FD", 24 + "NIX_SSL_CERT_FILE", 25 + "NIX_STORE", 26 + ) 27 + if var in environ 28 + ) or any( 29 + True 30 + for var in ("TMP", "TMPDIR", "TEMP", "TEMPDIR") 31 + if environ.get(var, "/tmp") == "/build" 32 + )
+3 -1319
packages/bundle/bundle/__init__.py
··· 1 - import operator 2 - import os 3 - import rich_click as click 4 - 5 - from addict import Dict 6 - from autoslot import SlotsMeta 7 - from beartype import beartype 8 - from beartype.door import is_bearable, die_if_unbearable 9 - from collections import defaultdict 10 - from collections.abc import Callable 11 - from contextlib import suppress 12 - from copy import deepcopy 13 - from docstring_parser import parse 14 - from functools import partial, reduce, wraps 15 - from inspect import signature, Parameter, isclass 16 - from itertools import chain 17 - from more_itertools import partition 18 - from plum import dispatch, Dispatcher 19 - from rich.pretty import pprint 20 - from rich_click.rich_help_formatter import RichHelpFormatter 21 - from string import ascii_lowercase as lower, ascii_uppercase as upper 22 - from types import EllipsisType, MappingProxyType 23 - from typing import ( 24 - Any, 25 - get_args, 26 - get_origin, 27 - Literal, 28 - Optional, 29 - Union, 30 - ) 31 - 32 - from .pipe import pipe, PipeArgs 33 - from .python import ( 34 - can_unpack_variable, 35 - deepfreeze, 36 - dirs, 37 - flatten, 38 - normalize, 39 - normalizeMultiline, 40 - pk_variables, 41 - plumeta, 42 - process, 43 - ) 44 - from .typing import ( 45 - ArgReturn, 46 - Attr, 47 - BString, 48 - Callback, 49 - Coll, 50 - Count, 51 - Eager, 52 - Obj, 53 - Origin, 54 - Pass, 55 - Password, 56 - Split, 57 - subtype, 58 - Switch, 59 - ValueReturn, 60 - ) 61 - from .model import BaseModel 62 - 63 - 64 - class Parent(metaclass=plumeta): 65 - @property 66 - def kwargs(self): 67 - kwargs = self.parent.kwargs | self.cls.kwargs 68 - kwargs.pop("name", None) 69 - return kwargs 70 - 71 - def __init__(self): 72 - self.cls = Dict() 73 - self.func = click 74 - 75 - def __init__(self, cls: "bundle"): 76 - self.cls = cls 77 - 78 - def __init__(self, cls: "Parent"): 79 - self.cls = cls.cls 80 - 81 - def __getattr__(self, attr): 82 - return getattr(self.cls, attr) 83 - 84 - def __bool__(self): 85 - return bool(self.func) 86 - 87 - 88 - @dispatch(precedence=1) 89 - def process_bundle( 90 - annotation: Any, 91 - value: None, 92 - ) -> None: ... 93 - 94 - 95 - @dispatch 96 - def process_bundle( 97 - annotation: Origin[Callback, Eager], 98 - value: Any, 99 - ): 100 - return process_bundle(get_args(annotation)[0], value) 101 - 102 - 103 - @dispatch 104 - def process_bundle( 105 - annotation: Origin[Literal], 106 - value: ValueReturn, 107 - ) -> ArgReturn | ValueReturn: 108 - for arg in get_args(annotation): 109 - if value in (arg, str(arg)): 110 - return arg 111 - die_if_unbearable(value, annotation) 112 - 113 - 114 - @dispatch 115 - def process_bundle( 116 - annotation: Origin[Split], 117 - value: BString, 118 - ) -> Coll: 119 - annotation, delimiter = get_args(annotation) 120 - return process_bundle(annotation, value.split(delimiter)) 121 - 122 - 123 - @dispatch 124 - def process_bundle( 125 - annotation: Origin[Split], 126 - value: Coll, 127 - ) -> Coll: 128 - annotation, delimiter = get_args(annotation) 129 - return process_bundle(annotation, flatten(item.split(delimiter) for item in value)) 130 - 131 - 132 - for method in process._pending + process._resolved: 133 - process_bundle.register(method[0], precedence=method[2]) 134 - 135 - 136 - class Model(BaseModel): 137 - def __init__(self, *args, **kwargs): 138 - self.__processor__ = process_bundle 139 - super().__init__(*args, **kwargs) 140 - 141 - 142 - names = ("__name__", "__qualname__") 143 - 144 - 145 - def deflambda(name, f=None): 146 - f = f or (lambda: None) 147 - for attr in names: 148 - setattr(f, attr, name) 149 - return f 150 - 151 - 152 - class Bundle(SlotsMeta, metaclass=plumeta): 153 - def __new__(cls, name, bases, ns, subclass=False): 154 - if subclass: 155 - cls._dunders = tuple() 156 - ns["__slots__"] = chain( 157 - tuple() if bases else ("__weakref__",), 158 - cls._dunders, 159 - names, 160 - ( 161 - "docstring", 162 - "superparams", 163 - "signature", 164 - "_func", 165 - "print", 166 - ), 167 - ) 168 - cls._specopts = {} 169 - cls._specdefs = Dict(disabled=False) 170 - cls._pathvars = ["PATH", "PATHS"] 171 - cls._defaults = Dict( 172 - option={"metavar": str.__name__, "show_default": True}, 173 - group={ 174 - "invoke_without_command": True, 175 - "no_args_is_help": True, 176 - }, 177 - ) 178 - cls._defaults.group.context_settings.help_option_names = ("-h", "--help") 179 - cls._globals = Dict() 180 - cls._class_roots = Dict() 181 - cls._class_instances = Dict() 182 - cls._roots = Dict() 183 - cls._instances = Dict() 184 - return super().__new__(cls, name, bases, ns) 185 - return subtype(name, bundler, ns) 186 - 187 - class __prepare__(dict): 188 - def __init__(self, name, bases, subclass=False): 189 - self.subclass = subclass 190 - if self.subclass: 191 - self.dispatcher = Dispatcher() 192 - super().__setitem__("_dispatcher", self.dispatcher) 193 - 194 - def __setitem__(self, key, value): 195 - if callable(value) and self.subclass: 196 - args, kwargs = getattr(value, "__plume__", (tuple(), {})) 197 - if not kwargs.get("disabled", False): 198 - value = getattr(self.get(key), "dispatch", self.dispatcher)( 199 - value, *args, **kwargs 200 - ) 201 - super().__setitem__(key, value) 202 - 203 - def drive(cls, func: Callable, *args, **kwargs): 204 - return cls(func).quark(*args, **kwargs) 205 - 206 - def drive(cls, **kwargs): 207 - return cls().quark(**kwargs) 208 - 209 - def up(cls, name: str, *args, **kwargs): 210 - def wrapper(self): 211 - cls._specopts[self][name] = Dict( 212 - {k: kwargs.pop(k, v) for k, v in cls._specdefs.items()} 213 - ) 214 - setattr( 215 - self, 216 - "__bundle__", 217 - getattr(self, "__bundle__", Dict()) 218 - | Dict(params={name: {"args": args, "kwargs": kwargs}}), 219 - ) 220 - return self 221 - 222 - return wrapper 223 - 224 - def up( 225 - cls, 226 - self: Union["Bundler", "bundle"], 227 - parent: Optional[Union["Bundler", "bundle"]] = None, 228 - **kwargs, 229 - ) -> Union["bundle", type["bundler"]]: 230 - self._rewrap(kwargs) 231 - if parent: 232 - parent._adopt(self) 233 - return self 234 - 235 - def up( 236 - cls, 237 - self: Callable, 238 - parent: None = None, 239 - **kwargs, 240 - ): 241 - name = kwargs.get("name", self.__name__) 242 - setattr( 243 - self, 244 - "__bundle__", 245 - getattr(self, "__bundle__", Dict()) | Dict(group=Dict(kwargs)), 246 - ) 247 - for attr in names: 248 - setattr(self, attr, name) 249 - return self 250 - 251 - def up( 252 - cls, 253 - parent: Optional[Union["Bundler", "bundle"]] = None, 254 - **kwargs, 255 - ): 256 - def wrapper(self: Callable): 257 - return cls.up(self, parent, **kwargs) 258 - 259 - return wrapper 260 - 261 - def __getattr__(cls, attr): 262 - return getattr(click, attr) 263 - 264 - 265 - class bundle(metaclass=Bundle, subclass=True): 266 - # TODO: There may be a few problems with the adoption... 267 - def __new__( 268 - cls, 269 - name: str, 270 - bases: tuple["bundle", ...], 271 - ns: dict | MappingProxyType, 272 - **kwargs, 273 - ): 274 - self = subtype(name, bundler, ns) 275 - self._rewrap(bases[0].kwargs) 276 - try: 277 - bases[0]._adopt(self) 278 - except AttributeError: 279 - with suppress(AttributeError, TypeError): 280 - bases[0].parent._adopt(self) 281 - return self 282 - 283 - def __new__( 284 - cls, 285 - self: "Bundler", 286 - parent: Optional[Union["Bundler", "bundle"]] = None, 287 - **kwargs, 288 - ): 289 - self._rewrap(kwargs) 290 - if parent: 291 - parent._adopt(self) 292 - return self 293 - 294 - def __new__( 295 - cls, 296 - self: type, 297 - parent: Optional[Union["Bundler", "bundle"]] = None, 298 - **kwargs, 299 - ): 300 - return cls.__new__(cls, subtype(self, bundler), parent, **kwargs) 301 - 302 - def __new__(cls, *args, **kwargs): 303 - return super().__new__(cls) 304 - 305 - class __prepare__(dict): 306 - def __init__(self, name, bases): 307 - pass 308 - 309 - def __init__( 310 - self, 311 - func: Callable, 312 - parent: Optional[Union[type["bundle"], "bundle", Parent]] = None, 313 - /, 314 - **kwargs, 315 - ): 316 - if isinstance(parent, (self.__class__, Parent)): 317 - self.parent = Parent(parent) 318 - self.root = self.parent.root 319 - else: 320 - self.parent = Parent() 321 - self.root = self 322 - 323 - self.children = Dict() 324 - self.kwargs = Dict(kwargs) 325 - self.func = None 326 - self._wrap(func) 327 - 328 - def __init__( 329 - self, 330 - parent: Union["bundle", Parent], 331 - kwargs: dict, 332 - ): 333 - self.parent = Parent(parent) 334 - self.root = self.parent.root 335 - self.children = Dict() 336 - self.kwargs = Dict(kwargs) 337 - self.func = None 338 - 339 - def __init__(self, **kwargs): 340 - self.parent = Parent() 341 - self.root = self 342 - self.children = Dict() 343 - self.kwargs = Dict(kwargs) 344 - self.func = None 345 - 346 - def __getattr__(self, attr): 347 - try: 348 - object.__getattribute__(self, "func") 349 - except AttributeError as e: 350 - raise AttributeError( 351 - f"'{self.__class__.__name__}' object has no attribute '{attr}'" 352 - ) from e 353 - else: 354 - try: 355 - return getattr(self.func, attr) 356 - except AttributeError as e: 357 - raise AttributeError( 358 - f"'{self.__class__.__name__}' and '{self.func.__class__.__name__}' objects have no attribute '{attr}'" 359 - ) from e 360 - 361 - def _run(self, arg): 362 - return is_bearable( 363 - arg, next(iter(self._func.__signature__.parameters.values())).annotation 364 - ) 365 - 366 - # TODO: There may be a few problems with the adoption... 367 - def __call__(self, cls: "Bundler"): 368 - if self._run(cls): 369 - return beartype(self._func)(cls) 370 - cls._rewrap(self.kwargs) 371 - try: 372 - self._adopt(cls) 373 - except AttributeError: 374 - with suppress(AttributeError, TypeError): 375 - self.parent._adopt(cls) 376 - return cls 377 - 378 - def __call__(self, cls: type): 379 - if self._run(cls): 380 - return beartype(self._func)(cls) 381 - return self(subtype(cls, bundler)) 382 - 383 - def __call__(self, func: Callable): 384 - if self.func is None: 385 - return self._wrap(func) 386 - if self._run(func): 387 - return beartype(self._func)(func) 388 - return self.__class__(func, self) 389 - 390 - def _run(self, args, kwargs): 391 - if args or not (args or kwargs): 392 - return True 393 - run_score = 0 394 - run_params = self._func.__signature__.parameters 395 - click_score = 0 396 - command_params = signature(click.Command).parameters 397 - group_params = signature(click.Group).parameters 398 - for k, v in kwargs.items(): 399 - if k in run_params: 400 - annotation = run_params[k].annotation 401 - if annotation is Parameter.empty or is_bearable(v, annotation): 402 - run_score += 1 403 - if k in command_params: 404 - command_annotation = command_params[k].annotation 405 - if command_annotation is Parameter.empty or is_bearable( 406 - v, command_annotation 407 - ): 408 - click_score += 1 409 - elif k in group_params: 410 - group_annotation = group_params[k].annotation 411 - if group_annotation is Parameter.empty or is_bearable( 412 - v, group_annotation 413 - ): 414 - click_score += 1 415 - return run_score >= click_score 416 - 417 - def __call__(self, *args, **kwargs): 418 - if self._run(args, kwargs): 419 - return beartype(self._func)(*args, **kwargs) 420 - return self.__class__(self, kwargs) 421 - 422 - def __del__(self): 423 - with suppress(AttributeError): 424 - for child in self.children.values(): 425 - del child 426 - 427 - def flip(self, **kwargs): 428 - return self.func(**kwargs) 429 - 430 - def _driver(self, parent: "bundle", func: Callable, name: str = ""): 431 - return self.__class__(func, parent, name=name) 432 - 433 - def _driver(self, parent: "bundle", args: Coll): 434 - for f in args: 435 - self._driver(parent, f) 436 - 437 - def _driver(self, parent: "bundle", kwargs: dict): 438 - for k, v in kwargs.items(): 439 - if isinstance(v, dict): 440 - self._driver(self._driver(parent, v.pop(k, deflambda(k)), k), v) 441 - elif isinstance(v, Coll): 442 - different_names, same_names = partition( 443 - lambda func: func.__name__ == k, v 444 - ) 445 - self._driver( 446 - self._driver(parent, next(same_names, deflambda(k)), k), 447 - different_names, 448 - ) 449 - else: 450 - self._driver(parent, v, k) 451 - 452 - def quark(self, *args, **kwargs): 453 - if self.func is None: 454 - self._wrap(args[0]) 455 - self._driver(self, args[1:]) 456 - else: 457 - self._driver(self, args) 458 - self._driver(self, kwargs) 459 - return self 460 - 461 - def quark(self, **kwargs): 462 - different_names = tuple() 463 - if self.func is None: 464 - if kwargs: 465 - name = self.kwargs.name = next(iter(kwargs.keys())) 466 - 467 - # TODO: If the first kwarg is not a callable, create a new root. 468 - # if isinstance(kwargs[name], dict): 469 - # func = kwargs[name].pop(name, deflambda(name)) 470 - # elif isinstance(kwargs[name], Coll): 471 - # different_names, same_names = partition( 472 - # lambda func: func.__name__ == name, kwargs[name] 473 - # ) 474 - # func = next(same_names, deflambda(name)) 475 - # else: 476 - # func = kwargs.pop(name, deflambda(name)) 477 - if callable(kwargs[name]): 478 - func = kwargs.pop(name, deflambda(name)) 479 - else: 480 - # NOTE: The name is for the dictionary or collection, not this. 481 - func = lambda: None 482 - 483 - else: 484 - func = lambda: None 485 - self._wrap(func) 486 - self._driver(self, different_names) 487 - self._driver(self, kwargs) 488 - return self 489 - 490 - def _adopt( 491 - self, 492 - cls: Union["bundle", "bundler", type["bundler"]], 493 - name: Optional[str] = None, 494 - ): 495 - name = name or cls.func.name 496 - cls = cls.__bundles__[name] if isinstance(cls, bundler) else cls 497 - self.func.commands[name] = cls.func 498 - self.children[name] = cls 499 - cls.parent = self 500 - 501 - def _filter_params( 502 - self, params: list[click.Parameter], param_type: type[click.Parameter] 503 - ): 504 - return list(filter(lambda param: isinstance(param, param_type), params)) 505 - 506 - @property 507 - def _arguments(self): 508 - return self._filter_params(self.params, click.Argument) 509 - 510 - @property 511 - def _options(self): 512 - return self._filter_params(self.params, click.Option) 513 - 514 - def _deepfreeze(self, x, *ignores): 515 - return deepfreeze( 516 - x, 517 - {t: lambda y: None for t in (Callable, RichHelpFormatter)}, 518 - ignores=ignores, 519 - ) 520 - 521 - def _equal( 522 - self, 523 - obj: click.Command | click.Parameter, 524 - other: click.Command | click.Parameter, 525 - *ignores, 526 - obj_name="", 527 - other_name="", 528 - ): 529 - ignores = (*ignores, "__dict__") 530 - dct = defaultdict(list) 531 - for k in dir(obj): 532 - if k not in ignores: 533 - dct[k].append( 534 - (obj_name or obj.name, self._deepfreeze(getattr(obj, k), *ignores)) 535 - ) 536 - dct[k].append( 537 - ( 538 - other_name or other.name, 539 - self._deepfreeze(getattr(other, k), *ignores), 540 - ) 541 - ) 542 - errors = {k: v for k, v in dct.items() if len({t[1] for t in v}) != 1} 543 - if errors: 544 - if "PYTEST_CURRENT_TEST" in os.environ: 545 - pprint(errors) 546 - return False 547 - return True 548 - 549 - def _equals(self, other: Union["Bundler", "bundle", click.Group], *ignores) -> bool: 550 - options = self._filter_params(other.params, click.Option) 551 - arguments = self._filter_params(other.params, click.Argument) 552 - return ( 553 - all( 554 - False 555 - for index, param in enumerate(self._arguments) 556 - if not self._equal( 557 - param, 558 - arguments[index], 559 - obj_name=f"{self.name}:{param.name}", 560 - other_name=f"{other.name}:{param.name}", 561 - ) 562 - ) 563 - and all( 564 - False 565 - for index, param in enumerate(self._options) 566 - if not self._equal( 567 - param, 568 - options[index], 569 - obj_name=f"{self.name}:{param.name}", 570 - other_name=f"{other.name}:{param.name}", 571 - ) 572 - ) 573 - and self._equal( 574 - self.func, 575 - other if isinstance(other, click.Group) else other.func, 576 - *ignores, 577 - "params", 578 - obj_name=f"self: {self.name}", 579 - other_name=f"other: {other.name}", 580 - ) 581 - # and all( 582 - # v._equals(other.commands[k], *ignores) if k in other.commands else False 583 - # for k, v in self.children.items() 584 - # ) 585 - and all( 586 - False 587 - for k, v in self.children.items() 588 - if not ( 589 - (k in other.commands) and v._equals(other.commands[k], *ignores) 590 - ) 591 - ) 592 - ) 593 - 594 - def equals(self, other: Union["Bundler", "bundle", click.Group], *ignores) -> bool: 595 - return self._equals(other, *ignores, "name") 596 - 597 - def __eq__(self, other: Union["Bundler", "bundle", click.Group]) -> bool: 598 - return ( 599 - self.equals(other) 600 - and self._func == other.callback 601 - # and all( 602 - # (v._func == other.commands[k].callback) 603 - # if k in other.commands 604 - # else False 605 - # for k, v in self.children.items() 606 - # ) 607 - and all( 608 - False 609 - for k, v in self.children.items() 610 - if not ( 611 - (k in other.commands) and (v._func == other.commands[k].callback) 612 - ) 613 - ) 614 - ) 615 - 616 - def _hash(self, *ignores): 617 - ignores = (*ignores, "params") 618 - return ( 619 - *[self._deepfreeze(dirs(arg)) for arg in self._arguments], 620 - *[self._deepfreeze(dirs(opt)) for opt in self._options], 621 - *[self._deepfreeze(dirs(self.func, ignores=ignores), *ignores)], 622 - *[child._hash(*ignores) for child in self.children.values()], 623 - ) 624 - 625 - def hash(self, *ignores): 626 - return hash(self._hash(*ignores, "name")) 627 - 628 - def __hash__(self): 629 - return hash( 630 - ( 631 - ( 632 - self.hash(), 633 - self._func, 634 - *(child._func for child in self.children.values()), 635 - ) 636 - ) 637 - ) 638 - 639 - def __eq__(self, other) -> bool: 640 - return False 641 - 642 - def _recursive_difference(self, before, after): 643 - difference = {} 644 - for k, v in after.items(): 645 - if k not in before: 646 - difference[k] = v 647 - elif before[k] != v: 648 - if isinstance(before[k], dict) and isinstance(v, dict): 649 - difference[k] = self._recursive_difference(before[k], v) 650 - elif not callable(before[k]) and not callable(v): 651 - difference[k] = v 652 - return difference 653 - 654 - # def _rewrap(self, **kwargs) -> "bundle": 655 - # subcommands = dict(self.func.commands) 656 - # self.kwargs |= kwargs 657 - # self._wrap() 658 - # kwargs.pop("name", None) 659 - # for child in self.children.values(): 660 - # child._rewrap(**kwargs) 661 - # self.func.commands = subcommands 662 - # return self 663 - 664 - def _rewrap(self, *args, **kwargs) -> "bundle": 665 - kwargs = reduce(operator.or_, [Dict(d) for d in (*args, kwargs)]) 666 - self.kwargs |= kwargs 667 - for k, v in self._make_group_kwargs().items(): 668 - setattr(self.func, k, v) 669 - kwargs.pop("name", None) 670 - for child in self.children.values(): 671 - child._rewrap(kwargs) 672 - return self 673 - 674 - def _make_group_kwargs(self, base_kwargs: Optional[dict] = None, **kwargs): 675 - return reduce( 676 - operator.or_, 677 - ( 678 - self.__class__._defaults.group, 679 - base_kwargs or {}, 680 - self.__class__._globals.group, 681 - self.__class__._roots[self.root._func].group, 682 - self.__class__._instances[self._func].group, 683 - self.parent.kwargs, 684 - getattr(self._func, "__bundle__", Dict()).group, 685 - self.kwargs, 686 - kwargs, 687 - ), 688 - ) 689 - 690 - # Adapted From: 691 - # Answer 1: https://stackoverflow.com/a/12627202 692 - # User 1: https://stackoverflow.com/users/748858/mgilson 693 - # Answer 2: https://stackoverflow.com/a/50177498 694 - # User 2: https://stackoverflow.com/users/2867928/mazdak 695 - def _wrap(self, func: Optional[Callable] = None, **kwargs) -> "bundle": 696 - if func: 697 - self.docstring = parse(func.__doc__) if func.__doc__ else Dict() 698 - self.signature = func.__signature__ = signature(func) 699 - self._func = func 700 - for attr in self.__class__._dunders: 701 - setattr(self, attr, getattr(self._func, attr)) 702 - self.superparams = Dict( 703 - disabled=[ 704 - k for k, v in self.__class__._specopts[func].items() if v.disabled 705 - ], 706 - pre={ 707 - param.name: param 708 - for param in getattr(func, "__click_params__", tuple()) 709 - }, 710 - # TODO: Test this. 711 - help={param.arg_name: param for param in self.docstring.params}, 712 - ) 713 - else: 714 - func = self._func 715 - 716 - ignore_and_accept = False 717 - for k, v in self.signature.parameters.items(): 718 - if v.kind is Parameter.VAR_KEYWORD: 719 - self.superparams.disabled.append(k) 720 - ignore_and_accept = True 721 - if k not in self.superparams.disabled: 722 - self.superparams.working[k] = v 723 - if k in self.superparams.pre: 724 - if v.annotation is not Parameter.empty: 725 - self.superparams.pre[k].callback = self._make_callback( 726 - k, v.annotation, func, self.superparams.pre[k].callback 727 - ) 728 - if k in self.superparams.help: 729 - self.superparams.pre[k].help = self.superparams.help[k] 730 - elif k not in self.superparams.disabled: 731 - self.superparams.processing[k] = v 732 - 733 - envvarg = False 734 - # Adapted From: 735 - # Answer: https://stackoverflow.com/a/47017849 736 - # User: https://stackoverflow.com/users/1583052/dipu 737 - for i, (k, v) in enumerate( 738 - dict(reversed(self.superparams.processing.items())).items() 739 - ): 740 - func = self._get_funky( 741 - len(self.superparams.processing) - 1 - i, 742 - k, 743 - v, 744 - func, 745 - )(func) 746 - self.superparams.processed[func.__click_params__[-1]] = v 747 - if k.isupper() and v.kind in ( 748 - Parameter.POSITIONAL_ONLY, 749 - Parameter.VAR_POSITIONAL, 750 - ): 751 - envvarg = True 752 - 753 - for param, value in dict(reversed(self.superparams.processed.items())).items(): 754 - if ( 755 - isinstance(param, click.Option) 756 - and not param.name.startswith("__") 757 - and not any( 758 - filter( 759 - self._shortform_predicate, 760 - flatten(param.opts, param.secondary_opts), 761 - ) 762 - ) 763 - ): 764 - forms = self._get_name(param.name, value, func, True) 765 - param.opts.append(forms[0]) 766 - if len(forms) > 1: 767 - param.secondary_opts.append(forms[1]) 768 - 769 - wrapped = normalize( 770 - func, 771 - exclude=self.superparams.disabled, 772 - ) 773 - 774 - # Adapted From: 775 - # Answer: https://stackoverflow.com/a/24734171 776 - # User: https://stackoverflow.com/users/681785/famousgarkin 777 - if envvarg: 778 - current_locals = locals().copy() 779 - current_locals["Dict"] = Dict 780 - lower_params = [str.lower(param) for param in self.superparams.working] 781 - exec( 782 - normalizeMultiline( 783 - f""" 784 - def wrapper({",".join(lower_params)}): 785 - return wrapped({",".join(lower_params)}) 786 - """ 787 - ), 788 - current_locals, 789 - ) 790 - wrapped = current_locals["wrapper"] 791 - 792 - base_kwargs = Dict() 793 - if ignore_and_accept: 794 - base_kwargs.context_settings = Dict( 795 - ignore_unknown_options=True, 796 - allow_extra_args=True, 797 - ) 798 - 799 - self.func = self.parent.func.group( 800 - **self._make_group_kwargs(base_kwargs, **kwargs), 801 - )(wraps(func)(wrapped)) 802 - self.parent.children[self.func.name] = self 803 - for attr in names: 804 - setattr(self, attr, self.func.name) 805 - return self 806 - 807 - def _longform_predicate(self, arg): 808 - return arg.startswith("--") 809 - 810 - def _get_longform(self, name: str, *args): 811 - return f"--{name.strip('_').lower().replace('_', '-')}" 812 - 813 - def _shortform_predicate(self, arg): 814 - return arg.startswith("-") and not arg.startswith("--") 815 - 816 - def _get_shortform(self, name: str, func: Callable): 817 - first_letter = name.strip("_")[0] 818 - 819 - if hasattr(func, "__click_params__"): 820 - popts = { 821 - opt[1] 822 - for opt in flatten( 823 - (param.opts, param.secondary_opts) 824 - for param in func.__click_params__ 825 - ) 826 - if self._shortform_predicate(opt) 827 - } 828 - if first_letter in popts: 829 - try: 830 - first_index = lower.index(first_letter) 831 - except ValueError: 832 - first_index = upper.index(first_letter) 833 - i = first_index 834 - while first_letter in popts: 835 - first_letter = first_letter.swapcase() 836 - if first_letter in popts: 837 - i = (i + 1) % 26 838 - if i == first_index: 839 - raise click.UsageError( 840 - "no more lowercase or uppercase letters left for shortform names" 841 - ) 842 - first_letter = lower[i] 843 - else: 844 - break 845 - 846 - return f"-{first_letter}" 847 - 848 - def _get_name( 849 - self, name: str, value: Parameter, func: Callable, shortform: bool = False 850 - ): 851 - if shortform: 852 - processor = self._get_shortform 853 - predicate = self._shortform_predicate 854 - else: 855 - processor = self._get_longform 856 - predicate = self._longform_predicate 857 - 858 - default = processor(name, func) 859 - if get_origin(value.annotation) is Switch: 860 - default += f"/{processor(get_args(value.annotation)[0], func)}" 861 - 862 - # Adapted From: 863 - # Answer: https://stackoverflow.com/a/9542768 864 - # User: https://stackoverflow.com/users/916657/niklas-b 865 - form = next( 866 - filter(predicate, getattr(func, "__bundle__", Dict()).params[name].args), 867 - default, 868 - ) 869 - if "/" in form: 870 - return form.split("/") 871 - return [form] 872 - 873 - def _get_allforms(self, func): 874 - return flatten( 875 - (param.name, param.human_readable_name, param.opts, param.secondary_opts) 876 - for param in getattr(func, "__click_params__", tuple()) 877 - ) 878 - 879 - def _get_shortforms(self, func): 880 - return list(filter(self._shortform_predicate, self._get_allforms(func))) 881 - 882 - def _get_longforms(self, func): 883 - return list(filter(self._longform_predicate, self._get_allforms(func))) 884 - 885 - def _get_forms(self, func): 886 - return [ 887 - param 888 - for param in self._get_allforms(func) 889 - if param.startswith("--") or not param.startswith("-") 890 - ] 891 - 892 - def _trysubclass(self, annotation: Any, *types): 893 - try: 894 - return issubclass(annotation, types) 895 - except TypeError: 896 - return is_bearable(annotation, types) 897 - 898 - def _callback(self, annotation, ctx, param, value, *, debug=False): 899 - if debug: 900 - print(annotation, ctx, param, value) 901 - return ctx, param, process_bundle(annotation, value) 902 - 903 - def _make_callback( 904 - self, 905 - name: str, 906 - annotation: Any, 907 - func: Callable, 908 - precallback: Optional[Callable] = None, 909 - ): 910 - return pipe( 911 - partial(self._callback, annotation, debug=False), 912 - precallback, 913 - *( 914 - get_args(annotation)[1:] 915 - if get_origin(annotation) is Callback 916 - else tuple() 917 - ), 918 - getattr(func, "__bundle__", Dict()).params[name].kwargs.callback, 919 - lambda ctx, param, value: value, 920 - cls=PipeArgs, 921 - ) 922 - 923 - def _make_kwargs( 924 - self, 925 - name: str, 926 - annotation: Any, 927 - default: Any, 928 - func: Callable, 929 - kwargs: dict, 930 - ): 931 - kwargs = Dict(kwargs) 932 - if default is Parameter.empty: 933 - if name.isupper(): 934 - kwargs.envvar = name 935 - else: 936 - kwargs.default = default 937 - return ( 938 - kwargs 939 - | getattr(func, "__bundle__", Dict()).params[name].kwargs 940 - | {"callback": self._make_callback(name, annotation, func)} 941 - ) 942 - 943 - def _default_annotation(self, value: Parameter): 944 - default = value.default 945 - annotation = value.annotation 946 - if annotation is Parameter.empty and default is not Parameter.empty: 947 - annotation = type(default) 948 - elif get_origin(annotation) is Switch: 949 - annotation = bool 950 - default = False 951 - return default, annotation 952 - 953 - # TODO: Implement and test 954 - def _get_funky( 955 - self, 956 - index: int, 957 - name: Literal["password"], 958 - value: Attr[Parameter.empty, "annotation"], 959 - func: Callable, 960 - ): 961 - return click.password_option 962 - 963 - def _get_funky( 964 - self, 965 - index: int, 966 - name: str, 967 - value: Attr[type[Password], "annotation"], 968 - func: Callable, 969 - ): 970 - return click.option( 971 - *self._get_name(name, value, func), 972 - prompt=True, 973 - hide_input=True, 974 - confirmation_prompt=True, 975 - ) 976 - 977 - def _get_funky(self, index: Literal[0], name: Literal["ctx"], *args): 978 - return click.pass_context 979 - 980 - def _get_funky( 981 - self, 982 - index: Literal[0], 983 - name: str, 984 - value: Attr[Origin[Pass], "annotation"], 985 - func: Callable, 986 - ): 987 - return click.make_pass_decorator(get_args(value.annotation)[0], ensure=True) 988 - 989 - def _get_funky( 990 - self, 991 - index: Literal[0], 992 - name: str, 993 - value: Attr[type[Obj], "annotation"], 994 - func: Callable, 995 - ): 996 - return click.pass_obj 997 - 998 - def _get_funky( 999 - self, 1000 - index: int, 1001 - name: str, 1002 - value: Attr[ 1003 - Literal[ 1004 - Parameter.POSITIONAL_ONLY, 1005 - Parameter.VAR_POSITIONAL, 1006 - ], 1007 - "kind", 1008 - ], 1009 - func: Callable, 1010 - ): 1011 - default, annotation = self._default_annotation(value) 1012 - 1013 - # TODO: Should arguments be required by default? 1014 - # opts = Dict(required=default is Parameter.empty and value.kind is Parameter.POSITIONAL_ONLY) 1015 - opts = Dict() 1016 - 1017 - args = get_args(annotation) 1018 - if self._trysubclass(annotation, Model): 1019 - opts.nargs = len(annotation.__annotations__) 1020 - elif args and self._trysubclass(get_origin(annotation), tuple): 1021 - opts.nargs = -1 if ... in args or EllipsisType in args else len(args) 1022 - else: 1023 - variable = value.kind is Parameter.VAR_POSITIONAL or self._trysubclass( 1024 - annotation, Coll 1025 - ) 1026 - opts = Dict(nargs=-1 if variable else 1) 1027 - with suppress(ValueError): 1028 - sig = signature(annotation) 1029 - opts.nargs = ( 1030 - -1 1031 - if variable or can_unpack_variable(sig) 1032 - else (len(pk_variables(sig)) or 1) 1033 - ) 1034 - 1035 - kwargs = self._make_kwargs( 1036 - name, 1037 - annotation, 1038 - default, 1039 - func, 1040 - reduce( 1041 - operator.or_, 1042 - ( 1043 - self.__class__._defaults.argument, 1044 - opts, 1045 - self.__class__._globals.argument, 1046 - self.__class__._roots[self.root._func].argument, 1047 - self.__class__._instances[self._func].argument, 1048 - ), 1049 - ), 1050 - ) 1051 - if kwargs.nargs != 1 and kwargs.envvar in self.__class__._pathvars: 1052 - kwargs.type = kwargs.type or click.Path() 1053 - return click.argument( 1054 - name, 1055 - **kwargs, 1056 - ) 1057 - 1058 - def _get_funky( 1059 - self, 1060 - index: int, 1061 - name: str, 1062 - value: Attr[ 1063 - Literal[ 1064 - Parameter.KEYWORD_ONLY, 1065 - Parameter.POSITIONAL_OR_KEYWORD, 1066 - ], 1067 - "kind", 1068 - ], 1069 - func: Callable, 1070 - ): 1071 - default, annotation = self._default_annotation(value) 1072 - origin = get_origin(annotation) or annotation 1073 - opts = Dict( 1074 - required=default is Parameter.empty 1075 - and value.kind is Parameter.KEYWORD_ONLY, 1076 - count=annotation is Count, 1077 - is_eager=origin is Eager, 1078 - help=self.superparams.help[name].description or None, 1079 - ) 1080 - 1081 - args = get_args(annotation) 1082 - if self._trysubclass(annotation, Model): 1083 - opts.nargs = len(annotation.__annotations__) 1084 - elif args and self._trysubclass(origin, tuple): 1085 - if ... in args or EllipsisType in args: 1086 - opts.multiple = True 1087 - else: 1088 - opts.nargs = len(args) 1089 - else: 1090 - opts.multiple = self._trysubclass(annotation, Coll) 1091 - 1092 - if ( 1093 - not self._trysubclass(annotation, bool) 1094 - and annotation is not Parameter.empty 1095 - ): 1096 - opts.metavar = annotation.__name__ 1097 - with suppress(ValueError): 1098 - sig = signature(annotation) 1099 - opts.multiple = opts.multiple or can_unpack_variable(sig) 1100 - if not opts.multiple: 1101 - opts.nargs = len(pk_variables(sig)) or 1 1102 - with suppress(TypeError): 1103 - if issubclass(annotation, bool): 1104 - opts.is_flag = True 1105 - kwargs = self._make_kwargs( 1106 - name, 1107 - annotation, 1108 - default, 1109 - func, 1110 - reduce( 1111 - operator.or_, 1112 - ( 1113 - self.__class__._defaults.option, 1114 - opts, 1115 - self.__class__._globals.option, 1116 - self.__class__._roots[self.root._func].option, 1117 - self.__class__._instances[self._func].option, 1118 - ), 1119 - ), 1120 - ) 1121 - if kwargs.multiple and kwargs.envvar in self.__class__._pathvars: 1122 - kwargs.type = kwargs.type or click.Path() 1123 - return click.option( 1124 - name, 1125 - "/".join(self._get_name(name, value, func)), 1126 - **kwargs, 1127 - ) 1128 - 1129 - 1130 - class Bundler(type, metaclass=plumeta): 1131 - def __new__( 1132 - cls, name: str, bases: tuple[Callable, ...], ns: dict | MappingProxyType 1133 - ): 1134 - result = super().__new__(cls, name, bases, ns) 1135 - for key, value in result.__classes__.items(): 1136 - if isinstance(value, Bundler): 1137 - result._adopt(value, key) 1138 - else: 1139 - result.__bundles__[key] = subtype(key, result, value.__dict__) 1140 - 1141 - # NOTE: This returns a `bundle`, not a `bundler`. 1142 - result._rewrap(getattr(result, "__bundle__", Dict()).group) 1143 - return result 1144 - 1145 - def __getattr__(cls, attr): 1146 - return getattr(cls.__cls__, attr) 1147 - 1148 - def __eq__(cls, other: Union["Bundler", "bundle", click.Group]) -> bool: 1149 - return cls.__cls__.__eq__(other) 1150 - 1151 - def __eq__(self, other) -> bool: 1152 - return False 1153 - 1154 - def __hash__(cls): 1155 - return cls.__cls__.__hash__() 1156 - 1157 - class __prepare__(dict): 1158 - @property 1159 - def cls(self): 1160 - return self.get("__cls__") 1161 - 1162 - @cls.setter 1163 - def cls(self, value): 1164 - super().__setitem__("__cls__", value) 1165 - 1166 - def __init__(self, name, bases): 1167 - self.name = name 1168 - self.bases = bases 1169 - self.names = ("__cls__", self.name) 1170 - self.root = not bases or bases[0] is bundler 1171 - self.parent = bundle if self.root else bases[0].__cls__ 1172 - self.cls = bundle(deflambda(self.name), self.parent, name=self.name) 1173 - self.bundles = Dict({self.name: self.cls}) 1174 - super().__setitem__("__bundles__", self.bundles) 1175 - self.classes = Dict() 1176 - super().__setitem__("__classes__", self.classes) 1177 - self.dispatcher = Dispatcher() 1178 - super().__setitem__("__dispatcher__", self.dispatcher) 1179 - self.original_dict = Dict() 1180 - super().__setitem__("__original_dict__", self.original_dict) 1181 - 1182 - def update(self, other: dict): 1183 - for k, v in other.items(): 1184 - self[k] = v 1185 - return self 1186 - 1187 - def __ior__(self, other: dict): 1188 - return self.update(other) 1189 - 1190 - def __or__(self, other: dict): 1191 - return deepcopy(self).update(other) 1192 - 1193 - def __ror__(self, other: dict): 1194 - return ( 1195 - self.__class__(self.name, self.bases) 1196 - .update(other) 1197 - .update(self.original_dict) 1198 - ) 1199 - 1200 - def __setitem__(self, key, value): 1201 - self.original_dict[key] = value 1202 - if callable(value): 1203 - if key.startswith("__") and key.endswith("__"): 1204 - args, kwargs = getattr(value, "__plume__", (tuple(), {})) 1205 - if not kwargs.get("disabled", False): 1206 - value = getattr(self.get(key), "dispatch", self.dispatcher)( 1207 - value, *args, **kwargs 1208 - ) 1209 - elif isclass(value): 1210 - return self.classes.__setitem__(key, value) 1211 - else: 1212 - kwargs = Dict(getattr(value, "__bundle__", {})).group 1213 - if key == self.name: 1214 - if isinstance(value, (bundle, bundler)): 1215 - if not self.root: 1216 - self.parent._adopt(value, key) 1217 - else: 1218 - name = kwargs.get("name", self.name) 1219 - 1220 - # Adapted From: 1221 - # Answer: https://stackoverflow.com/a/3655857 1222 - # User: https://stackoverflow.com/users/95810 1223 - if value.__name__ == (lambda: None).__name__: 1224 - for attr in names: 1225 - setattr(value, attr, name) 1226 - 1227 - value = bundle(value, self.parent, name=name) 1228 - for k, v in self.bundles.items(): 1229 - if not (isclass(v) or k in self.names): 1230 - value._adopt(v, k) 1231 - self.cls = value 1232 - elif isinstance(value, (bundle, bundler)): 1233 - self.cls._adopt(value, key) 1234 - else: 1235 - name = kwargs.get("name", key) 1236 - 1237 - # Adapted From: 1238 - # Answer: https://stackoverflow.com/a/3655857 1239 - # User: https://stackoverflow.com/users/95810 1240 - if value.__name__ == (lambda: None).__name__: 1241 - for attr in names: 1242 - setattr(value, attr, name) 1243 - 1244 - value = bundle(value, self.cls, name=name) 1245 - return self.bundles.__setitem__(key, value) 1246 - return super().__setitem__(key, value) 1247 - 1248 - 1249 - class bundler(metaclass=Bundler): 1250 - # TODO: There may be a few problems with the adoption... 1251 - def __new__( 1252 - cls, 1253 - name: str, 1254 - bases: tuple["bundler", ...], 1255 - ns: dict | MappingProxyType, 1256 - **kwargs, 1257 - ): 1258 - self = subtype(name, cls, ns) 1259 - self._rewrap(bases[0].__kwargs__) 1260 - try: 1261 - bases[0]._adopt(self) 1262 - except AttributeError: 1263 - with suppress(AttributeError, TypeError): 1264 - bases[0].parent._adopt(self) 1265 - return self 1266 - 1267 - def __new__( 1268 - cls, 1269 - self: "Bundler", 1270 - parent: Optional[Union["Bundler", "bundle"]] = None, 1271 - **kwargs, 1272 - ): 1273 - self._rewrap(kwargs) 1274 - if parent: 1275 - parent._adopt(self) 1276 - return self 1277 - 1278 - def __new__( 1279 - cls, 1280 - self: type, 1281 - parent: Optional[Union["Bundler", "bundle"]] = None, 1282 - **kwargs, 1283 - ): 1284 - return cls.__new__(cls, subtype(self, cls), parent, **kwargs) 1285 - 1286 - def __new__( 1287 - cls, 1288 - self: Callable, 1289 - parent: Optional[Union["Bundler", "bundle"]] = None, 1290 - **kwargs, 1291 - ): 1292 - return cls.__new__( 1293 - cls, subtype(self.__name__, cls, {self.__name__: self}), parent, **kwargs 1294 - ) 1295 - 1296 - def __new__(cls, *args, **kwargs): 1297 - if cls._run(args, kwargs) and cls is not bundler: 1298 - return beartype(cls._func)(*args, **kwargs) 1299 - return super().__new__(cls) 1300 - 1301 - class __prepare__(dict): 1302 - def __init__(self, name, bases): 1303 - pass 1304 - 1305 - def __init__(self, **kwargs): 1306 - self.__kwargs__ = Dict(kwargs) 1307 - 1308 - def __getattr__(self, attr): 1309 - return getattr(self.__cls__, attr) 1310 - 1311 - def __call__(self, cls: "Bundler"): 1312 - cls._rewrap(self.__kwargs__) 1313 - return cls 1314 - 1315 - def __call__(self, cls: type): 1316 - return self(subtype(cls, self.__class__)) 1317 - 1318 - def __call__(self, func: Callable): 1319 - return self(subtype(func.__name__, self.__class__, {func.__name__: func})) 1 + from .functions import * 2 + from .bundle import bundle, bundler, Bundle 3 + from .types import *
+1030
packages/bundle/bundle/bundle.py
··· 1 + import rich_click as click 2 + from click.testing import CliRunner 3 + from addict import Dict 4 + from autoslot import SlotsMeta 5 + from hydrox.helpers import ( 6 + as_decorator, 7 + query, 8 + flatten, 9 + _normalize as normalize, 10 + Collection, 11 + ) 12 + from hydrox.typing import ( 13 + anything, 14 + isinmrosubclass, 15 + partition_ellipsis, 16 + format_name, 17 + reducehash, 18 + gformat_name, 19 + generalize, 20 + get_name, 21 + ) 22 + import hydrox.typing as ht 23 + from jugulis import funcdict, Hydreigon, Deino, iscallableclass 24 + import re 25 + from functools import partial, cached_property, reduce, cache 26 + from inspect import Signature, Parameter, isclass 27 + import collections.abc as abc 28 + from string import ascii_lowercase as lower, ascii_uppercase as upper 29 + import typing 30 + import os 31 + import sys 32 + from collections import defaultdict, ChainMap 33 + from docstring_parser import Docstring 34 + from .functions import reducedict, eq, rich_repr 35 + from .types import Pass, Password 36 + from random import choice 37 + from rich.console import Console 38 + 39 + # TODO: Should `__qualname__` be checked as well if it has no `.`? 40 + 41 + 42 + class Bundles(dict): 43 + def __getattr__(self, attr): 44 + return self[attr] 45 + 46 + 47 + class bundler(Bundles): 48 + def __init__(self, start, stop, *types, name="f", shared_name="g"): 49 + self.start = start 50 + self.stop = stop 51 + self.rstop = self.stop + 1 52 + self.types = types or (int, str) 53 + self.name = name 54 + self.shared_name = shared_name 55 + self.under_share = self.shared_name + "_" 56 + self.dunder_share = self.shared_name + "__" 57 + super().__init__() 58 + tn = get_name(self.types[0]) 59 + exec( 60 + f"@bundle\ndef {self.name}({tn[0]}: {tn}) -> {tn}: ...", 61 + locals=ChainMap(self, {"bundle": bundle}), 62 + ) 63 + exec( 64 + f"@{self.name}.up\n@bundle\ndef {self.under_share}({tn[0]}: {tn}) -> {tn}: ...", 65 + locals=ChainMap(self, {"bundle": bundle}), 66 + ) 67 + exec( 68 + f"@{self.name}.up\n@bundle\ndef {self.dunder_share}({tn[0]}: {tn}) -> {tn}: ...", 69 + locals=ChainMap(self, {"bundle": bundle}), 70 + ) 71 + for t in self.types[1:]: 72 + tn = get_name(t) 73 + exec( 74 + f"@{self.name}.up\ndef {self.name}({tn[0]}: {tn}) -> {tn}: ...", 75 + locals=self, 76 + ) 77 + exec( 78 + f"@{self.name}.up\n@{self.under_share}.up\ndef {self.under_share}({tn[0]}: {tn}) -> {tn}: ...", 79 + locals=ChainMap(self, {"bundle": bundle}), 80 + ) 81 + exec( 82 + f"@{self.name}.up\n@{self.dunder_share}.up\ndef {self.dunder_share}({tn[0]}: {tn}) -> {tn}: ...", 83 + locals=ChainMap(self, {"bundle": bundle}), 84 + ) 85 + self.root = self[self.name] 86 + self.init(self.name) 87 + 88 + def __eq__(self, other): 89 + if isinstance(other, self.__class__): 90 + return ( 91 + (self.start == other.start) 92 + and (self.stop == other.stop) 93 + and (self.types == other.types) 94 + and (self.name == other.name) 95 + and (self.shared_name == other.shared_name) 96 + ) 97 + return NotImplemented 98 + 99 + def __hash__(self): 100 + return reducehash( 101 + self.__class__.__name__, 102 + self.start, 103 + self.stop, 104 + self.types, 105 + self.name, 106 + self.shared_name, 107 + ) 108 + 109 + def init(self, name=None, levels=None): 110 + name = self.name if name is None else name 111 + levels = self.start if levels is None else levels 112 + names = [] 113 + for i in range(self.start, self.rstop): 114 + n = name + str(i) 115 + names.append(n) 116 + for t in self.types: 117 + tn = get_name(t) 118 + exec(f"@{name}.up\ndef {n}({tn[0]}: {tn}) -> {tn}: ...", locals=self) 119 + if levels < self.stop: 120 + self.init(n, levels=levels + 1) 121 + for t in self.types: 122 + tn = get_name(t) 123 + exec( 124 + "@" 125 + + ".up\n@".join(names) 126 + + f".up\ndef {name.replace(self.name, self.shared_name)}({tn[0]}: {tn}) -> {tn}: ...", 127 + locals=self, 128 + ) 129 + 130 + @cached_property 131 + def functions(self): 132 + return Bundles( 133 + reduce( 134 + dict.__or__, 135 + ( 136 + { 137 + k + get_name(d.__signature__.return_annotation)[0]: d.__func__ 138 + for d in v.func 139 + } 140 + for k, v in self.items() 141 + ), 142 + ) 143 + ) 144 + 145 + @cached_property 146 + def function_lists(self): 147 + return Bundles( 148 + reduce( 149 + dict.__or__, 150 + ({k: [d.__func__ for d in v.func]} for k, v in self.items()), 151 + ) 152 + ) 153 + 154 + @cache 155 + def type(self, *types): 156 + bundles = Bundles( 157 + { 158 + k: bundle(*[self.functions[k + get_name(t)[0]] for t in types]) 159 + for k in self 160 + } 161 + ) 162 + bundles[self.name].up(bundles[self.under_share]) 163 + bundles[self.name].up(bundles[self.dunder_share]) 164 + for k, v in bundles.items(): 165 + if k.startswith(self.name) and k != self.name: 166 + bundles[k[0:-1]].up(v) 167 + v.up(bundles[k.replace(self.name, self.shared_name)[0:-1]]) 168 + return bundles 169 + 170 + @cache 171 + def cls(self, name=None): 172 + name_was_none = name is None 173 + bundles = Bundles() if name_was_none else {} 174 + name = self.name if name_was_none else name 175 + bases = (bundle,) 176 + names = (name, "__init__") 177 + namespace = Bundle.__prepare__(name, bases) 178 + for f in self.function_lists[name]: 179 + namespace[choice(names)] = f 180 + ns = [] 181 + for i in range(self.start, self.rstop): 182 + n = name + str(i) 183 + if n in self: 184 + ns.append(n) 185 + cls = self.cls(name=n) 186 + bundles.update(cls) 187 + namespace[n] = cls[n] 188 + bundles[name] = type(name, bases, namespace) 189 + if name_was_none: 190 + under_names = (self.under_share, "__init__") 191 + under_namespace = f"\n\t{choice(under_names)} = ".join( 192 + (self.under_share + get_name(t)[0]) for t in self.types 193 + ) 194 + exec( 195 + f"class {self.under_share}({self.name}):\n\t{choice(under_names)} = {under_namespace}", 196 + globals=self.functions, 197 + locals=bundles, 198 + ) 199 + dunder_names = (self.dunder_share, "__init__") 200 + dunder_namespace = f"\n\t{choice(dunder_names)} = ".join( 201 + (self.dunder_share + get_name(t)[0]) for t in self.types 202 + ) 203 + exec( 204 + f"class {self.dunder_share}({self.name}):\n\t{choice(dunder_names)} = {dunder_namespace}", 205 + globals=self.functions, 206 + locals=bundles, 207 + ) 208 + if ns: 209 + shared_n = name.replace(self.name, self.shared_name) 210 + shared_names = (shared_n, "__init__") 211 + shared_namespace = f"\n\t{choice(shared_names)} = ".join( 212 + (shared_n + get_name(t)[0]) for t in self.types 213 + ) 214 + exec( 215 + f"class {shared_n}({', '.join(ns)}):\n\t{choice(shared_names)} = {shared_namespace}", 216 + globals=self.functions, 217 + locals=bundles, 218 + ) 219 + return bundles 220 + 221 + 222 + class Bundle(SlotsMeta): 223 + class __prepare__(dict): 224 + def __init__(self, _name, _bases, *args, _subclass=False, **click_kwargs): 225 + self.name = _name 226 + self.bases = _bases 227 + self.subclass = _subclass 228 + self.click_kwargs = click_kwargs 229 + if not _subclass: 230 + self.bundle = bundled = bundle(**({"name": _name} | click_kwargs)) 231 + self.name = name = bundled.__name__ 232 + self.names = (_name, name) 233 + for arg in args: 234 + for k, v in dict(arg).items(): 235 + self[k] = v 236 + 237 + def __setitem__(self, key, value): 238 + if not (self.subclass or isinstance(value, bundle)) and iscallableclass( 239 + value 240 + ): 241 + if key in self.names: 242 + key = "__init__" 243 + value = self.bundle.append( 244 + value, name=self.name if key == "__init__" else key 245 + ) 246 + super().__setitem__(key, value) 247 + 248 + def update(self, other): 249 + for k, v in dict(other).items(): 250 + self[k] = v 251 + 252 + def __ior__(self, other): 253 + self.update(other) 254 + 255 + def copy(self): 256 + result = self.__class__( 257 + self.name, self.bases, self.subclass, **self.click_kwargs 258 + ) 259 + result.update(self) 260 + return result 261 + 262 + def __or__(self, other): 263 + result = self.copy() 264 + result.update(other) 265 + return result 266 + 267 + # TODO: Use this `__ror__` method with the other dictionary subclasses. 268 + def __ror__(self, other): 269 + if isinstance(other, self.__class__): 270 + result = other.copy() 271 + result.update(self) 272 + else: 273 + result = self.copy() 274 + result.update(other) 275 + return result 276 + 277 + def __new__(_cls, _name, _bases, _namespace, _subclass=False, **click_kwargs): 278 + if _subclass: 279 + return super().__new__(_cls, _name, _bases, _namespace) 280 + return _bases[0](_namespace, **({"name": _name} | click_kwargs)) 281 + 282 + defaults = Dict( 283 + { 284 + "argument": {}, 285 + "option": {"metavar": gformat_name(str), "show_default": True}, 286 + "group": { 287 + "invoke_without_command": True, 288 + "no_args_is_help": True, 289 + "context_settings": { 290 + "help_option_names": ("-h", "--help"), 291 + "ignore_unknown_options": True, 292 + "allow_extra_args": True, 293 + }, 294 + }, 295 + } 296 + ) 297 + runner = CliRunner() 298 + 299 + def __subclasscheck__(self, subclass): 300 + return isinmrosubclass(subclass, click.Group) or super().__subclasscheck__( 301 + subclass 302 + ) 303 + 304 + @staticmethod 305 + def shortform_predicate(name, arg): 306 + return name != arg and len(arg) == 2 and re.match("^[A-Za-z_]+$", arg[-1]) 307 + 308 + @staticmethod 309 + def get_longform(name: str): 310 + return f"--{name.strip('_').lower().replace('_', '-')}" 311 + 312 + @staticmethod 313 + def process_value(value, annotation): 314 + try: 315 + return value if anything(annotation, str, tuple) else annotation(value) 316 + except Exception: 317 + return value 318 + 319 + def get_shortform(cls, name: str, params: list[click.Parameter]): 320 + first_letter = name.strip("_")[0] 321 + 322 + popts = { 323 + opt[1] 324 + for opt in flatten((param.opts, param.secondary_opts) for param in params) 325 + if cls.shortform_predicate(name, opt) 326 + } 327 + if first_letter in popts: 328 + try: 329 + first_index = lower.index(first_letter) 330 + except ValueError: 331 + first_index = upper.index(first_letter) 332 + i = first_index 333 + while first_letter in popts: 334 + first_letter = first_letter.swapcase() 335 + if first_letter in popts: 336 + i = (i + 1) % 26 337 + if i == first_index: 338 + raise click.UsageError( 339 + "no more lowercase or uppercase letters left for shortform names" 340 + ) 341 + first_letter = lower[i] 342 + else: 343 + break 344 + 345 + return f"-{first_letter}" 346 + 347 + @staticmethod 348 + def up(*args, **kwargs): 349 + def wrapper(func): 350 + nonlocal args 351 + args = list(args) 352 + for arg in args: 353 + if re.match("^[A-Za-z_]+$", arg): 354 + name = arg 355 + break 356 + else: 357 + raise click.UsageError("bundle.up must get a name, not just options") 358 + if hasattr(func, "__bundled_args__"): 359 + func.__bundled_args__[name] = args 360 + else: 361 + func.__bundled_args__ = {name: args} 362 + if hasattr(func, "__bundled_kwargs__"): 363 + func.__bundled_kwargs__[name] |= kwargs 364 + else: 365 + func.__bundled_kwargs__ = Dict({name: kwargs}) 366 + return func 367 + 368 + return wrapper 369 + 370 + @staticmethod 371 + def process_clicker(kind): 372 + return ( 373 + "argument" 374 + if kind in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL) 375 + else "option" 376 + ) 377 + 378 + def process_callback(cls, ctx, param, value, *, annotation, is_option, callback): 379 + # TODO 380 + # if issubclass(typing.get_origin(annotation), ht.tuple): 381 + if isinmrosubclass(typing.get_origin(annotation), tuple): 382 + args = typing.get_args(annotation) 383 + if ... in args: 384 + pre_args, post_args = partition_ellipsis(args) 385 + pre_length, post_length = len(pre_args), len(post_args) 386 + if len(value) < (pre_length + post_length): 387 + if is_option: 388 + raise click.UsageError( 389 + f"Option '{next(iter(set(param.opts) & set(sys.argv)))}' requires at least {(pre_length or 1) + post_length} arguments." 390 + ) 391 + else: 392 + raise click.UsageError( 393 + f"Argument '{param.name}' takes at least {(pre_length or 1) + post_length} values." 394 + ) 395 + 396 + value = cls.process_value(value, annotation) 397 + return callback(ctx, param, value) if callback else value 398 + 399 + def process_kwargs(cls, name, param: Parameter, bundled_kwargs, docstring): 400 + annotation = param.annotation 401 + kind = param.kind 402 + kwargs = {} 403 + default = param.default 404 + if default is Parameter.empty: 405 + if name.isupper(): 406 + kwargs["envvar"] = name 407 + else: 408 + kwargs["default"] = default 409 + if flatten.set((kwargs | bundled_kwargs).envvar) & { 410 + "PATH", 411 + "PATHS", 412 + }: 413 + kwargs["type"] = click.Path() 414 + bundled_callback = bundled_kwargs.pop("callback", None) 415 + if kind is Parameter.POSITIONAL_ONLY: 416 + kwargs["nargs"] = -1 if issubclass(annotation, ht.Coll) else 1 417 + defaults = bundle.defaults["argument"] 418 + elif kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): 419 + if kind is Parameter.KEYWORD_ONLY: 420 + kwargs["required"] = param.default is Parameter.empty 421 + kwargs["multiple"] = issubclass(annotation, ht.Coll) 422 + defaults = bundle.defaults["option"] 423 + elif kind is Parameter.VAR_POSITIONAL: 424 + kwargs["nargs"] = -1 425 + defaults = bundle.defaults["argument"] 426 + elif kind is Parameter.VAR_KEYWORD: 427 + kwargs["multiple"] = True 428 + defaults = bundle.defaults["option"] 429 + if is_option := cls.process_clicker(kind) == "option": 430 + kwargs["metavar"] = docstring.type_name 431 + 432 + kwargs["callback"] = partial( 433 + cls.process_callback, 434 + annotation=annotation, 435 + is_option=is_option, 436 + callback=bundled_callback, 437 + ) 438 + 439 + origin = typing.get_origin(annotation) 440 + args = typing.get_args(annotation) 441 + # TODO: Test this entire section. 442 + 443 + # TODO: 444 + # if issubclass(annotation, ht.Literal): 445 + if origin == ht.Literal: 446 + if is_option: 447 + kwargs["type"] = click.Choice(args) 448 + else: 449 + 450 + def callback(ctx, param: click.Option | click.Argument, value): 451 + if value not in args: 452 + option = next(iter(set(sys.argv) & set(param.opts))) 453 + raise click.UsageError( 454 + f"""Invalid value for '{option}': '{value}' is not one of '{"', '".join(args)}'.""" 455 + ) 456 + if bundled_callback: 457 + return bundled_callback(ctx, param, value) 458 + return value 459 + 460 + kwargs["callback"].keywords["callback"] = callback 461 + kwargs["metavar"] = kwargs["metavar"] or f"[{'|'.join(args)}]" 462 + 463 + if is_option: 464 + kwargs["metavar"] = kwargs["metavar"] or format_name(annotation) 465 + 466 + # TODO 467 + # if issubclass(annotation, ht.bool): 468 + if isinmrosubclass(annotation, bool): 469 + if is_option: 470 + kwargs["is_flag"] = True 471 + else: 472 + kwargs["callback"] = lambda ctx, param, value: value in ( 473 + "True", 474 + "1", 475 + ) 476 + 477 + # TODO 478 + # if issubclass(typing.get_origin(annotation), ht.tuple): 479 + if isinmrosubclass(origin, tuple): 480 + if ... in args: 481 + if is_option: 482 + kwargs["multiple"] = True 483 + else: 484 + kwargs["nargs"] = -1 485 + else: 486 + kwargs["multiple"] = False 487 + kwargs["nargs"] = len(args) 488 + return ( 489 + defaults 490 + | kwargs 491 + | {k: v for k, v in bundled_kwargs.items() if k not in ("disabled",)} 492 + ) 493 + 494 + @staticmethod 495 + def process_param(param: Parameter): 496 + any_annotation = anything(param.annotation) 497 + if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD): 498 + return ( 499 + param.replace(annotation=generalize(tuple)) if any_annotation else param 500 + ) 501 + elif any_annotation: 502 + return param.replace(annotation=generalize(str)) 503 + else: 504 + return param 505 + 506 + # TODO: Test these methods. 507 + def __getattr__(cls, attr): 508 + if attr in ("argument", "option"): 509 + 510 + def wrapper(*args, **kwargs): 511 + def wrapped(func): 512 + if not kwargs.pop("disabled"): 513 + deino: Deino 514 + if isinstance(func, Hydreigon): 515 + deino = func[0] 516 + else: 517 + if not isinstance(func, Deino): 518 + func = Deino( 519 + func, 520 + default_va_annotation=typing.Any, 521 + default_vk_annotation=typing.Any, 522 + ) 523 + deino = func 524 + param = getattr(click, attr.capitalize())(args) 525 + name = param.name 526 + for k, v in cls.process_kwargs( 527 + name, 528 + deino.__signature__.parameters[name], 529 + Dict(kwargs), 530 + deino.__docstring__, 531 + ).items(): 532 + setattr(param, k, v) 533 + if hasattr(func, "__click_params__"): 534 + func.__click_params__.append(param) 535 + else: 536 + func.__click_params__ = [param] 537 + return func 538 + 539 + return wrapped 540 + 541 + else: 542 + 543 + def wrapper(*args, **kwargs): 544 + def wrapped(func): 545 + return getattr(click, attr)( 546 + *args, **(cls.defaults[attr] | Dict(kwargs)) 547 + )(func) 548 + 549 + return wrapped 550 + 551 + return wrapper 552 + 553 + 554 + # TODO: Plan: 555 + # * If the first function is a `Collection` or a dictionary and there are no more functions, 556 + # the bundle is rootless; otherwise, flatten collections and use dictionary keys as names for their values. 557 + 558 + 559 + class bundle(funcdict, metaclass=Bundle, _subclass=True): 560 + # TODO: Use the same trick with `Jugulis`, `Hydreigon`, and `Deino`. 561 + def __new__(_cls, *funcs, **click_kwargs): 562 + if as_decorator(query=query): 563 + 564 + def wrapper(func): 565 + return _cls(func, *funcs, **click_kwargs) 566 + 567 + return wrapper 568 + 569 + if funcs and isinstance(funcs[0], str): 570 + kwargs = {"name": funcs[0]} | click_kwargs 571 + func, *bases = funcs[1] 572 + result = func.append(funcs[2], **kwargs) 573 + for base in bases: 574 + base.append(result, **kwargs) 575 + return result 576 + 577 + return super().__new__(_cls, *funcs, **click_kwargs) 578 + 579 + @property 580 + def __name__(self): 581 + return self.__click_kwargs__.get("name", self.func.__name__) 582 + 583 + @__name__.setter 584 + def __name__(self, value): 585 + self.__click_kwargs__["name"] = self.func.__name__ = value 586 + 587 + def __call__(self, *args, **kwargs): 588 + return self.func(*args, **kwargs) 589 + 590 + def __hash__(self): 591 + return reducehash( 592 + self.__class__.__name__, 593 + self.__name__, 594 + self.func, 595 + reducedict(self.__click_kwargs__), 596 + reducedict(self), 597 + ) 598 + 599 + def __eq__(self, other): 600 + if isinstance(other, self.__class__): 601 + if not ( 602 + # (self.__name__ == other.__name__) 603 + # and (self.func == other.func) 604 + (self.func == other.func) 605 + and (self.__click_kwargs__ == other.__click_kwargs__) 606 + and super().__eq__(other) 607 + ): 608 + return NotImplemented 609 + other = other.compile() 610 + if isinstance(other, click.Group): 611 + return eq( 612 + self.compile(), 613 + other, 614 + "PYTEST_CURRENT_TEST" in os.environ, 615 + NotImplemented, 616 + ) 617 + return NotImplemented 618 + 619 + def dunder_check(self, attr): 620 + return not attr.startswith("__") or attr in ("__init__",) 621 + 622 + def process_click_kwargs(self, func, click_kwargs): 623 + return Dict( 624 + {"name": getattr(func, "__name__", "")} 625 + | getattr(func, "__click_kwargs__", {}) 626 + ) | Dict(click_kwargs) 627 + 628 + def process_func(self, func, click_kwargs, named_kwargs): 629 + fclick_kwargs = self.process_click_kwargs(func, click_kwargs) 630 + if not fclick_kwargs.name: 631 + fclick_kwargs |= named_kwargs 632 + fname = fclick_kwargs["name"] 633 + if not self.__name__ and fname: 634 + self.__name__ = fname 635 + return fclick_kwargs 636 + 637 + # TODO: Simplify and combine the various functions. 638 + def pre_process_extend(self, func=None, *funcs, **click_kwargs): 639 + named_kwargs = {} 640 + # TODO: If I have to do this anyway, just put all this in `append`. 641 + if not self.__name__ and "name" in click_kwargs: 642 + self.__name__ = named_kwargs["name"] = getattr( 643 + click_kwargs, "pop" if funcs else "get" 644 + )("name") 645 + fclick_kwargs_list = [] 646 + new_funcs = [] 647 + for f in (func, *funcs): 648 + if isclass(f): 649 + if funcs: 650 + for k in dir(f): 651 + if self.dunder_check(k): 652 + v = getattr(f, k) 653 + fclick_kwargs_list.append( 654 + self.process_func( 655 + v, 656 + click_kwargs 657 + | {"name": f.__name__ if k == "__init__" else k}, 658 + named_kwargs, 659 + ) 660 + ) 661 + new_funcs.append(v) 662 + else: 663 + fclick_kwargs_list.append( 664 + self.process_func(f, click_kwargs, named_kwargs) 665 + ) 666 + new_funcs.append(f) 667 + else: 668 + match f: 669 + case bundle(): 670 + fclick_kwargs_list.append( 671 + self.process_func(f, click_kwargs, named_kwargs) 672 + ) 673 + new_funcs.append(f) 674 + case dict(): 675 + if funcs: 676 + for k, v in f.items(): 677 + if self.dunder_check(k): 678 + fclick_kwargs_list.append( 679 + self.process_func( 680 + v, click_kwargs | {"name": k}, named_kwargs 681 + ) 682 + ) 683 + new_funcs.append(v) 684 + else: 685 + fclick_kwargs_list.append( 686 + self.process_func(f, click_kwargs, named_kwargs) 687 + ) 688 + new_funcs.append(f) 689 + case Hydreigon(): 690 + ... 691 + case Collection(): 692 + if funcs: 693 + for item in f: 694 + fclick_kwargs_list.append( 695 + self.process_func(item, click_kwargs, named_kwargs) 696 + ) 697 + new_funcs.append(item) 698 + else: 699 + fclick_kwargs_list.append( 700 + self.process_func(f, click_kwargs, named_kwargs) 701 + ) 702 + new_funcs.append(f) 703 + case _: 704 + fclick_kwargs_list.append( 705 + self.process_func(f, click_kwargs, named_kwargs) 706 + ) 707 + new_funcs.append(f) 708 + func, *funcs = new_funcs 709 + result = self.append(func, processed_click_kwargs=fclick_kwargs_list[0]) 710 + for f, kwargs in zip(funcs, fclick_kwargs_list[1:]): 711 + result.append(f, processed_click_kwargs=kwargs) 712 + return result 713 + 714 + def up(self, *funcs, **click_kwargs): 715 + if isinstance(self, bundle): 716 + if as_decorator(query=query): 717 + 718 + def wrapper(func): 719 + return self.pre_process_extend(func, *funcs, **click_kwargs) 720 + 721 + return wrapper 722 + 723 + return self.pre_process_extend(*funcs, **click_kwargs) 724 + return Bundle.up(self, *funcs, **click_kwargs) 725 + 726 + extend = up 727 + roar = up 728 + 729 + def __init__(self, func=None, *funcs, **click_kwargs): 730 + if not isinstance(func, str): 731 + super().__init__() 732 + self.func = Hydreigon( 733 + default_va_annotation=typing.Any, default_vk_annotation=typing.Any 734 + ) 735 + self.__click_kwargs__ = Dict(click_kwargs) 736 + self.up(func, *funcs, **click_kwargs) 737 + 738 + def process_append(self, func, fname, child): 739 + if child: 740 + if fname in self: 741 + result = self[fname] 742 + exists = True 743 + else: 744 + result = self 745 + exists = False 746 + else: 747 + result = self 748 + exists = True 749 + if isclass(func): 750 + if not exists: 751 + self[fname] = result = bundle(name=fname) 752 + for name in dir(func): 753 + if self.dunder_check(name): 754 + result.append( 755 + getattr(func, name), name=fname if name == "__init__" else name 756 + ) 757 + else: 758 + match func: 759 + case dict(): 760 + if not exists: 761 + self[fname] = result = bundle(name=fname) 762 + for name, attr in func.items(): 763 + if self.dunder_check(name): 764 + result.append(attr, name=name) 765 + case Hydreigon(): 766 + if exists: 767 + result.func.extend(func) 768 + else: 769 + self[fname] = result = bundle(name=fname) 770 + result.func = func 771 + 772 + # TODO 773 + # case ht.Coll(): 774 + # case abc.Iterable(): 775 + case Collection(): 776 + if not exists: 777 + self[fname] = result = bundle(name=fname) 778 + for f in func: 779 + if isclass(f): 780 + for name in dir(f): 781 + if self.dunder_check(name): 782 + result.append( 783 + getattr(f, name), 784 + name=f.__name__ if name == "__init__" else name, 785 + ) 786 + else: 787 + match f: 788 + case bundle(): 789 + result.append(f) 790 + case dict(): 791 + for name, attr in f.items(): 792 + if self.dunder_check(name): 793 + result.append(attr, name=name) 794 + # case Hydreigon(): 795 + # ... 796 + # case Collection(): 797 + # ... 798 + case _: 799 + result.append(f) 800 + 801 + case _: 802 + if exists: 803 + result.func.append(func) 804 + else: 805 + self[fname] = result = bundle(func, name=fname) 806 + 807 + return result 808 + 809 + def append(self, func=None, /, processed_click_kwargs=None, **click_kwargs): 810 + click_kwargs = ( 811 + self.process_click_kwargs(func, click_kwargs) 812 + if processed_click_kwargs is None 813 + else processed_click_kwargs 814 + ) 815 + if not (fname := click_kwargs.get("name")): 816 + click_kwargs["name"] = fname = getattr(func, "__name__", "") 817 + if fname in (self.__name__, "__init__"): 818 + self.__click_kwargs__ |= click_kwargs 819 + if func is not None: 820 + if not isinstance(func, bundle): 821 + return self.process_append(func, fname, False) 822 + 823 + # NOTE: At any given moment, there can be only one instance of `self`, 824 + # i.e., only one version of this command. 825 + # TODO: Not necessarily, as the result can be assigned to a variable with another name. 826 + # Therefore, find a way to update the old version of func as well. 827 + # TODO: ... Wouldn't `func` be updated anyway, as `self = func`? 828 + self |= func 829 + 830 + return self 831 + 832 + if fname: 833 + if isinstance(func, bundle): 834 + # NOTE: At any given moment, there can be only one instance of `fname`, 835 + # i.e., only one version of this command. 836 + # TODO: Not necessarily, as the result can be assigned to a variable with another name. 837 + # Therefore, find a way to update the old version of func as well. 838 + # TODO: ... Wouldn't `func` be updated anyway `if fname in self`, as `self[fname] = func`? 839 + if fname in self: 840 + self[fname] |= func 841 + else: 842 + self[fname] = func 843 + return self[fname] 844 + 845 + if func is None: 846 + if fname in self: 847 + return self[fname] 848 + self[fname] = result = bundle(**click_kwargs) 849 + return result 850 + return self.process_append(func, fname, True) 851 + 852 + def __rich_repr__(self): 853 + yield self.__name__ 854 + yield self.func 855 + yield dict(self) 856 + 857 + def update(self, other): 858 + if isinstance(other, self.__class__): 859 + self.func += other.func 860 + self.__click_kwargs__ |= other.__click_kwargs__ 861 + # TODO: Should the values of other be converted to bundles, 862 + # like in `funcdict`? 863 + return super().update(other) 864 + 865 + # TODO: This will make the result a rootless application. 866 + # Do I want that? 867 + def copy(self): 868 + result = self.__class__(self) 869 + result.func = self.func.copy() 870 + return result 871 + 872 + def __ror__(self, other): 873 + if isinstance(other, self.__class__): 874 + result = other.copy() 875 + result.update(self) 876 + else: 877 + result = self.copy() 878 + result.update(other) 879 + return result 880 + 881 + def process_names(self, name, kind, bundled_args, click_params): 882 + args = bundled_args.copy() 883 + if not args: 884 + args.append(name) 885 + if kind not in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL): 886 + if not any( 887 + True for arg in args if self.__class__.shortform_predicate(name, arg) 888 + ): 889 + args.append(self.__class__.get_shortform(name, click_params)) 890 + if len(args) < 3: 891 + args.append(self.__class__.get_longform(name)) 892 + return args 893 + 894 + # TODO: Simplify. 895 + def compile(self, parent=click, *, click_kwargs=None, only_params=True): 896 + hydreigon: Hydreigon 897 + deino: Deino 898 + func: abc.Callable 899 + if self.func: 900 + hydreigon = self.func 901 + else: 902 + hydreigon = Hydreigon(lambda: None) 903 + deino: Deino = hydreigon[0] 904 + func = deino.__func__ 905 + bundled_args = defaultdict( 906 + list, 907 + getattr(hydreigon, "__bundled_args__", {}) 908 + | getattr(deino, "__bundled_args__", {}) 909 + | getattr(func, "__bundled_args__", {}), 910 + ) 911 + bundled_kwargs = Dict( 912 + getattr(hydreigon, "__bundled_kwargs__", {}) 913 + | getattr(deino, "__bundled_kwargs__", {}) 914 + | getattr(func, "__bundled_kwargs__", {}) 915 + ) 916 + docstring: Docstring = deino.__docstring__ 917 + docstring_params = Dict({param.arg_name: param for param in docstring.params}) 918 + bundled_kwargs |= Dict( 919 + { 920 + k: {"help": bundled_kwargs[k].help or v.description} 921 + for k, v in docstring_params.items() 922 + } 923 + ) 924 + disabled = {k for k, v in bundled_kwargs.items() if v.disabled} 925 + sig: Signature = deino.__signature__ 926 + params: dict[str, Parameter] = { 927 + k: self.__class__.process_param(v) 928 + for k, v in sig.parameters.items() 929 + if k not in disabled 930 + } 931 + param_keys = params.keys() 932 + 933 + # TODO: What happens if I run the `compile` method twice? 934 + # Does this keep expounding? 935 + # What SHOULD be happening is that `getattr(func, "__click_params__", tuple())` 936 + # should be getting the `__click_params__` assigned to `func` from the last call to `compile`. 937 + func.__click_params__ = click_params = [ 938 + click_param 939 + for click_param in flatten( 940 + getattr(hydreigon, "__click_params__", tuple()), 941 + getattr(deino, "__click_params__", tuple()), 942 + getattr(func, "__click_params__", tuple()), 943 + ) 944 + if click_param.name not in disabled 945 + ] 946 + 947 + for name in param_keys - {param.name for param in click_params}: 948 + param: Parameter = params[name] 949 + annotation = param.annotation 950 + if annotation is Pass: 951 + func = click.pass_context(func) 952 + elif typing.get_origin(annotation) is Pass: 953 + arg = typing.get_args(annotation)[0] 954 + if arg is object: 955 + func = click.pass_obj(func) 956 + else: 957 + func = click.make_pass_decorator(arg)(func) 958 + elif annotation is Password: 959 + func = click.password_option(func) 960 + else: 961 + kind = param.kind 962 + func = getattr(click, self.__class__.process_clicker(kind))( 963 + *self.process_names(name, kind, bundled_args[name], click_params), 964 + **self.__class__.process_kwargs( 965 + name, param, bundled_kwargs[name], docstring_params[name] 966 + ), 967 + )(func) 968 + 969 + if docstring: 970 + dockwargs = {} 971 + dockwargs["help"] = "" 972 + if docstring.short_description: 973 + dockwargs["help"] = dockwargs["short_help"] = ( 974 + docstring.short_description 975 + ) 976 + if docstring.long_description: 977 + # TODO: Use the typical format for docstrings for arguments, but format them differently here. 978 + # TODO: Disabled args shouldn't be included in long descriptions. 979 + # TODO: Aren't options in the `long_description` as well...? 980 + # TODO: How do I match the case of the first word of the description to the names in the disabled list? 981 + dockwargs["help"] += "\b\n\n" + "\b\n\n".join( 982 + arg 983 + for arg in docstring.long_description.split("\n") 984 + if arg.split()[0] not in disabled 985 + ) 986 + else: 987 + dockwargs = {} 988 + click_kwargs = ( 989 + ( 990 + click_kwargs 991 + if isinstance(parent, click.Command) 992 + else bundle.defaults["group"] 993 + ) 994 + | {"name": self.__name__} 995 + | dockwargs 996 + | self.__click_kwargs__ 997 + ) 998 + if "cls" in click_kwargs: 999 + if not hasattr(cls := click_kwargs["cls"], "__rich_repr__"): 1000 + cls.__rich_repr__ = partial(rich_repr, only_params=only_params) 1001 + group = parent.group(**click_kwargs)(normalize.function(func, exclude=disabled)) 1002 + for v in self.values(): 1003 + v.compile(group, click_kwargs=click_kwargs) 1004 + return group 1005 + 1006 + def flip(self, *args, **kwargs): 1007 + return self.compile()(*args, **kwargs) 1008 + 1009 + def invoke(self, *args, **kwargs): 1010 + self.func.raise_if_empty() 1011 + return self.func.invoke(*args, **kwargs) 1012 + 1013 + def run(self, *args, runner=None, attr="return_value", **kwargs): 1014 + kwargs = {"standalone_mode": False, "catch_exceptions": False} | kwargs 1015 + result = (runner or self.__class__.runner).invoke( 1016 + self.compile(), args, **kwargs 1017 + ) 1018 + return result if attr == "result" else getattr(result, attr) 1019 + 1020 + @property 1021 + def root(self): 1022 + self.func.raise_if_empty() 1023 + return self.func[0] 1024 + 1025 + def document(self, docstring, *args, **kwargs): 1026 + if args or kwargs: 1027 + self.invoke(*args, **kwargs).__doc__ = docstring 1028 + else: 1029 + self.root.__doc__ = docstring 1030 + return self
+101
packages/bundle/bundle/functions.py
··· 1 + from operator import attrgetter 2 + from hydrox.helpers import Collection 3 + 4 + get_name = attrgetter("name") 5 + not_starts_with_ = lambda s: not (s.startswith("_") or s in ("console", "help_config")) 6 + 7 + 8 + def eq(a, b, debug=False, falsifier=False): 9 + try: 10 + for n1, n2 in zip( 11 + filter(not_starts_with_, dir(a)), 12 + filter(not_starts_with_, dir(b)), 13 + strict=True, 14 + ): 15 + if not ({n1, n2} & {"params", "commands"}): 16 + a1 = getattr(a, n1) 17 + a2 = getattr(b, n2) 18 + if not (callable(a1) or callable(a2)): 19 + if a1 != a2: 20 + if debug: 21 + print(a, b, n1, n2, a1, a2) 22 + return falsifier 23 + except ValueError as e: 24 + if debug: 25 + print( 26 + set(filter(not_starts_with_, dir(a))) 27 + - set(filter(not_starts_with_, dir(b))) 28 + ) 29 + print( 30 + set(filter(not_starts_with_, dir(b))) 31 + - set(filter(not_starts_with_, dir(a))) 32 + ) 33 + raise e 34 + for p1, p2 in zip( 35 + sorted(a.params, key=get_name), sorted(b.params, key=get_name), strict=True 36 + ): 37 + for pn1, pn2 in zip( 38 + filter(not_starts_with_, dir(p1)), 39 + filter(not_starts_with_, dir(p2)), 40 + strict=True, 41 + ): 42 + pa1 = getattr(p1, pn1) 43 + pa2 = getattr(p2, pn2) 44 + if not (callable(pa1) or callable(pa2)): 45 + if pa1 != pa2: 46 + if debug: 47 + print(a, b, p1, p2, pn1, pn2, pa1, pa2) 48 + return falsifier 49 + for c1, c2 in zip( 50 + sorted(a.commands.values(), key=get_name), 51 + sorted(b.commands.values(), key=get_name), 52 + strict=True, 53 + ): 54 + if not eq(c1, c2, debug=debug): 55 + return falsifier 56 + return True 57 + 58 + 59 + def rich_dict(self, only_params=True, repr=False): 60 + commands = {} 61 + for k in dir(self): 62 + if not k.startswith("_"): 63 + v = getattr(self, k, None) 64 + if k == "params": 65 + vparams = {} 66 + for param in v: 67 + vparams[param.name] = pparams = {} 68 + for p in dir(param): 69 + if not p.startswith("_"): 70 + if not callable(pattr := getattr(param, p, None)): 71 + pparams[p] = pattr 72 + commands[k] = vparams 73 + elif k == "commands": 74 + commands[k] = ( 75 + v 76 + if repr 77 + else { 78 + ck: rich_dict(cv, only_params=only_params, repr=repr) 79 + for ck, cv in v.items() 80 + } 81 + ) 82 + elif not (only_params or callable(v)): 83 + commands[k] = v 84 + return commands 85 + 86 + 87 + def rich_repr(self, only_params=True): 88 + yield self.name 89 + for k, v in rich_dict(self, only_params=only_params, repr=True).items(): 90 + yield k, v 91 + 92 + 93 + def reducedict(obj): 94 + if isinstance(obj, dict): 95 + for k, v in obj.items(): 96 + yield reducedict(k) 97 + yield reducedict(v) 98 + elif isinstance(obj, Collection): 99 + yield from map(reducedict, obj) 100 + else: 101 + yield obj
+929
packages/bundle/bundle/tests/_test_bundle.py
··· 1 + import magicattr 2 + import rich_click as click 3 + import operator 4 + 5 + from addict import Dict 6 + from beartype.roar import BeartypeDoorHintViolation 7 + from click.testing import CliRunner 8 + from functools import reduce 9 + from hypothesis import HealthCheck, Phase, given, settings 10 + from inspect import isclass 11 + from bundle import ( 12 + bundle, 13 + bundler, 14 + Check, 15 + DedupList, 16 + deflambda, 17 + dirs, 18 + Model, 19 + plumeta, 20 + literal_strat, 21 + subtype, 22 + SuperPath, 23 + Switch, 24 + ) 25 + from parametrized import parametrized 26 + from pathvalidate import is_valid_filepath, sanitize_filename 27 + from pytest import fixture, mark, skip 28 + from random import choice 29 + from rich.pretty import pprint 30 + from typing import Literal 31 + 32 + 33 + class ID(tuple): 34 + def __call__(self, obj): 35 + return obj 36 + 37 + 38 + def bundle_modifier(cls: type): 39 + return subtype(cls, bundle) 40 + 41 + 42 + def bundler_modifier(cls: type): 43 + return subtype(cls, bundler) 44 + 45 + 46 + def bundle_arguments_modifier(cls: type): 47 + return subtype(cls, bundle(no_args_is_help=False)) 48 + 49 + 50 + def bundler_arguments_modifier(cls: type): 51 + return subtype(cls, bundler(no_args_is_help=False)) 52 + 53 + 54 + def bundle_factory(cls): 55 + return bundle(no_args_is_help=False)(cls) 56 + 57 + 58 + def bundle_factory_init(cls): 59 + return bundle(cls, no_args_is_help=False) 60 + 61 + 62 + def bundler_factory(cls): 63 + return bundler(no_args_is_help=False)(cls) 64 + 65 + 66 + def bundler_factory_init(cls): 67 + return bundler(cls, no_args_is_help=False) 68 + 69 + 70 + function_decorators = ( 71 + bundle_factory, 72 + bundle_factory_init, 73 + bundler_factory, 74 + bundler_factory_init, 75 + ) 76 + 77 + decorators = (bundle, bundler, *function_decorators) 78 + 79 + modifiers = ( 80 + ID(), 81 + bundle_modifier, 82 + bundler_modifier, 83 + bundle_arguments_modifier, 84 + bundler_arguments_modifier, 85 + ) 86 + 87 + 88 + # Adapted From: 89 + # Answer: https://stackoverflow.com/a/5891486 90 + # User: https://stackoverflow.com/users/208997/simon 91 + characters = [chr(i) for i in range(128)] 92 + 93 + 94 + class F(metaclass=plumeta): 95 + def __init__(self, height, width, prefix="f", no_main=False): 96 + self.height = height 97 + self.width = width 98 + self.prefix = prefix 99 + self.no_main = no_main 100 + self.arguments = self.make_arguments(self.width, self.prefix) 101 + self.names = self.arguments.keys() 102 + self.args = self.arguments.values() 103 + self.prefixed_arguments = Dict( 104 + {k: [self.prefix] + v for k, v in self.arguments.items()} 105 + ) 106 + self.namespaces = self.make_namespaces() 107 + self.functions = Dict(reduce(operator.or_, self.namespaces.values())) 108 + self.classes = self.make_classes() 109 + self.nested_namespaces = self.make_nested_namespaces() 110 + self.nested_classes = self.make_nested_classes() 111 + self.bundled_functions = self.make_bundled_functions() 112 + self.click_functions = self.make_click_functions() 113 + self.singletons = self.make_singletons()[0].keys() 114 + assert self.check_classes() 115 + assert self.check_nested_classes() 116 + 117 + def make_arguments(self, width, prefix): 118 + height = self.height + 1 119 + arguments = DedupList() 120 + for h in range(1, height): 121 + new_width = width - 1 122 + new_prefix = [f"{prefix}{h}"] 123 + if new_width: 124 + for nest in self.make_arguments(new_width, new_prefix[0]): 125 + arguments.append(new_prefix + nest) 126 + else: 127 + arguments.append(new_prefix) 128 + for index, argument in enumerate(arguments): 129 + arguments.insert(index, argument[:-1]) 130 + if width == self.width: 131 + return Dict( 132 + {(args[-1] if args else self.prefix): args for args in arguments} 133 + ) 134 + return arguments 135 + 136 + def make_namespaces(self): 137 + namespaces = Dict() 138 + for name, value in self.arguments.items(): 139 + # Adapted From: 140 + # Answer: https://stackoverflow.com/a/7546307 141 + # User: https://stackoverflow.com/users/951890/vaughn-cato 142 + ns = {name: deflambda(name, lambda name=name: name)} 143 + 144 + if len(value) < self.width: 145 + namespaces[name] = {} if self.no_main else ns 146 + else: 147 + namespaces[value[-2]] |= ns 148 + return namespaces 149 + 150 + def make_class(self, name, ns): 151 + return type( 152 + name, 153 + tuple(), 154 + { 155 + k: (self.make_class(k, v) if isinstance(v, dict) else v) 156 + for k, v in ns.items() 157 + }, 158 + ) 159 + 160 + def make_classes(self): 161 + return Dict({k: self.make_class(k, v) for k, v in self.namespaces.items()}) 162 + 163 + def get_class(self, cls): 164 + return Dict( 165 + { 166 + k: (self.get_class(v) if isclass(v) else v) 167 + for k, v in cls.__dict__.items() 168 + if callable(v) 169 + } 170 + ) 171 + 172 + def get_classes(self, classes): 173 + return Dict({k: self.get_class(v) for k, v in classes.items()}) 174 + 175 + def no_functions(self, namespace): 176 + return { 177 + k: self.no_functions(v) if isinstance(v, dict) else callable(v) 178 + for k, v in namespace.items() 179 + } 180 + 181 + def check_classes(self): 182 + return self.no_functions(self.namespaces) == self.no_functions( 183 + self.get_classes(self.classes) 184 + ) 185 + 186 + def make_nested_namespace(self, indent=0): 187 + if indent >= self.width: 188 + return self.namespaces 189 + namespaces = Dict() 190 + for args in self.prefixed_arguments.values(): 191 + if indent + 1 < len(args) <= self.width: 192 + prefix = args[indent] 193 + args = args[indent + 1 :] 194 + namespaces |= Dict({prefix: self.namespaces[prefix]}) 195 + name = args[-1] 196 + joint_args = '"]["'.join(args) 197 + joint_attrs = f'{prefix}["{joint_args}"]' 198 + current = magicattr.get(namespaces, joint_attrs) 199 + ns = self.namespaces[name] 200 + if current: 201 + current |= ns 202 + else: 203 + magicattr.set(namespaces, joint_attrs, Dict(ns)) 204 + else: 205 + namespaces |= Dict( 206 + {prefix: self.namespaces[prefix] for prefix in args[: indent + 1]} 207 + ) 208 + return namespaces 209 + 210 + def make_nested_namespaces(self): 211 + return Dict({i: self.make_nested_namespace(i) for i in range(self.width)}) 212 + 213 + def make_nested_class(self, indent=0): 214 + if indent >= self.width: 215 + return self.classes 216 + return Dict( 217 + { 218 + k: self.make_class(k, v) 219 + for k, v in self.nested_namespaces[indent].items() 220 + } 221 + ) 222 + 223 + def make_nested_classes(self): 224 + return Dict({i: self.make_nested_class(i) for i in range(self.width)}) 225 + 226 + def check_nested_classes(self): 227 + return self.no_functions(self.nested_namespaces) == self.no_functions( 228 + { 229 + i: {k: self.get_class(v) for k, v in self.nested_classes[i].items()} 230 + for i in range(self.width) 231 + } 232 + ) 233 + 234 + def make_bundled_functions(self): 235 + functions = Dict() 236 + 237 + for args in self.prefixed_arguments.values(): 238 + name = args[-1] 239 + functions[name] = bundle( 240 + self.functions[name] or deflambda(name), 241 + functions[args[-2]] if len(args) > 1 else None, 242 + name=name, 243 + no_args_is_help=False, 244 + ) 245 + 246 + return functions 247 + 248 + def make_click_functions(self): 249 + functions = Dict() 250 + for args in self.prefixed_arguments.values(): 251 + name = args[-1] 252 + functions[name] = reduce( 253 + lambda a, b: b(a), 254 + reversed( 255 + ( 256 + (functions[args[-2]] if len(args) > 1 else click).group( 257 + name=name, 258 + **(bundle._defaults.group | {"no_args_is_help": False}), 259 + ), 260 + click.option( 261 + "-n", 262 + "--name", 263 + default=name, 264 + **bundle._defaults.option, 265 + ), 266 + self.functions[name] or deflambda(name), 267 + ) 268 + ), 269 + ) 270 + 271 + return functions 272 + 273 + def make_singleton(self, func, *decorators, decorator=bundle): 274 + bundle_decorators = [decorator] 275 + click_decorators = [ 276 + click.group( 277 + **( 278 + bundle._defaults.group 279 + | { 280 + "no_args_is_help": False 281 + if decorator 282 + in ( 283 + bundle_factory, 284 + bundle_factory_init, 285 + bundler_factory, 286 + bundler_factory_init, 287 + ) 288 + else bundle._defaults.group.no_args_is_help 289 + } 290 + ) 291 + ) 292 + ] 293 + for d in decorators: 294 + if d.__module__.split(".")[0] == "click": 295 + click_decorators.append(d) 296 + else: 297 + bundle_decorators.append(d) 298 + reducer = lambda a, b: b(a) 299 + return reduce(reducer, (func, *reversed(bundle_decorators))), reduce( 300 + reducer, (func, *reversed(click_decorators)) 301 + ) 302 + 303 + def make_singletons(self, decorator=bundle, names=False): 304 + bundled_singletons = Dict() 305 + click_singletons = Dict() 306 + 307 + singletons = Dict( 308 + basic=( 309 + deflambda("f", lambda b: None), 310 + bundle.up("b", "-8", "--8"), 311 + click.option("-8", "--8", "b", **bundle._defaults.option), 312 + ), 313 + conflicting_names=( 314 + deflambda("f", lambda a1, a2, a3: None), 315 + click.option("-a", "--a1", **bundle._defaults.option), 316 + click.option("-A", "--a2", **bundle._defaults.option), 317 + click.option("-b", "--a3", **bundle._defaults.option), 318 + ), 319 + ) 320 + 321 + def f(accept: Switch["reject"]): ... 322 + 323 + singletons.switch = ( 324 + f, 325 + click.option( 326 + "-a/-r", 327 + "--accept/--reject", 328 + default=False, 329 + **bundle._defaults.option, 330 + ), 331 + ) 332 + 333 + def f(a1: Switch["r1"], a2: Switch["r2"], a3: Switch["r3"]): ... 334 + 335 + singletons.conflicting_switches = ( 336 + f, 337 + click.option("-a/-r", "--a1/--r1", **bundle._defaults.option), 338 + click.option("-A/-R", "--a2/--r2", **bundle._defaults.option), 339 + click.option("-b/-s", "--a3/--r3", **bundle._defaults.option), 340 + ) 341 + 342 + def f(switch: bool): ... 343 + 344 + singletons.boolean_flags = ( 345 + f, 346 + click.option( 347 + "-s", "--switch", **(bundle._defaults.option | {"is_flag": True}) 348 + ), 349 + ) 350 + 351 + if names: 352 + return singletons.keys() 353 + else: 354 + for k, v in singletons.items(): 355 + bundled_singletons[k], click_singletons[k] = self.make_singleton( 356 + *v, decorator=decorator 357 + ) 358 + 359 + return bundled_singletons, click_singletons 360 + 361 + def make_bundled_classes(self, decorator): 362 + def inner(cls): 363 + ns = {} 364 + for k, v in cls.__dict__.items(): 365 + if callable(v): 366 + if decorator is bundle: 367 + value = bundle(no_args_is_help=False)( 368 + type(k, tuple(), inner(v)) if isclass(v) else v 369 + ) 370 + ns[k] = value 371 + elif isclass(v): 372 + ns[k] = bundler(no_args_is_help=False)( 373 + type(k, tuple(), inner(v)) 374 + ) 375 + else: 376 + ns[k] = bundle(no_args_is_help=False)(v) 377 + else: 378 + ns[k] = v 379 + return ns 380 + 381 + return Dict( 382 + { 383 + i: { 384 + k: type(k, tuple(), inner(v)) 385 + for k, v in self.nested_classes[i].items() 386 + } 387 + for i in range(self.width) 388 + } 389 + ) 390 + 391 + def make_inherited_classes( 392 + self, 393 + bases=ID(), 394 + decorator=ID(), 395 + indent=0, 396 + wrap_parent=0, 397 + wrap_child=0, 398 + ): 399 + all_classes = self.nested_classes[self.width - 1 - indent] 400 + classes = Dict() 401 + for args in self.prefixed_arguments.values(): 402 + name = args[-1] 403 + if 1 < len(args) <= self.width - indent: 404 + bases = classes[args[-2]] 405 + elif bases in (bundle_factory, bundle_factory_init): 406 + bases = bundle(no_args_is_help=False) 407 + elif bases in (bundler_factory, bundler_factory_init): 408 + bases = bundler(no_args_is_help=False) 409 + if name in all_classes and name not in classes: 410 + if name == self.prefix and wrap_parent: 411 + classes[name] = bases(self.functions.get(name, deflambda(name))) 412 + elif name != self.prefix and wrap_child: 413 + classes[name] = bases(all_classes[name]) 414 + else: 415 + classes[name] = decorator(subtype(all_classes[name], bases)) 416 + return classes 417 + 418 + 419 + class N(F): 420 + def __init__(self, height, width, prefix="n", no_main=True): 421 + super().__init__(height, width, prefix, no_main) 422 + 423 + def remove_main_class(self, cls): 424 + ns = {} 425 + for k, v in cls.__dict__.items(): 426 + if callable(v): 427 + if isclass(v): 428 + ns[k] = type(k, tuple(), self.remove_main_class(v)) 429 + elif k != cls.__name__: 430 + ns[k] = v 431 + else: 432 + ns[k] = v 433 + return ns 434 + 435 + def remove_main_namespace(self, parent, namespace): 436 + ns = Dict() 437 + for k, v in namespace.items(): 438 + if isinstance(v, dict): 439 + ns[k] = self.remove_main_namespace(k, v) 440 + elif k != parent: 441 + ns[k] = v 442 + return ns 443 + 444 + 445 + class RF(F): 446 + def __init__(self, height, width, prefix="f", no_main=False): 447 + super().__init__(height, width, prefix, no_main) 448 + self.random_names = [] 449 + for name in self.names: 450 + c = choice(characters) 451 + while c in self.random_names or c in ("_",): 452 + c = choice(characters) 453 + self.random_names.append(c) 454 + self.conversion_table = Dict(zip(self.names, self.random_names)) 455 + 456 + def randomize_namespace(self, namespace): 457 + return { 458 + self.conversion_table.get(k, k): ( 459 + self.randomize_namespace(v) if isinstance(v, dict) else v 460 + ) 461 + for k, v in namespace.items() 462 + } 463 + 464 + 465 + class RN(RF): 466 + def __init__(self, height, width, prefix="n", no_main=True): 467 + super().__init__(height, width, prefix, no_main) 468 + 469 + 470 + height, width = 2, 2 471 + 472 + 473 + @fixture 474 + def f(): 475 + return F(height, width) 476 + 477 + 478 + for k, v in dirs(F(height, width)).items(): 479 + if not k.startswith("_"): 480 + setattr(f, k, v) 481 + 482 + 483 + @fixture 484 + def n(): 485 + return N(height, width) 486 + 487 + 488 + for k, v in dirs(N(height, width)).items(): 489 + if not k.startswith("_"): 490 + setattr(n, k, v) 491 + 492 + 493 + @fixture 494 + def rf(): 495 + return RF(height, width) 496 + 497 + 498 + for k, v in dirs(RF(height, width)).items(): 499 + if not k.startswith("_"): 500 + setattr(rf, k, v) 501 + 502 + 503 + @fixture 504 + def rn(): 505 + return RN(height, width) 506 + 507 + 508 + for k, v in dirs(RN(height, width)).items(): 509 + if not k.startswith("_"): 510 + setattr(rn, k, v) 511 + 512 + 513 + class DEFAULT_RESULT: 514 + pass 515 + 516 + 517 + runner = CliRunner(mix_stderr=False) 518 + 519 + 520 + def invoker(cls, *args): 521 + result = runner.invoke( 522 + cls, 523 + args, 524 + standalone_mode=False, 525 + catch_exceptions=False, 526 + ) 527 + print(cls, args, result) 528 + pprint(dirs(getattr(cls, "__cls__", cls))) 529 + pprint(dirs(result)) 530 + return result.return_value 531 + 532 + 533 + def run(cls, args, result=DEFAULT_RESULT): 534 + return invoker(cls, *args) == ( 535 + (args[-1] if args else cls.__name__) if result is DEFAULT_RESULT else result 536 + ) 537 + 538 + 539 + @mark.bundle 540 + class TestClasses: 541 + @parametrized 542 + def test_bundle_up(self, f, arguments=f.arguments.values()): 543 + assert run( 544 + bundler(bundle.up(f.nested_classes[0][f.prefix], no_args_is_help=False)), 545 + arguments, 546 + ) 547 + 548 + # TODO: Is there a way to incorporate these into the inherited classes as well? 549 + @parametrized.product 550 + def test_bundled_classes( 551 + self, 552 + base=(bundle, bundler), 553 + modifier=modifiers, 554 + decorator=decorators, 555 + arguments=f.arguments.values(), 556 + ): 557 + assert run( 558 + decorator(modifier(f.make_bundled_classes(base)[0][f.prefix])), 559 + arguments, 560 + ) 561 + 562 + @parametrized.product 563 + def test_bundled_no_main( 564 + self, 565 + base=(bundle, bundler), 566 + modifier=modifiers, 567 + decorator=decorators, 568 + arguments=n.arguments.values(), 569 + ): 570 + assert run( 571 + decorator(modifier(n.make_bundled_classes(base)[0][n.prefix])), 572 + arguments, 573 + DEFAULT_RESULT if len(arguments) == n.width else None, 574 + ) 575 + 576 + @parametrized.product 577 + def test_inherited_classes( 578 + self, 579 + f, 580 + base=(ID(), *decorators), 581 + decorator=(ID(), *decorators), 582 + arguments=f.arguments.values(), 583 + indent=range(f.width), 584 + wrap_parent=range(2), 585 + wrap_child=range(2), 586 + ): 587 + if base: 588 + if indent >= f.width - 1 and wrap_parent: 589 + skip( 590 + "Wrapping (not subclassing) the parent uses the parent function, " 591 + "which does not include subcommands." 592 + ) 593 + else: 594 + if not decorator: 595 + skip("These classes are not bundler objects.") 596 + if wrap_parent: 597 + skip("We don't want to subclass a function.") 598 + cls = f.make_inherited_classes( 599 + base, decorator, indent, wrap_parent, wrap_child 600 + )[f.prefix] 601 + if not base or base in (bundle, bundler): 602 + cls._rewrap(no_args_is_help=False) 603 + assert cls == f.click_functions[f.prefix] 604 + assert run(cls, arguments) 605 + 606 + @parametrized.product 607 + def test_inherited_no_main( 608 + self, 609 + n, 610 + base=(ID(), *decorators), 611 + decorator=(ID(), *decorators), 612 + arguments=n.arguments.values(), 613 + indent=range(n.width), 614 + wrap_parent=range(2), 615 + wrap_child=range(2), 616 + ): 617 + if base: 618 + if indent >= n.width - 1 and wrap_parent: 619 + skip( 620 + "Wrapping (not subclassing) the parent uses the parent function, " 621 + "which does not include subcommands." 622 + ) 623 + else: 624 + if not decorator: 625 + skip("These classes are not bundler objects.") 626 + if wrap_parent: 627 + skip("We don't want to subclass a function.") 628 + cls = n.make_inherited_classes( 629 + base, decorator, indent, wrap_parent, wrap_child 630 + )[n.prefix] 631 + if not base or base in (bundle, bundler): 632 + cls._rewrap(no_args_is_help=False) 633 + if len(arguments) == n.width: 634 + assert cls == n.click_functions[n.prefix] 635 + assert run(cls, arguments) 636 + else: 637 + assert cls.equals(n.click_functions[n.prefix]) 638 + assert run(cls, arguments, None) 639 + 640 + 641 + @mark.bundle 642 + class TestBundleEquality: 643 + @parametrized.product 644 + def test_singletons( 645 + self, 646 + decorator=decorators, 647 + name=f.singletons, 648 + ): 649 + bundled_singletons, click_singletons = f.make_singletons(decorator) 650 + assert bundled_singletons[name] == click_singletons[name] 651 + 652 + def test_all(self): 653 + @bundle 654 + def f1(a, /, b, c=3, *args, d, e=5, **kwargs): 655 + pass 656 + 657 + @click.group( 658 + **( 659 + bundle._defaults.group 660 + | { 661 + "context_settings": { 662 + "ignore_unknown_options": True, 663 + "allow_extra_args": True, 664 + } 665 + } 666 + ) 667 + ) 668 + @click.argument("a", **bundle._defaults.argument) 669 + @click.option("-b", "--b", **bundle._defaults.option) 670 + @click.option( 671 + "-c", "--c", **(bundle._defaults.option | {"default": 3, "metavar": "int"}) 672 + ) 673 + @click.argument("args", **(bundle._defaults.argument | {"nargs": -1})) 674 + @click.option("-d", "--d", **(bundle._defaults.option | {"required": True})) 675 + @click.option( 676 + "-e", "--e", **(bundle._defaults.option | {"default": 5, "metavar": "int"}) 677 + ) 678 + def f2(a, b, c, args, d, e): 679 + pass 680 + 681 + assert f1.equals(f2) 682 + 683 + 684 + @mark.bundle 685 + class TestBundle: 686 + @parametrized 687 + def test_subcommands(self, f, args=f.arguments.values()): 688 + assert run(f.bundled_functions[f.prefix]._rewrap(no_args_is_help=False), args) 689 + 690 + @mark.process 691 + @mark.parametrize("decorator", function_decorators) 692 + @given(i=...) 693 + @settings(deadline=None) 694 + def test_click_annotation(self, decorator, i: int): 695 + @decorator 696 + @bundle.argument("j") 697 + def g(j: int): 698 + return isinstance(j, int) 699 + 700 + assert invoker(g, "--", str(i)) 701 + 702 + @mark.process 703 + @mark.parametrize("decorator", function_decorators) 704 + @given(i=...) 705 + @settings(deadline=None) 706 + def test_click_annotation_check(self, decorator, i: int): 707 + def check(k): 708 + return k < 1 709 + 710 + @decorator 711 + @bundle.argument("j") 712 + def g(j: Check[int, check]): 713 + return j 714 + 715 + try: 716 + assert invoker(g, "--", str(i)) == i 717 + except BeartypeDoorHintViolation: 718 + assert not check(i) 719 + 720 + @mark.process 721 + @parametrized 722 + def test_process_envvarg(self, decorator=function_decorators): 723 + @decorator 724 + def g(PATH: list[SuperPath]): 725 + return all(isinstance(path, SuperPath) for path in PATH) 726 + 727 + assert invoker(g) 728 + 729 + @mark.process 730 + @parametrized 731 + def test_process_envvarg_argument(self, decorator=function_decorators): 732 + @decorator 733 + def g(PATH: list[SuperPath], /): 734 + return all(isinstance(path, SuperPath) for path in PATH) 735 + 736 + assert invoker(g) 737 + 738 + @parametrized 739 + def test_disabled(self, decorator=function_decorators): 740 + @decorator 741 + @bundle.up("args", disabled=True) 742 + def g(*args): ... 743 + 744 + assert not g.params 745 + 746 + @parametrized.product 747 + def test_decorators(self, module=(click, bundle), decorator=function_decorators): 748 + @decorator 749 + @module.argument("path", envvar="PATH", nargs=-1, type=module.Path()) 750 + @module.version_option("0.0.1", "--version", "-v") 751 + def g(path): 752 + return all(is_valid_filepath(sanitize_filename(p)) for p in path) 753 + 754 + assert invoker(g) 755 + 756 + @mark.model 757 + @mark.process 758 + @mark.parametrize("decorator", function_decorators) 759 + @given( 760 + ..., 761 + literal_strat(), 762 + ) 763 + @settings( 764 + deadline=None, 765 + # max_examples=10, 766 + suppress_health_check=(HealthCheck.too_slow,), 767 + phases=( 768 + Phase.explicit, 769 + Phase.reuse, 770 + Phase.generate, 771 + Phase.target, 772 + Phase.explain, 773 + ), 774 + ) 775 + def test_model_processing(self, decorator, a: int, mps): 776 + hive, bees, b = mps 777 + 778 + class AB(Model): 779 + a: int 780 + b: Literal[*bees] 781 + 782 + @decorator 783 + def g(ab: AB, /): 784 + return ab 785 + 786 + try: 787 + result = invoker(g, "--", str(a), str(b)) 788 + except BeartypeDoorHintViolation: 789 + assert b in hive and b not in bees 790 + else: 791 + assert result.a == a 792 + assert (result.b == b) or (result.b is b) 793 + 794 + @parametrized 795 + def test_original(self, decorator=function_decorators): 796 + @decorator 797 + def greet(name, /): 798 + return f"Hello, {name}!" 799 + 800 + assert invoker(greet, "world") == "Hello, world!" 801 + assert greet("world") == "Hello, world!" 802 + 803 + @parametrized 804 + def test_original_default(self, decorator=function_decorators): 805 + @decorator 806 + def greet(name="world"): 807 + return f"Hello, {name}!" 808 + 809 + assert invoker(greet) == "Hello, world!" 810 + assert greet() == "Hello, world!" 811 + assert greet(name="Earth") == "Hello, Earth!" 812 + 813 + 814 + def quark_factory(*args, **kwargs): 815 + return bundle(no_args_is_help=False).quark(*args, **kwargs) 816 + 817 + 818 + drivers = ( 819 + bundle.drive, 820 + quark_factory, 821 + ) 822 + 823 + 824 + @fixture 825 + def namespaces(f): 826 + return f.nested_namespaces[0][f.prefix] 827 + 828 + 829 + @fixture 830 + def no_main_namespaces(n): 831 + return n.remove_main_namespace(n.prefix, n.nested_namespaces[0][n.prefix]) 832 + 833 + 834 + def get_function_namespace(namespace): 835 + dct = {} 836 + for k, v in namespace.items(): 837 + if isinstance(v, dict): 838 + dct[k] = get_function_namespace(v) 839 + elif callable(v): 840 + dct[k] = dirs(v, ignores=("__builtins__", "__globals__")) 841 + else: 842 + dct[k] = v 843 + return dct 844 + 845 + 846 + @mark.bundle 847 + class TestQuarkDrive: 848 + @parametrized.product 849 + def test_quark_drive( 850 + self, namespaces, driver=drivers, arguments=f.arguments.values() 851 + ): 852 + cls = driver(namespaces.pop(f.prefix), **namespaces) 853 + if driver.__name__ == "drive": 854 + cls._rewrap(no_args_is_help=False) 855 + assert run(cls, arguments) 856 + 857 + @parametrized.product 858 + def test_kwargs(self, namespaces, driver=drivers, arguments=f.arguments.values()): 859 + cls = driver(**namespaces) 860 + if driver.__name__ == "drive": 861 + cls._rewrap(no_args_is_help=False) 862 + assert run(cls, arguments) 863 + 864 + @parametrized 865 + def test_pre_root(self, namespaces, arguments=f.arguments.values()): 866 + assert run( 867 + bundle(namespaces.pop(f.prefix), no_args_is_help=False).quark(**namespaces), 868 + arguments, 869 + ) 870 + 871 + @parametrized.product 872 + def test_rename(self, namespaces, driver=drivers, arguments=f.arguments.values()): 873 + namespaces = rf.randomize_namespace(namespaces) 874 + cls = driver(namespaces.pop(rf.conversion_table[f.prefix]), **namespaces) 875 + if driver.__name__ == "drive": 876 + cls._rewrap(no_args_is_help=False) 877 + assert run( 878 + cls, 879 + (rf.conversion_table[arg] for arg in arguments), 880 + arguments[-1] if arguments else f.prefix, 881 + ) 882 + 883 + @parametrized.product 884 + def test_rename_kwargs( 885 + self, namespaces, driver=drivers, arguments=f.arguments.values() 886 + ): 887 + cls = driver(**rf.randomize_namespace(namespaces)) 888 + if driver.__name__ == "drive": 889 + cls._rewrap(no_args_is_help=False) 890 + assert run( 891 + cls, 892 + (rf.conversion_table[arg] for arg in arguments), 893 + arguments[-1] if arguments else f.prefix, 894 + ) 895 + 896 + @parametrized 897 + def test_rename_pre_root(self, namespaces, arguments=f.arguments.values()): 898 + namespaces = rf.randomize_namespace(namespaces) 899 + assert run( 900 + bundle( 901 + namespaces.pop(rf.conversion_table[f.prefix]), no_args_is_help=False 902 + ).quark(**namespaces), 903 + (rf.conversion_table[arg] for arg in arguments), 904 + arguments[-1] if arguments else f.prefix, 905 + ) 906 + 907 + @parametrized.product 908 + def test_no_main( 909 + self, no_main_namespaces, driver=drivers, arguments=n.arguments.values() 910 + ): 911 + cls = driver(**no_main_namespaces) 912 + if driver.__name__ == "drive": 913 + cls._rewrap(no_args_is_help=False) 914 + assert run( 915 + cls, arguments, DEFAULT_RESULT if len(arguments) == n.width else None 916 + ) 917 + 918 + @parametrized.product 919 + def test_no_main_rename( 920 + self, no_main_namespaces, driver=drivers, arguments=n.arguments.values() 921 + ): 922 + cls = driver(**rn.randomize_namespace(no_main_namespaces)) 923 + if driver.__name__ == "drive": 924 + cls._rewrap(no_args_is_help=False) 925 + assert run( 926 + cls, 927 + (rn.conversion_table[arg] for arg in arguments), 928 + arguments[-1] if len(arguments) == n.width else None, 929 + )
+573
packages/bundle/bundle/tests/test_bundle.py
··· 1 + import click 2 + import rich_click 3 + import os 4 + from bundle import bundle, eq, bundler, Bundle 5 + from pytest import fixture, mark 6 + from hydrox.typing import gformat_name 7 + from pathlib import Path 8 + from jugulis import AvailabilityError 9 + from rich.pretty import pprint 10 + from hypothesis import given, assume 11 + from collections.abc import Callable 12 + 13 + 14 + def fclick_apply(clicker, f): 15 + for wrapper in ( 16 + click.group(name="f", **bundle.defaults["group"]), 17 + click.argument("a", **bundle.defaults["argument"]), 18 + click.option("-b", "--b", **bundle.defaults["option"]), 19 + click.option( 20 + "-c", 21 + "--c", 22 + **( 23 + bundle.defaults["option"] | {"default": 3, "metavar": gformat_name(int)} 24 + ), 25 + ), 26 + click.argument("args", **(bundle.defaults["argument"] | {"nargs": -1})), 27 + click.option("-d", "--d", **(bundle.defaults["option"] | {"required": True})), 28 + click.option( 29 + "-e", 30 + "--e", 31 + **( 32 + bundle.defaults["option"] | {"default": 5, "metavar": gformat_name(int)} 33 + ), 34 + ), 35 + click.option( 36 + "-k", 37 + "--kwargs", 38 + **( 39 + bundle.defaults["option"] 40 + | {"metavar": gformat_name(tuple), "multiple": True} 41 + ), 42 + ), 43 + )[::-1]: 44 + f = wrapper(f) 45 + return f 46 + 47 + 48 + @fixture 49 + def f(): 50 + def f(a, /, b, c=3, *args, d, e=5, **kwargs): ... 51 + 52 + return f 53 + 54 + 55 + @fixture 56 + def cf(f): 57 + return fclick_apply(click, f) 58 + 59 + 60 + @fixture 61 + def rf(f): 62 + return fclick_apply(rich_click, f) 63 + 64 + 65 + @fixture 66 + def bf(f): 67 + return bundle(f) 68 + 69 + 70 + @fixture 71 + def bcf(f): 72 + @bundle(name="f") 73 + class F: 74 + __init__ = f 75 + 76 + return F 77 + 78 + 79 + def uclick_apply(clicker, u): 80 + for wrapper in ( 81 + click.group(name="u", **bundle.defaults["group"]), 82 + click.argument("z", **bundle.defaults["argument"]), 83 + click.option("-x", "--x", **bundle.defaults["option"]), 84 + click.option( 85 + "-y", 86 + "--y", 87 + **( 88 + bundle.defaults["option"] | {"default": 3, "metavar": gformat_name(int)} 89 + ), 90 + ), 91 + click.argument("args", **(bundle.defaults["argument"] | {"nargs": -1})), 92 + click.option("-w", "--w", **(bundle.defaults["option"] | {"required": True})), 93 + click.option( 94 + "-v", 95 + "--v", 96 + **( 97 + bundle.defaults["option"] | {"default": 5, "metavar": gformat_name(int)} 98 + ), 99 + ), 100 + click.option( 101 + "-k", 102 + "--kwargs", 103 + **( 104 + bundle.defaults["option"] 105 + | {"metavar": gformat_name(tuple), "multiple": True} 106 + ), 107 + ), 108 + )[::-1]: 109 + u = wrapper(u) 110 + return u 111 + 112 + 113 + @fixture 114 + def u(): 115 + def u(z, /, x, y=3, *args, w, v=5, **kwargs): ... 116 + 117 + return u 118 + 119 + 120 + @fixture 121 + def cu(u): 122 + return uclick_apply(click, u) 123 + 124 + 125 + @fixture 126 + def ru(u): 127 + return uclick_apply(rich_click, u) 128 + 129 + 130 + @fixture 131 + def bu(u): 132 + return bundle(u) 133 + 134 + 135 + @fixture 136 + def bcu(u): 137 + @bundle(name="u") 138 + class U: 139 + __init__ = u 140 + 141 + return U 142 + 143 + 144 + @fixture 145 + def bundles(scope="module"): 146 + return bundler(1, 2) 147 + 148 + 149 + @fixture 150 + def functions(bundles, scope="module"): 151 + return bundles.functions 152 + 153 + 154 + @fixture 155 + def integers(bundles, scope="module"): 156 + return bundles.type(int) 157 + 158 + 159 + @fixture 160 + def strings(bundles, scope="module"): 161 + return bundles.type(str) 162 + 163 + 164 + @fixture 165 + def classes(bundles, scope="module"): 166 + return bundles.cls() 167 + 168 + 169 + class TestEquality: 170 + def test_functions(self, cf, rf, bf, bcf, cu, ru, bu, bcu): 171 + fs = (cf, rf, bf, bcf) 172 + assert cf == bf == bcf 173 + assert rf == bf == bcf 174 + for fa in fs: 175 + if isinstance(fa, bundle): 176 + fa = fa.compile() 177 + for fb in fs: 178 + if isinstance(fb, bundle): 179 + fb = fb.compile() 180 + assert eq(fa, fb, True) 181 + 182 + us = (cu, ru, bu, bcu) 183 + assert cu == bu == bcu 184 + assert ru == bu == bcu 185 + for ua in us: 186 + if isinstance(ua, bundle): 187 + ua = ua.compile() 188 + for ub in us: 189 + if isinstance(ub, bundle): 190 + ub = ub.compile() 191 + assert eq(ua, ub, True) 192 + 193 + assert cf != bu == bcu 194 + assert rf != bu == bcu 195 + assert cu != bf == bcf 196 + assert ru != bf == bcf 197 + for fa, ua in zip(fs, us): 198 + if isinstance(fa, bundle): 199 + fa = fa.compile() 200 + if isinstance(ua, bundle): 201 + ua = ua.compile() 202 + for fb, ub in zip(fs, us): 203 + if isinstance(fb, bundle): 204 + fb = fb.compile() 205 + if isinstance(ub, bundle): 206 + ub = ub.compile() 207 + assert not eq(fa, ub, True) 208 + assert not eq(ua, fb, True) 209 + 210 + # TODO: Parametrize. 211 + def test_rootless_functions(self, functions, integers): 212 + pprint(integers.f) 213 + b1 = bundle( 214 + { 215 + "g_": functions.g_i, 216 + "g__": functions.g__i, 217 + "f1": { 218 + "g": functions.gi, 219 + "f1": functions.f1i, 220 + "f11": { 221 + "f11": functions.f11i, 222 + "g1": functions.g1i, 223 + }, 224 + "f12": { 225 + "f12": functions.f12i, 226 + "g1": functions.g1i, 227 + }, 228 + }, 229 + "f2": [ 230 + functions.gi, 231 + functions.f2i, 232 + { 233 + "f21": { 234 + "f21": functions.f21i, 235 + "g2": functions.g2i, 236 + } 237 + }, 238 + { 239 + "f22": { 240 + "f22": functions.f22i, 241 + "g2": functions.g2i, 242 + } 243 + }, 244 + ], 245 + } 246 + ) 247 + pprint(b1) 248 + b2 = bundle() 249 + b2.append(functions.g_i, name="g_") 250 + b2.append(functions.g__i, name="g__") 251 + b2.append(functions.f1i, name="f1") 252 + b2.f1.append(functions.gi, name="g") 253 + b2.f1.append(functions.f11i, name="f11") 254 + b2.f1.f11.append(functions.g1i, name="g1") 255 + b2.f1.append(functions.f12i, name="f12") 256 + b2.f1.f12.append(functions.g1i, name="g1") 257 + b2.append(functions.f2i, name="f2") 258 + b2.f2.append(functions.gi, name="g") 259 + b2.f2.append(functions.f21i, name="f21") 260 + b2.f2.f21.append(functions.g2i, name="g2") 261 + b2.f2.append(functions.f22i, name="f22") 262 + b2.f2.f22.append(functions.g2i, name="g2") 263 + pprint(b2) 264 + b3 = bundle( 265 + ( 266 + functions.g_i, 267 + {"g__": functions.g__i}, 268 + { 269 + "f1": { 270 + "g": functions.gi, 271 + "f1": functions.f1i, 272 + "f11": { 273 + "f11": functions.f11i, 274 + "g1": functions.g1i, 275 + }, 276 + "f12": { 277 + "f12": functions.f12i, 278 + "g1": functions.g1i, 279 + }, 280 + } 281 + }, 282 + { 283 + "f2": [ 284 + functions.gi, 285 + functions.f2i, 286 + { 287 + "f21": { 288 + "f21": functions.f21i, 289 + "g2": functions.g2i, 290 + } 291 + }, 292 + { 293 + "f22": { 294 + "f22": functions.f22i, 295 + "g2": functions.g2i, 296 + } 297 + }, 298 + ] 299 + }, 300 + ) 301 + ) 302 + pprint(b3) 303 + 304 + names = (b1.__name__, b2.__name__, b3.__name__) 305 + assert not any(names) 306 + assert integers.f.__name__ not in names 307 + 308 + funcs = (b1.func, b2.func, b3.func) 309 + assert not any(funcs) 310 + assert integers.f.func not in funcs 311 + 312 + assert b1 == b2 == b3 313 + assert integers.g_ == b1.g_ == b2.g_ == b3.g_ 314 + assert integers.g__ == b1.g__ == b2.g__ == b3.g__ 315 + assert integers.f1 == b1.f1 == b2.f1 == b3.f1 316 + assert integers.f2 == b1.f2 == b2.f2 == b3.f2 317 + 318 + @mark.skipif("PATH" not in os.environ, reason="No PATH variable") 319 + def test_path_envvar(self): 320 + @bundle(no_args_is_help=False) 321 + def f(*, PATH: list[Path]): 322 + return PATH 323 + 324 + assert f.run() == [Path(path) for path in os.environ["PATH"].split(os.pathsep)] 325 + 326 + def test_all(self, bundles, classes, functions): 327 + f = bundles.f 328 + pprint(f) 329 + cf = classes.f 330 + pprint(cf) 331 + b = bundle( 332 + functions.fi, 333 + functions.fs, 334 + functions.f1i, 335 + functions.f1s, 336 + functions.f2i, 337 + functions.f2s, 338 + [functions.g_i, functions.g_s, functions.g__i, functions.g__s], 339 + { 340 + "f1": [ 341 + { 342 + "f11": [ 343 + functions.f11i, 344 + functions.f11s, 345 + functions.g1i, 346 + functions.g1s, 347 + ] 348 + }, 349 + { 350 + "f12": [ 351 + functions.f12i, 352 + functions.f12s, 353 + functions.g1i, 354 + functions.g1s, 355 + ] 356 + }, 357 + functions.gi, 358 + functions.gs, 359 + ] 360 + }, 361 + { 362 + "f2": [ 363 + { 364 + "f21": [ 365 + functions.f21i, 366 + functions.f21s, 367 + functions.g2i, 368 + functions.g2s, 369 + ] 370 + }, 371 + { 372 + "f22": [ 373 + functions.f22i, 374 + functions.f22s, 375 + functions.g2i, 376 + functions.g2s, 377 + ] 378 + }, 379 + functions.gi, 380 + functions.gs, 381 + ] 382 + }, 383 + ) 384 + pprint(b) 385 + assert f == cf == b 386 + 387 + # TODO: Parametrize. 388 + def test_decorated_classes(self, strings, functions): 389 + pprint(strings.f1) 390 + 391 + @bundle(name="f1") 392 + class b1f1: 393 + __init__ = functions.f1i 394 + __init__ = functions.f1s 395 + 396 + class f11: 397 + __init__ = functions.f11i 398 + __init__ = functions.f11s 399 + 400 + class f12: 401 + __init__ = functions.f12i 402 + __init__ = functions.f12s 403 + 404 + class b1g(b1f1, name="g"): 405 + __init__ = functions.gs 406 + 407 + class b1g1(b1f1.f11, b1f1.f12, name="g1"): 408 + __init__ = functions.g1s 409 + 410 + pprint(b1f1) 411 + 412 + @bundle(name="f1") 413 + class b2f1: 414 + __init__ = functions.f1i 415 + __init__ = functions.f1s 416 + 417 + @bundle 418 + class f11: 419 + __init__ = functions.f11i 420 + __init__ = functions.f11s 421 + 422 + @bundle 423 + class f12: 424 + __init__ = functions.f12i 425 + __init__ = functions.f12s 426 + 427 + class b2g(b2f1, name="g"): 428 + __init__ = functions.gs 429 + 430 + class b2g1(b2f1.f11, b2f1.f12, name="g1"): 431 + __init__ = functions.g1s 432 + 433 + pprint(b2f1) 434 + 435 + assert False 436 + 437 + assert strings.f1 == b1f1 == b2f1 438 + 439 + def test_root_inherited_child_decorated_class(self, bundles, strings, functions): 440 + class f1(bundle): 441 + __init__ = functions.f1i 442 + __init__ = functions.f1s 443 + 444 + @bundle 445 + class f11: 446 + __init__ = functions.f11i 447 + __init__ = functions.f11s 448 + 449 + @bundle 450 + class f12: 451 + __init__ = functions.f12i 452 + __init__ = functions.f12s 453 + 454 + pprint(f1) 455 + 456 + assert bundles.f1.func == f1.func 457 + assert strings.f1.func != f1.func 458 + assert bundles.f11.func != f1.f11.func 459 + assert bundles.f12.func != f1.f12.func 460 + assert strings.f11.func == f1.f11.func 461 + assert strings.f12.func == f1.f12.func 462 + 463 + def test_root_decorated_child_inherited_class(self, bundles, strings, functions): 464 + @bundle 465 + class f1: 466 + __init__ = functions.f1i 467 + __init__ = functions.f1s 468 + 469 + class f11(bundle): 470 + __init__ = functions.f11i 471 + __init__ = functions.f11s 472 + 473 + class f12(bundle): 474 + __init__ = functions.f12i 475 + __init__ = functions.f12s 476 + 477 + pprint(f1) 478 + 479 + assert bundles.f1.func != f1.func 480 + assert strings.f1.func == f1.func 481 + assert bundles.f11.func == f1.f11.func 482 + assert bundles.f12.func == f1.f12.func 483 + assert strings.f11.func != f1.f11.func 484 + assert strings.f12.func != f1.f12.func 485 + 486 + def test_shared_children(self, bundles, functions): 487 + f11 = bundle(functions.f11i, functions.f11s) 488 + f12 = bundle(functions.f12i, functions.f12s) 489 + f21 = bundle(functions.f21i, functions.f21s) 490 + f22 = bundle(functions.f22i, functions.f22s) 491 + 492 + class g(bundle): 493 + @f11.up 494 + @f12.up 495 + class g1(bundle): 496 + __init__ = functions.g1i 497 + __init__ = functions.g1s 498 + 499 + @f21.up 500 + @f22.up 501 + class g2(bundle): 502 + __init__ = functions.g2i 503 + __init__ = functions.g2s 504 + 505 + assert bundles.f11 == f11 506 + assert bundles.f12 == f12 507 + assert bundles.f21 == f21 508 + assert bundles.f22 == f22 509 + 510 + @given(name_1=..., name_2=..., args_1=..., args_2=...) 511 + def test_inequality( 512 + self, 513 + name_1: str, 514 + name_2: str, 515 + args_1: dict[str, Callable], 516 + args_2: dict[str, Callable], 517 + ): 518 + assume((name_1 != name_2) or (args_1 != args_2)) 519 + namespace_1 = Bundle.__prepare__(name_1, tuple(), args_1) 520 + namespace_2 = Bundle.__prepare__(name_1, tuple(), args_2) 521 + assert bundle(type(name_1, (bundle,), namespace_1)) != bundle( 522 + type(name_2, (bundle,), namespace_2) 523 + ) 524 + 525 + 526 + class TestDocumentation: 527 + def test_short(self, functions): 528 + f = ( 529 + bundle(functions.f1i) 530 + .document("""A test function. 531 + 532 + Args: 533 + i (int): A test integer. 534 + """) 535 + .compile() 536 + ) 537 + assert f.help == "A test function." 538 + assert f.short_help == "A test function." 539 + param = f.params[0] 540 + assert param.metavar == "int" 541 + assert param.help == "A test integer." 542 + 543 + def test_long(self, functions): 544 + f = ( 545 + bundle(functions.f1i) 546 + .document("""A test function. 547 + 548 + This is a test of the help text. 549 + This is another test of the help text. 550 + 551 + Args: 552 + i (int): A test integer. 553 + """) 554 + .compile() 555 + ) 556 + assert ( 557 + f.help 558 + == "A test function.\b\n\nThis is a test of the help text.\b\n\nThis is another test of the help text." 559 + ) 560 + assert f.short_help == "A test function." 561 + param = f.params[0] 562 + assert param.metavar == "int" 563 + assert param.help == "A test integer." 564 + 565 + 566 + class TestErrors: 567 + @mark.xfail(raises=AvailabilityError) 568 + def test_rootless_bundle(self): 569 + bundle().root 570 + 571 + @mark.xfail(raises=AvailabilityError) 572 + def test_uninvokable_bundle(self): 573 + bundle().invoke(None)
+9
packages/bundle/bundle/types.py
··· 1 + from hydrox.typing import Generalize 2 + 3 + 4 + class Password: ... 5 + 6 + 7 + @Generalize 8 + class Pass: 9 + pass
+13 -2
packages/bundle/pyproject.toml
··· 12 12 requires-python = ">=3.14" 13 13 license = "MIT" 14 14 urls."Source code" = "https://tangled.sh/@syvl.org/bundle" 15 - dependencies = [ "jugulis" ] 15 + dependencies = [ 16 + "jugulis", 17 + "magicattr>=0.1.6", 18 + "pathvalidate>=3.2.3", 19 + "plum-dispatch>=2.5.7", 20 + "rich-click>=1.8.9", 21 + ] 16 22 17 23 [build-system] 18 24 requires = ["hatchling"] ··· 22 28 packages = [ "bundle" ] 23 29 24 30 [tool.uv.sources] 25 - jugulis = { workspace = true } 31 + jugulis = { workspace = true } 32 + 33 + [dependency-groups] 34 + dev = [ 35 + "thefuzz>=0.22.1", 36 + ]
packages/bundle/test1.py

This is a binary file and will not be displayed.

+26
packages/bundle/test2.py
··· 1 + import rich_click as click 2 + from rich.pretty import pprint 3 + import sys 4 + 5 + 6 + def callback(ctx, param, value): 7 + print(ctx, param, value, sys.argv) 8 + pprint({k: getattr(ctx, k, None) for k in dir(ctx)}) 9 + pprint({k: getattr(param, k, None) for k in dir(param)}) 10 + pprint({k: getattr(value, k, None) for k in dir(value)}) 11 + 12 + 13 + @click.command() 14 + @click.option("-i", "--i", nargs=2, callback=callback) 15 + def test(i): ... 16 + 17 + 18 + test() 19 + 20 + # @click.command() 21 + # @click.option('paths', '--path', envvar=['PATH', "PATHS"], multiple=True, 22 + # type=click.Path()) 23 + # def perform(paths): 24 + # print(paths) 25 + 26 + # perform()
+26
packages/bundle/test3.py
··· 1 + from rich import print 2 + from rich.pretty import pprint 3 + 4 + 5 + class meta1(type): 6 + def __repr__(self): 7 + return repr({}) 8 + 9 + 10 + class test1(metaclass=meta1): ... 11 + 12 + 13 + print(test1) 14 + pprint(test1) 15 + 16 + 17 + class meta2(type): 18 + def __rich_repr__(self): 19 + yield {} 20 + 21 + 22 + class test2(metaclass=meta2): ... 23 + 24 + 25 + print(test2) 26 + pprint(test2)
+9 -5
packages/hydrox/hydrox/helpers/__init__.py
··· 6 6 from importlib import import_module 7 7 from pathlib import Path 8 8 9 - modules = [] 9 + modules = {} 10 10 for file in Path(__file__).parent.iterdir(): 11 11 stem = file.stem 12 12 if not ( ··· 15 15 or stem.endswith("__") 16 16 or stem in "hypothesis" 17 17 ): 18 - modules.append(import_module("." + stem, package=__name__)) 18 + modules[stem] = import_module("." + stem, package=__name__) 19 19 # module = import_module("." + stem, package=__name__) 20 20 # globals().update( 21 21 # { ··· 38 38 def __getattr__(self, t: str): 39 39 if t.startswith("__"): 40 40 raise AttributeError(t) 41 - if t in ("tests", "hypothesis"): 42 - # return import_module("." + t, package=__name__) 41 + if t in ("tests",): 43 42 return eval(t) 44 43 if t in ("_inf", "_aleph", "_ninf", "_naleph"): 45 44 return eval(t.lstrip("_")) 45 + if t.startswith("_"): 46 + stripped = t.strip("_") 47 + if stripped in modules: 48 + return modules[stripped] 49 + raise AttributeError(t) 46 50 if t.startswith("infm"): 47 51 return inf("-" + t.removeprefix("infm")) 48 52 if t.startswith("inf"): ··· 59 63 return naleph("-" + t.removeprefix("nalephm")) 60 64 if t.startswith("naleph"): 61 65 return naleph(t.removeprefix("nalephp").removeprefix("naleph") or 0) 62 - for module in modules: 66 + for module in modules.values(): 63 67 if hasattr(module, t): 64 68 return getattr(module, t) 65 69 raise AttributeError(t)
+16 -11
packages/hydrox/hydrox/helpers/helpers.py
··· 7 7 import types 8 8 9 9 from contextlib import suppress, contextmanager 10 - from cytoolz.itertoolz import unique 11 10 from functools import partial 12 11 from inspect import ( 13 12 currentframe, ··· 15 14 getmodule, 16 15 ) 17 16 from itertools import permutations 18 - from more_itertools import powerset 17 + from more_itertools import powerset, unique_everseen 19 18 20 19 BString: typing.TypeAlias = str | bytes | bytearray 21 - 22 - 23 - def unwrap(func): 24 - return getattr(func, "__wrapped__", func) 25 20 26 21 27 22 def ziperr(err): ··· 41 36 raise e 42 37 43 38 39 + @contextmanager 40 + def hashsuppress(): 41 + try: 42 + yield 43 + except TypeError as e: 44 + if "unhashable type" not in str(e): 45 + raise e 46 + 47 + 44 48 def reversecut(l, i=None): 45 49 # length = len(l) 46 50 # if i is None: ··· 173 177 174 178 175 179 def superpowerset(*iterables): 176 - return unique( 180 + return unique_everseen( 177 181 flatten((powerset(p) for p in permutations(flatten(iterables))), levels=2) 178 182 ) 179 183 180 184 185 + # TODO: Stop at the first line that starts with `@` on the same level of indentation. 181 186 # Adapted From: 182 187 # Answer 1: https://stackoverflow.com/a/60778287/10827766 183 188 # User 1: https://stackoverflow.com/users/2729778/changaco ··· 267 272 # The source file seems to have been modified. 268 273 return default 269 274 call_line_ls = call_line.lstrip() 270 - if re.match(query, call_line_ls): 275 + if call_line_ls.startswith("@"): 271 276 # Note: there is a small probability of false positive here, if the 272 277 # function call is on the same line as a decorator call. 273 - return True 278 + return re.match(query, call_line_ls) or False 274 279 if call_line_ls.startswith("class ") or call_line_ls.startswith("def "): 275 280 # Note: there is a small probability of false positive here, if the 276 281 # function call is on the same line as a `class` or `def` keyword. 277 - return True 282 + return re.match(query, call_line_ls) or False 278 283 # Next, we try to find and examine the line after the function call. 279 284 # If that line doesn't start with a `class` or `def` keyword, then the 280 285 # function isn't being called as a decorator. ··· 305 310 line_ls = line.lstrip() 306 311 if line_ls[:1] in (")", ","): 307 312 continue 308 - return re.match(query, line_ls) or False 313 + return line_ls.startswith("@") 309 314 elif len(line_indentation) < len(def_line_indentation): 310 315 break 311 316 return default
+222 -158
packages/hydrox/hydrox/typing/generic.py
··· 12 12 import re 13 13 import sys 14 14 15 - from ..helpers import reversecut, Collection, flatten, zipsuppress, ziperr, unwrap 15 + from ..helpers import reversecut, Collection, flatten, zipsuppress, ziperr, hashsuppress 16 16 17 - from ..helpers.cache import CacheError, cache, cachehash 17 + from ..helpers.cache import cache, cachehash 18 18 from ..helpers.decorator import Decorator 19 19 from ..variables import unique_attributes 20 20 from abc import ABCMeta 21 21 from contextlib import suppress 22 22 from collections import defaultdict 23 23 from cytoolz.dicttoolz import keymap 24 - from cytoolz.itertoolz import unique 25 - from functools import partial, reduce, cache as defcache 24 + from functools import partial, reduce 26 25 from inspect import Parameter 27 - from inspect import getmro, stack 28 - from more_itertools import partition 26 + from inspect import getmro, stack, unwrap 27 + from more_itertools import partition, unique_everseen 29 28 from random import randint 30 29 from rich.table import Table 31 30 from rich.console import Console ··· 87 86 ) 88 87 89 88 90 - # TODO 91 - # @defcache 92 89 @cache 93 - def get_name(annotation: AnnotationString) -> str: 94 - """Get the name or representation of a type annotation. 90 + def get_name_origin(annotation: AnnotationString) -> str: 91 + """Get the original name or representation of a type annotation. 95 92 96 93 Args: 97 - annotation (AnnotationString): The annotation to get the name or representation of. 94 + annotation (AnnotationString): The annotation to get the original name or representation of. 98 95 99 96 Returns: 100 - str: The name or representation of the annotation. 97 + str: The original name or representation of the annotation. 101 98 """ 102 99 if isinstance(annotation, str): 103 100 return annotation ··· 112 109 return repr(annotation) 113 110 114 111 115 - @defcache 112 + @cache 113 + def get_name(annotation: AnnotationString) -> str: 114 + """Get the original name or representation of a type annotation. 115 + 116 + Args: 117 + annotation (AnnotationString): The annotation to get the original name or representation of. 118 + 119 + Returns: 120 + str: The original name or representation of the annotation. 121 + """ 122 + if isinstance(annotation, str): 123 + return annotation 124 + annotation_name = get_name_origin(annotation) 125 + if args := typing.get_args(annotation): 126 + return annotation_name + f"[{', '.join(get_name(arg) for arg in args)}]" 127 + return annotation_name 128 + 129 + 130 + @cache 116 131 def get_name_lowered(annotation: AnnotationString) -> str: 117 132 """Get the lowercase name or representation of a type annotation. 118 133 ··· 128 143 NameCheck: typing.TypeAlias = collections.abc.Callable[[str, str], bool] 129 144 130 145 131 - @defcache 146 + @cache 132 147 def get_name_check( 133 148 a: AnnotationString, 134 149 /, ··· 157 172 ) 158 173 159 174 160 - @defcache 175 + @cache 161 176 def get_union_check(annotation: AnnotationString) -> bool: 162 177 """Checks whether a type annotation is a _Union_ or _UnionType_. 163 178 ··· 170 185 return get_name_check(annotation, "union", "uniontype") 171 186 172 187 173 - @defcache 188 + @cache 174 189 def get_name_remove_suffix(annotation: AnnotationString, suffix: str) -> str: 175 190 """Removes a suffix from a type annotation name or representation. 176 191 ··· 196 211 Annotation: The origin of the annotation. 197 212 """ 198 213 if getattr(annotation, "__module__", None) == "typing": 199 - return getattr(typing, get_name(annotation)) 214 + return getattr(typing, get_name_origin(annotation)) 200 215 return typing.get_origin(annotation) 201 216 202 217 ··· 223 238 return args 224 239 225 240 226 - @defcache 241 + @cache 227 242 def add_camel_check(name): 228 243 name: str = get_name(name) 229 244 return name.isalpha() and name[0].isupper() 230 245 231 246 232 - @defcache 233 247 def dirattr(module): 234 - names = dir(module) 235 - attrs = set() 236 - for name in names: 248 + for name in dir(module): 249 + yield name 237 250 attr = getattr(module, name) 238 251 if isinstance(attr, Annotation): 239 - attrs.add(get_name(attr)) 240 - return flatten(names, attrs) 252 + n = get_name(attr) 253 + if name != n: 254 + yield n 241 255 242 256 243 257 modules = { ··· 309 323 super().__delitem__(get_name_lowered(key)) 310 324 311 325 def __getattr__(self, key): 326 + if len(key) >= 44: 327 + raise AttributeError() 312 328 return self[key] 313 329 314 330 def __getitem__(self, key): ··· 394 410 super().__delitem__(get_name(key)) 395 411 396 412 def __getattr__(self, key): 413 + if len(key) >= 44: 414 + raise AttributeError() 397 415 return self[key] 398 416 399 417 def __getitem__(self, key): 400 - if typing.get_args(key) or annotation_is_typing_instance(key): 401 - return GenericArgs[key] 402 - if not (isinstance(key, str) or isinstance(key, Annotation)): 403 - raise CacheError() 418 + if ( 419 + not (isinstance(key, str) or isinstance(key, Annotation)) 420 + or typing.get_args(key) 421 + or annotation_is_typing_instance(key) 422 + ): 423 + raise KeyError() 404 424 key = get_name(key) 405 425 try: 406 426 return super().__getitem__(key) ··· 484 504 default_subscription_size = -2 485 505 486 506 487 - @defcache 507 + @cache 488 508 def trysubscriptionsize(g): 489 509 try: 490 510 g[*[int] * test_subscription_size] ··· 519 539 return default_subscription_size 520 540 521 541 522 - @defcache 542 + @cache 523 543 def get_subscription_size(g): 524 544 if isinstance(g, str): 525 545 return default_subscription_size ··· 545 565 return tuple() 546 566 547 567 548 - @defcache 568 + @cache 549 569 def trymro_set(cls): 550 570 if cls is typing.Set or cls is collections.abc.Set: 551 571 return getmro(set) 552 572 return trymro(cls) 553 573 554 574 555 - @defcache 575 + @cache 556 576 def trymro_generic(cls): 557 577 return [m for m in trymro_set(cls) if not geeq(m, typing.Generic)] 558 578 ··· 564 584 return tuple() 565 585 566 586 567 - @defcache 587 + @cache 568 588 def check_subscription_size(size): 569 589 return size > default_subscription_size 570 590 571 591 572 - @defcache 592 + @cache 573 593 def random_subscription_size(rand, size): 574 594 global max_subscription_size 575 595 if size == -1 and rand: ··· 579 599 return size 580 600 581 601 582 - @defcache 602 + @cache 583 603 def get_inner_subscription_size(rand, generic, bases=tuple()): 584 604 randinner = partial(random_subscription_size, rand) 585 605 bases = bases or trybases(generic) ··· 602 622 603 623 604 624 # TODO: How did ht.UserDict originally get the 2, then...? 605 - @defcache 625 + @cache 606 626 def get_generic_subscription_size(t, /, *, root_bases=tuple(), rand=False): 607 627 randinner = partial(random_subscription_size, rand) 608 628 ··· 639 659 return 1 640 660 641 661 642 - @defcache 662 + @cache 643 663 def is_subscriptable(t): 644 664 if isinstance(t, GenericMeta): 645 665 return bool(t.__subscriptions__) ··· 650 670 non_generic_classes = (object, None, ...) + recursive_classes 651 671 652 672 653 - @defcache 673 + @cache 654 674 def get_named(n: str, getter): 655 675 match n: 656 676 case "Union" | "UnionType": ··· 661 681 return getter(n) 662 682 663 683 664 - @defcache 684 + @cache 665 685 def get_named_generics(n: str): 666 686 inner_generics = [] 667 687 for module in modules.values(): ··· 673 693 return tuple(inner_generics) 674 694 675 695 676 - @defcache 696 + @cache 677 697 def get_named_ht(n: str): 678 698 try: 679 699 return (Generics[n],) ··· 726 746 727 747 728 748 # TODO: Implement the cache additions in `GenericMeta` or the `get` functions for the `has` functions as well. 729 - @defcache 749 + @cache 730 750 def has_named_generics(n): 731 751 return tuple([module for module in modules.values() if hasattr(module, n)]) 732 752 733 753 734 - @defcache 754 + @cache 735 755 def has_named_ht(n): 736 756 return (Generics,) if n in Generics else tuple() 737 757 ··· 771 791 return has_all_generics(generic) + has_all_ht(generic) 772 792 773 793 774 - @defcache 794 + @cache 775 795 def not_generic_class_error(error): 776 796 error = error if isinstance(error, str) else str(error) 777 797 return error not in ( ··· 815 835 ignored = [get_name_lowered(i) for i in ignored] 816 836 if not inner: 817 837 818 - @defcache 838 + @cache 819 839 def inner(camel): 820 840 return [ 821 841 scls ··· 823 843 if get_name_lowered(scls) not in ignored and check_module(scls, modules) 824 844 ] 825 845 826 - gsc = gsc or defcache( 846 + gsc = gsc or cache( 827 847 partial(get_subclasses, ignored=ignored, modules=modules, inner=inner) 828 848 ) 829 849 if get_name_lowered(cls) not in ignored: ··· 852 872 return ([cls] if add_self else []) + classes 853 873 854 874 855 - @defcache 875 + @cache 856 876 def get_superclasses(cls, /, *, add_self=False, ignored=tuple()): 857 877 ignored = [get_name_lowered(i) for i in ignored] 858 878 return [t for t in trymro(cls)[int(not add_self) :] if get_name(t) not in ignored] ··· 920 940 return origin 921 941 922 942 923 - # TODO: Account for `ForwardRefs`. 924 - @cache(Generics) 943 + @cache 925 944 def generalize(annotation): 926 945 # TODO: Is this necessary? 927 946 # if annotation is None: ··· 985 1004 986 1005 987 1006 # NOTE: Can't cache this; the functions might have defaults with integers, booleans, floats, or strings. 988 - def signature(sig: inspect.Signature, t=typing.Any): 1007 + def signature(sig: inspect.Signature, t=typing.Any, ta=tuple, tk=dict[str, typing.Any]): 989 1008 if not isinstance(sig, inspect.Signature): 990 - sig = inspect.signature(sig, eval_str=True) 1009 + if hasattr(sig, "__signature__"): 1010 + sig = sig.__signature__ 1011 + else: 1012 + sig = inspect.signature(sig, eval_str=True) 991 1013 parameters = [] 992 1014 for param in sig.parameters.values(): 993 1015 if param.annotation is inspect.Parameter.empty: 994 - if param.kind in ( 995 - inspect.Parameter.VAR_POSITIONAL, 996 - inspect.Parameter.VAR_KEYWORD, 997 - ): 998 - parameters.append(param) 1016 + if param.kind is inspect.Parameter.VAR_POSITIONAL: 1017 + parameters.append( 1018 + param.replace( 1019 + annotation=( 1020 + generalize(ta) 1021 + if param.default is inspect.Parameter.empty 1022 + else get_type(param.default) 1023 + ) 1024 + ) 1025 + ) 1026 + elif param.kind is inspect.Parameter.VAR_KEYWORD: 1027 + parameters.append( 1028 + param.replace( 1029 + annotation=( 1030 + generalize(tk) 1031 + if param.default is inspect.Parameter.empty 1032 + else get_type(param.default) 1033 + ) 1034 + ) 1035 + ) 999 1036 else: 1000 1037 parameters.append( 1001 1038 param.replace( 1002 - annotation=generalize( 1003 - t 1039 + annotation=( 1040 + generalize(t) 1004 1041 if param.default is inspect.Parameter.empty 1005 1042 else get_type(param.default) 1006 1043 ) ··· 1118 1155 raise TypeError("type() takes between 1 and 3 arguments") 1119 1156 1120 1157 1121 - @defcache 1158 + @cache 1122 1159 def get_generic_names(generics): 1123 1160 return {get_name_lowered(g) for g in generics} 1124 1161 ··· 1185 1222 return False 1186 1223 1187 1224 1188 - @defcache 1225 + @cache 1189 1226 def _trysubclass(left, right): 1190 1227 try: 1191 1228 return builtins.issubclass(left, right) ··· 1200 1237 return _trybase(left, right, _trysubclass) 1201 1238 1202 1239 1203 - @defcache 1240 + @cache 1204 1241 def get_all_set_generics(annotation): 1205 1242 return (get_generics if geeq(get_origin(annotation), set) else get_all_generics)( 1206 1243 annotation 1207 1244 ) 1208 1245 1209 1246 1210 - @defcache 1247 + @cache 1211 1248 def _tryinmro(left, right): 1212 1249 return right in trymro(left) 1213 1250 ··· 1217 1254 return _trybase(left, right, _tryinmro) 1218 1255 1219 1256 1220 - @defcache 1257 + @cache 1221 1258 def _tryinmrosubclass(left, right): 1222 1259 return tryinmro(left, right) or trysubclass(left, right) 1223 1260 ··· 1238 1275 return flatten( 1239 1276 partition( 1240 1277 annotation_is_not_generic, 1241 - unique( 1278 + unique_everseen( 1242 1279 flatten( 1243 1280 annotation, 1244 1281 map( ··· 1251 1288 ) 1252 1289 1253 1290 1291 + @cache 1254 1292 def unwrap_check(cls): 1255 1293 if has_custom_init(cls): 1256 1294 cls = type(cls) ··· 1260 1298 ) 1261 1299 1262 1300 1301 + @cache 1302 + def unwrap_generic(cls): 1303 + return officiate(cls) if unwrap_check(cls) else cls 1304 + 1305 + 1263 1306 # TODO: This is going to trip up with types that have the same name as the official generics, 1264 1307 # but don't aren't connected to them, such as `hh.Collection` and `abc.Collection`. 1265 1308 @cache ··· 1267 1310 if left_right_collection_check(left, right): 1268 1311 if isinstance(right, abc.Iterator): 1269 1312 warn(f'variable "right" of value "{right}" will be used up', TypeWarning) 1270 - else: 1271 - if unwrap_check(right): 1272 - generic = officiate(right) 1273 - else: 1274 - generic = right 1313 + right = tuple(right) 1275 1314 for l in generic_flatten(left): 1276 1315 for r in generic_flatten(right): 1277 1316 if tryinmro(l, r): 1278 1317 return l, r 1279 - for r in generic_flatten(generic): 1318 + for r in generic_flatten(map(unwrap_generic, flatten(right))): 1280 1319 if trysubclass(l, r): 1281 1320 return l, r 1282 1321 return tuple() 1283 1322 1284 1323 1285 - @defcache 1324 + @cache 1286 1325 def argsinmrosubclass(left_args, right_args, strict): 1287 1326 with zipsuppress(): 1288 1327 return all( ··· 1372 1411 return None 1373 1412 1374 1413 1375 - @defcache 1414 + def anything(annotation, *extras): 1415 + if isinstance(annotation, Annotation): 1416 + if annotation is ...: 1417 + return True 1418 + for generic in flatten( 1419 + object, typing.Any, typing.Union, Parameter.empty, extras 1420 + ): 1421 + if geeq(annotation, generic): 1422 + return True 1423 + return False 1424 + 1425 + 1426 + @cache 1376 1427 def scoremro(left, right): 1377 1428 if left == right: 1378 1429 return 1 ··· 1381 1432 mro = (left, object) 1382 1433 else: 1383 1434 mro = trymro_generic(left) 1435 + unrelated_score = -(len(mro) + 1) 1436 + if anything(right): 1437 + return unrelated_score 1384 1438 try: 1385 1439 return 1 - mro.index(right) 1386 1440 except ValueError as e: 1387 1441 if "not in" in str(e): 1388 - return -(len(mro) + 1) 1442 + return unrelated_score 1389 1443 raise e 1390 1444 1391 1445 1392 - @defcache 1446 + @cache 1393 1447 def score(left, right): 1394 1448 if result := tryingenericmrosubclass(left, right): 1395 1449 return scoremro(*result) ··· 1398 1452 any_score = 1 1399 1453 1400 1454 1401 - @defcache 1455 + @cache 1402 1456 def scoreargs(left_args, right_args, strict): 1403 1457 return sumscore( 1404 1458 scoresubclass(la, ra) for la, ra in zip(left_args, right_args, strict=strict) 1405 1459 ) 1406 1460 1407 1461 1408 - @defcache 1462 + @cache 1409 1463 def _scoresubclass(left, right): 1410 1464 left_origin, right_origin = get_origin(left), get_origin(right) 1411 1465 right_args = typing.get_args(right) ··· 1493 1547 return score(left_origin, right_origin) 1494 1548 1495 1549 1496 - @defcache 1550 + @cache 1497 1551 def is_custom_init(init): 1498 1552 return init not in (object.__init__, None) 1499 1553 1500 1554 1501 - @defcache 1555 + @cache 1502 1556 def has_custom_init(cls): 1503 1557 return ( 1504 1558 cls.__custom_init__ ··· 1507 1561 ) 1508 1562 1509 1563 1510 - @defcache 1564 + @cache 1511 1565 def scoresubclass(left, right): 1566 + if anything(right): 1567 + return scoremro(left, right) 1512 1568 left, right = generalize(left), generalize(right) 1513 1569 if hasattr(right, "__subclassscore__"): 1514 1570 if has_custom_init(right): ··· 1520 1576 return _scoresubclass(left, right) 1521 1577 1522 1578 1523 - def anything(annotation): 1524 - if isinstance(annotation, Annotation): 1525 - if annotation is ...: 1526 - return True 1527 - for generic in (object, typing.Any, typing.Union): 1528 - if geeq(annotation, generic): 1529 - return True 1530 - return False 1531 - 1532 - 1533 1579 # NOTE: Can't cache this; booleans, floats, strings, and integers sometimes have the same hash values. 1534 1580 def scoreinstance(left, right): 1535 1581 if anything(right): 1536 - return any_score 1582 + return scoremro(get_type(left), right) 1537 1583 right = generalize(right) 1538 1584 if hasattr(right, "__instancescore__"): 1539 1585 if has_custom_init(right): ··· 1585 1631 raise TypeError(f"'{cls}' object is not callable") 1586 1632 1587 1633 1588 - # TODO: Can't cache this; it might get non-truthy objects. 1634 + # TODO: Modify this to work with type arguments. 1635 + # NOTE: Can't cache this; it might get non-truthy objects. 1589 1636 def format_name(obj): 1590 1637 if isinstance(obj, list): 1591 1638 return f"[{', '.join([format_name(a) for a in obj])}]" ··· 1658 1705 return r 1659 1706 1660 1707 1708 + def gformat_name(obj): 1709 + return format_name(generalize(obj)) 1710 + 1711 + 1661 1712 @cache 1662 1713 def geeq(left, right): 1663 1714 if not left_right_annotation_check(left, right): ··· 1673 1724 # Adapted From: 1674 1725 # Answer: https://stackoverflow.com/a/27952689/10827766 1675 1726 # User: https://stackoverflow.com/users/1774667/yakk-adam-nevraumont 1676 - @defcache 1727 + @cache 1677 1728 def reducecachehash(a, b): 1678 1729 return (cachehash(a) * 3) + cachehash(b) 1679 1730 ··· 1690 1741 # "hydrox/hydrox/typing/tests" in os.environ.get("PYTEST_CURRENT_TEST", "") 1691 1742 # ): 1692 1743 # return f"{''.join(part[0] for part in cls.__module__.split('.')[:2])}.{cls.__name__}" 1744 + # TODO: Set this up to work with a debug variable. 1693 1745 # if annotation_is_generic(cls) or isinstance(cls, FormatMeta): 1694 1746 # if getattr(cls, "__generic__", sentinel) is sentinel: 1695 1747 # return f"{''.join(part[0] for part in cls.__module__.split('.')[:2])}.{cls.__name__}" ··· 1954 2006 else: 1955 2007 alias_result.__generic__ = sentinel 1956 2008 1957 - try: 1958 - alias_result.__hash_value__ = reducehash( 1959 - origin, 1960 - ( 1961 - arg.items() 1962 - if isinstance(arg, abc.Mapping) 1963 - else arg 1964 - for arg in args 1965 - ), 1966 - ) 1967 - 1968 - GenericArgs[alias_result] = alias_result 2009 + with hashsuppress(): 2010 + generalize.cache[alias_result] = alias_result 1969 2011 1970 2012 officiate.cache[alias_result] = alias_result.__generic__ 1971 2013 ··· 1979 2021 current_generic_alias_results 1980 2022 ) 1981 2023 1982 - except TypeError as e: 1983 - if "unhashable type" not in str(e): 1984 - raise e 1985 - 1986 2024 alias_result.__repr_message__ = f"{origin}[{', '.join([format_name(arg) for arg in args])}]" 1987 2025 1988 2026 return alias_result ··· 1995 2033 1996 2034 r.__class_getitem__ = no_class_getitem 1997 2035 r.__subscriptions__ = s 1998 - new_defaults = defaults | {"name": n, "subscriptions": s} 1999 - del new_defaults["ns"] 2000 - r.__hash_value__ = reducehash( 2001 - sys.modules[__name__].__package__, 2002 - n, 2003 - new_defaults.items(), 2004 - generics, 2005 - ) 2006 - 2007 - # TODO: Put these in `Generalize` or similar. 2008 - # Although, if I do that, subclasses of Generics won't be added... 2009 - for rn in (r, n): 2010 - officiate.cache[rn] = r.__generic__ 2036 + r.__defaults__ = { 2037 + k: v for k, v in defaults.items() if k not in ("ns",) 2038 + } | {"name": n, "subscriptions": s} 2011 2039 2012 2040 current_generic_results = current_generics + (r,) 2013 2041 for gr in current_generic_results + (n,): 2014 - get_ht.cache[gr] = (r,) 2015 - get_generics.cache[gr] = current_generics 2016 - get_ht_generics.cache[gr] = current_generic_results 2042 + with hashsuppress(): 2043 + generalize.cache[gr] = r 2044 + officiate.cache[gr] = r.__generic__ 2045 + get_ht.cache[gr] = (r,) 2046 + get_generics.cache[gr] = current_generics 2047 + get_ht_generics.cache[gr] = current_generic_results 2017 2048 2018 2049 r.__repr_message__ = FormatMeta.__repr__(r) 2019 2050 ··· 2049 2080 results = (result, Result) 2050 2081 generic_results = result.__generics__ + results 2051 2082 for gr in generic_results + (name, Name): 2052 - get_all_ht.cache[gr] = results 2053 - get_all_generics.cache[gr] = result.__generics__ 2054 - get_all_ht_generics.cache[gr] = generic_results 2083 + with hashsuppress(): 2084 + get_all_ht.cache[gr] = results 2085 + get_all_generics.cache[gr] = result.__generics__ 2086 + get_all_ht_generics.cache[gr] = generic_results 2055 2087 2056 2088 alias_results = (result.__alias__, Result.__alias__) 2057 2089 both_alias_generics = ( ··· 2059 2091 ) 2060 2092 generic_alias_results = both_alias_generics + alias_results 2061 2093 for gar in generic_alias_results: 2062 - get_all_ht.cache[gar] = alias_results 2063 - get_all_generics.cache[gar] = both_alias_generics 2064 - get_all_ht_generics.cache[gar] = generic_alias_results 2094 + with hashsuppress(): 2095 + get_all_ht.cache[gar] = alias_results 2096 + get_all_generics.cache[gar] = both_alias_generics 2097 + get_all_ht_generics.cache[gar] = generic_alias_results 2065 2098 2066 2099 # return Result if camelized_name else result 2067 2100 return Result if Result.__name__ == defaults["name"] else result ··· 2079 2112 not isinstance(other, Annotation) 2080 2113 ): 2081 2114 return NotImplemented 2082 - # other_camel = getattr(other, "__camel__", None) 2083 - # return ( 2084 - # other is cls 2085 - # or other_camel is cls 2086 - # or other is cls.__camel__ 2087 - # or other_camel is cls.__camel__ 2088 - # # NOTE: Because the results are camelized, 2089 - # # `other` is being checked against the 2090 - # # `cls.__camel__` generics anyway. 2091 - # or other in cls.__generics__ 2092 - # or NotImplemented 2093 - # ) 2094 2115 return geeq(cls, other) or NotImplemented 2095 2116 2096 2117 # Adapted From: 2097 2118 # Answer: https://stackoverflow.com/a/7810592 2098 2119 # User: https://stackoverflow.com/users/68998/newtover 2099 - def __hash__(cls): 2100 - return cls.__hash_value__ 2120 + def __hash__(self): 2121 + return reducehash( 2122 + sys.modules[__name__].__package__, 2123 + self.__defaults__.items(), 2124 + self.__generics__, 2125 + ) 2101 2126 2102 2127 def __call__(cls, *args, **kwargs): 2103 2128 return call(cls, *args, **kwargs) ··· 2106 2131 def __repr__(cls): 2107 2132 return cls.__repr_message__ 2108 2133 2109 - @defcache 2134 + @cache 2110 2135 def __getattr__(cls, attr): 2136 + if len(attr) >= 44: 2137 + raise AttributeError() 2111 2138 errors = [] 2112 2139 errored = [] 2113 2140 for generic in cls.__generics__: ··· 2158 2185 # so this function will never be invoked in those cases, 2159 2186 # which means booleans, floats, strings, and integers having 2160 2187 # the same hash value won't matter. 2161 - @defcache 2188 + @cache 2162 2189 def partition_ellipsis(args): 2163 2190 if ... in args: 2164 2191 index = args.index(...) ··· 2178 2205 def __subclasscheck__(self, C: typing.Any) -> bool: 2179 2206 return isinmrosubclass(C, self) 2180 2207 2181 - @defcache 2208 + # TODO: Add support for Ellipsis in tuples. 2209 + @cache 2182 2210 def __add__(self, summand): 2183 2211 origin = typing.get_origin(self) 2184 2212 original_summand = summand ··· 2216 2244 f"unsupported operand type(s) for +: {FormatMeta.__format_type__(type(self))} and {FormatMeta.__format_type__(type(original_summand))}" 2217 2245 ) 2218 2246 2219 - @defcache 2247 + # TODO: Add support for Ellipsis in tuples. 2248 + @cache 2220 2249 def __radd__(self, summand): 2221 2250 origin = typing.get_origin(self) 2222 2251 original_summand = summand ··· 2266 2295 ) or NotImplemented 2267 2296 2268 2297 def __hash__(self): 2269 - if hasattr(self, "__hash_value__"): 2270 - return self.__hash_value__ 2271 - raise TypeError(f"unhashable type: {self}") 2298 + return reducehash( 2299 + self.__origin__, 2300 + ( 2301 + arg.items() if isinstance(arg, abc.Mapping) else arg 2302 + for arg in self.__args__ 2303 + ), 2304 + ) 2272 2305 2273 2306 # NOTE: Caching this just makes it confusing. 2274 2307 def __repr__(self): ··· 2277 2310 def __call__(self, *args, **kwargs): 2278 2311 origin = typing.get_origin(self) 2279 2312 if ( 2280 - isinmrosubclass(origin, Generics.Iterable) 2313 + isinmrosubclass(origin, abc.Iterable) 2281 2314 and args 2282 - and isinstance(first_arg := args[0], Generics.Iterable) 2315 + and isinstance(first_arg := args[0], abc.Iterable) 2283 2316 ): 2284 2317 self_args = get_args(self) 2285 - if isinmrosubclass(origin, Generics.tuple): 2286 - items = [t(i) for t, i in zip(self_args, first_arg, strict=True)] 2287 - elif isinmrosubclass(origin, Generics.Mapping): 2318 + if isinmrosubclass(origin, tuple): 2319 + # TODO: Test this with more, less, and equal number of self_args and first_args 2320 + if ... in self_args: 2321 + first_arg = list(first_arg) 2322 + pre_args, post_args = partition_ellipsis(self_args) 2323 + pre_index, post_length = len(pre_args), len(post_args) 2324 + first_length = len(first_arg) 2325 + # TODO: Ensure `tuple[..., str, str]` doesn't match `[1, 2]`. 2326 + # Is this really necessary? 2327 + # if first_length < (pre_index + post_length): 2328 + # raise TypeError( 2329 + # f"length of {first_arg} must be at least {(pre_index or 1) + post_length}" 2330 + # ) 2331 + post_index = first_length - post_length 2332 + items = ( 2333 + [ 2334 + t(i) 2335 + for t, i in zip( 2336 + pre_args, first_arg[:pre_index], strict=True 2337 + ) 2338 + ] 2339 + + first_arg[pre_index:post_index] 2340 + + [ 2341 + t(i) 2342 + for t, i in zip( 2343 + post_args, first_arg[post_index:], strict=True 2344 + ) 2345 + ] 2346 + ) 2347 + else: 2348 + items = [t(i) for t, i in zip(self_args, first_arg, strict=True)] 2349 + elif isinmrosubclass(origin, abc.Mapping): 2288 2350 dict_first_arg = self_args[0] 2289 2351 dict_second_arg = self_args[1] 2290 2352 items = { ··· 2297 2359 args = (items, *args[1:]) 2298 2360 return call(origin, *args, **kwargs) 2299 2361 2300 - @defcache 2362 + @cache 2301 2363 def __getattr__(self, attr): 2364 + if len(attr) >= 44: 2365 + raise AttributeError() 2302 2366 return getattr(typing.get_origin(self), attr) 2303 2367 2304 - @defcache 2368 + @cache 2305 2369 def __genericsupertype__(cls): 2306 2370 return type(cls) 2307 2371 ··· 2428 2492 ) 2429 2493 2430 2494 2431 - # TODO: Determine what will be private functions, convert them to `defcache`, 2495 + # TODO: Determine what will be private functions, convert them to `cache`, 2432 2496 # and keep the user-facing functions as `cache`.
+3 -1
packages/hydrox/hydrox/typing/tests/types/test_union.py
··· 1 1 from hydrox.hypothesis import AllGeneric, HTGeneric 2 2 import hydrox.typing as ht 3 - from hypothesis import given, assume, strategies as st 3 + import typing 4 + from hypothesis import given, assume, strategies as st, example 4 5 from hydrox.hypothesis import rand_strat 5 6 6 7 generic_types = list(ht.Generics.values()) ··· 72 73 73 74 class TestUnionSubclass: 74 75 @given(data=st.sampled_from(generic_types), generic=...) 76 + @example(data=typing._UnionGenericAlias, generic=ht.union) 75 77 def test_true(self, data, generic: HTGeneric[ht.Union]): 76 78 assert issubclass(data, generic) 77 79
+10 -7
packages/hydrox/hydrox/typing/types.py
··· 25 25 officiate, 26 26 reducehash, 27 27 scoreargs, 28 + scoremro, 28 29 scoresubclass, 29 30 sumscore, 30 31 supertype, 31 32 ) 32 33 from ..variables import conversion_table 33 - from cytoolz.itertoolz import unique 34 - from more_itertools import partition 34 + from more_itertools import partition, unique_everseen 35 35 36 36 37 37 @Generalize ··· 149 149 return sumscore( 150 150 total, scoreargs(tuple(ca), tuple(sa), True) 151 151 ) 152 + # return sumscore(total, scoresubclass(ca, sa)) 152 153 return sumscore( 153 154 total, any_score if sa is ... else scoresubclass(ca, sa) 154 155 ) ··· 246 247 class UnhashableLiteral(Literal, generic=True, force=True): 247 248 def __aliasargsfullprocessor__(origin, args): 248 249 new_args = [] 249 - for arg in unique(args if args and isinstance(args, tuple) else (args,)): 250 + for arg in unique_everseen( 251 + args if args and isinstance(args, tuple) else (args,) 252 + ): 250 253 if typing.get_origin(arg) == Generics.UnhashableLiteral: 251 254 new_args.extend(typing.get_args(arg)) 252 255 else: ··· 344 347 345 348 def __metasubclassscore__(cls, C: typing.Any): 346 349 if isinstance(C, Annotation): 347 - return 1 350 + return scoremro(C, typing.Any) 348 351 349 - def __metainstancescore__(cls, C: typing.Any): 350 - return any_score 352 + def __metainstancescore__(cls, instance: typing.Any): 353 + return scoremro(get_type(instance), typing.Any) 351 354 352 355 353 356 class UnionType: ... ··· 501 504 if t == s: 502 505 return 1 503 506 return None 504 - return any_score 507 + return scoremro(t, typing.Any) 505 508 506 509 def __subclasscheck__(self, C: typing.Any) -> bool: 507 510 check: abc.Callable[[typing.Any], bool] = isinmrosubclass
+4 -1
packages/hydrox/pyproject.toml
··· 13 13 license = "MIT" 14 14 urls."Source code" = "https://tangled.sh/@syvl.org/hydrox" 15 15 dependencies = [ 16 - "autoslot>=2024.12.1", 16 + "autoslot", 17 17 "cytoolz>=1.0.1", 18 18 "more-itertools>=10.7.0", 19 19 "rich==13.8.1", 20 20 ] 21 + 22 + [tool.uv.sources] 23 + autoslot = { git = "https://github.com/sylvorg/autoslot" } 21 24 22 25 [build-system] 23 26 requires = ["hatchling"]
+3 -3
packages/jugulis/jugulis/__init__.py
··· 1 - from .deino import Deino 2 - from .hydreigon import Hydreigon, AmbiguityError 3 - from .jugulis import Jugulis, jugulis, IronJugulis, IronJugudict, nydra, hydread 1 + from .deino import Deino, format_message 2 + from .hydreigon import Hydreigon, AmbiguityError, AvailabilityError 3 + from .jugulis import funcdict, Jugulis, jugulis, IronJugulis, IronJugudict 4 4 from .functions import * 5 5 from rich.traceback import install 6 6
+160 -35
packages/jugulis/jugulis/deino.py
··· 1 1 from .functions import scorefunction, processsignature 2 2 import hydrox.typing as ht 3 - from hydrox.helpers import as_decorator, query, flatten, cache_get 4 - from inspect import getfullargspec, Signature 5 - from functools import partial 3 + from hydrox.helpers import cache_get 4 + from inspect import getfullargspec, Signature, Parameter 5 + from functools import partial, cached_property 6 + from annotationlib import get_annotations 7 + from collections import ChainMap 8 + from contextlib import contextmanager 9 + from docstring_parser import parse 10 + from addict import Dict 11 + 12 + 13 + class ReturnTypeError(TypeError): ... 6 14 7 15 8 16 def format_callable(arg): ··· 26 34 return message 27 35 28 36 37 + # TODO: What happens if you initialize with a Deino? 29 38 class Deino: 30 - def __init__(self, *args, evolved=False, processors=tuple(), **cache_kwargs): 31 - self.__cache_kwargs__ = {"hashed": True, "cls": False} | cache_kwargs 39 + def __init__( 40 + self, 41 + /, 42 + func=None, 43 + *, 44 + cache=None, 45 + evolved=False, 46 + default_annotation=None, 47 + default_va_annotation=None, 48 + default_vk_annotation=None, 49 + precedence=None, 50 + hashed=None, 51 + cls=None, 52 + ): 53 + self.__cache = cache 54 + self.__func__ = func 32 55 self.__evolved__ = evolved 33 - self.__processors__ = flatten.list(processors) 34 - if as_decorator(query=query): 35 - self.__initialized__ = False 36 - self.__cache__ = args[0] if args else None 37 - else: 38 - self.__initialized__ = True 39 - # TODO: If `func` is an instance of `Deino`, combine the `kwargs`. Can I combine the caches as well? 40 - func = args[0] 41 - self.__cache__ = args[1] if len(args) > 1 else None 56 + self.__default_annotation__ = default_annotation 57 + self.__default_va_annotation__ = default_va_annotation 58 + self.__default_vk_annotation__ = default_vk_annotation 59 + self.__precedence__ = precedence 60 + self.__hashed__ = hashed 61 + self.__cls__ = cls 62 + if func: 42 63 self.__process__(func) 43 - for processor in self.__processors__[::-1]: 44 - func = processor(func) 45 - self.__func__ = func 64 + 65 + @property 66 + def __cache__(self): 67 + return self.__cache 68 + 69 + @__cache__.setter 70 + def __cache__(self, value): 71 + self.__cache = ( 72 + value if None in (value, self.__cache) else ChainMap(self.__cache, value) 73 + ) 74 + 75 + @__cache__.deleter 76 + def __cache__(self): 77 + self.__cache = None 78 + 79 + @contextmanager 80 + def __globalize__(self, **new_settings): 81 + old_settings = {} 82 + for k, v in new_settings.items(): 83 + k = f"__{k}__" 84 + if attr := getattr(self, k) is not None: 85 + old_settings[k] = attr 86 + setattr(self, k, v) 87 + yield 88 + for k, v in old_settings.items(): 89 + setattr(self, k, v) 46 90 47 91 def __eq__(self, other: Deino): 48 92 return ( 49 93 isinstance(other, self.__class__) 50 94 and (self.__func__ == other.__func__) 51 - and (self.__cache_kwargs__ == other.__cache_kwargs__) 95 + and (self.__evolved__ == other.__evolved__) 96 + and (self.__default_annotation__ == other.__default_annotation__) 97 + and (self.__default_va_annotation__ == other.__default_va_annotation__) 98 + and (self.__default_vk_annotation__ == other.__default_vk_annotation__) 99 + and (self.__precedence__ == other.__precedence__) 100 + and (self.__hashed__ == other.__hashed__) 101 + and (self.__cls__ == other.__cls__) 52 102 ) or NotImplemented 53 103 54 104 def __hash__(self): 55 - return ht.reducehash(self.__func__, self.__cache_kwargs__.items()) 105 + return ht.reducehash( 106 + self.__func__, 107 + self.__evolved__, 108 + self.__default_annotation__, 109 + self.__default_va_annotation__, 110 + self.__default_vk_annotation__, 111 + self.__precedence__, 112 + self.__hashed__, 113 + self.__cls__, 114 + ) 115 + 116 + @property 117 + def __signature__(self) -> Signature: 118 + self.__func__.__signature__ = sig = ht.signature( 119 + self.__func__, 120 + t=ht.Any 121 + if self.__default_annotation__ is None 122 + else self.__default_annotation__, 123 + ta=ht.tuple[...] 124 + if self.__default_va_annotation__ is None 125 + else self.__default_va_annotation__, 126 + tk=ht.dict[ht.str, ht.Any] 127 + if self.__default_vk_annotation__ is None 128 + else self.__default_vk_annotation__, 129 + ) 130 + return sig 131 + 132 + @property 133 + def __normal_signature__(self): 134 + return self.__signature__.replace( 135 + parameters={ 136 + k: v.replace(default=Parameter.empty) 137 + for k, v in self.__signature__.parameters.items() 138 + } 139 + ) 140 + 141 + @cached_property 142 + def __spec__(self): 143 + self.__func__.__spec__ = spec = getfullargspec(self.__func__) 144 + return spec 145 + 146 + @cached_property 147 + def __annotations__(self): 148 + return get_annotations(self.__func__, eval_str=True) 149 + 150 + @property 151 + def __doc__(self): 152 + return self.__func__.__doc__ 153 + 154 + @__doc__.setter 155 + def __doc__(self, value): 156 + self.__func__.__doc__ = value 157 + self.__func__.__docstring__ = parse(value) 158 + 159 + @property 160 + def __docstring__(self): 161 + return self.__func__.__docstring__ if self.__doc__ else Dict() 162 + 163 + @__docstring__.setter 164 + def __docstring__(self, value): 165 + self.__func__.__docstring__ = value 56 166 167 + # TODO: What happens if you process a Deino? 57 168 def __process__(self, func): 58 - func.__signature__ = ht.signature(func) 59 - func.__spec__ = getfullargspec(func) 60 169 for attr in dir(func): 61 - if attr not in ("__dict__", "__class__"): 170 + if attr not in ( 171 + "__dict__", 172 + "__class__", 173 + "__annotations__", 174 + "__signature__", 175 + ): 62 176 setattr(self, attr, getattr(func, attr)) 63 - self.__repr_message__ = ht.format_name(func) 177 + 178 + @cached_property 179 + def __repr_message__(self): 180 + return ht.format_name(self.__func__) 64 181 65 182 def __repr__(self): 66 183 return self.__repr_message__ 67 184 68 185 def __score__(self, *args, **kwargs): 69 - return scorefunction(self.__signature__, self.__spec__, args, kwargs) 186 + return self.__scorer__(args, kwargs) 70 187 188 + # TODO 189 + def __scorer__(self, args, kwargs, scorer=ht.scoreinstance): 190 + score = scorefunction( 191 + self.__signature__, self.__spec__, args, kwargs, scorer=scorer 192 + ) 193 + if score is not None: 194 + return score + (self.__precedence__ or 0) 195 + 196 + # TODO 71 197 def __check__(self, sig: Signature, args, kwargs, func): 72 - return scorefunction( 198 + score = scorefunction( 73 199 sig, 74 200 self.__spec__, 75 201 args, 76 202 kwargs, 77 - func, 78 - True, 203 + self=func, 204 + processed=True, 79 205 ) 206 + if score is not None: 207 + return score + (self.__precedence__ or 0) 80 208 81 209 def __process_result__(self, args, kwargs): 82 210 func = lambda: None ··· 95 223 if isinstance(result, return_type := sig.return_annotation): 96 224 return result 97 225 result_format = f"'{result}'" if isinstance(result, str) else result 98 - raise TypeError( 226 + raise ReturnTypeError( 99 227 f"{ht.format_name(func)} returned value {result_format} of type {ht.format_name(ht.get_type(result))}, " 100 228 f"but was expecting an instance of {ht.format_name(return_type)}" 101 229 ) 102 230 103 231 def __call__(self, *args, **kwargs): 104 - if self.__initialized__: 232 + if self.__func__: 105 233 return cache_get( 106 234 partial(self.__process_result__, args, kwargs), 107 235 self.__cache__, 108 - self.__cache_kwargs__["hashed"], 109 - self.__cache_kwargs__["cls"], 236 + True if self.__hashed__ is None else self.__hashed__, 237 + False if self.__cls__ is None else self.__cls__, 110 238 args, 111 239 kwargs, 112 240 ) 113 - self.__initialized__ = True 114 241 # TODO: If `func` is an instance of `Deino`, combine the `kwargs`. Can I combine the caches as well? 115 242 func = args[0] 116 - self.__process__(func) 117 - for processor in self.__processors__[::-1]: 118 - func = processor(func) 119 243 self.__func__ = func 244 + self.__process__(func) 120 245 return self
+30 -9
packages/jugulis/jugulis/functions.py
··· 3 3 from functools import cache as defcache, partial 4 4 from hydrox.typing import format_args, generalize, get_args, get_origin, scoreinstance 5 5 from hydrox.helpers import zipsuppress 6 - from inspect import FullArgSpec, Parameter, Signature 6 + from inspect import FullArgSpec, Parameter, Signature, isclass 7 7 from itertools import zip_longest 8 8 from types import MappingProxyType 9 9 from typing import Any, Optional, TypeVar 10 + 11 + 12 + def iscallableclass(value): 13 + return isclass(value) or callable(value) 10 14 11 15 12 16 @defcache 13 17 def process_key(key): 14 18 return key if isinstance(key, str) else key.__name__ 19 + 20 + 21 + def nydra(func): 22 + func.__disabled__ = True 23 + return func 15 24 16 25 17 26 @defcache ··· 131 140 # return score 132 141 133 142 143 + class ArgumentTypeError(TypeError): ... 144 + 145 + 146 + class KeywordArgumentTypeError(ArgumentTypeError): ... 147 + 148 + 134 149 # TODO: How does deal with positional and keyword only arguments, 135 150 # as well as default arguments? 136 151 def scorefunction( 137 - sig: Signature, spec: FullArgSpec, args, kwargs, self=None, processed=False 152 + sig: Signature, 153 + spec: FullArgSpec, 154 + args, 155 + kwargs, 156 + self=None, 157 + processed=False, 158 + scorer=scoreinstance, 138 159 ): 139 160 specargs = spec.args 140 161 speckwargs = spec.kwonlyargs ··· 147 168 else: 148 169 should_raise = True 149 170 150 - def format_error(name, arg, annotation: ht.Annotation): 171 + def raise_error(name, arg, annotation: ht.Annotation, keyword=False): 151 172 arg_format = f"'{arg}'" if isinstance(arg, str) else arg 152 - return ( 153 - f"{self_name} {'positional' if name in specargs else 'keyword'} " 173 + raise (KeywordArgumentTypeError if keyword else ArgumentTypeError)( 174 + f"{self_name} {'keyword' if keyword else 'positional'} " 154 175 f"argument '{name}' got value {arg_format} of type {ht.format_name(ht.get_type(arg))}, " 155 176 f"but was expecting an instance of {ht.format_name(annotation)}" 156 177 ) ··· 211 232 ) 212 233 return None 213 234 annotation = param.annotation 214 - kwarg_score: Optional[int] = scoreinstance(v, annotation) 235 + kwarg_score: Optional[int] = scorer(v, annotation) 215 236 if kwarg_score is None: 216 237 if should_raise: 217 - raise TypeError(format_error(k, v, annotation)) 238 + raise_error(k, v, annotation, True) 218 239 return None 219 240 total += kwarg_score 220 241 ··· 287 308 return None 288 309 else: 289 310 annotation = param.annotation 290 - arg_score: Optional[int] = scoreinstance(arg, annotation) 311 + arg_score: Optional[int] = scorer(arg, annotation) 291 312 if arg_score is None: 292 313 if should_raise: 293 - raise TypeError(format_error(name, arg, annotation)) 314 + raise_error(name, arg, annotation) 294 315 return None 295 316 total += arg_score 296 317
+227 -67
packages/jugulis/jugulis/hydreigon.py
··· 1 - from hydrox.helpers import as_decorator, query, cache_get 2 - from hydrox.typing import reducehash 1 + from hydrox.helpers import cache_get 2 + from hydrox.typing import reducehash, scoreinstance 3 3 from .deino import Deino, format_message 4 - from collections import defaultdict 4 + from .functions import iscallableclass 5 + from collections import defaultdict, ChainMap 5 6 from functools import partial 6 7 from rich.table import Table 7 8 from rich.console import Console 9 + from inspect import isclass 10 + 11 + sentinel = object() 8 12 9 13 console = Console() 10 14 ··· 12 16 class AmbiguityError(Exception): ... 13 17 14 18 19 + class AvailabilityError(Exception): ... 20 + 21 + 22 + # TODO: Jugulis or Hydreigon also needs to account for parent methods. 15 23 class Hydreigon(list): 16 24 def __init__( 17 - self, *args, cache=None, superhashed=True, supercls=False, **cache_kwargs 25 + self, 26 + /, 27 + func=None, 28 + *, 29 + supercache=None, 30 + superhashed=True, 31 + supercls=False, 32 + **deino_kwargs, 18 33 ): 34 + self.__name__ = "" 19 35 super().__init__() 20 - cache_kwargs.pop("evolved", None) 21 - self.__cache_kwargs__ = cache_kwargs 22 - self.__cache__ = cache 36 + deino_kwargs.pop("evolved", None) 37 + self.__deino_cache__ = deino_kwargs.pop("cache", None) 38 + self.__deino_kwargs__ = deino_kwargs 39 + self.__supercache__ = {} if supercache is None else supercache 23 40 self.__superhashed__ = superhashed 24 41 self.__supercls__ = supercls 25 - if as_decorator(query=query): 26 - self.__initialized__ = False 27 - self.__supercache__ = args[0] if args else None 28 - else: 29 - self.__initialized__ = True 30 - if args: 31 - self.__supercache__ = args[1] if len(args) > 1 else None 32 - self.__append_func__(args[0]) 33 - else: 34 - self.__supercache__ = None 42 + if func: 43 + self.append(func) 44 + 45 + def __add__(self, other): 46 + result = self.copy() 47 + result.extend(other) 48 + return result 35 49 36 - def __append_func__(self, func): 37 - if isinstance(func, Deino): 38 - func.__cache__ = self.__cache__ | func.__cache__ 39 - func.__cache_kwargs__ = self.__cache_kwargs__ | func.__cache_kwargs__ 40 - self.append(func) 41 - else: 42 - self.append( 43 - Deino(func, self.__cache__, evolved=True, **self.__cache_kwargs__) 50 + def __call__(self, *args, **kwargs): 51 + if self: 52 + return cache_get( 53 + partial(self.call, args, kwargs), 54 + self.__supercache__, 55 + self.__superhashed__, 56 + self.__supercls__, 57 + args, 58 + kwargs, 44 59 ) 60 + self.append(*args) 61 + return self 62 + 63 + def __eq__(self, other): 64 + return ( 65 + isinstance(other, self.__class__) 66 + and (self.__deino_kwargs__ == other.__deino_kwargs__) 67 + and (self.__deino_cache__ == other.__deino_cache__) 68 + and (self.__supercache__ == other.__supercache__) 69 + and (self.__superhashed__ == other.__superhashed__) 70 + and (self.__supercls__ == other.__supercls__) 71 + and super().__eq__(other) 72 + ) or NotImplemented 73 + 74 + # TODO: Implement this everywhere `__eq__` is overriden. 75 + # Adapted From: 76 + # Answer: https://stackoverflow.com/a/30676267 77 + # User: https://stackoverflow.com/users/541136/aaron-hall 78 + def __ne__(self, other): 79 + return not (self == other) 45 80 46 81 def __hash__(self): 47 - return reducehash(self) 82 + return reducehash( 83 + self.__class__.__name__, 84 + self.__name__, 85 + None if self.__deino_cache__ is None else self.__deino_cache__.items(), 86 + self.__deino_kwargs__.items(), 87 + None if self.__supercache__ is None else self.__supercache__.items(), 88 + self.__supercache__, 89 + self.__supercls__, 90 + self, 91 + ) 92 + 93 + def __iadd__(self, other): 94 + self.extend(other) 95 + return self 96 + 97 + def __mul__(self, value): 98 + result = self.copy() 99 + return super(result, self.__class__).__mul__(value) 100 + 101 + def __radd__(self, other): 102 + if isinstance(other, self.__class__): 103 + result = other.copy() 104 + result.extend(self) 105 + result = self.ecopy() 106 + result.extend(other) 107 + super(self.__class__, result).extend(self) 108 + return result 48 109 49 - def __score__(self, *args, **kwargs): 50 - return {func: func.__score__(*args, **kwargs) for func in self} 110 + def __rmul__(self, value): 111 + return self * value 51 112 52 - def __tabulate__(self, *args, **kwargs): 53 - table = Table( 54 - "Function", 55 - "Score", 56 - title=format_message("Function Scores for", args, kwargs, capitalize=True), 57 - ) 58 - scores = self.__score__(*args, **kwargs) 59 - for func, score in scores.items(): 60 - table.add_row(str(func), str(score)) 61 - console.print(table) 62 - return scores 113 + def __scorer__(self, args, kwargs, scorer=scoreinstance): 114 + return self.call(args, kwargs, scorer, return_score=True) 63 115 64 - def __funcall__(self, args, kwargs): 116 + def __setitem__(self, key, value): 117 + if isinstance(result := self.process(value), Deino): 118 + super().__setitem__(key, result) 119 + else: 120 + for index, func in enumerate(result, key): 121 + super().insert(index, func) 122 + 123 + def append(self, func): 124 + if func not in self: 125 + if not self: 126 + self.__name__ = func.__name__ 127 + if isinstance(result := self.process(func), Deino): 128 + if result not in self: 129 + super().append(result) 130 + else: 131 + new_result = [] 132 + for r in result: 133 + if not ((r in new_result) or (r in self)): 134 + new_result.append(r) 135 + super().extend(new_result) 136 + return self 137 + 138 + roar = append 139 + 140 + def raise_if_empty(self): 141 + if not self: 142 + raise AvailabilityError( 143 + "no callables have been registered with the dispatcher" 144 + ) 145 + 146 + def call( 147 + self, 148 + args, 149 + kwargs, 150 + scorer=scoreinstance, 151 + return_choice=False, 152 + return_score=False, 153 + ): 154 + self.raise_if_empty() 65 155 candidates = defaultdict(list) 66 156 for func in self: 67 - function_score = func.__score__(*args, **kwargs) 157 + function_score = func.__scorer__(args, kwargs, scorer) 68 158 if function_score is not None: 69 159 candidates[function_score].append(func) 70 160 if not candidates: ··· 73 163 + " from the following candidates:" 74 164 ) 75 165 for choice in self: 76 - message += f"\n\t\t- {choice}" 77 - raise AmbiguityError(message) 78 - best_choice = candidates[max(candidates)] 166 + message += f"\n\t\t - {choice}" 167 + raise AvailabilityError(message) 168 + best_score = max(candidates) 169 + if return_score: 170 + return best_score 171 + best_choice = candidates[best_score] 79 172 if len(best_choice) > 1: 80 173 message = format_message("multiple callables match", args, kwargs) + ":" 81 174 for choice in best_choice: 82 175 message += f"\n\t\t- {choice}" 83 176 raise AmbiguityError(message) 84 - return best_choice[0](*args, **kwargs) 177 + choice = best_choice[0] 178 + if return_choice: 179 + return choice 180 + return choice(*args, **kwargs) 181 + 182 + def copy(self): 183 + result = self.ecopy() 184 + result.__name__ = self.__name__ 185 + super(self.__class__, result).extend(self) 186 + return result 187 + 188 + def ecopy(self): 189 + return Hydreigon( 190 + supercache=None 191 + if self.__supercache__ is None 192 + else self.__supercache__.copy(), 193 + superhashed=self.__superhashed__, 194 + supercls=self.__supercls__, 195 + cache=None if self.__deino_cache__ is None else self.__deino_cache__.copy(), 196 + **self.__deino_kwargs__, 197 + ) 198 + 199 + def extend(self, funcs): 200 + if isinstance(funcs, self.__class__): 201 + self.merge(funcs) 202 + for func in funcs: 203 + self.append(func) 204 + return self 205 + 206 + def insert(self, index, value): 207 + if isinstance(result := self.process(value), Deino): 208 + super().insert(index, result) 209 + else: 210 + for index, func in enumerate(result, index): 211 + super().insert(index, func) 212 + 213 + def invoke(self, *args, **kwargs): 214 + # TODO: Should this use `operator.eq` or `operator.is_` instead of `scoresubclass`? 215 + # return self.call(args, kwargs, scoresubclass, True) 216 + return self.call(args, kwargs, lambda a, b: int(a == b) or None, True) 217 + 218 + def merge(self, func): 219 + self.__deino_kwargs__ = ChainMap(func.__deino_kwargs__, self.__deino_kwargs__) 220 + if self.__deino_cache__ is None: 221 + if func.__deino_cache__ is not None: 222 + self.__deino_cache__ = ChainMap(func.__deino_cache__, {}) 223 + elif func.__deino_cache__ is not None: 224 + self.__deino_cache__ = ChainMap(func.__deino_cache__, self.__deino_cache__) 225 + if self.__supercache__ is None: 226 + if func.__supercache__ is not None: 227 + self.__supercache__ = ChainMap(func.__supercache__, {}) 228 + elif func.__supercache__ is not None: 229 + self.__supercache__ = ChainMap(func.__supercache__, self.__supercache__) 85 230 86 - def roar(self, *args, **kwargs): 87 - kwargs.pop("evolved", None) 88 - if as_decorator(query=query): 231 + # TODO: Jugulis or Hydreigon also needs to account for parent methods; 232 + # if `func` is a method, extend the Hydreigon with the parent methods. 233 + def process(self, func): 234 + if isclass(func): 235 + funcs = [] 236 + for name in dir(func): 237 + if not name.startswith("__") or name in ("__init__",): 238 + attr = getattr(func, name) 239 + if iscallableclass(attr): 240 + funcs.append(self.process(attr)) 241 + return funcs 89 242 90 - def wrapper(func): 91 - self.append(Deino(func, *args, evolved=True, **kwargs)) 92 - return self 243 + if isinstance(func, self.__class__): 244 + self.merge(func) 245 + return [self.process(f) for f in func] 93 246 94 - return wrapper 247 + if isinstance(func, Deino): 248 + if self.__deino_cache__ is not None: 249 + func.__cache__ = self.__deino_cache__ 250 + for k, v in self.__deino_kwargs__.items(): 251 + k = f"__{k}__" 252 + if attr := getattr(func, k) is not None: 253 + setattr(func, k, v) 254 + func.__evolved__ = True 255 + return func 256 + return Deino(func, evolved=True, **self.__deino_kwargs__) 95 257 96 - self.append(Deino(*args, evolved=True, **kwargs)) 97 - return self 258 + def score(self, *args, **kwargs): 259 + return {func: func.__score__(*args, **kwargs) for func in self} 98 260 99 - def __call__(self, *args, **kwargs): 100 - if self.__initialized__: 101 - return cache_get( 102 - partial(self.__funcall__, args, kwargs), 103 - self.__supercache__, 104 - self.__superhashed__, 105 - self.__supercls__, 106 - args, 107 - kwargs, 108 - ) 109 - self.__initialized__ = True 110 - self.__append_func__(args[0]) 111 - return self 261 + def tabulate(self, *args, **kwargs): 262 + table = Table( 263 + "Function", 264 + "Score", 265 + title=format_message("Function Scores for", args, kwargs, capitalize=True), 266 + ) 267 + scores = self.score(*args, **kwargs) 268 + for func, score in scores.items(): 269 + table.add_row(str(func), str(score)) 270 + console.print(table) 271 + return scores
+100 -28
packages/jugulis/jugulis/jugulis.py
··· 1 1 from autoslot import SlotsMeta, SlotsPlusDictMeta 2 - from hydrox.helpers import as_decorator, query 3 2 from .functions import process_key 4 3 from .hydreigon import Hydreigon 5 - from collections import defaultdict 4 + from cytoolz.dicttoolz import keymap 5 + import collections.abc as abc 6 + from rich.console import Console 6 7 8 + sentinel = object() 9 + console = Console() 7 10 8 - # TODO: Implement all dict methods here. 9 - class Jugulis(defaultdict): 10 - def __init__(self): 11 - super().__init__(Hydreigon) 12 11 13 - def __call__(self, *args, **kwargs): 14 - if as_decorator(query=query): 12 + class funcdict(dict): 13 + def __contains__(self, key): 14 + return super().__contains__(process_key(key)) 15 15 16 - def wrapper(func): 17 - return self[func].roar(func, *args, **kwargs) 16 + def __delattr__(self, key): 17 + del self[key] 18 18 19 - return wrapper 20 - func = args[0] 21 - return self[func].roar(func, *args[1:], **kwargs) 19 + def __delitem__(self, key): 20 + super().__delitem__(process_key(key)) 22 21 23 - def __contains__(self, key): 24 - return super().__contains__(process_key(key)) 22 + def __getattr__(self, key): 23 + if len(key) >= 44: 24 + raise AttributeError() 25 + try: 26 + return self[key] 27 + except KeyError: 28 + raise AttributeError() 25 29 26 30 def __getitem__(self, key): 27 31 return super().__getitem__(process_key(key)) 28 32 33 + def __ior__(self, other): 34 + self.update(other) 35 + return self 36 + 37 + # TODO: Implement this everywhere `__eq__` is overriden. 38 + # Adapted From: 39 + # Answer: https://stackoverflow.com/a/30676267 40 + # User: https://stackoverflow.com/users/541136/aaron-hall 41 + def __ne__(self, other): 42 + return not (self == other) 43 + 44 + def __or__(self, other): 45 + result = self.copy() 46 + result.update(other) 47 + return result 48 + 49 + # Adapted From: https://github.com/Textualize/rich/discussions/3431#discussion-6948729 50 + def __repr__(self): 51 + with console.capture() as capture: 52 + console.print(self) 53 + return capture.get().strip() 54 + 55 + def __ror__(self, other): 56 + result = self.__class__(other) 57 + result.update(self) 58 + return result 59 + 60 + # def __setattr__(self, key, value): 61 + # if key.startswith("_"): 62 + # return object.__setattr__(self, key, value) 63 + # self[key] = value 64 + 29 65 def __setitem__(self, key, value): 30 - super().__setitem__(key if isinstance(key, str) else key.__name__, value) 66 + super().__setitem__(process_key(key), value) 31 67 68 + @classmethod 69 + def fromkeys(cls, default_factory, keys, value=None): 70 + result = cls(default_factory) 71 + for k in keys: 72 + result[k] = value 73 + return result 32 74 33 - jugulis = Jugulis() 75 + def copy(self): 76 + return self.__class__(self) 34 77 78 + def get(self, key, default=None): 79 + try: 80 + return self[key] 81 + except KeyError: 82 + return default 35 83 36 - def nydra(func): 37 - func.__hydread__ = (tuple(), {"disabled": True}) 38 - return func 84 + def pop(self, key, default=sentinel): 85 + if default is sentinel: 86 + return super().pop(process_key(key)) 87 + return super().pop(process_key(key), default) 39 88 89 + def update(self, other=(), **kwargs): 90 + if isinstance(other, abc.Mapping): 91 + super().update(keymap(process_key, other)) 92 + else: 93 + super().update(other) 94 + super().update(keymap(process_key, kwargs)) 40 95 41 - def hydread(*args, **kwargs): 42 - def wrapper(func): 43 - func.__hydread__ = (args, kwargs) 44 - return func 45 96 46 - return wrapper 97 + # TODO: Jugulis or Hydreigon also needs to account for parent methods. 98 + # For the `prepare` class below, I can add all the methods of the same name 99 + # from all the bases passed to the `__init__` method. 100 + # However, not using `IronJugulis` and applying `Hydreigon` manually to methods 101 + # will not let me check if the function passed to `Hydreigon` is a method or not. 102 + class Jugulis(funcdict): 103 + def append(self, func, **kwargs): 104 + if func in self: 105 + return self[func].append(func) 106 + self[func] = result = Hydreigon(func, **kwargs) 107 + return result 108 + 109 + def __call__(self, func=None, **kwargs): 110 + if func: 111 + return self.append(func, **kwargs) 112 + 113 + def wrapper(_func): 114 + return self.append(_func, **kwargs) 115 + 116 + return wrapper 117 + 118 + 119 + jugulis = Jugulis() 47 120 48 121 49 122 class prepare(dict): ··· 53 126 54 127 def __setitem__(self, key, value): 55 128 if callable(value): 56 - args, kwargs = getattr(value, "__hydread__", (tuple(), {})) 57 - if not kwargs.get("disabled", False): 58 - value = self.__jugulis__(value, *args, **kwargs) 129 + if not getattr(value, "__disabled__"): 130 + value = self.__jugulis__(value) 59 131 super().__setitem__(key, value) 60 132 61 133
+26 -19
packages/jugulis/jugulis/tests/test_hydreigon.py
··· 1 - from jugulis import AmbiguityError 1 + from jugulis import AmbiguityError, AvailabilityError 2 2 3 3 4 4 class TestHydreigon: ··· 20 20 def test_different_varkw(self, f): 21 21 assert f(0, 0, 0, d=0, z=0) == 4 22 22 23 + def test_deduplicated(self, f): 24 + length = len(f) 25 + f.append(f[0]) 26 + assert len(f) == length 27 + f.append(f[0].__func__) 28 + assert len(f) == length 29 + 23 30 def test_multiple_callables(self, f): 24 - @f.roar 31 + @f.append 25 32 def f(b: int, /) -> int: ... 26 33 27 34 try: ··· 40 47 41 48 try: 42 49 result = f(e=1) 43 - except AmbiguityError as e: 50 + except AvailabilityError as e: 44 51 assert str(e) == ( 45 52 "no callables match keyword arguments {'e': 1} from the following candidates:" 46 - "\n\t\t- f(a: ht.int, /) -> ht.int" 47 - "\n\t\t- f(a: ht.int, /, b: ht.int) -> ht.int" 48 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c) -> ht.int" 49 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c, d: ht.int) -> ht.int" 53 + "\n\t\t - f(a: ht.int, /) -> ht.int" 54 + "\n\t\t - f(a: ht.int, /, b: ht.int) -> ht.int" 55 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...]) -> ht.int" 56 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...], d: ht.int) -> ht.int" 50 57 ), str(e) 51 58 else: 52 59 assert False, result ··· 54 61 def test_no_callable_arg_string(self, f): 55 62 try: 56 63 result = f("") 57 - except AmbiguityError as e: 64 + except AvailabilityError as e: 58 65 assert str(e) == ( 59 66 "no callables match arguments [''] from the following candidates:" 60 - "\n\t\t- f(a: ht.int, /) -> ht.int" 61 - "\n\t\t- f(a: ht.int, /, b: ht.int) -> ht.int" 62 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c) -> ht.int" 63 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c, d: ht.int) -> ht.int" 64 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int" 67 + "\n\t\t - f(a: ht.int, /) -> ht.int" 68 + "\n\t\t - f(a: ht.int, /, b: ht.int) -> ht.int" 69 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...]) -> ht.int" 70 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...], d: ht.int) -> ht.int" 71 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...], d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int" 65 72 ), str(e) 66 73 else: 67 74 assert False, result ··· 69 76 def test_no_callable_kwarg_string(self, f): 70 77 try: 71 78 result = f(d="") 72 - except AmbiguityError as e: 79 + except AvailabilityError as e: 73 80 assert str(e) == ( 74 81 "no callables match keyword arguments {'d': ''} from the following candidates:" 75 - "\n\t\t- f(a: ht.int, /) -> ht.int" 76 - "\n\t\t- f(a: ht.int, /, b: ht.int) -> ht.int" 77 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c) -> ht.int" 78 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c, d: ht.int) -> ht.int" 79 - "\n\t\t- f(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int" 82 + "\n\t\t - f(a: ht.int, /) -> ht.int" 83 + "\n\t\t - f(a: ht.int, /, b: ht.int) -> ht.int" 84 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...]) -> ht.int" 85 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...], d: ht.int) -> ht.int" 86 + "\n\t\t - f(a: ht.int, /, b: ht.int, *c: ht.tuple[...], d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int" 80 87 ), str(e) 81 88 else: 82 89 assert False, result
+20 -9
packages/jugulis/jugulis/tests/test_scoring.py
··· 9 9 subarg_strat, 10 10 ) 11 11 from hypothesis import strategies as st, given, example 12 - from jugulis import scorefunction 12 + from jugulis import scorefunction, ArgumentTypeError, KeywordArgumentTypeError 13 13 from inspect import Signature, FullArgSpec, getfullargspec 14 14 from pytest import fixture 15 15 ··· 219 219 except TypeError as te: 220 220 assert ( 221 221 str(te) 222 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int missing 2 required positional arguments: 'a' and 'b'" 222 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int missing 2 required positional arguments: 'a' and 'b'" 223 223 ), str(te) 224 224 else: 225 225 assert False, result ··· 230 230 except TypeError as te: 231 231 assert ( 232 232 str(te) 233 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int got some positional-only arguments passed as keyword arguments: 'a'" 233 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int got some positional-only arguments passed as keyword arguments: 'a'" 234 234 ), str(te) 235 235 else: 236 236 assert False, result ··· 241 241 except TypeError as te: 242 242 assert ( 243 243 str(te) 244 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int missing 1 required positional argument: 'a'" 244 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int missing 1 required positional argument: 'a'" 245 245 ), str(te) 246 246 else: 247 247 assert False, result ··· 253 253 except TypeError as te: 254 254 assert ( 255 255 str(te) 256 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int missing 2 required positional arguments: 'a' and 'b'" 256 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int missing 2 required positional arguments: 'a' and 'b'" 257 257 ), str(te) 258 258 else: 259 259 assert False, result ··· 264 264 except TypeError as te: 265 265 assert ( 266 266 str(te) 267 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int missing 1 required keyword-only argument: 'd'" 267 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int missing 1 required keyword-only argument: 'd'" 268 268 ), str(te) 269 269 else: 270 270 assert False, result 271 271 272 - def test_wrong_argument_type(self, e): 272 + def test_wrong_positional_argument_type(self, e): 273 273 try: 274 274 error = scorefunction(*e[1:], ("0",), {}, e[0]) 275 - except TypeError as te: 275 + except ArgumentTypeError as te: 276 + assert ( 277 + str(te) 278 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int positional argument 'a' got value '0' of type ht.str, but was expecting an instance of ht.int" 279 + ), str(te) 280 + else: 281 + assert False, error 282 + 283 + def test_wrong_keyword_argument_type(self, e): 284 + try: 285 + error = scorefunction(*e[1:], (0,), {"b":"0"}, e[0]) 286 + except KeywordArgumentTypeError as te: 276 287 assert ( 277 288 str(te) 278 - == "e(a: ht.int, /, b: ht.int, *c, d: ht.int, **e) -> ht.int positional argument 'a' got value '0' of type ht.str, but was expecting an instance of ht.int" 289 + == "e(a: ht.int, /, b: ht.int, *c: ht.tuple, d: ht.int, **e: ht.dict[ht.str, ht.Any]) -> ht.int keyword argument 'b' got value '0' of type ht.str, but was expecting an instance of ht.int" 279 290 ), str(te) 280 291 else: 281 292 assert False, error
+6 -2
packages/jugulis/pyproject.toml
··· 12 12 requires-python = ">=3.14" 13 13 license = "MIT" 14 14 urls."Source code" = "https://tangled.sh/@syvl.org/jugulis" 15 - dependencies = [ "hydrox" ] 15 + dependencies = [ 16 + "addict>=2.4.0", 17 + "docstring-parser>=0.16", 18 + "hydrox", 19 + ] 16 20 17 21 [build-system] 18 22 requires = ["hatchling"] ··· 22 26 packages = [ "jugulis" ] 23 27 24 28 [tool.uv.sources] 25 - hydrox = { workspace = true } 29 + hydrox = { workspace = true }
+22
packages/jugulis/test4.py
··· 1 + from jugulis import Hydreigon 2 + 3 + a = {"a": 1} 4 + b = {"b": 2} 5 + c = {"c": 3} 6 + d = {"d": 4} 7 + A = Hydreigon(cache=a) 8 + B = Hydreigon(cache=b) 9 + C = Hydreigon(cache=c) 10 + D = Hydreigon(cache=d) 11 + B.extend(A) 12 + C.extend(B) 13 + D.extend(C) 14 + B.__deino_cache__["B"] = 22 15 + C.__deino_cache__["C"] = 33 16 + D.__deino_cache__["D"] = 44 17 + 18 + from rich.pretty import pprint 19 + 20 + pprint(B.__deino_cache__) 21 + pprint(C.__deino_cache__) 22 + pprint(D.__deino_cache__)
+10 -1
pyproject.toml
··· 13 13 license = "MIT" 14 14 scripts.oreo = "oreo.oreo:oreo" 15 15 urls."Source code" = "https://tangled.sh/@syvl.org/oreo" 16 - dependencies = ["bundle"] 16 + dependencies = [ 17 + "bundle", 18 + "thefuzz>=0.22.1", 19 + ] 17 20 18 21 [build-system] 19 22 requires = ["hatchling"] ··· 31 34 [tool.pytest.ini_options] 32 35 xfail_strict = true 33 36 addopts = "--dist loadgroup -n auto --order-group-scope class -rs --strict-markers --suppress-no-test-exit-code -vvv -x" 37 + markers = [ 38 + "oreo", 39 + "oreo_runner", 40 + "oreo_subprocess", 41 + ] 34 42 35 43 [dependency-groups] 36 44 dev = [ ··· 48 56 lint = [ 49 57 "ruff>=0.11.9", 50 58 ] 59 +
+166 -31
uv.lock
··· 11 11 ] 12 12 13 13 [[package]] 14 + name = "addict" 15 + version = "2.4.0" 16 + source = { registry = "https://pypi.org/simple" } 17 + sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } 18 + wheels = [ 19 + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, 20 + ] 21 + 22 + [[package]] 14 23 name = "attrs" 15 24 version = "25.3.0" 16 25 source = { registry = "https://pypi.org/simple" } ··· 22 31 [[package]] 23 32 name = "autoslot" 24 33 version = "2024.12.1" 34 + source = { git = "https://github.com/sylvorg/autoslot#3c682bd5515f76dc4d2da0b46e5b948ad7fac344" } 35 + 36 + [[package]] 37 + name = "beartype" 38 + version = "0.21.0" 25 39 source = { registry = "https://pypi.org/simple" } 26 - sdist = { url = "https://files.pythonhosted.org/packages/d4/2b/d62d12200bac293891a5dd9f3cae605f04aad0c440a5fecced80e9ce99cb/autoslot-2024.12.1.tar.gz", hash = "sha256:e52e3ce5b98d5fb2d7dbd5c7773627bee552a9cfb49a53b2bddb689adf2a3a5a", size = 11010, upload-time = "2024-12-06T14:42:53.317Z" } 40 + sdist = { url = "https://files.pythonhosted.org/packages/0d/f9/21e5a9c731e14f08addd53c71fea2e70794e009de5b98e6a2c3d2f3015d6/beartype-0.21.0.tar.gz", hash = "sha256:f9a5078f5ce87261c2d22851d19b050b64f6a805439e8793aecf01ce660d3244", size = 1437066, upload-time = "2025-05-22T05:09:27.116Z" } 27 41 wheels = [ 28 - { url = "https://files.pythonhosted.org/packages/05/1a/39b527bdd3220d490f63960efd361afa611d3ca314cfbf389cc2c7dd7df7/autoslot-2024.12.1-py2.py3-none-any.whl", hash = "sha256:b50098372bf99caf1f135e4f1bbd9f20d56cb23a046e8a3bf402a20399526bfe", size = 7858, upload-time = "2024-12-06T14:42:51.046Z" }, 42 + { url = "https://files.pythonhosted.org/packages/94/31/87045d1c66ee10a52486c9d2047bc69f00f2689f69401bb1e998afb4b205/beartype-0.21.0-py3-none-any.whl", hash = "sha256:b6a1bd56c72f31b0a496a36cc55df6e2f475db166ad07fa4acc7e74f4c7f34c0", size = 1191340, upload-time = "2025-05-22T05:09:24.606Z" }, 29 43 ] 30 44 31 45 [[package]] ··· 34 48 source = { editable = "packages/bundle" } 35 49 dependencies = [ 36 50 { name = "jugulis" }, 51 + { name = "magicattr" }, 52 + { name = "pathvalidate" }, 53 + { name = "plum-dispatch" }, 54 + { name = "rich-click" }, 55 + ] 56 + 57 + [package.dev-dependencies] 58 + dev = [ 59 + { name = "thefuzz" }, 37 60 ] 38 61 39 62 [package.metadata] 40 - requires-dist = [{ name = "jugulis", editable = "packages/jugulis" }] 63 + requires-dist = [ 64 + { name = "jugulis", editable = "packages/jugulis" }, 65 + { name = "magicattr", specifier = ">=0.1.6" }, 66 + { name = "pathvalidate", specifier = ">=3.2.3" }, 67 + { name = "plum-dispatch", specifier = ">=2.5.7" }, 68 + { name = "rich-click", specifier = ">=1.8.9" }, 69 + ] 70 + 71 + [package.metadata.requires-dev] 72 + dev = [{ name = "thefuzz", specifier = ">=0.22.1" }] 73 + 74 + [[package]] 75 + name = "click" 76 + version = "8.2.1" 77 + source = { registry = "https://pypi.org/simple" } 78 + dependencies = [ 79 + { name = "colorama", marker = "sys_platform == 'win32'" }, 80 + ] 81 + sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 82 + wheels = [ 83 + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 84 + ] 41 85 42 86 [[package]] 43 87 name = "colorama" ··· 58 102 sdist = { url = "https://files.pythonhosted.org/packages/a7/f9/3243eed3a6545c2a33a21f74f655e3fcb5d2192613cd3db81a93369eb339/cytoolz-1.0.1.tar.gz", hash = "sha256:89cc3161b89e1bb3ed7636f74ed2e55984fd35516904fc878cae216e42b2c7d6", size = 626652, upload-time = "2024-12-13T05:47:36.672Z" } 59 103 60 104 [[package]] 105 + name = "docstring-parser" 106 + version = "0.16" 107 + source = { registry = "https://pypi.org/simple" } 108 + sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } 109 + wheels = [ 110 + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, 111 + ] 112 + 113 + [[package]] 61 114 name = "execnet" 62 115 version = "2.1.1" 63 116 source = { registry = "https://pypi.org/simple" } ··· 79 132 80 133 [package.metadata] 81 134 requires-dist = [ 82 - { name = "autoslot", specifier = ">=2024.12.1" }, 135 + { name = "autoslot", git = "https://github.com/sylvorg/autoslot" }, 83 136 { name = "cytoolz", specifier = ">=1.0.1" }, 84 137 { name = "more-itertools", specifier = ">=10.7.0" }, 85 138 { name = "rich", specifier = "==13.8.1" }, ··· 87 140 88 141 [[package]] 89 142 name = "hypothesis" 90 - version = "6.131.16" 143 + version = "6.131.28" 91 144 source = { registry = "https://pypi.org/simple" } 92 145 dependencies = [ 93 146 { name = "attrs" }, 94 147 { name = "sortedcontainers" }, 95 148 ] 96 - sdist = { url = "https://files.pythonhosted.org/packages/3c/05/fcda52dd0817080d9a953ef926ca1ba63ce660c437a5f36cdc13c6f74ef3/hypothesis-6.131.16.tar.gz", hash = "sha256:4953b8dbb79ae3399c4243b1799b3286c7809d08457269274f77581aef8b7048", size = 436840, upload-time = "2025-05-13T08:20:18.087Z" } 149 + sdist = { url = "https://files.pythonhosted.org/packages/1e/3b/4f87f1c3e19bd66f60259c55deb0cc0e05d84556d7bbc0a905c391b47d75/hypothesis-6.131.28.tar.gz", hash = "sha256:8294567dd46c21049deb095881ce76e7766e31468c5fb93a37f13431c9b38ad1", size = 441837, upload-time = "2025-05-25T17:58:35.024Z" } 97 150 wheels = [ 98 - { url = "https://files.pythonhosted.org/packages/dc/f1/a51627f39787b21ee1392fce4685a9480037b548009761e833b89fe3c6db/hypothesis-6.131.16-py3-none-any.whl", hash = "sha256:574e0ebc28e469a891c2c4fcc092ed7d5da81255b84505b599e6768b39d7ca61", size = 501365, upload-time = "2025-05-13T08:20:13.741Z" }, 151 + { url = "https://files.pythonhosted.org/packages/d9/f4/cc00d61c6eee87490453a39bb47999ee0bab7a5ff5bd246f0d16b86c7f63/hypothesis-6.131.28-py3-none-any.whl", hash = "sha256:8f34eb2789d8cb47f4b82f67832cf99596fb2a9919a2753b7aba11458f71edb2", size = 506437, upload-time = "2025-05-25T17:58:31.301Z" }, 99 152 ] 100 153 101 154 [[package]] ··· 112 165 version = "0.1.0" 113 166 source = { editable = "packages/jugulis" } 114 167 dependencies = [ 168 + { name = "addict" }, 169 + { name = "docstring-parser" }, 115 170 { name = "hydrox" }, 116 171 ] 117 172 118 173 [package.metadata] 119 - requires-dist = [{ name = "hydrox", editable = "packages/hydrox" }] 174 + requires-dist = [ 175 + { name = "addict", specifier = ">=2.4.0" }, 176 + { name = "docstring-parser", specifier = ">=0.16" }, 177 + { name = "hydrox", editable = "packages/hydrox" }, 178 + ] 179 + 180 + [[package]] 181 + name = "magicattr" 182 + version = "0.1.6" 183 + source = { registry = "https://pypi.org/simple" } 184 + wheels = [ 185 + { url = "https://files.pythonhosted.org/packages/2a/7e/76b7e0c391bee7e9273725c29c8fe41c4df62a215ce58aa8e3518baee0bb/magicattr-0.1.6-py2.py3-none-any.whl", hash = "sha256:d96b18ee45b5ee83b09c17e15d3459a64de62d538808c2f71182777dd9dbbbdf", size = 4664, upload-time = "2022-01-25T16:56:47.074Z" }, 186 + ] 120 187 121 188 [[package]] 122 189 name = "markdown-it-py" ··· 154 221 source = { editable = "." } 155 222 dependencies = [ 156 223 { name = "bundle" }, 224 + { name = "thefuzz" }, 157 225 ] 158 226 159 227 [package.dev-dependencies] ··· 174 242 ] 175 243 176 244 [package.metadata] 177 - requires-dist = [{ name = "bundle", editable = "packages/bundle" }] 245 + requires-dist = [ 246 + { name = "bundle", editable = "packages/bundle" }, 247 + { name = "thefuzz", specifier = ">=0.22.1" }, 248 + ] 178 249 179 250 [package.metadata.requires-dev] 180 251 dev = [ ··· 201 272 ] 202 273 203 274 [[package]] 275 + name = "pathvalidate" 276 + version = "3.2.3" 277 + source = { registry = "https://pypi.org/simple" } 278 + sdist = { url = "https://files.pythonhosted.org/packages/92/87/c7a2f51cc62df0495acb0ed2533a7c74cc895e569a1b020ee5f6e9fa4e21/pathvalidate-3.2.3.tar.gz", hash = "sha256:59b5b9278e30382d6d213497623043ebe63f10e29055be4419a9c04c721739cb", size = 61717, upload-time = "2025-01-03T14:06:42.789Z" } 279 + wheels = [ 280 + { url = "https://files.pythonhosted.org/packages/50/14/c5a0e1a947909810fc4c043b84cac472b70e438148d34f5393be1bac663f/pathvalidate-3.2.3-py3-none-any.whl", hash = "sha256:5eaf0562e345d4b6d0c0239d0f690c3bd84d2a9a3c4c73b99ea667401b27bee1", size = 24130, upload-time = "2025-01-03T14:06:39.568Z" }, 281 + ] 282 + 283 + [[package]] 204 284 name = "pluggy" 205 - version = "1.5.0" 285 + version = "1.6.0" 286 + source = { registry = "https://pypi.org/simple" } 287 + sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 288 + wheels = [ 289 + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 290 + ] 291 + 292 + [[package]] 293 + name = "plum-dispatch" 294 + version = "2.5.7" 206 295 source = { registry = "https://pypi.org/simple" } 207 - sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 296 + dependencies = [ 297 + { name = "beartype" }, 298 + { name = "rich" }, 299 + { name = "typing-extensions" }, 300 + ] 301 + sdist = { url = "https://files.pythonhosted.org/packages/f4/46/ab3928e864b0a88a8ae6987b3da3b7ae32fe0a610264f33272139275dab5/plum_dispatch-2.5.7.tar.gz", hash = "sha256:a7908ad5563b93f387e3817eb0412ad40cfbad04bc61d869cf7a76cd58a3895d", size = 35452, upload-time = "2025-01-17T20:07:31.026Z" } 208 302 wheels = [ 209 - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 303 + { url = "https://files.pythonhosted.org/packages/2b/31/21609a9be48e877bc33b089a7f495c853215def5aeb9564a31c210d9d769/plum_dispatch-2.5.7-py3-none-any.whl", hash = "sha256:06471782eea0b3798c1e79dca2af2165bafcfa5eb595540b514ddd81053b1ede", size = 42612, upload-time = "2025-01-17T20:07:26.461Z" }, 210 304 ] 211 305 212 306 [[package]] ··· 321 415 ] 322 416 323 417 [[package]] 418 + name = "rapidfuzz" 419 + version = "3.13.0" 420 + source = { registry = "https://pypi.org/simple" } 421 + sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } 422 + 423 + [[package]] 324 424 name = "rich" 325 425 version = "13.8.1" 326 426 source = { registry = "https://pypi.org/simple" } ··· 334 434 ] 335 435 336 436 [[package]] 437 + name = "rich-click" 438 + version = "1.8.9" 439 + source = { registry = "https://pypi.org/simple" } 440 + dependencies = [ 441 + { name = "click" }, 442 + { name = "rich" }, 443 + { name = "typing-extensions" }, 444 + ] 445 + sdist = { url = "https://files.pythonhosted.org/packages/b7/a8/dcc0a8ec9e91d76ecad9413a84b6d3a3310c6111cfe012d75ed385c78d96/rich_click-1.8.9.tar.gz", hash = "sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136", size = 39378, upload-time = "2025-05-19T21:33:05.569Z" } 446 + wheels = [ 447 + { url = "https://files.pythonhosted.org/packages/b6/c2/9fce4c8a9587c4e90500114d742fe8ef0fd92d7bad29d136bb9941add271/rich_click-1.8.9-py3-none-any.whl", hash = "sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2", size = 36082, upload-time = "2025-05-19T21:33:04.195Z" }, 448 + ] 449 + 450 + [[package]] 337 451 name = "ruff" 338 - version = "0.11.9" 452 + version = "0.11.11" 339 453 source = { registry = "https://pypi.org/simple" } 340 - sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134, upload-time = "2025-05-09T16:19:41.511Z" } 454 + sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" } 341 455 wheels = [ 342 - { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453, upload-time = "2025-05-09T16:18:58.2Z" }, 343 - { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566, upload-time = "2025-05-09T16:19:01.432Z" }, 344 - { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020, upload-time = "2025-05-09T16:19:03.897Z" }, 345 - { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935, upload-time = "2025-05-09T16:19:06.455Z" }, 346 - { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971, upload-time = "2025-05-09T16:19:10.261Z" }, 347 - { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631, upload-time = "2025-05-09T16:19:12.307Z" }, 348 - { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236, upload-time = "2025-05-09T16:19:15.006Z" }, 349 - { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436, upload-time = "2025-05-09T16:19:17.063Z" }, 350 - { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759, upload-time = "2025-05-09T16:19:19.693Z" }, 351 - { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985, upload-time = "2025-05-09T16:19:21.831Z" }, 352 - { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775, upload-time = "2025-05-09T16:19:24.401Z" }, 353 - { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957, upload-time = "2025-05-09T16:19:27.08Z" }, 354 - { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307, upload-time = "2025-05-09T16:19:29.462Z" }, 355 - { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026, upload-time = "2025-05-09T16:19:31.569Z" }, 356 - { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627, upload-time = "2025-05-09T16:19:33.657Z" }, 357 - { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340, upload-time = "2025-05-09T16:19:35.815Z" }, 358 - { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, 456 + { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" }, 457 + { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" }, 458 + { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" }, 459 + { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" }, 460 + { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" }, 461 + { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" }, 462 + { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" }, 463 + { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" }, 464 + { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" }, 465 + { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" }, 466 + { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" }, 467 + { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" }, 468 + { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" }, 469 + { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" }, 470 + { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" }, 471 + { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" }, 472 + { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" }, 359 473 ] 360 474 361 475 [[package]] ··· 377 491 ] 378 492 379 493 [[package]] 494 + name = "thefuzz" 495 + version = "0.22.1" 496 + source = { registry = "https://pypi.org/simple" } 497 + dependencies = [ 498 + { name = "rapidfuzz" }, 499 + ] 500 + sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" } 501 + wheels = [ 502 + { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, 503 + ] 504 + 505 + [[package]] 380 506 name = "toolz" 381 507 version = "1.0.0" 382 508 source = { registry = "https://pypi.org/simple" } 383 509 sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790, upload-time = "2024-10-04T16:17:04.001Z" } 384 510 wheels = [ 385 511 { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" }, 512 + ] 513 + 514 + [[package]] 515 + name = "typing-extensions" 516 + version = "4.13.2" 517 + source = { registry = "https://pypi.org/simple" } 518 + sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 519 + wheels = [ 520 + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 386 521 ] 387 522 388 523 [[package]]