···11+from rich.prompt import PromptBase, InvalidResponse
22+from rich.text import Text
33+44+55+# Adapted From: https://github.com/Textualize/rich/blob/master/rich/prompt.py#L322
66+class Confirm(PromptBase[bool]):
77+ """A yes / no confirmation prompt.
88+ Example:
99+ >>> if Confirm.ask("Continue"):
1010+ run_job()
1111+ """
1212+1313+ response_type = bool
1414+ validate_error_message = "[prompt.invalid]Please enter Y/Yes or N/No"
1515+ choices: list[str] = ["y", "yes", "n", "no"]
1616+1717+ def render_default(self, default) -> Text:
1818+ """Render the default as (y) or (n) rather than True/False."""
1919+ y, yes, n, no = self.choices
2020+ return Text(
2121+ f"({y})/({yes})" if default else f"({n})/({no})", style="prompt.default"
2222+ )
2323+2424+ def process_response(self, value: str) -> bool:
2525+ """Convert choices to a bool."""
2626+ value = value.strip().lower()
2727+ if value not in self.choices:
2828+ raise InvalidResponse(self.validate_error_message)
2929+ return value == self.choices[0]
3030+3131+3232+class ConfirmOreo(Confirm):
3333+ prompt_suffix = ""
3434+3535+ def __init__(
3636+ self, *args, show_default: bool = False, show_choices: bool = False, **kwargs
3737+ ):
3838+ super().__init__(
3939+ *args, show_default=show_default, show_choices=show_choices, **kwargs
4040+ )
+23
oreo/functions.py
···11+from itertools import product, chain
22+from string import ascii_lowercase as lower
33+44+55+def product_intersperse(a, *args, b="", **kwargs):
66+ for seq in product(
77+ args,
88+ (a, b, *(kwargs[c] for c in lower if c in kwargs)),
99+ repeat=len(args),
1010+ ):
1111+ if all(
1212+ False
1313+ for i, s in enumerate(item for item in seq if item in args)
1414+ if args[i] != s
1515+ ):
1616+ yield [item for item in seq if item != ""]
1717+1818+1919+def powerset_intersperse(a, *args, **kwargs):
2020+ return chain.from_iterable(
2121+ product_intersperse(a, *args[: index + 1], **kwargs)
2222+ for index in range(len(args))
2323+ )
+232
oreo/oreo.py
···11+#!/usr/bin/env python3
22+33+# Adapted From:
44+# Answer: https://stackoverflow.com/a/50824001
55+# User: https://stackoverflow.com/users/9931167/phrreakk
66+77+import rich_click as click
88+99+# Keeping this separate allows me to override the options and arguments
1010+# passed to the application, as importing `argv` directly sets the arguments
1111+# when the module is first initialized. This also happens when I put calls to `argv`
1212+# in the global namespace and not in a function.
1313+import sys
1414+1515+from addict import Dict
1616+from bundle import bundle, Bundle
1717+from contextlib import contextmanager
1818+from functools import partial
1919+from hydrox.helpers import flatten
2020+from itertools import dropwhile
2121+from more_itertools import split_at
2222+from rich import print
2323+from thefuzz import process
2424+from unittest import mock
2525+from rich.pretty import pprint
2626+2727+from .confirm import ConfirmOreo
2828+2929+3030+# Adapted From: https://realpython.com/inherit-python-list/#inheriting-from-pythons-built-in-list-class
3131+class Args(list):
3232+ def __init__(self, args):
3333+ super().__init__(map(flatten, args))
3434+3535+ def __getitem__(self, index):
3636+ try:
3737+ return super().__getitem__(index)
3838+ except IndexError:
3939+ return []
4040+4141+ def __setitem__(self, index, item):
4242+ super().__setitem__(index, flatten(item))
4343+4444+ def insert(self, index, item):
4545+ super().insert(index, flatten(item))
4646+4747+ def append(self, item):
4848+ super().append(flatten(item))
4949+5050+ def extend(self, other):
5151+ if isinstance(other, self.__class__):
5252+ return super().extend(other)
5353+ return super().extend(flatten(item) for item in other)
5454+5555+5656+# Adapted From: https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases
5757+class FuzzedGroup(click.RichGroup):
5858+ def get_command(self, ctx, cmd_name):
5959+ commands = self.list_commands(ctx)
6060+ return super().get_command(ctx, cmd_name) or super().get_command(
6161+ ctx, process.extractOne(cmd_name, commands)[0] if commands else ""
6262+ )
6363+6464+ def resolve_command(self, ctx, args):
6565+ _, cmd, args = super().resolve_command(
6666+ ctx,
6767+ list(dropwhile(lambda arg: arg.startswith("-"), args)),
6868+ )
6969+ return cmd.name, cmd, args
7070+7171+ # def make_context(
7272+ # self,
7373+ # info_name: Optional[str],
7474+ # args: list[str],
7575+ # parent: Optional[click.Context] = None,
7676+ # **extra: Any,
7777+ # ) -> click.Context:
7878+ # return super().make_context(
7979+ # info_name,
8080+ # list(dropwhile(lambda arg: arg.startswith("-"), args)),
8181+ # parent,
8282+ # **extra,
8383+ # )
8484+8585+8686+types = ("double-stuf", "mini", "thins")
8787+flavors = ("carrot-cake", "golden")
8888+click_kwargs = {"cls": FuzzedGroup, "no_args_is_help": False}
8989+9090+# Adapted From: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams
9191+# And: https://github.com/pallets/click/issues/1370#issuecomment-522549260
9292+# echo "Hello!" | ./test.py
9393+# ./test.py <<< "Hello!"
9494+# ./test.py < <(echo "Hello!")
9595+# ./test.py < tmpfile
9696+9797+9898+def oreo(argv=tuple(), *args, **kwargs):
9999+ argv = argv or sys.argv
100100+101101+ # Adapted From:
102102+ # Answer: https://stackoverflow.com/a/53541456
103103+ # User: https://stackoverflow.com/users/10553976/charles-landau
104104+ # And: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams
105105+ # And: https://github.com/pallets/click/issues/1370#issuecomment-522549260
106106+ if not (argv[0] is None or sys.stdin.isatty()):
107107+ argv = [argv[0], *sys.stdin.read().strip().split(), *argv[1:]]
108108+109109+ # Adapted From:
110110+ # Answer 1: https://stackoverflow.com/a/27765993
111111+ # User 1: https://stackoverflow.com/users/211734/jason-antman
112112+ with mock.patch.object(sys, "argv", argv):
113113+ opts = [
114114+ f"--{opt}" for opt in ("list", "password", "prompt", "stderr", "stdout")
115115+ ]
116116+ items = []
117117+ skip = False
118118+ for item in argv:
119119+ if skip:
120120+ skip = False
121121+ elif item == "--code":
122122+ skip = True
123123+ elif item not in opts:
124124+ items.append(item)
125125+ _argvs = Args(split_at(items[1:], lambda arg: not arg.startswith("-")))
126126+ if len(_argvs) > 2:
127127+ argvs = Args(_argvs[:2])
128128+ argvs.append(_argvs[2:])
129129+ else:
130130+ argvs = _argvs
131131+ root = len(argvs) < 2
132132+ flavored = len(argvs) == 2
133133+134134+ obj = Dict()
135135+136136+ def stdprint(*args, **kwargs):
137137+ args = ("oreo", *args)
138138+ if obj.password or obj.prompt:
139139+ ConfirmOreo.ask(
140140+ ("\n" if obj.list else " ").join(args),
141141+ password=obj.password,
142142+ show_choices=False,
143143+ )
144144+ else:
145145+ for std in ("out", "err"):
146146+ if obj[std]:
147147+ kwargs.pop("file", None)
148148+ if std == "err":
149149+ kwargs["file"] = getattr(sys, "stderr")
150150+ fprint = partial(print, **kwargs)
151151+ if obj.list:
152152+ for item in args:
153153+ fprint(item)
154154+ else:
155155+ fprint(*args)
156156+ if obj.code is not None:
157157+ exit(obj.code)
158158+159159+ @bundle(name="oreo", **click_kwargs)
160160+ @bundle.up("args", disabled=not root)
161161+ def f(
162162+ code: int,
163163+ _list: bool,
164164+ prompt: bool,
165165+ password: bool,
166166+ stdout: bool,
167167+ stderr: bool,
168168+ *args,
169169+ ):
170170+ obj.code = code
171171+ obj.list = _list
172172+ obj.password = password
173173+ obj.prompt = prompt
174174+ obj.err = stderr
175175+ obj.out = stdout or not (obj.err or stdout)
176176+ if root:
177177+ stdprint(*args)
178178+179179+ f.print = stdprint
180180+181181+ # Adapted From:
182182+ # Answer: https://stackoverflow.com/a/49631500
183183+ # User: https://stackoverflow.com/users/379311/yann-vernier
184184+ def type_wrapper(flavor, type):
185185+ @flavor.up(name=type, **click_kwargs)
186186+ def f1(*args):
187187+ flavor.print(type, *args)
188188+189189+ return f1
190190+191191+ def flavor_wrapper(flavor):
192192+ fprint = partial(f.print, *argvs[0], flavor, *argvs[1])
193193+194194+ @f.up(name=flavor, **click_kwargs)
195195+ @bundle.up("args", disabled=not flavored)
196196+ def f2(*args):
197197+ if flavored:
198198+ fprint()
199199+200200+ f2.print = fprint
201201+ return f2
202202+203203+ globals().update(
204204+ {type.replace("-", "_"): type_wrapper(f, type) for type in types}
205205+ )
206206+207207+ for flavor in flavors:
208208+ f2 = flavor_wrapper(flavor)
209209+ globals()[flavor.replace("-", "_")] = f2
210210+ globals().update(
211211+ {
212212+ f"{flavor}-{type}".replace("-", "_"): type_wrapper(f2, type)
213213+ for type in types
214214+ }
215215+ )
216216+217217+ if argv[0] is None:
218218+ return f.compile()
219219+ return f.flip(*args, **kwargs)
220220+221221+222222+# Adapted From:
223223+# Answer: https://stackoverflow.com/a/27765993
224224+# User: https://stackoverflow.com/users/211734/jason-antman
225225+@contextmanager
226226+def hydrox(*args):
227227+ with mock.patch.object(sys, "argv", (None, *args)):
228228+ yield Bundle.runner.invoke(oreo(sys.argv), args)
229229+230230+231231+if __name__ == "__main__":
232232+ oreo()
+124
oreo/tests/test_app.py
···11+import os
22+33+from addict import Dict
44+from itertools import chain
55+from oreo import powerset_intersperse, building, hydrox
66+from parametrized import parametrized
77+from pytest import mark
88+99+from subprocess import run
1010+1111+oreos = Dict(
1212+ args=("c", "d"),
1313+ flavor="carrot-cake",
1414+ type="double-stuf",
1515+)
1616+oreos.joint.args = (oreos.flavor, oreos.type)
1717+1818+# NOTE: This is not a mistake; `powerset_intersperse` takes a `b` keyword argument
1919+oreos.kwargs.b = oreos.joint.kwargs.b = "-0"
2020+2121+oreos.powerset = list(powerset_intersperse("", "oreo", *oreos.args, **oreos.kwargs))
2222+oreos.superpowerset = list(
2323+ chain(
2424+ oreos.powerset,
2525+ ((*o, "e") for o in oreos.powerset if "c" in o and "d" in o),
2626+ ((*o, "e", "-0") for o in oreos.powerset if "c" in o and "d" in o),
2727+ ((*o, "e", "-0", "f") for o in oreos.powerset if "c" in o and "d" in o),
2828+ )
2929+)
3030+oreos.joint.powerset = oreos.noreo.joint.powerset = list(
3131+ powerset_intersperse("", "oreo", *oreos.joint.args, **oreos.joint.kwargs)
3232+)
3333+oreos.joint.superpowerset = oreos.noreo.joint.superpowerset = list(
3434+ chain(
3535+ oreos.joint.powerset,
3636+ (
3737+ (*o, "e")
3838+ for o in oreos.joint.powerset
3939+ if oreos.flavor in o and oreos.type in o
4040+ ),
4141+ (
4242+ (*o, "e", "-0")
4343+ for o in oreos.joint.powerset
4444+ if oreos.flavor in o and oreos.type in o
4545+ ),
4646+ (
4747+ (*o, "e", "-0", "f")
4848+ for o in oreos.joint.powerset
4949+ if oreos.flavor in o and oreos.type in o
5050+ ),
5151+ )
5252+)
5353+oreos.noreo.powerset = [powerset[1:] for powerset in oreos.powerset]
5454+oreos.noreo.superpowerset = list(
5555+ chain(
5656+ oreos.noreo.powerset,
5757+ ((*o, "e") for o in oreos.noreo.powerset if "c" in o and "d" in o),
5858+ ((*o, "e", "-0") for o in oreos.noreo.powerset if "c" in o and "d" in o),
5959+ ((*o, "e", "-0", "f") for o in oreos.noreo.powerset if "c" in o and "d" in o),
6060+ )
6161+)
6262+6363+# Adapted From: https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams
6464+# And: https://github.com/pallets/click/issues/1370#issuecomment-522549260
6565+# echo "Jeet" | ./test.py
6666+# ./test.py <<< "Jeet"
6767+# ./test.py < <(echo "Jeet")
6868+# ./test.py < tmpfile
6969+7070+7171+@mark.oreo
7272+@mark.oreo_runner
7373+class TestRunner:
7474+ @parametrized.zip
7575+ def test_oreos(
7676+ self,
7777+ args=oreos.noreo.superpowerset,
7878+ Oreo=[" ".join(o) for o in oreos.noreo.joint.superpowerset],
7979+ ):
8080+ with hydrox(*args) as oreo:
8181+ assert oreo.output.strip() == Oreo, oreo.stderr.strip()
8282+8383+ @parametrized.zip
8484+ def test_list(
8585+ self,
8686+ args=oreos.noreo.superpowerset,
8787+ Oreo=["\n".join(o) for o in oreos.noreo.joint.superpowerset],
8888+ ):
8989+ with hydrox("--list", *args) as oreo:
9090+ assert oreo.output.strip() == Oreo, oreo.stderr.strip()
9191+9292+9393+# Adapted From: https://github.com/dstathis/fastprocess/issues/5#issuecomment-938794618
9494+@mark.oreo
9595+@mark.oreo_subprocess
9696+@mark.skipif(building, reason="oreo app isn't available while building oreo")
9797+class TestSubprocess:
9898+ @parametrized.zip
9999+ def test_oreos(
100100+ self,
101101+ args=oreos.superpowerset,
102102+ Oreo=[" ".join(o) for o in oreos.joint.superpowerset],
103103+ ):
104104+ output = run(args, capture_output=True)
105105+ print(output.stdout)
106106+ assert output.stdout.decode().strip() == Oreo, output.stderr.decode().strip()
107107+ # r, w = os.pipe()
108108+ # pid = FastProcess(args, stdout=os.fdopen(w))
109109+ # pid.wait()
110110+ # assert os.fdopen(r).read().strip() == Oreo
111111+112112+ @parametrized.zip
113113+ def test_list(
114114+ self,
115115+ args=oreos.superpowerset,
116116+ Oreo=[os.linesep.join(o) for o in oreos.joint.superpowerset],
117117+ ):
118118+ output = run((args[0], "--list", *args[1:]), capture_output=True)
119119+ print(output.stdout)
120120+ assert output.stdout.decode().strip() == Oreo, output.stderr.decode().strip()
121121+ # r, w = os.pipe()
122122+ # pid = FastProcess((args[0], "--list", *args[1:]), stdout=os.fdopen(w))
123123+ # pid.wait()
124124+ # assert os.fdopen(r).read().strip() == Oreo
+32
oreo/variables.py
···11+from os import environ
22+33+__all__ = ("building",)
44+55+# Adapted From:
66+# Answer: https://stackoverflow.com/a/68938778
77+# User: https://stackoverflow.com/users/14030287/ariadne-paradis
88+building = any(
99+ True
1010+ for var in (
1111+ "NIX_BINTOOLS",
1212+ "NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu",
1313+ "NIX_BUILD_CORES",
1414+ "NIX_BUILD_TOP",
1515+ "NIX_CC",
1616+ "NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu",
1717+ "NIX_CFLAGS_COMPILE",
1818+ "NIX_ENFORCE_NO_NATIVE",
1919+ "NIX_ENFORCE_PURITY",
2020+ "NIX_HARDENING_ENABLE",
2121+ "NIX_INDENT_MAKE",
2222+ "NIX_LDFLAGS",
2323+ "NIX_LOG_FD",
2424+ "NIX_SSL_CERT_FILE",
2525+ "NIX_STORE",
2626+ )
2727+ if var in environ
2828+) or any(
2929+ True
3030+ for var in ("TMP", "TMPDIR", "TEMP", "TEMPDIR")
3131+ if environ.get(var, "/tmp") == "/build"
3232+)
+3-1319
packages/bundle/bundle/__init__.py
···11-import operator
22-import os
33-import rich_click as click
44-55-from addict import Dict
66-from autoslot import SlotsMeta
77-from beartype import beartype
88-from beartype.door import is_bearable, die_if_unbearable
99-from collections import defaultdict
1010-from collections.abc import Callable
1111-from contextlib import suppress
1212-from copy import deepcopy
1313-from docstring_parser import parse
1414-from functools import partial, reduce, wraps
1515-from inspect import signature, Parameter, isclass
1616-from itertools import chain
1717-from more_itertools import partition
1818-from plum import dispatch, Dispatcher
1919-from rich.pretty import pprint
2020-from rich_click.rich_help_formatter import RichHelpFormatter
2121-from string import ascii_lowercase as lower, ascii_uppercase as upper
2222-from types import EllipsisType, MappingProxyType
2323-from typing import (
2424- Any,
2525- get_args,
2626- get_origin,
2727- Literal,
2828- Optional,
2929- Union,
3030-)
3131-3232-from .pipe import pipe, PipeArgs
3333-from .python import (
3434- can_unpack_variable,
3535- deepfreeze,
3636- dirs,
3737- flatten,
3838- normalize,
3939- normalizeMultiline,
4040- pk_variables,
4141- plumeta,
4242- process,
4343-)
4444-from .typing import (
4545- ArgReturn,
4646- Attr,
4747- BString,
4848- Callback,
4949- Coll,
5050- Count,
5151- Eager,
5252- Obj,
5353- Origin,
5454- Pass,
5555- Password,
5656- Split,
5757- subtype,
5858- Switch,
5959- ValueReturn,
6060-)
6161-from .model import BaseModel
6262-6363-6464-class Parent(metaclass=plumeta):
6565- @property
6666- def kwargs(self):
6767- kwargs = self.parent.kwargs | self.cls.kwargs
6868- kwargs.pop("name", None)
6969- return kwargs
7070-7171- def __init__(self):
7272- self.cls = Dict()
7373- self.func = click
7474-7575- def __init__(self, cls: "bundle"):
7676- self.cls = cls
7777-7878- def __init__(self, cls: "Parent"):
7979- self.cls = cls.cls
8080-8181- def __getattr__(self, attr):
8282- return getattr(self.cls, attr)
8383-8484- def __bool__(self):
8585- return bool(self.func)
8686-8787-8888-@dispatch(precedence=1)
8989-def process_bundle(
9090- annotation: Any,
9191- value: None,
9292-) -> None: ...
9393-9494-9595-@dispatch
9696-def process_bundle(
9797- annotation: Origin[Callback, Eager],
9898- value: Any,
9999-):
100100- return process_bundle(get_args(annotation)[0], value)
101101-102102-103103-@dispatch
104104-def process_bundle(
105105- annotation: Origin[Literal],
106106- value: ValueReturn,
107107-) -> ArgReturn | ValueReturn:
108108- for arg in get_args(annotation):
109109- if value in (arg, str(arg)):
110110- return arg
111111- die_if_unbearable(value, annotation)
112112-113113-114114-@dispatch
115115-def process_bundle(
116116- annotation: Origin[Split],
117117- value: BString,
118118-) -> Coll:
119119- annotation, delimiter = get_args(annotation)
120120- return process_bundle(annotation, value.split(delimiter))
121121-122122-123123-@dispatch
124124-def process_bundle(
125125- annotation: Origin[Split],
126126- value: Coll,
127127-) -> Coll:
128128- annotation, delimiter = get_args(annotation)
129129- return process_bundle(annotation, flatten(item.split(delimiter) for item in value))
130130-131131-132132-for method in process._pending + process._resolved:
133133- process_bundle.register(method[0], precedence=method[2])
134134-135135-136136-class Model(BaseModel):
137137- def __init__(self, *args, **kwargs):
138138- self.__processor__ = process_bundle
139139- super().__init__(*args, **kwargs)
140140-141141-142142-names = ("__name__", "__qualname__")
143143-144144-145145-def deflambda(name, f=None):
146146- f = f or (lambda: None)
147147- for attr in names:
148148- setattr(f, attr, name)
149149- return f
150150-151151-152152-class Bundle(SlotsMeta, metaclass=plumeta):
153153- def __new__(cls, name, bases, ns, subclass=False):
154154- if subclass:
155155- cls._dunders = tuple()
156156- ns["__slots__"] = chain(
157157- tuple() if bases else ("__weakref__",),
158158- cls._dunders,
159159- names,
160160- (
161161- "docstring",
162162- "superparams",
163163- "signature",
164164- "_func",
165165- "print",
166166- ),
167167- )
168168- cls._specopts = {}
169169- cls._specdefs = Dict(disabled=False)
170170- cls._pathvars = ["PATH", "PATHS"]
171171- cls._defaults = Dict(
172172- option={"metavar": str.__name__, "show_default": True},
173173- group={
174174- "invoke_without_command": True,
175175- "no_args_is_help": True,
176176- },
177177- )
178178- cls._defaults.group.context_settings.help_option_names = ("-h", "--help")
179179- cls._globals = Dict()
180180- cls._class_roots = Dict()
181181- cls._class_instances = Dict()
182182- cls._roots = Dict()
183183- cls._instances = Dict()
184184- return super().__new__(cls, name, bases, ns)
185185- return subtype(name, bundler, ns)
186186-187187- class __prepare__(dict):
188188- def __init__(self, name, bases, subclass=False):
189189- self.subclass = subclass
190190- if self.subclass:
191191- self.dispatcher = Dispatcher()
192192- super().__setitem__("_dispatcher", self.dispatcher)
193193-194194- def __setitem__(self, key, value):
195195- if callable(value) and self.subclass:
196196- args, kwargs = getattr(value, "__plume__", (tuple(), {}))
197197- if not kwargs.get("disabled", False):
198198- value = getattr(self.get(key), "dispatch", self.dispatcher)(
199199- value, *args, **kwargs
200200- )
201201- super().__setitem__(key, value)
202202-203203- def drive(cls, func: Callable, *args, **kwargs):
204204- return cls(func).quark(*args, **kwargs)
205205-206206- def drive(cls, **kwargs):
207207- return cls().quark(**kwargs)
208208-209209- def up(cls, name: str, *args, **kwargs):
210210- def wrapper(self):
211211- cls._specopts[self][name] = Dict(
212212- {k: kwargs.pop(k, v) for k, v in cls._specdefs.items()}
213213- )
214214- setattr(
215215- self,
216216- "__bundle__",
217217- getattr(self, "__bundle__", Dict())
218218- | Dict(params={name: {"args": args, "kwargs": kwargs}}),
219219- )
220220- return self
221221-222222- return wrapper
223223-224224- def up(
225225- cls,
226226- self: Union["Bundler", "bundle"],
227227- parent: Optional[Union["Bundler", "bundle"]] = None,
228228- **kwargs,
229229- ) -> Union["bundle", type["bundler"]]:
230230- self._rewrap(kwargs)
231231- if parent:
232232- parent._adopt(self)
233233- return self
234234-235235- def up(
236236- cls,
237237- self: Callable,
238238- parent: None = None,
239239- **kwargs,
240240- ):
241241- name = kwargs.get("name", self.__name__)
242242- setattr(
243243- self,
244244- "__bundle__",
245245- getattr(self, "__bundle__", Dict()) | Dict(group=Dict(kwargs)),
246246- )
247247- for attr in names:
248248- setattr(self, attr, name)
249249- return self
250250-251251- def up(
252252- cls,
253253- parent: Optional[Union["Bundler", "bundle"]] = None,
254254- **kwargs,
255255- ):
256256- def wrapper(self: Callable):
257257- return cls.up(self, parent, **kwargs)
258258-259259- return wrapper
260260-261261- def __getattr__(cls, attr):
262262- return getattr(click, attr)
263263-264264-265265-class bundle(metaclass=Bundle, subclass=True):
266266- # TODO: There may be a few problems with the adoption...
267267- def __new__(
268268- cls,
269269- name: str,
270270- bases: tuple["bundle", ...],
271271- ns: dict | MappingProxyType,
272272- **kwargs,
273273- ):
274274- self = subtype(name, bundler, ns)
275275- self._rewrap(bases[0].kwargs)
276276- try:
277277- bases[0]._adopt(self)
278278- except AttributeError:
279279- with suppress(AttributeError, TypeError):
280280- bases[0].parent._adopt(self)
281281- return self
282282-283283- def __new__(
284284- cls,
285285- self: "Bundler",
286286- parent: Optional[Union["Bundler", "bundle"]] = None,
287287- **kwargs,
288288- ):
289289- self._rewrap(kwargs)
290290- if parent:
291291- parent._adopt(self)
292292- return self
293293-294294- def __new__(
295295- cls,
296296- self: type,
297297- parent: Optional[Union["Bundler", "bundle"]] = None,
298298- **kwargs,
299299- ):
300300- return cls.__new__(cls, subtype(self, bundler), parent, **kwargs)
301301-302302- def __new__(cls, *args, **kwargs):
303303- return super().__new__(cls)
304304-305305- class __prepare__(dict):
306306- def __init__(self, name, bases):
307307- pass
308308-309309- def __init__(
310310- self,
311311- func: Callable,
312312- parent: Optional[Union[type["bundle"], "bundle", Parent]] = None,
313313- /,
314314- **kwargs,
315315- ):
316316- if isinstance(parent, (self.__class__, Parent)):
317317- self.parent = Parent(parent)
318318- self.root = self.parent.root
319319- else:
320320- self.parent = Parent()
321321- self.root = self
322322-323323- self.children = Dict()
324324- self.kwargs = Dict(kwargs)
325325- self.func = None
326326- self._wrap(func)
327327-328328- def __init__(
329329- self,
330330- parent: Union["bundle", Parent],
331331- kwargs: dict,
332332- ):
333333- self.parent = Parent(parent)
334334- self.root = self.parent.root
335335- self.children = Dict()
336336- self.kwargs = Dict(kwargs)
337337- self.func = None
338338-339339- def __init__(self, **kwargs):
340340- self.parent = Parent()
341341- self.root = self
342342- self.children = Dict()
343343- self.kwargs = Dict(kwargs)
344344- self.func = None
345345-346346- def __getattr__(self, attr):
347347- try:
348348- object.__getattribute__(self, "func")
349349- except AttributeError as e:
350350- raise AttributeError(
351351- f"'{self.__class__.__name__}' object has no attribute '{attr}'"
352352- ) from e
353353- else:
354354- try:
355355- return getattr(self.func, attr)
356356- except AttributeError as e:
357357- raise AttributeError(
358358- f"'{self.__class__.__name__}' and '{self.func.__class__.__name__}' objects have no attribute '{attr}'"
359359- ) from e
360360-361361- def _run(self, arg):
362362- return is_bearable(
363363- arg, next(iter(self._func.__signature__.parameters.values())).annotation
364364- )
365365-366366- # TODO: There may be a few problems with the adoption...
367367- def __call__(self, cls: "Bundler"):
368368- if self._run(cls):
369369- return beartype(self._func)(cls)
370370- cls._rewrap(self.kwargs)
371371- try:
372372- self._adopt(cls)
373373- except AttributeError:
374374- with suppress(AttributeError, TypeError):
375375- self.parent._adopt(cls)
376376- return cls
377377-378378- def __call__(self, cls: type):
379379- if self._run(cls):
380380- return beartype(self._func)(cls)
381381- return self(subtype(cls, bundler))
382382-383383- def __call__(self, func: Callable):
384384- if self.func is None:
385385- return self._wrap(func)
386386- if self._run(func):
387387- return beartype(self._func)(func)
388388- return self.__class__(func, self)
389389-390390- def _run(self, args, kwargs):
391391- if args or not (args or kwargs):
392392- return True
393393- run_score = 0
394394- run_params = self._func.__signature__.parameters
395395- click_score = 0
396396- command_params = signature(click.Command).parameters
397397- group_params = signature(click.Group).parameters
398398- for k, v in kwargs.items():
399399- if k in run_params:
400400- annotation = run_params[k].annotation
401401- if annotation is Parameter.empty or is_bearable(v, annotation):
402402- run_score += 1
403403- if k in command_params:
404404- command_annotation = command_params[k].annotation
405405- if command_annotation is Parameter.empty or is_bearable(
406406- v, command_annotation
407407- ):
408408- click_score += 1
409409- elif k in group_params:
410410- group_annotation = group_params[k].annotation
411411- if group_annotation is Parameter.empty or is_bearable(
412412- v, group_annotation
413413- ):
414414- click_score += 1
415415- return run_score >= click_score
416416-417417- def __call__(self, *args, **kwargs):
418418- if self._run(args, kwargs):
419419- return beartype(self._func)(*args, **kwargs)
420420- return self.__class__(self, kwargs)
421421-422422- def __del__(self):
423423- with suppress(AttributeError):
424424- for child in self.children.values():
425425- del child
426426-427427- def flip(self, **kwargs):
428428- return self.func(**kwargs)
429429-430430- def _driver(self, parent: "bundle", func: Callable, name: str = ""):
431431- return self.__class__(func, parent, name=name)
432432-433433- def _driver(self, parent: "bundle", args: Coll):
434434- for f in args:
435435- self._driver(parent, f)
436436-437437- def _driver(self, parent: "bundle", kwargs: dict):
438438- for k, v in kwargs.items():
439439- if isinstance(v, dict):
440440- self._driver(self._driver(parent, v.pop(k, deflambda(k)), k), v)
441441- elif isinstance(v, Coll):
442442- different_names, same_names = partition(
443443- lambda func: func.__name__ == k, v
444444- )
445445- self._driver(
446446- self._driver(parent, next(same_names, deflambda(k)), k),
447447- different_names,
448448- )
449449- else:
450450- self._driver(parent, v, k)
451451-452452- def quark(self, *args, **kwargs):
453453- if self.func is None:
454454- self._wrap(args[0])
455455- self._driver(self, args[1:])
456456- else:
457457- self._driver(self, args)
458458- self._driver(self, kwargs)
459459- return self
460460-461461- def quark(self, **kwargs):
462462- different_names = tuple()
463463- if self.func is None:
464464- if kwargs:
465465- name = self.kwargs.name = next(iter(kwargs.keys()))
466466-467467- # TODO: If the first kwarg is not a callable, create a new root.
468468- # if isinstance(kwargs[name], dict):
469469- # func = kwargs[name].pop(name, deflambda(name))
470470- # elif isinstance(kwargs[name], Coll):
471471- # different_names, same_names = partition(
472472- # lambda func: func.__name__ == name, kwargs[name]
473473- # )
474474- # func = next(same_names, deflambda(name))
475475- # else:
476476- # func = kwargs.pop(name, deflambda(name))
477477- if callable(kwargs[name]):
478478- func = kwargs.pop(name, deflambda(name))
479479- else:
480480- # NOTE: The name is for the dictionary or collection, not this.
481481- func = lambda: None
482482-483483- else:
484484- func = lambda: None
485485- self._wrap(func)
486486- self._driver(self, different_names)
487487- self._driver(self, kwargs)
488488- return self
489489-490490- def _adopt(
491491- self,
492492- cls: Union["bundle", "bundler", type["bundler"]],
493493- name: Optional[str] = None,
494494- ):
495495- name = name or cls.func.name
496496- cls = cls.__bundles__[name] if isinstance(cls, bundler) else cls
497497- self.func.commands[name] = cls.func
498498- self.children[name] = cls
499499- cls.parent = self
500500-501501- def _filter_params(
502502- self, params: list[click.Parameter], param_type: type[click.Parameter]
503503- ):
504504- return list(filter(lambda param: isinstance(param, param_type), params))
505505-506506- @property
507507- def _arguments(self):
508508- return self._filter_params(self.params, click.Argument)
509509-510510- @property
511511- def _options(self):
512512- return self._filter_params(self.params, click.Option)
513513-514514- def _deepfreeze(self, x, *ignores):
515515- return deepfreeze(
516516- x,
517517- {t: lambda y: None for t in (Callable, RichHelpFormatter)},
518518- ignores=ignores,
519519- )
520520-521521- def _equal(
522522- self,
523523- obj: click.Command | click.Parameter,
524524- other: click.Command | click.Parameter,
525525- *ignores,
526526- obj_name="",
527527- other_name="",
528528- ):
529529- ignores = (*ignores, "__dict__")
530530- dct = defaultdict(list)
531531- for k in dir(obj):
532532- if k not in ignores:
533533- dct[k].append(
534534- (obj_name or obj.name, self._deepfreeze(getattr(obj, k), *ignores))
535535- )
536536- dct[k].append(
537537- (
538538- other_name or other.name,
539539- self._deepfreeze(getattr(other, k), *ignores),
540540- )
541541- )
542542- errors = {k: v for k, v in dct.items() if len({t[1] for t in v}) != 1}
543543- if errors:
544544- if "PYTEST_CURRENT_TEST" in os.environ:
545545- pprint(errors)
546546- return False
547547- return True
548548-549549- def _equals(self, other: Union["Bundler", "bundle", click.Group], *ignores) -> bool:
550550- options = self._filter_params(other.params, click.Option)
551551- arguments = self._filter_params(other.params, click.Argument)
552552- return (
553553- all(
554554- False
555555- for index, param in enumerate(self._arguments)
556556- if not self._equal(
557557- param,
558558- arguments[index],
559559- obj_name=f"{self.name}:{param.name}",
560560- other_name=f"{other.name}:{param.name}",
561561- )
562562- )
563563- and all(
564564- False
565565- for index, param in enumerate(self._options)
566566- if not self._equal(
567567- param,
568568- options[index],
569569- obj_name=f"{self.name}:{param.name}",
570570- other_name=f"{other.name}:{param.name}",
571571- )
572572- )
573573- and self._equal(
574574- self.func,
575575- other if isinstance(other, click.Group) else other.func,
576576- *ignores,
577577- "params",
578578- obj_name=f"self: {self.name}",
579579- other_name=f"other: {other.name}",
580580- )
581581- # and all(
582582- # v._equals(other.commands[k], *ignores) if k in other.commands else False
583583- # for k, v in self.children.items()
584584- # )
585585- and all(
586586- False
587587- for k, v in self.children.items()
588588- if not (
589589- (k in other.commands) and v._equals(other.commands[k], *ignores)
590590- )
591591- )
592592- )
593593-594594- def equals(self, other: Union["Bundler", "bundle", click.Group], *ignores) -> bool:
595595- return self._equals(other, *ignores, "name")
596596-597597- def __eq__(self, other: Union["Bundler", "bundle", click.Group]) -> bool:
598598- return (
599599- self.equals(other)
600600- and self._func == other.callback
601601- # and all(
602602- # (v._func == other.commands[k].callback)
603603- # if k in other.commands
604604- # else False
605605- # for k, v in self.children.items()
606606- # )
607607- and all(
608608- False
609609- for k, v in self.children.items()
610610- if not (
611611- (k in other.commands) and (v._func == other.commands[k].callback)
612612- )
613613- )
614614- )
615615-616616- def _hash(self, *ignores):
617617- ignores = (*ignores, "params")
618618- return (
619619- *[self._deepfreeze(dirs(arg)) for arg in self._arguments],
620620- *[self._deepfreeze(dirs(opt)) for opt in self._options],
621621- *[self._deepfreeze(dirs(self.func, ignores=ignores), *ignores)],
622622- *[child._hash(*ignores) for child in self.children.values()],
623623- )
624624-625625- def hash(self, *ignores):
626626- return hash(self._hash(*ignores, "name"))
627627-628628- def __hash__(self):
629629- return hash(
630630- (
631631- (
632632- self.hash(),
633633- self._func,
634634- *(child._func for child in self.children.values()),
635635- )
636636- )
637637- )
638638-639639- def __eq__(self, other) -> bool:
640640- return False
641641-642642- def _recursive_difference(self, before, after):
643643- difference = {}
644644- for k, v in after.items():
645645- if k not in before:
646646- difference[k] = v
647647- elif before[k] != v:
648648- if isinstance(before[k], dict) and isinstance(v, dict):
649649- difference[k] = self._recursive_difference(before[k], v)
650650- elif not callable(before[k]) and not callable(v):
651651- difference[k] = v
652652- return difference
653653-654654- # def _rewrap(self, **kwargs) -> "bundle":
655655- # subcommands = dict(self.func.commands)
656656- # self.kwargs |= kwargs
657657- # self._wrap()
658658- # kwargs.pop("name", None)
659659- # for child in self.children.values():
660660- # child._rewrap(**kwargs)
661661- # self.func.commands = subcommands
662662- # return self
663663-664664- def _rewrap(self, *args, **kwargs) -> "bundle":
665665- kwargs = reduce(operator.or_, [Dict(d) for d in (*args, kwargs)])
666666- self.kwargs |= kwargs
667667- for k, v in self._make_group_kwargs().items():
668668- setattr(self.func, k, v)
669669- kwargs.pop("name", None)
670670- for child in self.children.values():
671671- child._rewrap(kwargs)
672672- return self
673673-674674- def _make_group_kwargs(self, base_kwargs: Optional[dict] = None, **kwargs):
675675- return reduce(
676676- operator.or_,
677677- (
678678- self.__class__._defaults.group,
679679- base_kwargs or {},
680680- self.__class__._globals.group,
681681- self.__class__._roots[self.root._func].group,
682682- self.__class__._instances[self._func].group,
683683- self.parent.kwargs,
684684- getattr(self._func, "__bundle__", Dict()).group,
685685- self.kwargs,
686686- kwargs,
687687- ),
688688- )
689689-690690- # Adapted From:
691691- # Answer 1: https://stackoverflow.com/a/12627202
692692- # User 1: https://stackoverflow.com/users/748858/mgilson
693693- # Answer 2: https://stackoverflow.com/a/50177498
694694- # User 2: https://stackoverflow.com/users/2867928/mazdak
695695- def _wrap(self, func: Optional[Callable] = None, **kwargs) -> "bundle":
696696- if func:
697697- self.docstring = parse(func.__doc__) if func.__doc__ else Dict()
698698- self.signature = func.__signature__ = signature(func)
699699- self._func = func
700700- for attr in self.__class__._dunders:
701701- setattr(self, attr, getattr(self._func, attr))
702702- self.superparams = Dict(
703703- disabled=[
704704- k for k, v in self.__class__._specopts[func].items() if v.disabled
705705- ],
706706- pre={
707707- param.name: param
708708- for param in getattr(func, "__click_params__", tuple())
709709- },
710710- # TODO: Test this.
711711- help={param.arg_name: param for param in self.docstring.params},
712712- )
713713- else:
714714- func = self._func
715715-716716- ignore_and_accept = False
717717- for k, v in self.signature.parameters.items():
718718- if v.kind is Parameter.VAR_KEYWORD:
719719- self.superparams.disabled.append(k)
720720- ignore_and_accept = True
721721- if k not in self.superparams.disabled:
722722- self.superparams.working[k] = v
723723- if k in self.superparams.pre:
724724- if v.annotation is not Parameter.empty:
725725- self.superparams.pre[k].callback = self._make_callback(
726726- k, v.annotation, func, self.superparams.pre[k].callback
727727- )
728728- if k in self.superparams.help:
729729- self.superparams.pre[k].help = self.superparams.help[k]
730730- elif k not in self.superparams.disabled:
731731- self.superparams.processing[k] = v
732732-733733- envvarg = False
734734- # Adapted From:
735735- # Answer: https://stackoverflow.com/a/47017849
736736- # User: https://stackoverflow.com/users/1583052/dipu
737737- for i, (k, v) in enumerate(
738738- dict(reversed(self.superparams.processing.items())).items()
739739- ):
740740- func = self._get_funky(
741741- len(self.superparams.processing) - 1 - i,
742742- k,
743743- v,
744744- func,
745745- )(func)
746746- self.superparams.processed[func.__click_params__[-1]] = v
747747- if k.isupper() and v.kind in (
748748- Parameter.POSITIONAL_ONLY,
749749- Parameter.VAR_POSITIONAL,
750750- ):
751751- envvarg = True
752752-753753- for param, value in dict(reversed(self.superparams.processed.items())).items():
754754- if (
755755- isinstance(param, click.Option)
756756- and not param.name.startswith("__")
757757- and not any(
758758- filter(
759759- self._shortform_predicate,
760760- flatten(param.opts, param.secondary_opts),
761761- )
762762- )
763763- ):
764764- forms = self._get_name(param.name, value, func, True)
765765- param.opts.append(forms[0])
766766- if len(forms) > 1:
767767- param.secondary_opts.append(forms[1])
768768-769769- wrapped = normalize(
770770- func,
771771- exclude=self.superparams.disabled,
772772- )
773773-774774- # Adapted From:
775775- # Answer: https://stackoverflow.com/a/24734171
776776- # User: https://stackoverflow.com/users/681785/famousgarkin
777777- if envvarg:
778778- current_locals = locals().copy()
779779- current_locals["Dict"] = Dict
780780- lower_params = [str.lower(param) for param in self.superparams.working]
781781- exec(
782782- normalizeMultiline(
783783- f"""
784784- def wrapper({",".join(lower_params)}):
785785- return wrapped({",".join(lower_params)})
786786- """
787787- ),
788788- current_locals,
789789- )
790790- wrapped = current_locals["wrapper"]
791791-792792- base_kwargs = Dict()
793793- if ignore_and_accept:
794794- base_kwargs.context_settings = Dict(
795795- ignore_unknown_options=True,
796796- allow_extra_args=True,
797797- )
798798-799799- self.func = self.parent.func.group(
800800- **self._make_group_kwargs(base_kwargs, **kwargs),
801801- )(wraps(func)(wrapped))
802802- self.parent.children[self.func.name] = self
803803- for attr in names:
804804- setattr(self, attr, self.func.name)
805805- return self
806806-807807- def _longform_predicate(self, arg):
808808- return arg.startswith("--")
809809-810810- def _get_longform(self, name: str, *args):
811811- return f"--{name.strip('_').lower().replace('_', '-')}"
812812-813813- def _shortform_predicate(self, arg):
814814- return arg.startswith("-") and not arg.startswith("--")
815815-816816- def _get_shortform(self, name: str, func: Callable):
817817- first_letter = name.strip("_")[0]
818818-819819- if hasattr(func, "__click_params__"):
820820- popts = {
821821- opt[1]
822822- for opt in flatten(
823823- (param.opts, param.secondary_opts)
824824- for param in func.__click_params__
825825- )
826826- if self._shortform_predicate(opt)
827827- }
828828- if first_letter in popts:
829829- try:
830830- first_index = lower.index(first_letter)
831831- except ValueError:
832832- first_index = upper.index(first_letter)
833833- i = first_index
834834- while first_letter in popts:
835835- first_letter = first_letter.swapcase()
836836- if first_letter in popts:
837837- i = (i + 1) % 26
838838- if i == first_index:
839839- raise click.UsageError(
840840- "no more lowercase or uppercase letters left for shortform names"
841841- )
842842- first_letter = lower[i]
843843- else:
844844- break
845845-846846- return f"-{first_letter}"
847847-848848- def _get_name(
849849- self, name: str, value: Parameter, func: Callable, shortform: bool = False
850850- ):
851851- if shortform:
852852- processor = self._get_shortform
853853- predicate = self._shortform_predicate
854854- else:
855855- processor = self._get_longform
856856- predicate = self._longform_predicate
857857-858858- default = processor(name, func)
859859- if get_origin(value.annotation) is Switch:
860860- default += f"/{processor(get_args(value.annotation)[0], func)}"
861861-862862- # Adapted From:
863863- # Answer: https://stackoverflow.com/a/9542768
864864- # User: https://stackoverflow.com/users/916657/niklas-b
865865- form = next(
866866- filter(predicate, getattr(func, "__bundle__", Dict()).params[name].args),
867867- default,
868868- )
869869- if "/" in form:
870870- return form.split("/")
871871- return [form]
872872-873873- def _get_allforms(self, func):
874874- return flatten(
875875- (param.name, param.human_readable_name, param.opts, param.secondary_opts)
876876- for param in getattr(func, "__click_params__", tuple())
877877- )
878878-879879- def _get_shortforms(self, func):
880880- return list(filter(self._shortform_predicate, self._get_allforms(func)))
881881-882882- def _get_longforms(self, func):
883883- return list(filter(self._longform_predicate, self._get_allforms(func)))
884884-885885- def _get_forms(self, func):
886886- return [
887887- param
888888- for param in self._get_allforms(func)
889889- if param.startswith("--") or not param.startswith("-")
890890- ]
891891-892892- def _trysubclass(self, annotation: Any, *types):
893893- try:
894894- return issubclass(annotation, types)
895895- except TypeError:
896896- return is_bearable(annotation, types)
897897-898898- def _callback(self, annotation, ctx, param, value, *, debug=False):
899899- if debug:
900900- print(annotation, ctx, param, value)
901901- return ctx, param, process_bundle(annotation, value)
902902-903903- def _make_callback(
904904- self,
905905- name: str,
906906- annotation: Any,
907907- func: Callable,
908908- precallback: Optional[Callable] = None,
909909- ):
910910- return pipe(
911911- partial(self._callback, annotation, debug=False),
912912- precallback,
913913- *(
914914- get_args(annotation)[1:]
915915- if get_origin(annotation) is Callback
916916- else tuple()
917917- ),
918918- getattr(func, "__bundle__", Dict()).params[name].kwargs.callback,
919919- lambda ctx, param, value: value,
920920- cls=PipeArgs,
921921- )
922922-923923- def _make_kwargs(
924924- self,
925925- name: str,
926926- annotation: Any,
927927- default: Any,
928928- func: Callable,
929929- kwargs: dict,
930930- ):
931931- kwargs = Dict(kwargs)
932932- if default is Parameter.empty:
933933- if name.isupper():
934934- kwargs.envvar = name
935935- else:
936936- kwargs.default = default
937937- return (
938938- kwargs
939939- | getattr(func, "__bundle__", Dict()).params[name].kwargs
940940- | {"callback": self._make_callback(name, annotation, func)}
941941- )
942942-943943- def _default_annotation(self, value: Parameter):
944944- default = value.default
945945- annotation = value.annotation
946946- if annotation is Parameter.empty and default is not Parameter.empty:
947947- annotation = type(default)
948948- elif get_origin(annotation) is Switch:
949949- annotation = bool
950950- default = False
951951- return default, annotation
952952-953953- # TODO: Implement and test
954954- def _get_funky(
955955- self,
956956- index: int,
957957- name: Literal["password"],
958958- value: Attr[Parameter.empty, "annotation"],
959959- func: Callable,
960960- ):
961961- return click.password_option
962962-963963- def _get_funky(
964964- self,
965965- index: int,
966966- name: str,
967967- value: Attr[type[Password], "annotation"],
968968- func: Callable,
969969- ):
970970- return click.option(
971971- *self._get_name(name, value, func),
972972- prompt=True,
973973- hide_input=True,
974974- confirmation_prompt=True,
975975- )
976976-977977- def _get_funky(self, index: Literal[0], name: Literal["ctx"], *args):
978978- return click.pass_context
979979-980980- def _get_funky(
981981- self,
982982- index: Literal[0],
983983- name: str,
984984- value: Attr[Origin[Pass], "annotation"],
985985- func: Callable,
986986- ):
987987- return click.make_pass_decorator(get_args(value.annotation)[0], ensure=True)
988988-989989- def _get_funky(
990990- self,
991991- index: Literal[0],
992992- name: str,
993993- value: Attr[type[Obj], "annotation"],
994994- func: Callable,
995995- ):
996996- return click.pass_obj
997997-998998- def _get_funky(
999999- self,
10001000- index: int,
10011001- name: str,
10021002- value: Attr[
10031003- Literal[
10041004- Parameter.POSITIONAL_ONLY,
10051005- Parameter.VAR_POSITIONAL,
10061006- ],
10071007- "kind",
10081008- ],
10091009- func: Callable,
10101010- ):
10111011- default, annotation = self._default_annotation(value)
10121012-10131013- # TODO: Should arguments be required by default?
10141014- # opts = Dict(required=default is Parameter.empty and value.kind is Parameter.POSITIONAL_ONLY)
10151015- opts = Dict()
10161016-10171017- args = get_args(annotation)
10181018- if self._trysubclass(annotation, Model):
10191019- opts.nargs = len(annotation.__annotations__)
10201020- elif args and self._trysubclass(get_origin(annotation), tuple):
10211021- opts.nargs = -1 if ... in args or EllipsisType in args else len(args)
10221022- else:
10231023- variable = value.kind is Parameter.VAR_POSITIONAL or self._trysubclass(
10241024- annotation, Coll
10251025- )
10261026- opts = Dict(nargs=-1 if variable else 1)
10271027- with suppress(ValueError):
10281028- sig = signature(annotation)
10291029- opts.nargs = (
10301030- -1
10311031- if variable or can_unpack_variable(sig)
10321032- else (len(pk_variables(sig)) or 1)
10331033- )
10341034-10351035- kwargs = self._make_kwargs(
10361036- name,
10371037- annotation,
10381038- default,
10391039- func,
10401040- reduce(
10411041- operator.or_,
10421042- (
10431043- self.__class__._defaults.argument,
10441044- opts,
10451045- self.__class__._globals.argument,
10461046- self.__class__._roots[self.root._func].argument,
10471047- self.__class__._instances[self._func].argument,
10481048- ),
10491049- ),
10501050- )
10511051- if kwargs.nargs != 1 and kwargs.envvar in self.__class__._pathvars:
10521052- kwargs.type = kwargs.type or click.Path()
10531053- return click.argument(
10541054- name,
10551055- **kwargs,
10561056- )
10571057-10581058- def _get_funky(
10591059- self,
10601060- index: int,
10611061- name: str,
10621062- value: Attr[
10631063- Literal[
10641064- Parameter.KEYWORD_ONLY,
10651065- Parameter.POSITIONAL_OR_KEYWORD,
10661066- ],
10671067- "kind",
10681068- ],
10691069- func: Callable,
10701070- ):
10711071- default, annotation = self._default_annotation(value)
10721072- origin = get_origin(annotation) or annotation
10731073- opts = Dict(
10741074- required=default is Parameter.empty
10751075- and value.kind is Parameter.KEYWORD_ONLY,
10761076- count=annotation is Count,
10771077- is_eager=origin is Eager,
10781078- help=self.superparams.help[name].description or None,
10791079- )
10801080-10811081- args = get_args(annotation)
10821082- if self._trysubclass(annotation, Model):
10831083- opts.nargs = len(annotation.__annotations__)
10841084- elif args and self._trysubclass(origin, tuple):
10851085- if ... in args or EllipsisType in args:
10861086- opts.multiple = True
10871087- else:
10881088- opts.nargs = len(args)
10891089- else:
10901090- opts.multiple = self._trysubclass(annotation, Coll)
10911091-10921092- if (
10931093- not self._trysubclass(annotation, bool)
10941094- and annotation is not Parameter.empty
10951095- ):
10961096- opts.metavar = annotation.__name__
10971097- with suppress(ValueError):
10981098- sig = signature(annotation)
10991099- opts.multiple = opts.multiple or can_unpack_variable(sig)
11001100- if not opts.multiple:
11011101- opts.nargs = len(pk_variables(sig)) or 1
11021102- with suppress(TypeError):
11031103- if issubclass(annotation, bool):
11041104- opts.is_flag = True
11051105- kwargs = self._make_kwargs(
11061106- name,
11071107- annotation,
11081108- default,
11091109- func,
11101110- reduce(
11111111- operator.or_,
11121112- (
11131113- self.__class__._defaults.option,
11141114- opts,
11151115- self.__class__._globals.option,
11161116- self.__class__._roots[self.root._func].option,
11171117- self.__class__._instances[self._func].option,
11181118- ),
11191119- ),
11201120- )
11211121- if kwargs.multiple and kwargs.envvar in self.__class__._pathvars:
11221122- kwargs.type = kwargs.type or click.Path()
11231123- return click.option(
11241124- name,
11251125- "/".join(self._get_name(name, value, func)),
11261126- **kwargs,
11271127- )
11281128-11291129-11301130-class Bundler(type, metaclass=plumeta):
11311131- def __new__(
11321132- cls, name: str, bases: tuple[Callable, ...], ns: dict | MappingProxyType
11331133- ):
11341134- result = super().__new__(cls, name, bases, ns)
11351135- for key, value in result.__classes__.items():
11361136- if isinstance(value, Bundler):
11371137- result._adopt(value, key)
11381138- else:
11391139- result.__bundles__[key] = subtype(key, result, value.__dict__)
11401140-11411141- # NOTE: This returns a `bundle`, not a `bundler`.
11421142- result._rewrap(getattr(result, "__bundle__", Dict()).group)
11431143- return result
11441144-11451145- def __getattr__(cls, attr):
11461146- return getattr(cls.__cls__, attr)
11471147-11481148- def __eq__(cls, other: Union["Bundler", "bundle", click.Group]) -> bool:
11491149- return cls.__cls__.__eq__(other)
11501150-11511151- def __eq__(self, other) -> bool:
11521152- return False
11531153-11541154- def __hash__(cls):
11551155- return cls.__cls__.__hash__()
11561156-11571157- class __prepare__(dict):
11581158- @property
11591159- def cls(self):
11601160- return self.get("__cls__")
11611161-11621162- @cls.setter
11631163- def cls(self, value):
11641164- super().__setitem__("__cls__", value)
11651165-11661166- def __init__(self, name, bases):
11671167- self.name = name
11681168- self.bases = bases
11691169- self.names = ("__cls__", self.name)
11701170- self.root = not bases or bases[0] is bundler
11711171- self.parent = bundle if self.root else bases[0].__cls__
11721172- self.cls = bundle(deflambda(self.name), self.parent, name=self.name)
11731173- self.bundles = Dict({self.name: self.cls})
11741174- super().__setitem__("__bundles__", self.bundles)
11751175- self.classes = Dict()
11761176- super().__setitem__("__classes__", self.classes)
11771177- self.dispatcher = Dispatcher()
11781178- super().__setitem__("__dispatcher__", self.dispatcher)
11791179- self.original_dict = Dict()
11801180- super().__setitem__("__original_dict__", self.original_dict)
11811181-11821182- def update(self, other: dict):
11831183- for k, v in other.items():
11841184- self[k] = v
11851185- return self
11861186-11871187- def __ior__(self, other: dict):
11881188- return self.update(other)
11891189-11901190- def __or__(self, other: dict):
11911191- return deepcopy(self).update(other)
11921192-11931193- def __ror__(self, other: dict):
11941194- return (
11951195- self.__class__(self.name, self.bases)
11961196- .update(other)
11971197- .update(self.original_dict)
11981198- )
11991199-12001200- def __setitem__(self, key, value):
12011201- self.original_dict[key] = value
12021202- if callable(value):
12031203- if key.startswith("__") and key.endswith("__"):
12041204- args, kwargs = getattr(value, "__plume__", (tuple(), {}))
12051205- if not kwargs.get("disabled", False):
12061206- value = getattr(self.get(key), "dispatch", self.dispatcher)(
12071207- value, *args, **kwargs
12081208- )
12091209- elif isclass(value):
12101210- return self.classes.__setitem__(key, value)
12111211- else:
12121212- kwargs = Dict(getattr(value, "__bundle__", {})).group
12131213- if key == self.name:
12141214- if isinstance(value, (bundle, bundler)):
12151215- if not self.root:
12161216- self.parent._adopt(value, key)
12171217- else:
12181218- name = kwargs.get("name", self.name)
12191219-12201220- # Adapted From:
12211221- # Answer: https://stackoverflow.com/a/3655857
12221222- # User: https://stackoverflow.com/users/95810
12231223- if value.__name__ == (lambda: None).__name__:
12241224- for attr in names:
12251225- setattr(value, attr, name)
12261226-12271227- value = bundle(value, self.parent, name=name)
12281228- for k, v in self.bundles.items():
12291229- if not (isclass(v) or k in self.names):
12301230- value._adopt(v, k)
12311231- self.cls = value
12321232- elif isinstance(value, (bundle, bundler)):
12331233- self.cls._adopt(value, key)
12341234- else:
12351235- name = kwargs.get("name", key)
12361236-12371237- # Adapted From:
12381238- # Answer: https://stackoverflow.com/a/3655857
12391239- # User: https://stackoverflow.com/users/95810
12401240- if value.__name__ == (lambda: None).__name__:
12411241- for attr in names:
12421242- setattr(value, attr, name)
12431243-12441244- value = bundle(value, self.cls, name=name)
12451245- return self.bundles.__setitem__(key, value)
12461246- return super().__setitem__(key, value)
12471247-12481248-12491249-class bundler(metaclass=Bundler):
12501250- # TODO: There may be a few problems with the adoption...
12511251- def __new__(
12521252- cls,
12531253- name: str,
12541254- bases: tuple["bundler", ...],
12551255- ns: dict | MappingProxyType,
12561256- **kwargs,
12571257- ):
12581258- self = subtype(name, cls, ns)
12591259- self._rewrap(bases[0].__kwargs__)
12601260- try:
12611261- bases[0]._adopt(self)
12621262- except AttributeError:
12631263- with suppress(AttributeError, TypeError):
12641264- bases[0].parent._adopt(self)
12651265- return self
12661266-12671267- def __new__(
12681268- cls,
12691269- self: "Bundler",
12701270- parent: Optional[Union["Bundler", "bundle"]] = None,
12711271- **kwargs,
12721272- ):
12731273- self._rewrap(kwargs)
12741274- if parent:
12751275- parent._adopt(self)
12761276- return self
12771277-12781278- def __new__(
12791279- cls,
12801280- self: type,
12811281- parent: Optional[Union["Bundler", "bundle"]] = None,
12821282- **kwargs,
12831283- ):
12841284- return cls.__new__(cls, subtype(self, cls), parent, **kwargs)
12851285-12861286- def __new__(
12871287- cls,
12881288- self: Callable,
12891289- parent: Optional[Union["Bundler", "bundle"]] = None,
12901290- **kwargs,
12911291- ):
12921292- return cls.__new__(
12931293- cls, subtype(self.__name__, cls, {self.__name__: self}), parent, **kwargs
12941294- )
12951295-12961296- def __new__(cls, *args, **kwargs):
12971297- if cls._run(args, kwargs) and cls is not bundler:
12981298- return beartype(cls._func)(*args, **kwargs)
12991299- return super().__new__(cls)
13001300-13011301- class __prepare__(dict):
13021302- def __init__(self, name, bases):
13031303- pass
13041304-13051305- def __init__(self, **kwargs):
13061306- self.__kwargs__ = Dict(kwargs)
13071307-13081308- def __getattr__(self, attr):
13091309- return getattr(self.__cls__, attr)
13101310-13111311- def __call__(self, cls: "Bundler"):
13121312- cls._rewrap(self.__kwargs__)
13131313- return cls
13141314-13151315- def __call__(self, cls: type):
13161316- return self(subtype(cls, self.__class__))
13171317-13181318- def __call__(self, func: Callable):
13191319- return self(subtype(func.__name__, self.__class__, {func.__name__: func}))
11+from .functions import *
22+from .bundle import bundle, bundler, Bundle
33+from .types import *
+1030
packages/bundle/bundle/bundle.py
···11+import rich_click as click
22+from click.testing import CliRunner
33+from addict import Dict
44+from autoslot import SlotsMeta
55+from hydrox.helpers import (
66+ as_decorator,
77+ query,
88+ flatten,
99+ _normalize as normalize,
1010+ Collection,
1111+)
1212+from hydrox.typing import (
1313+ anything,
1414+ isinmrosubclass,
1515+ partition_ellipsis,
1616+ format_name,
1717+ reducehash,
1818+ gformat_name,
1919+ generalize,
2020+ get_name,
2121+)
2222+import hydrox.typing as ht
2323+from jugulis import funcdict, Hydreigon, Deino, iscallableclass
2424+import re
2525+from functools import partial, cached_property, reduce, cache
2626+from inspect import Signature, Parameter, isclass
2727+import collections.abc as abc
2828+from string import ascii_lowercase as lower, ascii_uppercase as upper
2929+import typing
3030+import os
3131+import sys
3232+from collections import defaultdict, ChainMap
3333+from docstring_parser import Docstring
3434+from .functions import reducedict, eq, rich_repr
3535+from .types import Pass, Password
3636+from random import choice
3737+from rich.console import Console
3838+3939+# TODO: Should `__qualname__` be checked as well if it has no `.`?
4040+4141+4242+class Bundles(dict):
4343+ def __getattr__(self, attr):
4444+ return self[attr]
4545+4646+4747+class bundler(Bundles):
4848+ def __init__(self, start, stop, *types, name="f", shared_name="g"):
4949+ self.start = start
5050+ self.stop = stop
5151+ self.rstop = self.stop + 1
5252+ self.types = types or (int, str)
5353+ self.name = name
5454+ self.shared_name = shared_name
5555+ self.under_share = self.shared_name + "_"
5656+ self.dunder_share = self.shared_name + "__"
5757+ super().__init__()
5858+ tn = get_name(self.types[0])
5959+ exec(
6060+ f"@bundle\ndef {self.name}({tn[0]}: {tn}) -> {tn}: ...",
6161+ locals=ChainMap(self, {"bundle": bundle}),
6262+ )
6363+ exec(
6464+ f"@{self.name}.up\n@bundle\ndef {self.under_share}({tn[0]}: {tn}) -> {tn}: ...",
6565+ locals=ChainMap(self, {"bundle": bundle}),
6666+ )
6767+ exec(
6868+ f"@{self.name}.up\n@bundle\ndef {self.dunder_share}({tn[0]}: {tn}) -> {tn}: ...",
6969+ locals=ChainMap(self, {"bundle": bundle}),
7070+ )
7171+ for t in self.types[1:]:
7272+ tn = get_name(t)
7373+ exec(
7474+ f"@{self.name}.up\ndef {self.name}({tn[0]}: {tn}) -> {tn}: ...",
7575+ locals=self,
7676+ )
7777+ exec(
7878+ f"@{self.name}.up\n@{self.under_share}.up\ndef {self.under_share}({tn[0]}: {tn}) -> {tn}: ...",
7979+ locals=ChainMap(self, {"bundle": bundle}),
8080+ )
8181+ exec(
8282+ f"@{self.name}.up\n@{self.dunder_share}.up\ndef {self.dunder_share}({tn[0]}: {tn}) -> {tn}: ...",
8383+ locals=ChainMap(self, {"bundle": bundle}),
8484+ )
8585+ self.root = self[self.name]
8686+ self.init(self.name)
8787+8888+ def __eq__(self, other):
8989+ if isinstance(other, self.__class__):
9090+ return (
9191+ (self.start == other.start)
9292+ and (self.stop == other.stop)
9393+ and (self.types == other.types)
9494+ and (self.name == other.name)
9595+ and (self.shared_name == other.shared_name)
9696+ )
9797+ return NotImplemented
9898+9999+ def __hash__(self):
100100+ return reducehash(
101101+ self.__class__.__name__,
102102+ self.start,
103103+ self.stop,
104104+ self.types,
105105+ self.name,
106106+ self.shared_name,
107107+ )
108108+109109+ def init(self, name=None, levels=None):
110110+ name = self.name if name is None else name
111111+ levels = self.start if levels is None else levels
112112+ names = []
113113+ for i in range(self.start, self.rstop):
114114+ n = name + str(i)
115115+ names.append(n)
116116+ for t in self.types:
117117+ tn = get_name(t)
118118+ exec(f"@{name}.up\ndef {n}({tn[0]}: {tn}) -> {tn}: ...", locals=self)
119119+ if levels < self.stop:
120120+ self.init(n, levels=levels + 1)
121121+ for t in self.types:
122122+ tn = get_name(t)
123123+ exec(
124124+ "@"
125125+ + ".up\n@".join(names)
126126+ + f".up\ndef {name.replace(self.name, self.shared_name)}({tn[0]}: {tn}) -> {tn}: ...",
127127+ locals=self,
128128+ )
129129+130130+ @cached_property
131131+ def functions(self):
132132+ return Bundles(
133133+ reduce(
134134+ dict.__or__,
135135+ (
136136+ {
137137+ k + get_name(d.__signature__.return_annotation)[0]: d.__func__
138138+ for d in v.func
139139+ }
140140+ for k, v in self.items()
141141+ ),
142142+ )
143143+ )
144144+145145+ @cached_property
146146+ def function_lists(self):
147147+ return Bundles(
148148+ reduce(
149149+ dict.__or__,
150150+ ({k: [d.__func__ for d in v.func]} for k, v in self.items()),
151151+ )
152152+ )
153153+154154+ @cache
155155+ def type(self, *types):
156156+ bundles = Bundles(
157157+ {
158158+ k: bundle(*[self.functions[k + get_name(t)[0]] for t in types])
159159+ for k in self
160160+ }
161161+ )
162162+ bundles[self.name].up(bundles[self.under_share])
163163+ bundles[self.name].up(bundles[self.dunder_share])
164164+ for k, v in bundles.items():
165165+ if k.startswith(self.name) and k != self.name:
166166+ bundles[k[0:-1]].up(v)
167167+ v.up(bundles[k.replace(self.name, self.shared_name)[0:-1]])
168168+ return bundles
169169+170170+ @cache
171171+ def cls(self, name=None):
172172+ name_was_none = name is None
173173+ bundles = Bundles() if name_was_none else {}
174174+ name = self.name if name_was_none else name
175175+ bases = (bundle,)
176176+ names = (name, "__init__")
177177+ namespace = Bundle.__prepare__(name, bases)
178178+ for f in self.function_lists[name]:
179179+ namespace[choice(names)] = f
180180+ ns = []
181181+ for i in range(self.start, self.rstop):
182182+ n = name + str(i)
183183+ if n in self:
184184+ ns.append(n)
185185+ cls = self.cls(name=n)
186186+ bundles.update(cls)
187187+ namespace[n] = cls[n]
188188+ bundles[name] = type(name, bases, namespace)
189189+ if name_was_none:
190190+ under_names = (self.under_share, "__init__")
191191+ under_namespace = f"\n\t{choice(under_names)} = ".join(
192192+ (self.under_share + get_name(t)[0]) for t in self.types
193193+ )
194194+ exec(
195195+ f"class {self.under_share}({self.name}):\n\t{choice(under_names)} = {under_namespace}",
196196+ globals=self.functions,
197197+ locals=bundles,
198198+ )
199199+ dunder_names = (self.dunder_share, "__init__")
200200+ dunder_namespace = f"\n\t{choice(dunder_names)} = ".join(
201201+ (self.dunder_share + get_name(t)[0]) for t in self.types
202202+ )
203203+ exec(
204204+ f"class {self.dunder_share}({self.name}):\n\t{choice(dunder_names)} = {dunder_namespace}",
205205+ globals=self.functions,
206206+ locals=bundles,
207207+ )
208208+ if ns:
209209+ shared_n = name.replace(self.name, self.shared_name)
210210+ shared_names = (shared_n, "__init__")
211211+ shared_namespace = f"\n\t{choice(shared_names)} = ".join(
212212+ (shared_n + get_name(t)[0]) for t in self.types
213213+ )
214214+ exec(
215215+ f"class {shared_n}({', '.join(ns)}):\n\t{choice(shared_names)} = {shared_namespace}",
216216+ globals=self.functions,
217217+ locals=bundles,
218218+ )
219219+ return bundles
220220+221221+222222+class Bundle(SlotsMeta):
223223+ class __prepare__(dict):
224224+ def __init__(self, _name, _bases, *args, _subclass=False, **click_kwargs):
225225+ self.name = _name
226226+ self.bases = _bases
227227+ self.subclass = _subclass
228228+ self.click_kwargs = click_kwargs
229229+ if not _subclass:
230230+ self.bundle = bundled = bundle(**({"name": _name} | click_kwargs))
231231+ self.name = name = bundled.__name__
232232+ self.names = (_name, name)
233233+ for arg in args:
234234+ for k, v in dict(arg).items():
235235+ self[k] = v
236236+237237+ def __setitem__(self, key, value):
238238+ if not (self.subclass or isinstance(value, bundle)) and iscallableclass(
239239+ value
240240+ ):
241241+ if key in self.names:
242242+ key = "__init__"
243243+ value = self.bundle.append(
244244+ value, name=self.name if key == "__init__" else key
245245+ )
246246+ super().__setitem__(key, value)
247247+248248+ def update(self, other):
249249+ for k, v in dict(other).items():
250250+ self[k] = v
251251+252252+ def __ior__(self, other):
253253+ self.update(other)
254254+255255+ def copy(self):
256256+ result = self.__class__(
257257+ self.name, self.bases, self.subclass, **self.click_kwargs
258258+ )
259259+ result.update(self)
260260+ return result
261261+262262+ def __or__(self, other):
263263+ result = self.copy()
264264+ result.update(other)
265265+ return result
266266+267267+ # TODO: Use this `__ror__` method with the other dictionary subclasses.
268268+ def __ror__(self, other):
269269+ if isinstance(other, self.__class__):
270270+ result = other.copy()
271271+ result.update(self)
272272+ else:
273273+ result = self.copy()
274274+ result.update(other)
275275+ return result
276276+277277+ def __new__(_cls, _name, _bases, _namespace, _subclass=False, **click_kwargs):
278278+ if _subclass:
279279+ return super().__new__(_cls, _name, _bases, _namespace)
280280+ return _bases[0](_namespace, **({"name": _name} | click_kwargs))
281281+282282+ defaults = Dict(
283283+ {
284284+ "argument": {},
285285+ "option": {"metavar": gformat_name(str), "show_default": True},
286286+ "group": {
287287+ "invoke_without_command": True,
288288+ "no_args_is_help": True,
289289+ "context_settings": {
290290+ "help_option_names": ("-h", "--help"),
291291+ "ignore_unknown_options": True,
292292+ "allow_extra_args": True,
293293+ },
294294+ },
295295+ }
296296+ )
297297+ runner = CliRunner()
298298+299299+ def __subclasscheck__(self, subclass):
300300+ return isinmrosubclass(subclass, click.Group) or super().__subclasscheck__(
301301+ subclass
302302+ )
303303+304304+ @staticmethod
305305+ def shortform_predicate(name, arg):
306306+ return name != arg and len(arg) == 2 and re.match("^[A-Za-z_]+$", arg[-1])
307307+308308+ @staticmethod
309309+ def get_longform(name: str):
310310+ return f"--{name.strip('_').lower().replace('_', '-')}"
311311+312312+ @staticmethod
313313+ def process_value(value, annotation):
314314+ try:
315315+ return value if anything(annotation, str, tuple) else annotation(value)
316316+ except Exception:
317317+ return value
318318+319319+ def get_shortform(cls, name: str, params: list[click.Parameter]):
320320+ first_letter = name.strip("_")[0]
321321+322322+ popts = {
323323+ opt[1]
324324+ for opt in flatten((param.opts, param.secondary_opts) for param in params)
325325+ if cls.shortform_predicate(name, opt)
326326+ }
327327+ if first_letter in popts:
328328+ try:
329329+ first_index = lower.index(first_letter)
330330+ except ValueError:
331331+ first_index = upper.index(first_letter)
332332+ i = first_index
333333+ while first_letter in popts:
334334+ first_letter = first_letter.swapcase()
335335+ if first_letter in popts:
336336+ i = (i + 1) % 26
337337+ if i == first_index:
338338+ raise click.UsageError(
339339+ "no more lowercase or uppercase letters left for shortform names"
340340+ )
341341+ first_letter = lower[i]
342342+ else:
343343+ break
344344+345345+ return f"-{first_letter}"
346346+347347+ @staticmethod
348348+ def up(*args, **kwargs):
349349+ def wrapper(func):
350350+ nonlocal args
351351+ args = list(args)
352352+ for arg in args:
353353+ if re.match("^[A-Za-z_]+$", arg):
354354+ name = arg
355355+ break
356356+ else:
357357+ raise click.UsageError("bundle.up must get a name, not just options")
358358+ if hasattr(func, "__bundled_args__"):
359359+ func.__bundled_args__[name] = args
360360+ else:
361361+ func.__bundled_args__ = {name: args}
362362+ if hasattr(func, "__bundled_kwargs__"):
363363+ func.__bundled_kwargs__[name] |= kwargs
364364+ else:
365365+ func.__bundled_kwargs__ = Dict({name: kwargs})
366366+ return func
367367+368368+ return wrapper
369369+370370+ @staticmethod
371371+ def process_clicker(kind):
372372+ return (
373373+ "argument"
374374+ if kind in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL)
375375+ else "option"
376376+ )
377377+378378+ def process_callback(cls, ctx, param, value, *, annotation, is_option, callback):
379379+ # TODO
380380+ # if issubclass(typing.get_origin(annotation), ht.tuple):
381381+ if isinmrosubclass(typing.get_origin(annotation), tuple):
382382+ args = typing.get_args(annotation)
383383+ if ... in args:
384384+ pre_args, post_args = partition_ellipsis(args)
385385+ pre_length, post_length = len(pre_args), len(post_args)
386386+ if len(value) < (pre_length + post_length):
387387+ if is_option:
388388+ raise click.UsageError(
389389+ f"Option '{next(iter(set(param.opts) & set(sys.argv)))}' requires at least {(pre_length or 1) + post_length} arguments."
390390+ )
391391+ else:
392392+ raise click.UsageError(
393393+ f"Argument '{param.name}' takes at least {(pre_length or 1) + post_length} values."
394394+ )
395395+396396+ value = cls.process_value(value, annotation)
397397+ return callback(ctx, param, value) if callback else value
398398+399399+ def process_kwargs(cls, name, param: Parameter, bundled_kwargs, docstring):
400400+ annotation = param.annotation
401401+ kind = param.kind
402402+ kwargs = {}
403403+ default = param.default
404404+ if default is Parameter.empty:
405405+ if name.isupper():
406406+ kwargs["envvar"] = name
407407+ else:
408408+ kwargs["default"] = default
409409+ if flatten.set((kwargs | bundled_kwargs).envvar) & {
410410+ "PATH",
411411+ "PATHS",
412412+ }:
413413+ kwargs["type"] = click.Path()
414414+ bundled_callback = bundled_kwargs.pop("callback", None)
415415+ if kind is Parameter.POSITIONAL_ONLY:
416416+ kwargs["nargs"] = -1 if issubclass(annotation, ht.Coll) else 1
417417+ defaults = bundle.defaults["argument"]
418418+ elif kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
419419+ if kind is Parameter.KEYWORD_ONLY:
420420+ kwargs["required"] = param.default is Parameter.empty
421421+ kwargs["multiple"] = issubclass(annotation, ht.Coll)
422422+ defaults = bundle.defaults["option"]
423423+ elif kind is Parameter.VAR_POSITIONAL:
424424+ kwargs["nargs"] = -1
425425+ defaults = bundle.defaults["argument"]
426426+ elif kind is Parameter.VAR_KEYWORD:
427427+ kwargs["multiple"] = True
428428+ defaults = bundle.defaults["option"]
429429+ if is_option := cls.process_clicker(kind) == "option":
430430+ kwargs["metavar"] = docstring.type_name
431431+432432+ kwargs["callback"] = partial(
433433+ cls.process_callback,
434434+ annotation=annotation,
435435+ is_option=is_option,
436436+ callback=bundled_callback,
437437+ )
438438+439439+ origin = typing.get_origin(annotation)
440440+ args = typing.get_args(annotation)
441441+ # TODO: Test this entire section.
442442+443443+ # TODO:
444444+ # if issubclass(annotation, ht.Literal):
445445+ if origin == ht.Literal:
446446+ if is_option:
447447+ kwargs["type"] = click.Choice(args)
448448+ else:
449449+450450+ def callback(ctx, param: click.Option | click.Argument, value):
451451+ if value not in args:
452452+ option = next(iter(set(sys.argv) & set(param.opts)))
453453+ raise click.UsageError(
454454+ f"""Invalid value for '{option}': '{value}' is not one of '{"', '".join(args)}'."""
455455+ )
456456+ if bundled_callback:
457457+ return bundled_callback(ctx, param, value)
458458+ return value
459459+460460+ kwargs["callback"].keywords["callback"] = callback
461461+ kwargs["metavar"] = kwargs["metavar"] or f"[{'|'.join(args)}]"
462462+463463+ if is_option:
464464+ kwargs["metavar"] = kwargs["metavar"] or format_name(annotation)
465465+466466+ # TODO
467467+ # if issubclass(annotation, ht.bool):
468468+ if isinmrosubclass(annotation, bool):
469469+ if is_option:
470470+ kwargs["is_flag"] = True
471471+ else:
472472+ kwargs["callback"] = lambda ctx, param, value: value in (
473473+ "True",
474474+ "1",
475475+ )
476476+477477+ # TODO
478478+ # if issubclass(typing.get_origin(annotation), ht.tuple):
479479+ if isinmrosubclass(origin, tuple):
480480+ if ... in args:
481481+ if is_option:
482482+ kwargs["multiple"] = True
483483+ else:
484484+ kwargs["nargs"] = -1
485485+ else:
486486+ kwargs["multiple"] = False
487487+ kwargs["nargs"] = len(args)
488488+ return (
489489+ defaults
490490+ | kwargs
491491+ | {k: v for k, v in bundled_kwargs.items() if k not in ("disabled",)}
492492+ )
493493+494494+ @staticmethod
495495+ def process_param(param: Parameter):
496496+ any_annotation = anything(param.annotation)
497497+ if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
498498+ return (
499499+ param.replace(annotation=generalize(tuple)) if any_annotation else param
500500+ )
501501+ elif any_annotation:
502502+ return param.replace(annotation=generalize(str))
503503+ else:
504504+ return param
505505+506506+ # TODO: Test these methods.
507507+ def __getattr__(cls, attr):
508508+ if attr in ("argument", "option"):
509509+510510+ def wrapper(*args, **kwargs):
511511+ def wrapped(func):
512512+ if not kwargs.pop("disabled"):
513513+ deino: Deino
514514+ if isinstance(func, Hydreigon):
515515+ deino = func[0]
516516+ else:
517517+ if not isinstance(func, Deino):
518518+ func = Deino(
519519+ func,
520520+ default_va_annotation=typing.Any,
521521+ default_vk_annotation=typing.Any,
522522+ )
523523+ deino = func
524524+ param = getattr(click, attr.capitalize())(args)
525525+ name = param.name
526526+ for k, v in cls.process_kwargs(
527527+ name,
528528+ deino.__signature__.parameters[name],
529529+ Dict(kwargs),
530530+ deino.__docstring__,
531531+ ).items():
532532+ setattr(param, k, v)
533533+ if hasattr(func, "__click_params__"):
534534+ func.__click_params__.append(param)
535535+ else:
536536+ func.__click_params__ = [param]
537537+ return func
538538+539539+ return wrapped
540540+541541+ else:
542542+543543+ def wrapper(*args, **kwargs):
544544+ def wrapped(func):
545545+ return getattr(click, attr)(
546546+ *args, **(cls.defaults[attr] | Dict(kwargs))
547547+ )(func)
548548+549549+ return wrapped
550550+551551+ return wrapper
552552+553553+554554+# TODO: Plan:
555555+# * If the first function is a `Collection` or a dictionary and there are no more functions,
556556+# the bundle is rootless; otherwise, flatten collections and use dictionary keys as names for their values.
557557+558558+559559+class bundle(funcdict, metaclass=Bundle, _subclass=True):
560560+ # TODO: Use the same trick with `Jugulis`, `Hydreigon`, and `Deino`.
561561+ def __new__(_cls, *funcs, **click_kwargs):
562562+ if as_decorator(query=query):
563563+564564+ def wrapper(func):
565565+ return _cls(func, *funcs, **click_kwargs)
566566+567567+ return wrapper
568568+569569+ if funcs and isinstance(funcs[0], str):
570570+ kwargs = {"name": funcs[0]} | click_kwargs
571571+ func, *bases = funcs[1]
572572+ result = func.append(funcs[2], **kwargs)
573573+ for base in bases:
574574+ base.append(result, **kwargs)
575575+ return result
576576+577577+ return super().__new__(_cls, *funcs, **click_kwargs)
578578+579579+ @property
580580+ def __name__(self):
581581+ return self.__click_kwargs__.get("name", self.func.__name__)
582582+583583+ @__name__.setter
584584+ def __name__(self, value):
585585+ self.__click_kwargs__["name"] = self.func.__name__ = value
586586+587587+ def __call__(self, *args, **kwargs):
588588+ return self.func(*args, **kwargs)
589589+590590+ def __hash__(self):
591591+ return reducehash(
592592+ self.__class__.__name__,
593593+ self.__name__,
594594+ self.func,
595595+ reducedict(self.__click_kwargs__),
596596+ reducedict(self),
597597+ )
598598+599599+ def __eq__(self, other):
600600+ if isinstance(other, self.__class__):
601601+ if not (
602602+ # (self.__name__ == other.__name__)
603603+ # and (self.func == other.func)
604604+ (self.func == other.func)
605605+ and (self.__click_kwargs__ == other.__click_kwargs__)
606606+ and super().__eq__(other)
607607+ ):
608608+ return NotImplemented
609609+ other = other.compile()
610610+ if isinstance(other, click.Group):
611611+ return eq(
612612+ self.compile(),
613613+ other,
614614+ "PYTEST_CURRENT_TEST" in os.environ,
615615+ NotImplemented,
616616+ )
617617+ return NotImplemented
618618+619619+ def dunder_check(self, attr):
620620+ return not attr.startswith("__") or attr in ("__init__",)
621621+622622+ def process_click_kwargs(self, func, click_kwargs):
623623+ return Dict(
624624+ {"name": getattr(func, "__name__", "")}
625625+ | getattr(func, "__click_kwargs__", {})
626626+ ) | Dict(click_kwargs)
627627+628628+ def process_func(self, func, click_kwargs, named_kwargs):
629629+ fclick_kwargs = self.process_click_kwargs(func, click_kwargs)
630630+ if not fclick_kwargs.name:
631631+ fclick_kwargs |= named_kwargs
632632+ fname = fclick_kwargs["name"]
633633+ if not self.__name__ and fname:
634634+ self.__name__ = fname
635635+ return fclick_kwargs
636636+637637+ # TODO: Simplify and combine the various functions.
638638+ def pre_process_extend(self, func=None, *funcs, **click_kwargs):
639639+ named_kwargs = {}
640640+ # TODO: If I have to do this anyway, just put all this in `append`.
641641+ if not self.__name__ and "name" in click_kwargs:
642642+ self.__name__ = named_kwargs["name"] = getattr(
643643+ click_kwargs, "pop" if funcs else "get"
644644+ )("name")
645645+ fclick_kwargs_list = []
646646+ new_funcs = []
647647+ for f in (func, *funcs):
648648+ if isclass(f):
649649+ if funcs:
650650+ for k in dir(f):
651651+ if self.dunder_check(k):
652652+ v = getattr(f, k)
653653+ fclick_kwargs_list.append(
654654+ self.process_func(
655655+ v,
656656+ click_kwargs
657657+ | {"name": f.__name__ if k == "__init__" else k},
658658+ named_kwargs,
659659+ )
660660+ )
661661+ new_funcs.append(v)
662662+ else:
663663+ fclick_kwargs_list.append(
664664+ self.process_func(f, click_kwargs, named_kwargs)
665665+ )
666666+ new_funcs.append(f)
667667+ else:
668668+ match f:
669669+ case bundle():
670670+ fclick_kwargs_list.append(
671671+ self.process_func(f, click_kwargs, named_kwargs)
672672+ )
673673+ new_funcs.append(f)
674674+ case dict():
675675+ if funcs:
676676+ for k, v in f.items():
677677+ if self.dunder_check(k):
678678+ fclick_kwargs_list.append(
679679+ self.process_func(
680680+ v, click_kwargs | {"name": k}, named_kwargs
681681+ )
682682+ )
683683+ new_funcs.append(v)
684684+ else:
685685+ fclick_kwargs_list.append(
686686+ self.process_func(f, click_kwargs, named_kwargs)
687687+ )
688688+ new_funcs.append(f)
689689+ case Hydreigon():
690690+ ...
691691+ case Collection():
692692+ if funcs:
693693+ for item in f:
694694+ fclick_kwargs_list.append(
695695+ self.process_func(item, click_kwargs, named_kwargs)
696696+ )
697697+ new_funcs.append(item)
698698+ else:
699699+ fclick_kwargs_list.append(
700700+ self.process_func(f, click_kwargs, named_kwargs)
701701+ )
702702+ new_funcs.append(f)
703703+ case _:
704704+ fclick_kwargs_list.append(
705705+ self.process_func(f, click_kwargs, named_kwargs)
706706+ )
707707+ new_funcs.append(f)
708708+ func, *funcs = new_funcs
709709+ result = self.append(func, processed_click_kwargs=fclick_kwargs_list[0])
710710+ for f, kwargs in zip(funcs, fclick_kwargs_list[1:]):
711711+ result.append(f, processed_click_kwargs=kwargs)
712712+ return result
713713+714714+ def up(self, *funcs, **click_kwargs):
715715+ if isinstance(self, bundle):
716716+ if as_decorator(query=query):
717717+718718+ def wrapper(func):
719719+ return self.pre_process_extend(func, *funcs, **click_kwargs)
720720+721721+ return wrapper
722722+723723+ return self.pre_process_extend(*funcs, **click_kwargs)
724724+ return Bundle.up(self, *funcs, **click_kwargs)
725725+726726+ extend = up
727727+ roar = up
728728+729729+ def __init__(self, func=None, *funcs, **click_kwargs):
730730+ if not isinstance(func, str):
731731+ super().__init__()
732732+ self.func = Hydreigon(
733733+ default_va_annotation=typing.Any, default_vk_annotation=typing.Any
734734+ )
735735+ self.__click_kwargs__ = Dict(click_kwargs)
736736+ self.up(func, *funcs, **click_kwargs)
737737+738738+ def process_append(self, func, fname, child):
739739+ if child:
740740+ if fname in self:
741741+ result = self[fname]
742742+ exists = True
743743+ else:
744744+ result = self
745745+ exists = False
746746+ else:
747747+ result = self
748748+ exists = True
749749+ if isclass(func):
750750+ if not exists:
751751+ self[fname] = result = bundle(name=fname)
752752+ for name in dir(func):
753753+ if self.dunder_check(name):
754754+ result.append(
755755+ getattr(func, name), name=fname if name == "__init__" else name
756756+ )
757757+ else:
758758+ match func:
759759+ case dict():
760760+ if not exists:
761761+ self[fname] = result = bundle(name=fname)
762762+ for name, attr in func.items():
763763+ if self.dunder_check(name):
764764+ result.append(attr, name=name)
765765+ case Hydreigon():
766766+ if exists:
767767+ result.func.extend(func)
768768+ else:
769769+ self[fname] = result = bundle(name=fname)
770770+ result.func = func
771771+772772+ # TODO
773773+ # case ht.Coll():
774774+ # case abc.Iterable():
775775+ case Collection():
776776+ if not exists:
777777+ self[fname] = result = bundle(name=fname)
778778+ for f in func:
779779+ if isclass(f):
780780+ for name in dir(f):
781781+ if self.dunder_check(name):
782782+ result.append(
783783+ getattr(f, name),
784784+ name=f.__name__ if name == "__init__" else name,
785785+ )
786786+ else:
787787+ match f:
788788+ case bundle():
789789+ result.append(f)
790790+ case dict():
791791+ for name, attr in f.items():
792792+ if self.dunder_check(name):
793793+ result.append(attr, name=name)
794794+ # case Hydreigon():
795795+ # ...
796796+ # case Collection():
797797+ # ...
798798+ case _:
799799+ result.append(f)
800800+801801+ case _:
802802+ if exists:
803803+ result.func.append(func)
804804+ else:
805805+ self[fname] = result = bundle(func, name=fname)
806806+807807+ return result
808808+809809+ def append(self, func=None, /, processed_click_kwargs=None, **click_kwargs):
810810+ click_kwargs = (
811811+ self.process_click_kwargs(func, click_kwargs)
812812+ if processed_click_kwargs is None
813813+ else processed_click_kwargs
814814+ )
815815+ if not (fname := click_kwargs.get("name")):
816816+ click_kwargs["name"] = fname = getattr(func, "__name__", "")
817817+ if fname in (self.__name__, "__init__"):
818818+ self.__click_kwargs__ |= click_kwargs
819819+ if func is not None:
820820+ if not isinstance(func, bundle):
821821+ return self.process_append(func, fname, False)
822822+823823+ # NOTE: At any given moment, there can be only one instance of `self`,
824824+ # i.e., only one version of this command.
825825+ # TODO: Not necessarily, as the result can be assigned to a variable with another name.
826826+ # Therefore, find a way to update the old version of func as well.
827827+ # TODO: ... Wouldn't `func` be updated anyway, as `self = func`?
828828+ self |= func
829829+830830+ return self
831831+832832+ if fname:
833833+ if isinstance(func, bundle):
834834+ # NOTE: At any given moment, there can be only one instance of `fname`,
835835+ # i.e., only one version of this command.
836836+ # TODO: Not necessarily, as the result can be assigned to a variable with another name.
837837+ # Therefore, find a way to update the old version of func as well.
838838+ # TODO: ... Wouldn't `func` be updated anyway `if fname in self`, as `self[fname] = func`?
839839+ if fname in self:
840840+ self[fname] |= func
841841+ else:
842842+ self[fname] = func
843843+ return self[fname]
844844+845845+ if func is None:
846846+ if fname in self:
847847+ return self[fname]
848848+ self[fname] = result = bundle(**click_kwargs)
849849+ return result
850850+ return self.process_append(func, fname, True)
851851+852852+ def __rich_repr__(self):
853853+ yield self.__name__
854854+ yield self.func
855855+ yield dict(self)
856856+857857+ def update(self, other):
858858+ if isinstance(other, self.__class__):
859859+ self.func += other.func
860860+ self.__click_kwargs__ |= other.__click_kwargs__
861861+ # TODO: Should the values of other be converted to bundles,
862862+ # like in `funcdict`?
863863+ return super().update(other)
864864+865865+ # TODO: This will make the result a rootless application.
866866+ # Do I want that?
867867+ def copy(self):
868868+ result = self.__class__(self)
869869+ result.func = self.func.copy()
870870+ return result
871871+872872+ def __ror__(self, other):
873873+ if isinstance(other, self.__class__):
874874+ result = other.copy()
875875+ result.update(self)
876876+ else:
877877+ result = self.copy()
878878+ result.update(other)
879879+ return result
880880+881881+ def process_names(self, name, kind, bundled_args, click_params):
882882+ args = bundled_args.copy()
883883+ if not args:
884884+ args.append(name)
885885+ if kind not in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL):
886886+ if not any(
887887+ True for arg in args if self.__class__.shortform_predicate(name, arg)
888888+ ):
889889+ args.append(self.__class__.get_shortform(name, click_params))
890890+ if len(args) < 3:
891891+ args.append(self.__class__.get_longform(name))
892892+ return args
893893+894894+ # TODO: Simplify.
895895+ def compile(self, parent=click, *, click_kwargs=None, only_params=True):
896896+ hydreigon: Hydreigon
897897+ deino: Deino
898898+ func: abc.Callable
899899+ if self.func:
900900+ hydreigon = self.func
901901+ else:
902902+ hydreigon = Hydreigon(lambda: None)
903903+ deino: Deino = hydreigon[0]
904904+ func = deino.__func__
905905+ bundled_args = defaultdict(
906906+ list,
907907+ getattr(hydreigon, "__bundled_args__", {})
908908+ | getattr(deino, "__bundled_args__", {})
909909+ | getattr(func, "__bundled_args__", {}),
910910+ )
911911+ bundled_kwargs = Dict(
912912+ getattr(hydreigon, "__bundled_kwargs__", {})
913913+ | getattr(deino, "__bundled_kwargs__", {})
914914+ | getattr(func, "__bundled_kwargs__", {})
915915+ )
916916+ docstring: Docstring = deino.__docstring__
917917+ docstring_params = Dict({param.arg_name: param for param in docstring.params})
918918+ bundled_kwargs |= Dict(
919919+ {
920920+ k: {"help": bundled_kwargs[k].help or v.description}
921921+ for k, v in docstring_params.items()
922922+ }
923923+ )
924924+ disabled = {k for k, v in bundled_kwargs.items() if v.disabled}
925925+ sig: Signature = deino.__signature__
926926+ params: dict[str, Parameter] = {
927927+ k: self.__class__.process_param(v)
928928+ for k, v in sig.parameters.items()
929929+ if k not in disabled
930930+ }
931931+ param_keys = params.keys()
932932+933933+ # TODO: What happens if I run the `compile` method twice?
934934+ # Does this keep expounding?
935935+ # What SHOULD be happening is that `getattr(func, "__click_params__", tuple())`
936936+ # should be getting the `__click_params__` assigned to `func` from the last call to `compile`.
937937+ func.__click_params__ = click_params = [
938938+ click_param
939939+ for click_param in flatten(
940940+ getattr(hydreigon, "__click_params__", tuple()),
941941+ getattr(deino, "__click_params__", tuple()),
942942+ getattr(func, "__click_params__", tuple()),
943943+ )
944944+ if click_param.name not in disabled
945945+ ]
946946+947947+ for name in param_keys - {param.name for param in click_params}:
948948+ param: Parameter = params[name]
949949+ annotation = param.annotation
950950+ if annotation is Pass:
951951+ func = click.pass_context(func)
952952+ elif typing.get_origin(annotation) is Pass:
953953+ arg = typing.get_args(annotation)[0]
954954+ if arg is object:
955955+ func = click.pass_obj(func)
956956+ else:
957957+ func = click.make_pass_decorator(arg)(func)
958958+ elif annotation is Password:
959959+ func = click.password_option(func)
960960+ else:
961961+ kind = param.kind
962962+ func = getattr(click, self.__class__.process_clicker(kind))(
963963+ *self.process_names(name, kind, bundled_args[name], click_params),
964964+ **self.__class__.process_kwargs(
965965+ name, param, bundled_kwargs[name], docstring_params[name]
966966+ ),
967967+ )(func)
968968+969969+ if docstring:
970970+ dockwargs = {}
971971+ dockwargs["help"] = ""
972972+ if docstring.short_description:
973973+ dockwargs["help"] = dockwargs["short_help"] = (
974974+ docstring.short_description
975975+ )
976976+ if docstring.long_description:
977977+ # TODO: Use the typical format for docstrings for arguments, but format them differently here.
978978+ # TODO: Disabled args shouldn't be included in long descriptions.
979979+ # TODO: Aren't options in the `long_description` as well...?
980980+ # TODO: How do I match the case of the first word of the description to the names in the disabled list?
981981+ dockwargs["help"] += "\b\n\n" + "\b\n\n".join(
982982+ arg
983983+ for arg in docstring.long_description.split("\n")
984984+ if arg.split()[0] not in disabled
985985+ )
986986+ else:
987987+ dockwargs = {}
988988+ click_kwargs = (
989989+ (
990990+ click_kwargs
991991+ if isinstance(parent, click.Command)
992992+ else bundle.defaults["group"]
993993+ )
994994+ | {"name": self.__name__}
995995+ | dockwargs
996996+ | self.__click_kwargs__
997997+ )
998998+ if "cls" in click_kwargs:
999999+ if not hasattr(cls := click_kwargs["cls"], "__rich_repr__"):
10001000+ cls.__rich_repr__ = partial(rich_repr, only_params=only_params)
10011001+ group = parent.group(**click_kwargs)(normalize.function(func, exclude=disabled))
10021002+ for v in self.values():
10031003+ v.compile(group, click_kwargs=click_kwargs)
10041004+ return group
10051005+10061006+ def flip(self, *args, **kwargs):
10071007+ return self.compile()(*args, **kwargs)
10081008+10091009+ def invoke(self, *args, **kwargs):
10101010+ self.func.raise_if_empty()
10111011+ return self.func.invoke(*args, **kwargs)
10121012+10131013+ def run(self, *args, runner=None, attr="return_value", **kwargs):
10141014+ kwargs = {"standalone_mode": False, "catch_exceptions": False} | kwargs
10151015+ result = (runner or self.__class__.runner).invoke(
10161016+ self.compile(), args, **kwargs
10171017+ )
10181018+ return result if attr == "result" else getattr(result, attr)
10191019+10201020+ @property
10211021+ def root(self):
10221022+ self.func.raise_if_empty()
10231023+ return self.func[0]
10241024+10251025+ def document(self, docstring, *args, **kwargs):
10261026+ if args or kwargs:
10271027+ self.invoke(*args, **kwargs).__doc__ = docstring
10281028+ else:
10291029+ self.root.__doc__ = docstring
10301030+ return self
+101
packages/bundle/bundle/functions.py
···11+from operator import attrgetter
22+from hydrox.helpers import Collection
33+44+get_name = attrgetter("name")
55+not_starts_with_ = lambda s: not (s.startswith("_") or s in ("console", "help_config"))
66+77+88+def eq(a, b, debug=False, falsifier=False):
99+ try:
1010+ for n1, n2 in zip(
1111+ filter(not_starts_with_, dir(a)),
1212+ filter(not_starts_with_, dir(b)),
1313+ strict=True,
1414+ ):
1515+ if not ({n1, n2} & {"params", "commands"}):
1616+ a1 = getattr(a, n1)
1717+ a2 = getattr(b, n2)
1818+ if not (callable(a1) or callable(a2)):
1919+ if a1 != a2:
2020+ if debug:
2121+ print(a, b, n1, n2, a1, a2)
2222+ return falsifier
2323+ except ValueError as e:
2424+ if debug:
2525+ print(
2626+ set(filter(not_starts_with_, dir(a)))
2727+ - set(filter(not_starts_with_, dir(b)))
2828+ )
2929+ print(
3030+ set(filter(not_starts_with_, dir(b)))
3131+ - set(filter(not_starts_with_, dir(a)))
3232+ )
3333+ raise e
3434+ for p1, p2 in zip(
3535+ sorted(a.params, key=get_name), sorted(b.params, key=get_name), strict=True
3636+ ):
3737+ for pn1, pn2 in zip(
3838+ filter(not_starts_with_, dir(p1)),
3939+ filter(not_starts_with_, dir(p2)),
4040+ strict=True,
4141+ ):
4242+ pa1 = getattr(p1, pn1)
4343+ pa2 = getattr(p2, pn2)
4444+ if not (callable(pa1) or callable(pa2)):
4545+ if pa1 != pa2:
4646+ if debug:
4747+ print(a, b, p1, p2, pn1, pn2, pa1, pa2)
4848+ return falsifier
4949+ for c1, c2 in zip(
5050+ sorted(a.commands.values(), key=get_name),
5151+ sorted(b.commands.values(), key=get_name),
5252+ strict=True,
5353+ ):
5454+ if not eq(c1, c2, debug=debug):
5555+ return falsifier
5656+ return True
5757+5858+5959+def rich_dict(self, only_params=True, repr=False):
6060+ commands = {}
6161+ for k in dir(self):
6262+ if not k.startswith("_"):
6363+ v = getattr(self, k, None)
6464+ if k == "params":
6565+ vparams = {}
6666+ for param in v:
6767+ vparams[param.name] = pparams = {}
6868+ for p in dir(param):
6969+ if not p.startswith("_"):
7070+ if not callable(pattr := getattr(param, p, None)):
7171+ pparams[p] = pattr
7272+ commands[k] = vparams
7373+ elif k == "commands":
7474+ commands[k] = (
7575+ v
7676+ if repr
7777+ else {
7878+ ck: rich_dict(cv, only_params=only_params, repr=repr)
7979+ for ck, cv in v.items()
8080+ }
8181+ )
8282+ elif not (only_params or callable(v)):
8383+ commands[k] = v
8484+ return commands
8585+8686+8787+def rich_repr(self, only_params=True):
8888+ yield self.name
8989+ for k, v in rich_dict(self, only_params=only_params, repr=True).items():
9090+ yield k, v
9191+9292+9393+def reducedict(obj):
9494+ if isinstance(obj, dict):
9595+ for k, v in obj.items():
9696+ yield reducedict(k)
9797+ yield reducedict(v)
9898+ elif isinstance(obj, Collection):
9999+ yield from map(reducedict, obj)
100100+ else:
101101+ yield obj
+929
packages/bundle/bundle/tests/_test_bundle.py
···11+import magicattr
22+import rich_click as click
33+import operator
44+55+from addict import Dict
66+from beartype.roar import BeartypeDoorHintViolation
77+from click.testing import CliRunner
88+from functools import reduce
99+from hypothesis import HealthCheck, Phase, given, settings
1010+from inspect import isclass
1111+from bundle import (
1212+ bundle,
1313+ bundler,
1414+ Check,
1515+ DedupList,
1616+ deflambda,
1717+ dirs,
1818+ Model,
1919+ plumeta,
2020+ literal_strat,
2121+ subtype,
2222+ SuperPath,
2323+ Switch,
2424+)
2525+from parametrized import parametrized
2626+from pathvalidate import is_valid_filepath, sanitize_filename
2727+from pytest import fixture, mark, skip
2828+from random import choice
2929+from rich.pretty import pprint
3030+from typing import Literal
3131+3232+3333+class ID(tuple):
3434+ def __call__(self, obj):
3535+ return obj
3636+3737+3838+def bundle_modifier(cls: type):
3939+ return subtype(cls, bundle)
4040+4141+4242+def bundler_modifier(cls: type):
4343+ return subtype(cls, bundler)
4444+4545+4646+def bundle_arguments_modifier(cls: type):
4747+ return subtype(cls, bundle(no_args_is_help=False))
4848+4949+5050+def bundler_arguments_modifier(cls: type):
5151+ return subtype(cls, bundler(no_args_is_help=False))
5252+5353+5454+def bundle_factory(cls):
5555+ return bundle(no_args_is_help=False)(cls)
5656+5757+5858+def bundle_factory_init(cls):
5959+ return bundle(cls, no_args_is_help=False)
6060+6161+6262+def bundler_factory(cls):
6363+ return bundler(no_args_is_help=False)(cls)
6464+6565+6666+def bundler_factory_init(cls):
6767+ return bundler(cls, no_args_is_help=False)
6868+6969+7070+function_decorators = (
7171+ bundle_factory,
7272+ bundle_factory_init,
7373+ bundler_factory,
7474+ bundler_factory_init,
7575+)
7676+7777+decorators = (bundle, bundler, *function_decorators)
7878+7979+modifiers = (
8080+ ID(),
8181+ bundle_modifier,
8282+ bundler_modifier,
8383+ bundle_arguments_modifier,
8484+ bundler_arguments_modifier,
8585+)
8686+8787+8888+# Adapted From:
8989+# Answer: https://stackoverflow.com/a/5891486
9090+# User: https://stackoverflow.com/users/208997/simon
9191+characters = [chr(i) for i in range(128)]
9292+9393+9494+class F(metaclass=plumeta):
9595+ def __init__(self, height, width, prefix="f", no_main=False):
9696+ self.height = height
9797+ self.width = width
9898+ self.prefix = prefix
9999+ self.no_main = no_main
100100+ self.arguments = self.make_arguments(self.width, self.prefix)
101101+ self.names = self.arguments.keys()
102102+ self.args = self.arguments.values()
103103+ self.prefixed_arguments = Dict(
104104+ {k: [self.prefix] + v for k, v in self.arguments.items()}
105105+ )
106106+ self.namespaces = self.make_namespaces()
107107+ self.functions = Dict(reduce(operator.or_, self.namespaces.values()))
108108+ self.classes = self.make_classes()
109109+ self.nested_namespaces = self.make_nested_namespaces()
110110+ self.nested_classes = self.make_nested_classes()
111111+ self.bundled_functions = self.make_bundled_functions()
112112+ self.click_functions = self.make_click_functions()
113113+ self.singletons = self.make_singletons()[0].keys()
114114+ assert self.check_classes()
115115+ assert self.check_nested_classes()
116116+117117+ def make_arguments(self, width, prefix):
118118+ height = self.height + 1
119119+ arguments = DedupList()
120120+ for h in range(1, height):
121121+ new_width = width - 1
122122+ new_prefix = [f"{prefix}{h}"]
123123+ if new_width:
124124+ for nest in self.make_arguments(new_width, new_prefix[0]):
125125+ arguments.append(new_prefix + nest)
126126+ else:
127127+ arguments.append(new_prefix)
128128+ for index, argument in enumerate(arguments):
129129+ arguments.insert(index, argument[:-1])
130130+ if width == self.width:
131131+ return Dict(
132132+ {(args[-1] if args else self.prefix): args for args in arguments}
133133+ )
134134+ return arguments
135135+136136+ def make_namespaces(self):
137137+ namespaces = Dict()
138138+ for name, value in self.arguments.items():
139139+ # Adapted From:
140140+ # Answer: https://stackoverflow.com/a/7546307
141141+ # User: https://stackoverflow.com/users/951890/vaughn-cato
142142+ ns = {name: deflambda(name, lambda name=name: name)}
143143+144144+ if len(value) < self.width:
145145+ namespaces[name] = {} if self.no_main else ns
146146+ else:
147147+ namespaces[value[-2]] |= ns
148148+ return namespaces
149149+150150+ def make_class(self, name, ns):
151151+ return type(
152152+ name,
153153+ tuple(),
154154+ {
155155+ k: (self.make_class(k, v) if isinstance(v, dict) else v)
156156+ for k, v in ns.items()
157157+ },
158158+ )
159159+160160+ def make_classes(self):
161161+ return Dict({k: self.make_class(k, v) for k, v in self.namespaces.items()})
162162+163163+ def get_class(self, cls):
164164+ return Dict(
165165+ {
166166+ k: (self.get_class(v) if isclass(v) else v)
167167+ for k, v in cls.__dict__.items()
168168+ if callable(v)
169169+ }
170170+ )
171171+172172+ def get_classes(self, classes):
173173+ return Dict({k: self.get_class(v) for k, v in classes.items()})
174174+175175+ def no_functions(self, namespace):
176176+ return {
177177+ k: self.no_functions(v) if isinstance(v, dict) else callable(v)
178178+ for k, v in namespace.items()
179179+ }
180180+181181+ def check_classes(self):
182182+ return self.no_functions(self.namespaces) == self.no_functions(
183183+ self.get_classes(self.classes)
184184+ )
185185+186186+ def make_nested_namespace(self, indent=0):
187187+ if indent >= self.width:
188188+ return self.namespaces
189189+ namespaces = Dict()
190190+ for args in self.prefixed_arguments.values():
191191+ if indent + 1 < len(args) <= self.width:
192192+ prefix = args[indent]
193193+ args = args[indent + 1 :]
194194+ namespaces |= Dict({prefix: self.namespaces[prefix]})
195195+ name = args[-1]
196196+ joint_args = '"]["'.join(args)
197197+ joint_attrs = f'{prefix}["{joint_args}"]'
198198+ current = magicattr.get(namespaces, joint_attrs)
199199+ ns = self.namespaces[name]
200200+ if current:
201201+ current |= ns
202202+ else:
203203+ magicattr.set(namespaces, joint_attrs, Dict(ns))
204204+ else:
205205+ namespaces |= Dict(
206206+ {prefix: self.namespaces[prefix] for prefix in args[: indent + 1]}
207207+ )
208208+ return namespaces
209209+210210+ def make_nested_namespaces(self):
211211+ return Dict({i: self.make_nested_namespace(i) for i in range(self.width)})
212212+213213+ def make_nested_class(self, indent=0):
214214+ if indent >= self.width:
215215+ return self.classes
216216+ return Dict(
217217+ {
218218+ k: self.make_class(k, v)
219219+ for k, v in self.nested_namespaces[indent].items()
220220+ }
221221+ )
222222+223223+ def make_nested_classes(self):
224224+ return Dict({i: self.make_nested_class(i) for i in range(self.width)})
225225+226226+ def check_nested_classes(self):
227227+ return self.no_functions(self.nested_namespaces) == self.no_functions(
228228+ {
229229+ i: {k: self.get_class(v) for k, v in self.nested_classes[i].items()}
230230+ for i in range(self.width)
231231+ }
232232+ )
233233+234234+ def make_bundled_functions(self):
235235+ functions = Dict()
236236+237237+ for args in self.prefixed_arguments.values():
238238+ name = args[-1]
239239+ functions[name] = bundle(
240240+ self.functions[name] or deflambda(name),
241241+ functions[args[-2]] if len(args) > 1 else None,
242242+ name=name,
243243+ no_args_is_help=False,
244244+ )
245245+246246+ return functions
247247+248248+ def make_click_functions(self):
249249+ functions = Dict()
250250+ for args in self.prefixed_arguments.values():
251251+ name = args[-1]
252252+ functions[name] = reduce(
253253+ lambda a, b: b(a),
254254+ reversed(
255255+ (
256256+ (functions[args[-2]] if len(args) > 1 else click).group(
257257+ name=name,
258258+ **(bundle._defaults.group | {"no_args_is_help": False}),
259259+ ),
260260+ click.option(
261261+ "-n",
262262+ "--name",
263263+ default=name,
264264+ **bundle._defaults.option,
265265+ ),
266266+ self.functions[name] or deflambda(name),
267267+ )
268268+ ),
269269+ )
270270+271271+ return functions
272272+273273+ def make_singleton(self, func, *decorators, decorator=bundle):
274274+ bundle_decorators = [decorator]
275275+ click_decorators = [
276276+ click.group(
277277+ **(
278278+ bundle._defaults.group
279279+ | {
280280+ "no_args_is_help": False
281281+ if decorator
282282+ in (
283283+ bundle_factory,
284284+ bundle_factory_init,
285285+ bundler_factory,
286286+ bundler_factory_init,
287287+ )
288288+ else bundle._defaults.group.no_args_is_help
289289+ }
290290+ )
291291+ )
292292+ ]
293293+ for d in decorators:
294294+ if d.__module__.split(".")[0] == "click":
295295+ click_decorators.append(d)
296296+ else:
297297+ bundle_decorators.append(d)
298298+ reducer = lambda a, b: b(a)
299299+ return reduce(reducer, (func, *reversed(bundle_decorators))), reduce(
300300+ reducer, (func, *reversed(click_decorators))
301301+ )
302302+303303+ def make_singletons(self, decorator=bundle, names=False):
304304+ bundled_singletons = Dict()
305305+ click_singletons = Dict()
306306+307307+ singletons = Dict(
308308+ basic=(
309309+ deflambda("f", lambda b: None),
310310+ bundle.up("b", "-8", "--8"),
311311+ click.option("-8", "--8", "b", **bundle._defaults.option),
312312+ ),
313313+ conflicting_names=(
314314+ deflambda("f", lambda a1, a2, a3: None),
315315+ click.option("-a", "--a1", **bundle._defaults.option),
316316+ click.option("-A", "--a2", **bundle._defaults.option),
317317+ click.option("-b", "--a3", **bundle._defaults.option),
318318+ ),
319319+ )
320320+321321+ def f(accept: Switch["reject"]): ...
322322+323323+ singletons.switch = (
324324+ f,
325325+ click.option(
326326+ "-a/-r",
327327+ "--accept/--reject",
328328+ default=False,
329329+ **bundle._defaults.option,
330330+ ),
331331+ )
332332+333333+ def f(a1: Switch["r1"], a2: Switch["r2"], a3: Switch["r3"]): ...
334334+335335+ singletons.conflicting_switches = (
336336+ f,
337337+ click.option("-a/-r", "--a1/--r1", **bundle._defaults.option),
338338+ click.option("-A/-R", "--a2/--r2", **bundle._defaults.option),
339339+ click.option("-b/-s", "--a3/--r3", **bundle._defaults.option),
340340+ )
341341+342342+ def f(switch: bool): ...
343343+344344+ singletons.boolean_flags = (
345345+ f,
346346+ click.option(
347347+ "-s", "--switch", **(bundle._defaults.option | {"is_flag": True})
348348+ ),
349349+ )
350350+351351+ if names:
352352+ return singletons.keys()
353353+ else:
354354+ for k, v in singletons.items():
355355+ bundled_singletons[k], click_singletons[k] = self.make_singleton(
356356+ *v, decorator=decorator
357357+ )
358358+359359+ return bundled_singletons, click_singletons
360360+361361+ def make_bundled_classes(self, decorator):
362362+ def inner(cls):
363363+ ns = {}
364364+ for k, v in cls.__dict__.items():
365365+ if callable(v):
366366+ if decorator is bundle:
367367+ value = bundle(no_args_is_help=False)(
368368+ type(k, tuple(), inner(v)) if isclass(v) else v
369369+ )
370370+ ns[k] = value
371371+ elif isclass(v):
372372+ ns[k] = bundler(no_args_is_help=False)(
373373+ type(k, tuple(), inner(v))
374374+ )
375375+ else:
376376+ ns[k] = bundle(no_args_is_help=False)(v)
377377+ else:
378378+ ns[k] = v
379379+ return ns
380380+381381+ return Dict(
382382+ {
383383+ i: {
384384+ k: type(k, tuple(), inner(v))
385385+ for k, v in self.nested_classes[i].items()
386386+ }
387387+ for i in range(self.width)
388388+ }
389389+ )
390390+391391+ def make_inherited_classes(
392392+ self,
393393+ bases=ID(),
394394+ decorator=ID(),
395395+ indent=0,
396396+ wrap_parent=0,
397397+ wrap_child=0,
398398+ ):
399399+ all_classes = self.nested_classes[self.width - 1 - indent]
400400+ classes = Dict()
401401+ for args in self.prefixed_arguments.values():
402402+ name = args[-1]
403403+ if 1 < len(args) <= self.width - indent:
404404+ bases = classes[args[-2]]
405405+ elif bases in (bundle_factory, bundle_factory_init):
406406+ bases = bundle(no_args_is_help=False)
407407+ elif bases in (bundler_factory, bundler_factory_init):
408408+ bases = bundler(no_args_is_help=False)
409409+ if name in all_classes and name not in classes:
410410+ if name == self.prefix and wrap_parent:
411411+ classes[name] = bases(self.functions.get(name, deflambda(name)))
412412+ elif name != self.prefix and wrap_child:
413413+ classes[name] = bases(all_classes[name])
414414+ else:
415415+ classes[name] = decorator(subtype(all_classes[name], bases))
416416+ return classes
417417+418418+419419+class N(F):
420420+ def __init__(self, height, width, prefix="n", no_main=True):
421421+ super().__init__(height, width, prefix, no_main)
422422+423423+ def remove_main_class(self, cls):
424424+ ns = {}
425425+ for k, v in cls.__dict__.items():
426426+ if callable(v):
427427+ if isclass(v):
428428+ ns[k] = type(k, tuple(), self.remove_main_class(v))
429429+ elif k != cls.__name__:
430430+ ns[k] = v
431431+ else:
432432+ ns[k] = v
433433+ return ns
434434+435435+ def remove_main_namespace(self, parent, namespace):
436436+ ns = Dict()
437437+ for k, v in namespace.items():
438438+ if isinstance(v, dict):
439439+ ns[k] = self.remove_main_namespace(k, v)
440440+ elif k != parent:
441441+ ns[k] = v
442442+ return ns
443443+444444+445445+class RF(F):
446446+ def __init__(self, height, width, prefix="f", no_main=False):
447447+ super().__init__(height, width, prefix, no_main)
448448+ self.random_names = []
449449+ for name in self.names:
450450+ c = choice(characters)
451451+ while c in self.random_names or c in ("_",):
452452+ c = choice(characters)
453453+ self.random_names.append(c)
454454+ self.conversion_table = Dict(zip(self.names, self.random_names))
455455+456456+ def randomize_namespace(self, namespace):
457457+ return {
458458+ self.conversion_table.get(k, k): (
459459+ self.randomize_namespace(v) if isinstance(v, dict) else v
460460+ )
461461+ for k, v in namespace.items()
462462+ }
463463+464464+465465+class RN(RF):
466466+ def __init__(self, height, width, prefix="n", no_main=True):
467467+ super().__init__(height, width, prefix, no_main)
468468+469469+470470+height, width = 2, 2
471471+472472+473473+@fixture
474474+def f():
475475+ return F(height, width)
476476+477477+478478+for k, v in dirs(F(height, width)).items():
479479+ if not k.startswith("_"):
480480+ setattr(f, k, v)
481481+482482+483483+@fixture
484484+def n():
485485+ return N(height, width)
486486+487487+488488+for k, v in dirs(N(height, width)).items():
489489+ if not k.startswith("_"):
490490+ setattr(n, k, v)
491491+492492+493493+@fixture
494494+def rf():
495495+ return RF(height, width)
496496+497497+498498+for k, v in dirs(RF(height, width)).items():
499499+ if not k.startswith("_"):
500500+ setattr(rf, k, v)
501501+502502+503503+@fixture
504504+def rn():
505505+ return RN(height, width)
506506+507507+508508+for k, v in dirs(RN(height, width)).items():
509509+ if not k.startswith("_"):
510510+ setattr(rn, k, v)
511511+512512+513513+class DEFAULT_RESULT:
514514+ pass
515515+516516+517517+runner = CliRunner(mix_stderr=False)
518518+519519+520520+def invoker(cls, *args):
521521+ result = runner.invoke(
522522+ cls,
523523+ args,
524524+ standalone_mode=False,
525525+ catch_exceptions=False,
526526+ )
527527+ print(cls, args, result)
528528+ pprint(dirs(getattr(cls, "__cls__", cls)))
529529+ pprint(dirs(result))
530530+ return result.return_value
531531+532532+533533+def run(cls, args, result=DEFAULT_RESULT):
534534+ return invoker(cls, *args) == (
535535+ (args[-1] if args else cls.__name__) if result is DEFAULT_RESULT else result
536536+ )
537537+538538+539539+@mark.bundle
540540+class TestClasses:
541541+ @parametrized
542542+ def test_bundle_up(self, f, arguments=f.arguments.values()):
543543+ assert run(
544544+ bundler(bundle.up(f.nested_classes[0][f.prefix], no_args_is_help=False)),
545545+ arguments,
546546+ )
547547+548548+ # TODO: Is there a way to incorporate these into the inherited classes as well?
549549+ @parametrized.product
550550+ def test_bundled_classes(
551551+ self,
552552+ base=(bundle, bundler),
553553+ modifier=modifiers,
554554+ decorator=decorators,
555555+ arguments=f.arguments.values(),
556556+ ):
557557+ assert run(
558558+ decorator(modifier(f.make_bundled_classes(base)[0][f.prefix])),
559559+ arguments,
560560+ )
561561+562562+ @parametrized.product
563563+ def test_bundled_no_main(
564564+ self,
565565+ base=(bundle, bundler),
566566+ modifier=modifiers,
567567+ decorator=decorators,
568568+ arguments=n.arguments.values(),
569569+ ):
570570+ assert run(
571571+ decorator(modifier(n.make_bundled_classes(base)[0][n.prefix])),
572572+ arguments,
573573+ DEFAULT_RESULT if len(arguments) == n.width else None,
574574+ )
575575+576576+ @parametrized.product
577577+ def test_inherited_classes(
578578+ self,
579579+ f,
580580+ base=(ID(), *decorators),
581581+ decorator=(ID(), *decorators),
582582+ arguments=f.arguments.values(),
583583+ indent=range(f.width),
584584+ wrap_parent=range(2),
585585+ wrap_child=range(2),
586586+ ):
587587+ if base:
588588+ if indent >= f.width - 1 and wrap_parent:
589589+ skip(
590590+ "Wrapping (not subclassing) the parent uses the parent function, "
591591+ "which does not include subcommands."
592592+ )
593593+ else:
594594+ if not decorator:
595595+ skip("These classes are not bundler objects.")
596596+ if wrap_parent:
597597+ skip("We don't want to subclass a function.")
598598+ cls = f.make_inherited_classes(
599599+ base, decorator, indent, wrap_parent, wrap_child
600600+ )[f.prefix]
601601+ if not base or base in (bundle, bundler):
602602+ cls._rewrap(no_args_is_help=False)
603603+ assert cls == f.click_functions[f.prefix]
604604+ assert run(cls, arguments)
605605+606606+ @parametrized.product
607607+ def test_inherited_no_main(
608608+ self,
609609+ n,
610610+ base=(ID(), *decorators),
611611+ decorator=(ID(), *decorators),
612612+ arguments=n.arguments.values(),
613613+ indent=range(n.width),
614614+ wrap_parent=range(2),
615615+ wrap_child=range(2),
616616+ ):
617617+ if base:
618618+ if indent >= n.width - 1 and wrap_parent:
619619+ skip(
620620+ "Wrapping (not subclassing) the parent uses the parent function, "
621621+ "which does not include subcommands."
622622+ )
623623+ else:
624624+ if not decorator:
625625+ skip("These classes are not bundler objects.")
626626+ if wrap_parent:
627627+ skip("We don't want to subclass a function.")
628628+ cls = n.make_inherited_classes(
629629+ base, decorator, indent, wrap_parent, wrap_child
630630+ )[n.prefix]
631631+ if not base or base in (bundle, bundler):
632632+ cls._rewrap(no_args_is_help=False)
633633+ if len(arguments) == n.width:
634634+ assert cls == n.click_functions[n.prefix]
635635+ assert run(cls, arguments)
636636+ else:
637637+ assert cls.equals(n.click_functions[n.prefix])
638638+ assert run(cls, arguments, None)
639639+640640+641641+@mark.bundle
642642+class TestBundleEquality:
643643+ @parametrized.product
644644+ def test_singletons(
645645+ self,
646646+ decorator=decorators,
647647+ name=f.singletons,
648648+ ):
649649+ bundled_singletons, click_singletons = f.make_singletons(decorator)
650650+ assert bundled_singletons[name] == click_singletons[name]
651651+652652+ def test_all(self):
653653+ @bundle
654654+ def f1(a, /, b, c=3, *args, d, e=5, **kwargs):
655655+ pass
656656+657657+ @click.group(
658658+ **(
659659+ bundle._defaults.group
660660+ | {
661661+ "context_settings": {
662662+ "ignore_unknown_options": True,
663663+ "allow_extra_args": True,
664664+ }
665665+ }
666666+ )
667667+ )
668668+ @click.argument("a", **bundle._defaults.argument)
669669+ @click.option("-b", "--b", **bundle._defaults.option)
670670+ @click.option(
671671+ "-c", "--c", **(bundle._defaults.option | {"default": 3, "metavar": "int"})
672672+ )
673673+ @click.argument("args", **(bundle._defaults.argument | {"nargs": -1}))
674674+ @click.option("-d", "--d", **(bundle._defaults.option | {"required": True}))
675675+ @click.option(
676676+ "-e", "--e", **(bundle._defaults.option | {"default": 5, "metavar": "int"})
677677+ )
678678+ def f2(a, b, c, args, d, e):
679679+ pass
680680+681681+ assert f1.equals(f2)
682682+683683+684684+@mark.bundle
685685+class TestBundle:
686686+ @parametrized
687687+ def test_subcommands(self, f, args=f.arguments.values()):
688688+ assert run(f.bundled_functions[f.prefix]._rewrap(no_args_is_help=False), args)
689689+690690+ @mark.process
691691+ @mark.parametrize("decorator", function_decorators)
692692+ @given(i=...)
693693+ @settings(deadline=None)
694694+ def test_click_annotation(self, decorator, i: int):
695695+ @decorator
696696+ @bundle.argument("j")
697697+ def g(j: int):
698698+ return isinstance(j, int)
699699+700700+ assert invoker(g, "--", str(i))
701701+702702+ @mark.process
703703+ @mark.parametrize("decorator", function_decorators)
704704+ @given(i=...)
705705+ @settings(deadline=None)
706706+ def test_click_annotation_check(self, decorator, i: int):
707707+ def check(k):
708708+ return k < 1
709709+710710+ @decorator
711711+ @bundle.argument("j")
712712+ def g(j: Check[int, check]):
713713+ return j
714714+715715+ try:
716716+ assert invoker(g, "--", str(i)) == i
717717+ except BeartypeDoorHintViolation:
718718+ assert not check(i)
719719+720720+ @mark.process
721721+ @parametrized
722722+ def test_process_envvarg(self, decorator=function_decorators):
723723+ @decorator
724724+ def g(PATH: list[SuperPath]):
725725+ return all(isinstance(path, SuperPath) for path in PATH)
726726+727727+ assert invoker(g)
728728+729729+ @mark.process
730730+ @parametrized
731731+ def test_process_envvarg_argument(self, decorator=function_decorators):
732732+ @decorator
733733+ def g(PATH: list[SuperPath], /):
734734+ return all(isinstance(path, SuperPath) for path in PATH)
735735+736736+ assert invoker(g)
737737+738738+ @parametrized
739739+ def test_disabled(self, decorator=function_decorators):
740740+ @decorator
741741+ @bundle.up("args", disabled=True)
742742+ def g(*args): ...
743743+744744+ assert not g.params
745745+746746+ @parametrized.product
747747+ def test_decorators(self, module=(click, bundle), decorator=function_decorators):
748748+ @decorator
749749+ @module.argument("path", envvar="PATH", nargs=-1, type=module.Path())
750750+ @module.version_option("0.0.1", "--version", "-v")
751751+ def g(path):
752752+ return all(is_valid_filepath(sanitize_filename(p)) for p in path)
753753+754754+ assert invoker(g)
755755+756756+ @mark.model
757757+ @mark.process
758758+ @mark.parametrize("decorator", function_decorators)
759759+ @given(
760760+ ...,
761761+ literal_strat(),
762762+ )
763763+ @settings(
764764+ deadline=None,
765765+ # max_examples=10,
766766+ suppress_health_check=(HealthCheck.too_slow,),
767767+ phases=(
768768+ Phase.explicit,
769769+ Phase.reuse,
770770+ Phase.generate,
771771+ Phase.target,
772772+ Phase.explain,
773773+ ),
774774+ )
775775+ def test_model_processing(self, decorator, a: int, mps):
776776+ hive, bees, b = mps
777777+778778+ class AB(Model):
779779+ a: int
780780+ b: Literal[*bees]
781781+782782+ @decorator
783783+ def g(ab: AB, /):
784784+ return ab
785785+786786+ try:
787787+ result = invoker(g, "--", str(a), str(b))
788788+ except BeartypeDoorHintViolation:
789789+ assert b in hive and b not in bees
790790+ else:
791791+ assert result.a == a
792792+ assert (result.b == b) or (result.b is b)
793793+794794+ @parametrized
795795+ def test_original(self, decorator=function_decorators):
796796+ @decorator
797797+ def greet(name, /):
798798+ return f"Hello, {name}!"
799799+800800+ assert invoker(greet, "world") == "Hello, world!"
801801+ assert greet("world") == "Hello, world!"
802802+803803+ @parametrized
804804+ def test_original_default(self, decorator=function_decorators):
805805+ @decorator
806806+ def greet(name="world"):
807807+ return f"Hello, {name}!"
808808+809809+ assert invoker(greet) == "Hello, world!"
810810+ assert greet() == "Hello, world!"
811811+ assert greet(name="Earth") == "Hello, Earth!"
812812+813813+814814+def quark_factory(*args, **kwargs):
815815+ return bundle(no_args_is_help=False).quark(*args, **kwargs)
816816+817817+818818+drivers = (
819819+ bundle.drive,
820820+ quark_factory,
821821+)
822822+823823+824824+@fixture
825825+def namespaces(f):
826826+ return f.nested_namespaces[0][f.prefix]
827827+828828+829829+@fixture
830830+def no_main_namespaces(n):
831831+ return n.remove_main_namespace(n.prefix, n.nested_namespaces[0][n.prefix])
832832+833833+834834+def get_function_namespace(namespace):
835835+ dct = {}
836836+ for k, v in namespace.items():
837837+ if isinstance(v, dict):
838838+ dct[k] = get_function_namespace(v)
839839+ elif callable(v):
840840+ dct[k] = dirs(v, ignores=("__builtins__", "__globals__"))
841841+ else:
842842+ dct[k] = v
843843+ return dct
844844+845845+846846+@mark.bundle
847847+class TestQuarkDrive:
848848+ @parametrized.product
849849+ def test_quark_drive(
850850+ self, namespaces, driver=drivers, arguments=f.arguments.values()
851851+ ):
852852+ cls = driver(namespaces.pop(f.prefix), **namespaces)
853853+ if driver.__name__ == "drive":
854854+ cls._rewrap(no_args_is_help=False)
855855+ assert run(cls, arguments)
856856+857857+ @parametrized.product
858858+ def test_kwargs(self, namespaces, driver=drivers, arguments=f.arguments.values()):
859859+ cls = driver(**namespaces)
860860+ if driver.__name__ == "drive":
861861+ cls._rewrap(no_args_is_help=False)
862862+ assert run(cls, arguments)
863863+864864+ @parametrized
865865+ def test_pre_root(self, namespaces, arguments=f.arguments.values()):
866866+ assert run(
867867+ bundle(namespaces.pop(f.prefix), no_args_is_help=False).quark(**namespaces),
868868+ arguments,
869869+ )
870870+871871+ @parametrized.product
872872+ def test_rename(self, namespaces, driver=drivers, arguments=f.arguments.values()):
873873+ namespaces = rf.randomize_namespace(namespaces)
874874+ cls = driver(namespaces.pop(rf.conversion_table[f.prefix]), **namespaces)
875875+ if driver.__name__ == "drive":
876876+ cls._rewrap(no_args_is_help=False)
877877+ assert run(
878878+ cls,
879879+ (rf.conversion_table[arg] for arg in arguments),
880880+ arguments[-1] if arguments else f.prefix,
881881+ )
882882+883883+ @parametrized.product
884884+ def test_rename_kwargs(
885885+ self, namespaces, driver=drivers, arguments=f.arguments.values()
886886+ ):
887887+ cls = driver(**rf.randomize_namespace(namespaces))
888888+ if driver.__name__ == "drive":
889889+ cls._rewrap(no_args_is_help=False)
890890+ assert run(
891891+ cls,
892892+ (rf.conversion_table[arg] for arg in arguments),
893893+ arguments[-1] if arguments else f.prefix,
894894+ )
895895+896896+ @parametrized
897897+ def test_rename_pre_root(self, namespaces, arguments=f.arguments.values()):
898898+ namespaces = rf.randomize_namespace(namespaces)
899899+ assert run(
900900+ bundle(
901901+ namespaces.pop(rf.conversion_table[f.prefix]), no_args_is_help=False
902902+ ).quark(**namespaces),
903903+ (rf.conversion_table[arg] for arg in arguments),
904904+ arguments[-1] if arguments else f.prefix,
905905+ )
906906+907907+ @parametrized.product
908908+ def test_no_main(
909909+ self, no_main_namespaces, driver=drivers, arguments=n.arguments.values()
910910+ ):
911911+ cls = driver(**no_main_namespaces)
912912+ if driver.__name__ == "drive":
913913+ cls._rewrap(no_args_is_help=False)
914914+ assert run(
915915+ cls, arguments, DEFAULT_RESULT if len(arguments) == n.width else None
916916+ )
917917+918918+ @parametrized.product
919919+ def test_no_main_rename(
920920+ self, no_main_namespaces, driver=drivers, arguments=n.arguments.values()
921921+ ):
922922+ cls = driver(**rn.randomize_namespace(no_main_namespaces))
923923+ if driver.__name__ == "drive":
924924+ cls._rewrap(no_args_is_help=False)
925925+ assert run(
926926+ cls,
927927+ (rn.conversion_table[arg] for arg in arguments),
928928+ arguments[-1] if len(arguments) == n.width else None,
929929+ )
···66from importlib import import_module
77from pathlib import Path
8899-modules = []
99+modules = {}
1010for file in Path(__file__).parent.iterdir():
1111 stem = file.stem
1212 if not (
···1515 or stem.endswith("__")
1616 or stem in "hypothesis"
1717 ):
1818- modules.append(import_module("." + stem, package=__name__))
1818+ modules[stem] = import_module("." + stem, package=__name__)
1919 # module = import_module("." + stem, package=__name__)
2020 # globals().update(
2121 # {
···3838 def __getattr__(self, t: str):
3939 if t.startswith("__"):
4040 raise AttributeError(t)
4141- if t in ("tests", "hypothesis"):
4242- # return import_module("." + t, package=__name__)
4141+ if t in ("tests",):
4342 return eval(t)
4443 if t in ("_inf", "_aleph", "_ninf", "_naleph"):
4544 return eval(t.lstrip("_"))
4545+ if t.startswith("_"):
4646+ stripped = t.strip("_")
4747+ if stripped in modules:
4848+ return modules[stripped]
4949+ raise AttributeError(t)
4650 if t.startswith("infm"):
4751 return inf("-" + t.removeprefix("infm"))
4852 if t.startswith("inf"):
···5963 return naleph("-" + t.removeprefix("nalephm"))
6064 if t.startswith("naleph"):
6165 return naleph(t.removeprefix("nalephp").removeprefix("naleph") or 0)
6262- for module in modules:
6666+ for module in modules.values():
6367 if hasattr(module, t):
6468 return getattr(module, t)
6569 raise AttributeError(t)
+16-11
packages/hydrox/hydrox/helpers/helpers.py
···77import types
8899from contextlib import suppress, contextmanager
1010-from cytoolz.itertoolz import unique
1110from functools import partial
1211from inspect import (
1312 currentframe,
···1514 getmodule,
1615)
1716from itertools import permutations
1818-from more_itertools import powerset
1717+from more_itertools import powerset, unique_everseen
19182019BString: typing.TypeAlias = str | bytes | bytearray
2121-2222-2323-def unwrap(func):
2424- return getattr(func, "__wrapped__", func)
252026212722def ziperr(err):
···4136 raise e
423743383939+@contextmanager
4040+def hashsuppress():
4141+ try:
4242+ yield
4343+ except TypeError as e:
4444+ if "unhashable type" not in str(e):
4545+ raise e
4646+4747+4448def reversecut(l, i=None):
4549 # length = len(l)
4650 # if i is None:
···173177174178175179def superpowerset(*iterables):
176176- return unique(
180180+ return unique_everseen(
177181 flatten((powerset(p) for p in permutations(flatten(iterables))), levels=2)
178182 )
179183180184185185+# TODO: Stop at the first line that starts with `@` on the same level of indentation.
181186# Adapted From:
182187# Answer 1: https://stackoverflow.com/a/60778287/10827766
183188# User 1: https://stackoverflow.com/users/2729778/changaco
···267272 # The source file seems to have been modified.
268273 return default
269274 call_line_ls = call_line.lstrip()
270270- if re.match(query, call_line_ls):
275275+ if call_line_ls.startswith("@"):
271276 # Note: there is a small probability of false positive here, if the
272277 # function call is on the same line as a decorator call.
273273- return True
278278+ return re.match(query, call_line_ls) or False
274279 if call_line_ls.startswith("class ") or call_line_ls.startswith("def "):
275280 # Note: there is a small probability of false positive here, if the
276281 # function call is on the same line as a `class` or `def` keyword.
277277- return True
282282+ return re.match(query, call_line_ls) or False
278283 # Next, we try to find and examine the line after the function call.
279284 # If that line doesn't start with a `class` or `def` keyword, then the
280285 # function isn't being called as a decorator.
···305310 line_ls = line.lstrip()
306311 if line_ls[:1] in (")", ","):
307312 continue
308308- return re.match(query, line_ls) or False
313313+ return line_ls.startswith("@")
309314 elif len(line_indentation) < len(def_line_indentation):
310315 break
311316 return default
+222-158
packages/hydrox/hydrox/typing/generic.py
···1212import re
1313import sys
14141515-from ..helpers import reversecut, Collection, flatten, zipsuppress, ziperr, unwrap
1515+from ..helpers import reversecut, Collection, flatten, zipsuppress, ziperr, hashsuppress
16161717-from ..helpers.cache import CacheError, cache, cachehash
1717+from ..helpers.cache import cache, cachehash
1818from ..helpers.decorator import Decorator
1919from ..variables import unique_attributes
2020from abc import ABCMeta
2121from contextlib import suppress
2222from collections import defaultdict
2323from cytoolz.dicttoolz import keymap
2424-from cytoolz.itertoolz import unique
2525-from functools import partial, reduce, cache as defcache
2424+from functools import partial, reduce
2625from inspect import Parameter
2727-from inspect import getmro, stack
2828-from more_itertools import partition
2626+from inspect import getmro, stack, unwrap
2727+from more_itertools import partition, unique_everseen
2928from random import randint
3029from rich.table import Table
3130from rich.console import Console
···8786 )
888789889090-# TODO
9191-# @defcache
9289@cache
9393-def get_name(annotation: AnnotationString) -> str:
9494- """Get the name or representation of a type annotation.
9090+def get_name_origin(annotation: AnnotationString) -> str:
9191+ """Get the original name or representation of a type annotation.
95929693 Args:
9797- annotation (AnnotationString): The annotation to get the name or representation of.
9494+ annotation (AnnotationString): The annotation to get the original name or representation of.
98959996 Returns:
100100- str: The name or representation of the annotation.
9797+ str: The original name or representation of the annotation.
10198 """
10299 if isinstance(annotation, str):
103100 return annotation
···112109 return repr(annotation)
113110114111115115-@defcache
112112+@cache
113113+def get_name(annotation: AnnotationString) -> str:
114114+ """Get the original name or representation of a type annotation.
115115+116116+ Args:
117117+ annotation (AnnotationString): The annotation to get the original name or representation of.
118118+119119+ Returns:
120120+ str: The original name or representation of the annotation.
121121+ """
122122+ if isinstance(annotation, str):
123123+ return annotation
124124+ annotation_name = get_name_origin(annotation)
125125+ if args := typing.get_args(annotation):
126126+ return annotation_name + f"[{', '.join(get_name(arg) for arg in args)}]"
127127+ return annotation_name
128128+129129+130130+@cache
116131def get_name_lowered(annotation: AnnotationString) -> str:
117132 """Get the lowercase name or representation of a type annotation.
118133···128143NameCheck: typing.TypeAlias = collections.abc.Callable[[str, str], bool]
129144130145131131-@defcache
146146+@cache
132147def get_name_check(
133148 a: AnnotationString,
134149 /,
···157172 )
158173159174160160-@defcache
175175+@cache
161176def get_union_check(annotation: AnnotationString) -> bool:
162177 """Checks whether a type annotation is a _Union_ or _UnionType_.
163178···170185 return get_name_check(annotation, "union", "uniontype")
171186172187173173-@defcache
188188+@cache
174189def get_name_remove_suffix(annotation: AnnotationString, suffix: str) -> str:
175190 """Removes a suffix from a type annotation name or representation.
176191···196211 Annotation: The origin of the annotation.
197212 """
198213 if getattr(annotation, "__module__", None) == "typing":
199199- return getattr(typing, get_name(annotation))
214214+ return getattr(typing, get_name_origin(annotation))
200215 return typing.get_origin(annotation)
201216202217···223238 return args
224239225240226226-@defcache
241241+@cache
227242def add_camel_check(name):
228243 name: str = get_name(name)
229244 return name.isalpha() and name[0].isupper()
230245231246232232-@defcache
233247def dirattr(module):
234234- names = dir(module)
235235- attrs = set()
236236- for name in names:
248248+ for name in dir(module):
249249+ yield name
237250 attr = getattr(module, name)
238251 if isinstance(attr, Annotation):
239239- attrs.add(get_name(attr))
240240- return flatten(names, attrs)
252252+ n = get_name(attr)
253253+ if name != n:
254254+ yield n
241255242256243257modules = {
···309323 super().__delitem__(get_name_lowered(key))
310324311325 def __getattr__(self, key):
326326+ if len(key) >= 44:
327327+ raise AttributeError()
312328 return self[key]
313329314330 def __getitem__(self, key):
···394410 super().__delitem__(get_name(key))
395411396412 def __getattr__(self, key):
413413+ if len(key) >= 44:
414414+ raise AttributeError()
397415 return self[key]
398416399417 def __getitem__(self, key):
400400- if typing.get_args(key) or annotation_is_typing_instance(key):
401401- return GenericArgs[key]
402402- if not (isinstance(key, str) or isinstance(key, Annotation)):
403403- raise CacheError()
418418+ if (
419419+ not (isinstance(key, str) or isinstance(key, Annotation))
420420+ or typing.get_args(key)
421421+ or annotation_is_typing_instance(key)
422422+ ):
423423+ raise KeyError()
404424 key = get_name(key)
405425 try:
406426 return super().__getitem__(key)
···484504default_subscription_size = -2
485505486506487487-@defcache
507507+@cache
488508def trysubscriptionsize(g):
489509 try:
490510 g[*[int] * test_subscription_size]
···519539 return default_subscription_size
520540521541522522-@defcache
542542+@cache
523543def get_subscription_size(g):
524544 if isinstance(g, str):
525545 return default_subscription_size
···545565 return tuple()
546566547567548548-@defcache
568568+@cache
549569def trymro_set(cls):
550570 if cls is typing.Set or cls is collections.abc.Set:
551571 return getmro(set)
552572 return trymro(cls)
553573554574555555-@defcache
575575+@cache
556576def trymro_generic(cls):
557577 return [m for m in trymro_set(cls) if not geeq(m, typing.Generic)]
558578···564584 return tuple()
565585566586567567-@defcache
587587+@cache
568588def check_subscription_size(size):
569589 return size > default_subscription_size
570590571591572572-@defcache
592592+@cache
573593def random_subscription_size(rand, size):
574594 global max_subscription_size
575595 if size == -1 and rand:
···579599 return size
580600581601582582-@defcache
602602+@cache
583603def get_inner_subscription_size(rand, generic, bases=tuple()):
584604 randinner = partial(random_subscription_size, rand)
585605 bases = bases or trybases(generic)
···602622603623604624# TODO: How did ht.UserDict originally get the 2, then...?
605605-@defcache
625625+@cache
606626def get_generic_subscription_size(t, /, *, root_bases=tuple(), rand=False):
607627 randinner = partial(random_subscription_size, rand)
608628···639659 return 1
640660641661642642-@defcache
662662+@cache
643663def is_subscriptable(t):
644664 if isinstance(t, GenericMeta):
645665 return bool(t.__subscriptions__)
···650670non_generic_classes = (object, None, ...) + recursive_classes
651671652672653653-@defcache
673673+@cache
654674def get_named(n: str, getter):
655675 match n:
656676 case "Union" | "UnionType":
···661681 return getter(n)
662682663683664664-@defcache
684684+@cache
665685def get_named_generics(n: str):
666686 inner_generics = []
667687 for module in modules.values():
···673693 return tuple(inner_generics)
674694675695676676-@defcache
696696+@cache
677697def get_named_ht(n: str):
678698 try:
679699 return (Generics[n],)
···726746727747728748# TODO: Implement the cache additions in `GenericMeta` or the `get` functions for the `has` functions as well.
729729-@defcache
749749+@cache
730750def has_named_generics(n):
731751 return tuple([module for module in modules.values() if hasattr(module, n)])
732752733753734734-@defcache
754754+@cache
735755def has_named_ht(n):
736756 return (Generics,) if n in Generics else tuple()
737757···771791 return has_all_generics(generic) + has_all_ht(generic)
772792773793774774-@defcache
794794+@cache
775795def not_generic_class_error(error):
776796 error = error if isinstance(error, str) else str(error)
777797 return error not in (
···815835 ignored = [get_name_lowered(i) for i in ignored]
816836 if not inner:
817837818818- @defcache
838838+ @cache
819839 def inner(camel):
820840 return [
821841 scls
···823843 if get_name_lowered(scls) not in ignored and check_module(scls, modules)
824844 ]
825845826826- gsc = gsc or defcache(
846846+ gsc = gsc or cache(
827847 partial(get_subclasses, ignored=ignored, modules=modules, inner=inner)
828848 )
829849 if get_name_lowered(cls) not in ignored:
···852872 return ([cls] if add_self else []) + classes
853873854874855855-@defcache
875875+@cache
856876def get_superclasses(cls, /, *, add_self=False, ignored=tuple()):
857877 ignored = [get_name_lowered(i) for i in ignored]
858878 return [t for t in trymro(cls)[int(not add_self) :] if get_name(t) not in ignored]
···920940 return origin
921941922942923923-# TODO: Account for `ForwardRefs`.
924924-@cache(Generics)
943943+@cache
925944def generalize(annotation):
926945 # TODO: Is this necessary?
927946 # if annotation is None:
···985100498610059871006# NOTE: Can't cache this; the functions might have defaults with integers, booleans, floats, or strings.
988988-def signature(sig: inspect.Signature, t=typing.Any):
10071007+def signature(sig: inspect.Signature, t=typing.Any, ta=tuple, tk=dict[str, typing.Any]):
9891008 if not isinstance(sig, inspect.Signature):
990990- sig = inspect.signature(sig, eval_str=True)
10091009+ if hasattr(sig, "__signature__"):
10101010+ sig = sig.__signature__
10111011+ else:
10121012+ sig = inspect.signature(sig, eval_str=True)
9911013 parameters = []
9921014 for param in sig.parameters.values():
9931015 if param.annotation is inspect.Parameter.empty:
994994- if param.kind in (
995995- inspect.Parameter.VAR_POSITIONAL,
996996- inspect.Parameter.VAR_KEYWORD,
997997- ):
998998- parameters.append(param)
10161016+ if param.kind is inspect.Parameter.VAR_POSITIONAL:
10171017+ parameters.append(
10181018+ param.replace(
10191019+ annotation=(
10201020+ generalize(ta)
10211021+ if param.default is inspect.Parameter.empty
10221022+ else get_type(param.default)
10231023+ )
10241024+ )
10251025+ )
10261026+ elif param.kind is inspect.Parameter.VAR_KEYWORD:
10271027+ parameters.append(
10281028+ param.replace(
10291029+ annotation=(
10301030+ generalize(tk)
10311031+ if param.default is inspect.Parameter.empty
10321032+ else get_type(param.default)
10331033+ )
10341034+ )
10351035+ )
9991036 else:
10001037 parameters.append(
10011038 param.replace(
10021002- annotation=generalize(
10031003- t
10391039+ annotation=(
10401040+ generalize(t)
10041041 if param.default is inspect.Parameter.empty
10051042 else get_type(param.default)
10061043 )
···11181155 raise TypeError("type() takes between 1 and 3 arguments")
111911561120115711211121-@defcache
11581158+@cache
11221159def get_generic_names(generics):
11231160 return {get_name_lowered(g) for g in generics}
11241161···11851222 return False
118612231187122411881188-@defcache
12251225+@cache
11891226def _trysubclass(left, right):
11901227 try:
11911228 return builtins.issubclass(left, right)
···12001237 return _trybase(left, right, _trysubclass)
120112381202123912031203-@defcache
12401240+@cache
12041241def get_all_set_generics(annotation):
12051242 return (get_generics if geeq(get_origin(annotation), set) else get_all_generics)(
12061243 annotation
12071244 )
120812451209124612101210-@defcache
12471247+@cache
12111248def _tryinmro(left, right):
12121249 return right in trymro(left)
12131250···12171254 return _trybase(left, right, _tryinmro)
121812551219125612201220-@defcache
12571257+@cache
12211258def _tryinmrosubclass(left, right):
12221259 return tryinmro(left, right) or trysubclass(left, right)
12231260···12381275 return flatten(
12391276 partition(
12401277 annotation_is_not_generic,
12411241- unique(
12781278+ unique_everseen(
12421279 flatten(
12431280 annotation,
12441281 map(
···12511288 )
125212891253129012911291+@cache
12541292def unwrap_check(cls):
12551293 if has_custom_init(cls):
12561294 cls = type(cls)
···12601298 )
126112991262130013011301+@cache
13021302+def unwrap_generic(cls):
13031303+ return officiate(cls) if unwrap_check(cls) else cls
13041304+13051305+12631306# TODO: This is going to trip up with types that have the same name as the official generics,
12641307# but don't aren't connected to them, such as `hh.Collection` and `abc.Collection`.
12651308@cache
···12671310 if left_right_collection_check(left, right):
12681311 if isinstance(right, abc.Iterator):
12691312 warn(f'variable "right" of value "{right}" will be used up', TypeWarning)
12701270- else:
12711271- if unwrap_check(right):
12721272- generic = officiate(right)
12731273- else:
12741274- generic = right
13131313+ right = tuple(right)
12751314 for l in generic_flatten(left):
12761315 for r in generic_flatten(right):
12771316 if tryinmro(l, r):
12781317 return l, r
12791279- for r in generic_flatten(generic):
13181318+ for r in generic_flatten(map(unwrap_generic, flatten(right))):
12801319 if trysubclass(l, r):
12811320 return l, r
12821321 return tuple()
128313221284132312851285-@defcache
13241324+@cache
12861325def argsinmrosubclass(left_args, right_args, strict):
12871326 with zipsuppress():
12881327 return all(
···13721411 return None
137314121374141313751375-@defcache
14141414+def anything(annotation, *extras):
14151415+ if isinstance(annotation, Annotation):
14161416+ if annotation is ...:
14171417+ return True
14181418+ for generic in flatten(
14191419+ object, typing.Any, typing.Union, Parameter.empty, extras
14201420+ ):
14211421+ if geeq(annotation, generic):
14221422+ return True
14231423+ return False
14241424+14251425+14261426+@cache
13761427def scoremro(left, right):
13771428 if left == right:
13781429 return 1
···13811432 mro = (left, object)
13821433 else:
13831434 mro = trymro_generic(left)
14351435+ unrelated_score = -(len(mro) + 1)
14361436+ if anything(right):
14371437+ return unrelated_score
13841438 try:
13851439 return 1 - mro.index(right)
13861440 except ValueError as e:
13871441 if "not in" in str(e):
13881388- return -(len(mro) + 1)
14421442+ return unrelated_score
13891443 raise e
139014441391144513921392-@defcache
14461446+@cache
13931447def score(left, right):
13941448 if result := tryingenericmrosubclass(left, right):
13951449 return scoremro(*result)
···13981452any_score = 1
139914531400145414011401-@defcache
14551455+@cache
14021456def scoreargs(left_args, right_args, strict):
14031457 return sumscore(
14041458 scoresubclass(la, ra) for la, ra in zip(left_args, right_args, strict=strict)
14051459 )
140614601407146114081408-@defcache
14621462+@cache
14091463def _scoresubclass(left, right):
14101464 left_origin, right_origin = get_origin(left), get_origin(right)
14111465 right_args = typing.get_args(right)
···14931547 return score(left_origin, right_origin)
149415481495154914961496-@defcache
15501550+@cache
14971551def is_custom_init(init):
14981552 return init not in (object.__init__, None)
149915531500155415011501-@defcache
15551555+@cache
15021556def has_custom_init(cls):
15031557 return (
15041558 cls.__custom_init__
···15071561 )
150815621509156315101510-@defcache
15641564+@cache
15111565def scoresubclass(left, right):
15661566+ if anything(right):
15671567+ return scoremro(left, right)
15121568 left, right = generalize(left), generalize(right)
15131569 if hasattr(right, "__subclassscore__"):
15141570 if has_custom_init(right):
···15201576 return _scoresubclass(left, right)
152115771522157815231523-def anything(annotation):
15241524- if isinstance(annotation, Annotation):
15251525- if annotation is ...:
15261526- return True
15271527- for generic in (object, typing.Any, typing.Union):
15281528- if geeq(annotation, generic):
15291529- return True
15301530- return False
15311531-15321532-15331579# NOTE: Can't cache this; booleans, floats, strings, and integers sometimes have the same hash values.
15341580def scoreinstance(left, right):
15351581 if anything(right):
15361536- return any_score
15821582+ return scoremro(get_type(left), right)
15371583 right = generalize(right)
15381584 if hasattr(right, "__instancescore__"):
15391585 if has_custom_init(right):
···15851631 raise TypeError(f"'{cls}' object is not callable")
158616321587163315881588-# TODO: Can't cache this; it might get non-truthy objects.
16341634+# TODO: Modify this to work with type arguments.
16351635+# NOTE: Can't cache this; it might get non-truthy objects.
15891636def format_name(obj):
15901637 if isinstance(obj, list):
15911638 return f"[{', '.join([format_name(a) for a in obj])}]"
···16581705 return r
165917061660170717081708+def gformat_name(obj):
17091709+ return format_name(generalize(obj))
17101710+17111711+16611712@cache
16621713def geeq(left, right):
16631714 if not left_right_annotation_check(left, right):
···16731724# Adapted From:
16741725# Answer: https://stackoverflow.com/a/27952689/10827766
16751726# User: https://stackoverflow.com/users/1774667/yakk-adam-nevraumont
16761676-@defcache
17271727+@cache
16771728def reducecachehash(a, b):
16781729 return (cachehash(a) * 3) + cachehash(b)
16791730···16901741 # "hydrox/hydrox/typing/tests" in os.environ.get("PYTEST_CURRENT_TEST", "")
16911742 # ):
16921743 # return f"{''.join(part[0] for part in cls.__module__.split('.')[:2])}.{cls.__name__}"
17441744+ # TODO: Set this up to work with a debug variable.
16931745 # if annotation_is_generic(cls) or isinstance(cls, FormatMeta):
16941746 # if getattr(cls, "__generic__", sentinel) is sentinel:
16951747 # return f"{''.join(part[0] for part in cls.__module__.split('.')[:2])}.{cls.__name__}"
···19542006 else:
19552007 alias_result.__generic__ = sentinel
1956200819571957- try:
19581958- alias_result.__hash_value__ = reducehash(
19591959- origin,
19601960- (
19611961- arg.items()
19621962- if isinstance(arg, abc.Mapping)
19631963- else arg
19641964- for arg in args
19651965- ),
19661966- )
19671967-19681968- GenericArgs[alias_result] = alias_result
20092009+ with hashsuppress():
20102010+ generalize.cache[alias_result] = alias_result
1969201119702012 officiate.cache[alias_result] = alias_result.__generic__
19712013···19792021 current_generic_alias_results
19802022 )
1981202319821982- except TypeError as e:
19831983- if "unhashable type" not in str(e):
19841984- raise e
19851985-19862024 alias_result.__repr_message__ = f"{origin}[{', '.join([format_name(arg) for arg in args])}]"
1987202519882026 return alias_result
···1995203319962034 r.__class_getitem__ = no_class_getitem
19972035 r.__subscriptions__ = s
19981998- new_defaults = defaults | {"name": n, "subscriptions": s}
19991999- del new_defaults["ns"]
20002000- r.__hash_value__ = reducehash(
20012001- sys.modules[__name__].__package__,
20022002- n,
20032003- new_defaults.items(),
20042004- generics,
20052005- )
20062006-20072007- # TODO: Put these in `Generalize` or similar.
20082008- # Although, if I do that, subclasses of Generics won't be added...
20092009- for rn in (r, n):
20102010- officiate.cache[rn] = r.__generic__
20362036+ r.__defaults__ = {
20372037+ k: v for k, v in defaults.items() if k not in ("ns",)
20382038+ } | {"name": n, "subscriptions": s}
2011203920122040 current_generic_results = current_generics + (r,)
20132041 for gr in current_generic_results + (n,):
20142014- get_ht.cache[gr] = (r,)
20152015- get_generics.cache[gr] = current_generics
20162016- get_ht_generics.cache[gr] = current_generic_results
20422042+ with hashsuppress():
20432043+ generalize.cache[gr] = r
20442044+ officiate.cache[gr] = r.__generic__
20452045+ get_ht.cache[gr] = (r,)
20462046+ get_generics.cache[gr] = current_generics
20472047+ get_ht_generics.cache[gr] = current_generic_results
2017204820182049 r.__repr_message__ = FormatMeta.__repr__(r)
20192050···20492080 results = (result, Result)
20502081 generic_results = result.__generics__ + results
20512082 for gr in generic_results + (name, Name):
20522052- get_all_ht.cache[gr] = results
20532053- get_all_generics.cache[gr] = result.__generics__
20542054- get_all_ht_generics.cache[gr] = generic_results
20832083+ with hashsuppress():
20842084+ get_all_ht.cache[gr] = results
20852085+ get_all_generics.cache[gr] = result.__generics__
20862086+ get_all_ht_generics.cache[gr] = generic_results
2055208720562088 alias_results = (result.__alias__, Result.__alias__)
20572089 both_alias_generics = (
···20592091 )
20602092 generic_alias_results = both_alias_generics + alias_results
20612093 for gar in generic_alias_results:
20622062- get_all_ht.cache[gar] = alias_results
20632063- get_all_generics.cache[gar] = both_alias_generics
20642064- get_all_ht_generics.cache[gar] = generic_alias_results
20942094+ with hashsuppress():
20952095+ get_all_ht.cache[gar] = alias_results
20962096+ get_all_generics.cache[gar] = both_alias_generics
20972097+ get_all_ht_generics.cache[gar] = generic_alias_results
2065209820662099 # return Result if camelized_name else result
20672100 return Result if Result.__name__ == defaults["name"] else result
···20792112 not isinstance(other, Annotation)
20802113 ):
20812114 return NotImplemented
20822082- # other_camel = getattr(other, "__camel__", None)
20832083- # return (
20842084- # other is cls
20852085- # or other_camel is cls
20862086- # or other is cls.__camel__
20872087- # or other_camel is cls.__camel__
20882088- # # NOTE: Because the results are camelized,
20892089- # # `other` is being checked against the
20902090- # # `cls.__camel__` generics anyway.
20912091- # or other in cls.__generics__
20922092- # or NotImplemented
20932093- # )
20942115 return geeq(cls, other) or NotImplemented
2095211620962117 # Adapted From:
20972118 # Answer: https://stackoverflow.com/a/7810592
20982119 # User: https://stackoverflow.com/users/68998/newtover
20992099- def __hash__(cls):
21002100- return cls.__hash_value__
21202120+ def __hash__(self):
21212121+ return reducehash(
21222122+ sys.modules[__name__].__package__,
21232123+ self.__defaults__.items(),
21242124+ self.__generics__,
21252125+ )
2101212621022127 def __call__(cls, *args, **kwargs):
21032128 return call(cls, *args, **kwargs)
···21062131 def __repr__(cls):
21072132 return cls.__repr_message__
2108213321092109- @defcache
21342134+ @cache
21102135 def __getattr__(cls, attr):
21362136+ if len(attr) >= 44:
21372137+ raise AttributeError()
21112138 errors = []
21122139 errored = []
21132140 for generic in cls.__generics__:
···21582185# so this function will never be invoked in those cases,
21592186# which means booleans, floats, strings, and integers having
21602187# the same hash value won't matter.
21612161-@defcache
21882188+@cache
21622189def partition_ellipsis(args):
21632190 if ... in args:
21642191 index = args.index(...)
···21782205 def __subclasscheck__(self, C: typing.Any) -> bool:
21792206 return isinmrosubclass(C, self)
2180220721812181- @defcache
22082208+ # TODO: Add support for Ellipsis in tuples.
22092209+ @cache
21822210 def __add__(self, summand):
21832211 origin = typing.get_origin(self)
21842212 original_summand = summand
···22162244 f"unsupported operand type(s) for +: {FormatMeta.__format_type__(type(self))} and {FormatMeta.__format_type__(type(original_summand))}"
22172245 )
2218224622192219- @defcache
22472247+ # TODO: Add support for Ellipsis in tuples.
22482248+ @cache
22202249 def __radd__(self, summand):
22212250 origin = typing.get_origin(self)
22222251 original_summand = summand
···22662295 ) or NotImplemented
2267229622682297 def __hash__(self):
22692269- if hasattr(self, "__hash_value__"):
22702270- return self.__hash_value__
22712271- raise TypeError(f"unhashable type: {self}")
22982298+ return reducehash(
22992299+ self.__origin__,
23002300+ (
23012301+ arg.items() if isinstance(arg, abc.Mapping) else arg
23022302+ for arg in self.__args__
23032303+ ),
23042304+ )
2272230522732306 # NOTE: Caching this just makes it confusing.
22742307 def __repr__(self):
···22772310 def __call__(self, *args, **kwargs):
22782311 origin = typing.get_origin(self)
22792312 if (
22802280- isinmrosubclass(origin, Generics.Iterable)
23132313+ isinmrosubclass(origin, abc.Iterable)
22812314 and args
22822282- and isinstance(first_arg := args[0], Generics.Iterable)
23152315+ and isinstance(first_arg := args[0], abc.Iterable)
22832316 ):
22842317 self_args = get_args(self)
22852285- if isinmrosubclass(origin, Generics.tuple):
22862286- items = [t(i) for t, i in zip(self_args, first_arg, strict=True)]
22872287- elif isinmrosubclass(origin, Generics.Mapping):
23182318+ if isinmrosubclass(origin, tuple):
23192319+ # TODO: Test this with more, less, and equal number of self_args and first_args
23202320+ if ... in self_args:
23212321+ first_arg = list(first_arg)
23222322+ pre_args, post_args = partition_ellipsis(self_args)
23232323+ pre_index, post_length = len(pre_args), len(post_args)
23242324+ first_length = len(first_arg)
23252325+ # TODO: Ensure `tuple[..., str, str]` doesn't match `[1, 2]`.
23262326+ # Is this really necessary?
23272327+ # if first_length < (pre_index + post_length):
23282328+ # raise TypeError(
23292329+ # f"length of {first_arg} must be at least {(pre_index or 1) + post_length}"
23302330+ # )
23312331+ post_index = first_length - post_length
23322332+ items = (
23332333+ [
23342334+ t(i)
23352335+ for t, i in zip(
23362336+ pre_args, first_arg[:pre_index], strict=True
23372337+ )
23382338+ ]
23392339+ + first_arg[pre_index:post_index]
23402340+ + [
23412341+ t(i)
23422342+ for t, i in zip(
23432343+ post_args, first_arg[post_index:], strict=True
23442344+ )
23452345+ ]
23462346+ )
23472347+ else:
23482348+ items = [t(i) for t, i in zip(self_args, first_arg, strict=True)]
23492349+ elif isinmrosubclass(origin, abc.Mapping):
22882350 dict_first_arg = self_args[0]
22892351 dict_second_arg = self_args[1]
22902352 items = {
···22972359 args = (items, *args[1:])
22982360 return call(origin, *args, **kwargs)
2299236123002300- @defcache
23622362+ @cache
23012363 def __getattr__(self, attr):
23642364+ if len(attr) >= 44:
23652365+ raise AttributeError()
23022366 return getattr(typing.get_origin(self), attr)
2303236723042304- @defcache
23682368+ @cache
23052369 def __genericsupertype__(cls):
23062370 return type(cls)
23072371···24282492 )
242924932430249424312431-# TODO: Determine what will be private functions, convert them to `defcache`,
24952495+# TODO: Determine what will be private functions, convert them to `cache`,
24322496# and keep the user-facing functions as `cache`.