Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
optmanager.py638 linesDownload Raw Back to mitmproxy
1from __future__ import annotations2 3import contextlib4import copy5import pprint6import textwrap7import weakref8from collections.abc import Callable9from collections.abc import Iterable10from collections.abc import Sequence11from dataclasses import dataclass12from pathlib import Path13from typing import Any14from typing import Optional15from typing import TextIO16 17import ruamel.yaml18 19from mitmproxy import exceptions20from mitmproxy.utils import signals21from mitmproxy.utils import typecheck22 23"""24    The base implementation for Options.25"""26 27unset = object()28 29 30class _Option:31    __slots__ = ("name", "typespec", "value", "_default", "choices", "help")32 33    def __init__(34        self,35        name: str,36        typespec: type | object,  # object for Optional[x], which is not a type.37        default: Any,38        help: str,39        choices: Sequence[str] | None,40    ) -> None:41        typecheck.check_option_type(name, default, typespec)42        self.name = name43        self.typespec = typespec44        self._default = default45        self.value = unset46        self.help = textwrap.dedent(help).strip().replace("\n", " ")47        self.choices = choices48 49    def __repr__(self):50        return f"{self.current()} [{self.typespec}]"51 52    @property53    def default(self):54        return copy.deepcopy(self._default)55 56    def current(self) -> Any:57        if self.value is unset:58            v = self.default59        else:60            v = self.value61        return copy.deepcopy(v)62 63    def set(self, value: Any) -> None:64        typecheck.check_option_type(self.name, value, self.typespec)65        self.value = value66 67    def reset(self) -> None:68        self.value = unset69 70    def has_changed(self) -> bool:71        return self.current() != self.default72 73    def __eq__(self, other) -> bool:74        for i in self.__slots__:75            if getattr(self, i) != getattr(other, i):76                return False77        return True78 79    def __deepcopy__(self, _):80        o = _Option(self.name, self.typespec, self.default, self.help, self.choices)81        if self.has_changed():82            o.value = self.current()83        return o84 85 86@dataclass87class _UnconvertedStrings:88    val: list[str]89 90 91def _sig_changed_spec(updated: set[str]) -> None:  # pragma: no cover92    ...  # expected function signature for OptManager.changed receivers.93 94 95def _sig_errored_spec(exc: Exception) -> None:  # pragma: no cover96    ...  # expected function signature for OptManager.errored receivers.97 98 99class OptManager:100    """101    OptManager is the base class from which Options objects are derived.102 103    .changed is a Signal that triggers whenever options are104    updated. If any handler in the chain raises an exceptions.OptionsError105    exception, all changes are rolled back, the exception is suppressed,106    and the .errored signal is notified.107 108    Optmanager always returns a deep copy of options to ensure that109    mutation doesn't change the option state inadvertently.110    """111 112    def __init__(self) -> None:113        self.deferred: dict[str, Any] = {}114        self.changed = signals.SyncSignal(_sig_changed_spec)115        self.changed.connect(self._notify_subscribers)116        self.errored = signals.SyncSignal(_sig_errored_spec)117        self._subscriptions: list[tuple[weakref.ref[Callable], set[str]]] = []118        # Options must be the last attribute here - after that, we raise an119        # error for attribute assignment to unknown options.120        self._options: dict[str, Any] = {}121 122    def add_option(123        self,124        name: str,125        typespec: type | object,126        default: Any,127        help: str,128        choices: Sequence[str] | None = None,129    ) -> None:130        self._options[name] = _Option(name, typespec, default, help, choices)131        self.changed.send(updated={name})132 133    @contextlib.contextmanager134    def rollback(self, updated, reraise=False):135        old = copy.deepcopy(self._options)136        try:137            yield138        except exceptions.OptionsError as e:139            # Notify error handlers140            self.errored.send(exc=e)141            # Rollback142            self.__dict__["_options"] = old143            self.changed.send(updated=updated)144            if reraise:145                raise e146 147    def subscribe(self, func, opts):148        """149        Subscribe a callable to the .changed signal, but only for a150        specified list of options. The callable should accept arguments151        (options, updated), and may raise an OptionsError.152 153        The event will automatically be unsubscribed if the callable goes out of scope.154        """155        for i in opts:156            if i not in self._options:157                raise exceptions.OptionsError("No such option: %s" % i)158 159        self._subscriptions.append((signals.make_weak_ref(func), set(opts)))160 161    def _notify_subscribers(self, updated) -> None:162        cleanup = False163        for ref, opts in self._subscriptions:164            callback = ref()165            if callback is not None:166                if opts & updated:167                    callback(self, updated)168            else:169                cleanup = True170 171        if cleanup:172            self.__dict__["_subscriptions"] = [173                (ref, opts) for (ref, opts) in self._subscriptions if ref() is not None174            ]175 176    def __eq__(self, other):177        if isinstance(other, OptManager):178            return self._options == other._options179        return False180 181    def __deepcopy__(self, memodict=None):182        o = OptManager()183        o.__dict__["_options"] = copy.deepcopy(self._options, memodict)184        return o185 186    __copy__ = __deepcopy__187 188    def __getattr__(self, attr):189        if attr in self._options:190            return self._options[attr].current()191        else:192            raise AttributeError("No such option: %s" % attr)193 194    def __setattr__(self, attr, value):195        # This is slightly tricky. We allow attributes to be set on the instance196        # until we have an _options attribute. After that, assignment is sent to197        # the update function, and will raise an error for unknown options.198        opts = self.__dict__.get("_options")199        if not opts:200            super().__setattr__(attr, value)201        else:202            self.update(**{attr: value})203 204    def keys(self):205        return set(self._options.keys())206 207    def items(self):208        return self._options.items()209 210    def __contains__(self, k):211        return k in self._options212 213    def reset(self):214        """215        Restore defaults for all options.216        """217        for o in self._options.values():218            o.reset()219        self.changed.send(updated=set(self._options.keys()))220 221    def update_known(self, **kwargs):222        """223        Update and set all known options from kwargs. Returns a dictionary224        of unknown options.225        """226        known, unknown = {}, {}227        for k, v in kwargs.items():228            if k in self._options:229                known[k] = v230            else:231                unknown[k] = v232        updated = set(known.keys())233        if updated:234            with self.rollback(updated, reraise=True):235                for k, v in known.items():236                    self._options[k].set(v)237                self.changed.send(updated=updated)238        return unknown239 240    def update_defer(self, **kwargs):241        unknown = self.update_known(**kwargs)242        self.deferred.update(unknown)243 244    def update(self, **kwargs):245        u = self.update_known(**kwargs)246        if u:247            raise KeyError("Unknown options: %s" % ", ".join(u.keys()))248 249    def setter(self, attr):250        """251        Generate a setter for a given attribute. This returns a callable252        taking a single argument.253        """254        if attr not in self._options:255            raise KeyError("No such option: %s" % attr)256 257        def setter(x):258            setattr(self, attr, x)259 260        return setter261 262    def toggler(self, attr):263        """264        Generate a toggler for a boolean attribute. This returns a callable265        that takes no arguments.266        """267        if attr not in self._options:268            raise KeyError("No such option: %s" % attr)269        o = self._options[attr]270        if o.typespec is not bool:271            raise ValueError("Toggler can only be used with boolean options")272 273        def toggle():274            setattr(self, attr, not getattr(self, attr))275 276        return toggle277 278    def default(self, option: str) -> Any:279        return self._options[option].default280 281    def has_changed(self, option):282        """283        Has the option changed from the default?284        """285        return self._options[option].has_changed()286 287    def merge(self, opts):288        """289        Merge a dict of options into this object. Options that have None290        value are ignored. Lists and tuples are appended to the current291        option value.292        """293        toset = {}294        for k, v in opts.items():295            if v is not None:296                if isinstance(v, (list, tuple)):297                    toset[k] = getattr(self, k) + v298                else:299                    toset[k] = v300        self.update(**toset)301 302    def __repr__(self):303        options = pprint.pformat(self._options, indent=4).strip(" {}")304        if "\n" in options:305            options = "\n    " + options + "\n"306        return "{mod}.{cls}({{{options}}})".format(307            mod=type(self).__module__, cls=type(self).__name__, options=options308        )309 310    def set(self, *specs: str, defer: bool = False) -> None:311        """312        Takes a list of set specification in standard form (option=value).313        Options that are known are updated immediately. If defer is true,314        options that are not known are deferred, and will be set once they315        are added.316 317        May raise an `OptionsError` if a value is malformed or an option is unknown and defer is False.318        """319        # First, group specs by option name.320        unprocessed: dict[str, list[str]] = {}321        for spec in specs:322            if "=" in spec:323                name, value = spec.split("=", maxsplit=1)324                unprocessed.setdefault(name, []).append(value)325            else:326                unprocessed.setdefault(spec, [])327 328        # Second, convert values to the correct type.329        processed: dict[str, Any] = {}330        for name in list(unprocessed.keys()):331            if name in self._options:332                processed[name] = self._parse_setval(333                    self._options[name], unprocessed.pop(name)334                )335 336        # Third, stash away unrecognized options or complain about them.337        if defer:338            self.deferred.update(339                {k: _UnconvertedStrings(v) for k, v in unprocessed.items()}340            )341        elif unprocessed:342            raise exceptions.OptionsError(343                f"Unknown option(s): {', '.join(unprocessed)}"344            )345 346        # Finally, apply updated options.347        self.update(**processed)348 349    def process_deferred(self) -> None:350        """351        Processes options that were deferred in previous calls to set, and352        have since been added.353        """354        update: dict[str, Any] = {}355        for optname, value in self.deferred.items():356            if optname in self._options:357                if isinstance(value, _UnconvertedStrings):358                    value = self._parse_setval(self._options[optname], value.val)359                update[optname] = value360        self.update(**update)361        for k in update.keys():362            del self.deferred[k]363 364    def _parse_setval(self, o: _Option, values: list[str]) -> Any:365        """366        Convert a string to a value appropriate for the option type.367        """368        if o.typespec == Sequence[str]:369            return values370        if len(values) > 1:371            raise exceptions.OptionsError(372                f"Received multiple values for {o.name}: {values}"373            )374 375        optstr: str | None376        if values:377            optstr = values[0]378        else:379            optstr = None380 381        if o.typespec in (str, Optional[str]):382            if o.typespec is str and optstr is None:383                raise exceptions.OptionsError(f"Option is required: {o.name}")384            return optstr385        elif o.typespec in (int, Optional[int]):386            if optstr:387                try:388                    return int(optstr)389                except ValueError:390                    raise exceptions.OptionsError(391                        f"Failed to parse option {o.name}: not an integer: {optstr}"392                    )393            elif o.typespec is int:394                raise exceptions.OptionsError(f"Option is required: {o.name}")395            else:396                return None397        elif o.typespec is bool:398            if optstr == "toggle":399                return not o.current()400            if not optstr or optstr == "true":401                return True402            elif optstr == "false":403                return False404            else:405                raise exceptions.OptionsError(406                    f'Failed to parse option {o.name}: boolean must be "true", "false", or have the value omitted (a synonym for "true").'407                )408        raise NotImplementedError(409            f"Failed to parse option {o.name}: unsupported option type: {o.typespec}"410        )411 412    def make_parser(self, parser, optname, metavar=None, short=None):413        """414        Auto-Create a command-line parser entry for a named option. If the415        option does not exist, it is ignored.416        """417        if optname not in self._options:418            return419 420        o = self._options[optname]421 422        def mkf(x, s):423            x = x.replace("_", "-")424            f = ["--%s" % x]425            if s:426                f.append("-" + s)427            return f428 429        flags = mkf(optname, short)430 431        if o.typespec is bool:432            g = parser.add_mutually_exclusive_group(required=False)433            onf = mkf(optname, None)434            offf = mkf("no-" + optname, None)435            # The short option for a bool goes to whatever is NOT the default436            if short:437                if o.default:438                    offf = mkf("no-" + optname, short)439                else:440                    onf = mkf(optname, short)441            g.add_argument(442                *offf,443                action="store_false",444                dest=optname,445            )446            g.add_argument(*onf, action="store_true", dest=optname, help=o.help)447            parser.set_defaults(**{optname: None})448        elif o.typespec in (int, Optional[int]):449            parser.add_argument(450                *flags,451                action="store",452                type=int,453                dest=optname,454                help=o.help,455                metavar=metavar,456            )457        elif o.typespec in (str, Optional[str]):458            parser.add_argument(459                *flags,460                action="store",461                type=str,462                dest=optname,463                help=o.help,464                metavar=metavar,465                choices=o.choices,466            )467        elif o.typespec == Sequence[str]:468            parser.add_argument(469                *flags,470                action="append",471                type=str,472                dest=optname,473                help=o.help + " May be passed multiple times.",474                metavar=metavar,475                choices=o.choices,476            )477        else:478            raise ValueError("Unsupported option type: %s", o.typespec)479 480 481def dump_defaults(opts, out: TextIO):482    """483    Dumps an annotated file with all options.484    """485    # Sort data486    s = ruamel.yaml.comments.CommentedMap()487    for k in sorted(opts.keys()):488        o = opts._options[k]489        s[k] = o.default490        txt = o.help.strip()491 492        if o.choices:493            txt += " Valid values are %s." % ", ".join(repr(c) for c in o.choices)494        else:495            t = typecheck.typespec_to_str(o.typespec)496            txt += " Type %s." % t497 498        txt = "\n".join(textwrap.wrap(txt))499        s.yaml_set_comment_before_after_key(k, before="\n" + txt)500    return ruamel.yaml.YAML().dump(s, out)501 502 503def dump_dicts(opts, keys: Iterable[str] | None = None) -> dict:504    """505    Dumps the options into a list of dict object.506 507    Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} }508    """509    options_dict = {}510    if keys is None:511        keys = opts.keys()512    for k in sorted(keys):513        o = opts._options[k]514        t = typecheck.typespec_to_str(o.typespec)515        option = {516            "type": t,517            "default": o.default,518            "value": o.current(),519            "help": o.help,520            "choices": o.choices,521        }522        options_dict[k] = option523    return options_dict524 525 526def parse(text):527    if not text:528        return {}529    try:530        yaml = ruamel.yaml.YAML(typ="safe", pure=True)531        data = yaml.load(text)532    except ruamel.yaml.error.YAMLError as v:533        if hasattr(v, "problem_mark"):534            snip = v.problem_mark.get_snippet()535            raise exceptions.OptionsError(536                "Config error at line %s:\n%s\n%s"537                % (v.problem_mark.line + 1, snip, getattr(v, "problem", ""))538            )539        else:540            raise exceptions.OptionsError("Could not parse options.")541    if isinstance(data, str):542        raise exceptions.OptionsError("Config error - no keys found.")543    elif data is None:544        return {}545    return data546 547 548def load(opts: OptManager, text: str, cwd: Path | str | None = None) -> None:549    """550    Load configuration from text, over-writing options already set in551    this object. May raise OptionsError if the config file is invalid.552    """553    data = parse(text)554 555    scripts = data.get("scripts")556    if scripts is not None and cwd is not None:557        data["scripts"] = [558            str(relative_path(Path(path), relative_to=Path(cwd))) for path in scripts559        ]560 561    opts.update_defer(**data)562 563 564def load_paths(opts: OptManager, *paths: Path | str) -> None:565    """566    Load paths in order. Each path takes precedence over the previous567    path. Paths that don't exist are ignored, errors raise an568    OptionsError.569    """570    for p in paths:571        p = Path(p).expanduser()572        if p.exists() and p.is_file():573            with p.open(encoding="utf8") as f:574                try:575                    txt = f.read()576                except UnicodeDecodeError as e:577                    raise exceptions.OptionsError(f"Error reading {p}: {e}")578            try:579                load(opts, txt, cwd=p.absolute().parent)580            except exceptions.OptionsError as e:581                raise exceptions.OptionsError(f"Error reading {p}: {e}")582 583 584def serialize(585    opts: OptManager, file: TextIO, text: str, defaults: bool = False586) -> None:587    """588    Performs a round-trip serialization. If text is not None, it is589    treated as a previous serialization that should be modified590    in-place.591 592    - If "defaults" is False, only options with non-default values are593        serialized. Default values in text are preserved.594    - Unknown options in text are removed.595    - Raises OptionsError if text is invalid.596    """597    data = parse(text)598    for k in opts.keys():599        if defaults or opts.has_changed(k):600            data[k] = getattr(opts, k)601    for k in list(data.keys()):602        if k not in opts._options:603            del data[k]604 605    ruamel.yaml.YAML().dump(data, file)606 607 608def save(opts: OptManager, path: Path | str, defaults: bool = False) -> None:609    """610    Save to path. If the destination file exists, modify it in-place.611 612    Raises OptionsError if the existing data is corrupt.613    """614    path = Path(path).expanduser()615    if path.exists() and path.is_file():616        with path.open(encoding="utf8") as f:617            try:618                data = f.read()619            except UnicodeDecodeError as e:620                raise exceptions.OptionsError(f"Error trying to modify {path}: {e}")621    else:622        data = ""623 624    with path.open("w", encoding="utf8") as f:625        serialize(opts, f, data, defaults)626 627 628def relative_path(script_path: Path | str, *, relative_to: Path | str) -> Path:629    """630    Make relative paths found in config files relative to said config file,631    instead of relative to where the command is ran.632    """633    script_path = Path(script_path)634    # Edge case when $HOME is not an absolute path635    if script_path.expanduser() != script_path and not script_path.is_absolute():636        script_path = script_path.expanduser().absolute()637    return (relative_to / script_path.expanduser()).absolute()638 
codekingpro/portable-devtools · Team Ai