Core: Allow common collections in OptionSet and OptionList constructors (#2874)

* allow common collection in set and list option constructors

* allow any iterable of strings

* add return None

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
This commit is contained in:
Jérémie Bolduc 2024-03-03 16:30:51 -05:00 committed by GitHub
parent 113c54f9be
commit 37a871eab1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 25 additions and 12 deletions

View File

@ -1,19 +1,18 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import logging
from copy import deepcopy
from dataclasses import dataclass
import functools import functools
import logging
import math import math
import numbers import numbers
import random import random
import typing import typing
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass
from schema import And, Optional, Or, Schema from schema import And, Optional, Or, Schema
from Utils import get_fuzzy_results from Utils import get_fuzzy_results, is_iterable_of_str
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
@ -59,6 +58,7 @@ class AssembleOptions(abc.ABCMeta):
def verify(self, *args, **kwargs) -> None: def verify(self, *args, **kwargs) -> None:
for f in verifiers: for f in verifiers:
f(self, *args, **kwargs) f(self, *args, **kwargs)
attrs["verify"] = verify attrs["verify"] = verify
else: else:
assert verifiers, "class Option is supposed to implement def verify" assert verifiers, "class Option is supposed to implement def verify"
@ -183,6 +183,7 @@ class FreeText(Option[str]):
class NumericOption(Option[int], numbers.Integral, abc.ABC): class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0 default = 0
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards # note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs # `int` is not a `numbers.Integral` according to the official typestubs
# (even though isinstance(5, numbers.Integral) == True) # (even though isinstance(5, numbers.Integral) == True)
@ -765,7 +766,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
value: typing.Any value: typing.Any
@classmethod @classmethod
def verify_keys(cls, data: typing.List[str]): def verify_keys(cls, data: typing.Iterable[str]) -> None:
if cls.valid_keys: if cls.valid_keys:
data = set(data) data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
@ -843,11 +844,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead. # If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
# Not a docstring so it doesn't get grabbed by the options system. # Not a docstring so it doesn't get grabbed by the options system.
default: typing.List[typing.Any] = [] default: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any, ...]] = ()
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.List[typing.Any]): def __init__(self, value: typing.Iterable[str]):
self.value = deepcopy(value) self.value = list(deepcopy(value))
super(OptionList, self).__init__() super(OptionList, self).__init__()
@classmethod @classmethod
@ -856,7 +857,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if type(data) == list: if is_iterable_of_str(data):
cls.verify_keys(data) cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@ -882,7 +883,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if isinstance(data, (list, set, frozenset)): if is_iterable_of_str(data):
cls.verify_keys(data) cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))

View File

@ -19,6 +19,7 @@ import warnings
from argparse import Namespace from argparse import Namespace
from settings import Settings, get_settings from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump from yaml import load, load_all, dump
try: try:
@ -966,3 +967,13 @@ class RepeatableChain:
def __len__(self): def __len__(self):
return sum(len(iterable) for iterable in self.iterable) return sum(len(iterable) for iterable in self.iterable)
def is_iterable_of_str(obj: object) -> TypeGuard[typing.Iterable[str]]:
""" but not a `str` (because technically, `str` is `Iterable[str]`) """
if isinstance(obj, str):
return False
if not isinstance(obj, typing.Iterable):
return False
obj_it: typing.Iterable[object] = obj
return all(isinstance(v, str) for v in obj_it)

View File

@ -11,3 +11,4 @@ certifi>=2023.11.17
cython>=3.0.8 cython>=3.0.8
cymem>=2.0.8 cymem>=2.0.8
orjson>=3.9.10 orjson>=3.9.10
typing-extensions>=4.7.0