Handle ruff complaints in string module

This commit is contained in:
Dominik Jain 2023-10-03 20:59:06 +02:00
parent de70147752
commit ce26e25923
2 changed files with 128 additions and 110 deletions

View File

@ -125,6 +125,8 @@ ignore = [
"B011", # assert false. we don't use python -O
"B028", # we don't need explicit stacklevel for warnings
"D100", "D101", "D102", "D104", "D105", "D107", "D203", "D213", "D401", "D402", "D106", "D205", # docstring stuff
"G004", # logging (no f-strings)
"RUF012", # disallows mutable class variables unless annotated
"DTZ005", # we don't need that
"RET505", # sacrifices visual discernability of control flow paths for brevity (regarding return statements)
# remaining rules from https://github.com/psf/black/blob/main/.flake8 (except W503)

View File

@ -1,6 +1,4 @@
"""
This is a copy of sensai.util.string from sensAI commit d7b4afcc89b4d2e922a816cb07dffde27f297354
"""
"""Copy of sensai.util.string from sensAI commit d7b4afcc89b4d2e922a816cb07dffde27f297354."""
import functools
@ -9,7 +7,10 @@ import re
import sys
import types
from abc import ABC, abstractmethod
from typing import Union, List, Dict, Any, Sequence, Iterable, Optional, Mapping, Callable
from collections.abc import Callable, Iterable, Mapping, Sequence
from typing import (
Any,
)
reCommaWhitespacePotentiallyBreaks = re.compile(r",\s+")
@ -17,18 +18,16 @@ log = logging.getLogger(__name__)
class StringConverter(ABC):
"""
Abstraction for a string conversion mechanism
"""
"""Abstraction for a string conversion mechanism."""
@abstractmethod
def to_string(self, x) -> str:
pass
def dict_string(d: Mapping, brackets: Optional[str] = None, converter: StringConverter = None):
"""
Converts a dictionary to a string of the form "<key>=<value>, <key>=<value>, ...", optionally enclosed
by brackets
def dict_string(d: Mapping, brackets: str | None = None, converter: StringConverter = None):
"""Converts a dictionary to a string of the form "<key>=<value>, <key>=<value>, ...", optionally enclosed
by brackets.
:param d: the dictionary
:param brackets: a two-character string containing the opening and closing bracket to use, e.g. ``"{}"``;
@ -36,16 +35,20 @@ def dict_string(d: Mapping, brackets: Optional[str] = None, converter: StringCon
:param converter: the string converter to use for values
:return: the string representation
"""
s = ', '.join([f'{k}={to_string(v, converter=converter, context=k)}' for k, v in d.items()])
s = ", ".join([f"{k}={to_string(v, converter=converter, context=k)}" for k, v in d.items()])
if brackets is not None:
return brackets[:1] + s + brackets[-1:]
else:
return s
def list_string(l: Iterable[Any], brackets="[]", quote: Optional[str] = None, converter: StringConverter = None):
"""
Converts a list or any other iterable to a string of the form "[<value>, <value>, ...]", optionally enclosed
def list_string(
l: Iterable[Any],
brackets="[]",
quote: str | None = None,
converter: StringConverter = None,
):
"""Converts a list or any other iterable to a string of the form "[<value>, <value>, ...]", optionally enclosed
by different brackets or with the values quoted.
:param l: the list
@ -55,22 +58,28 @@ def list_string(l: Iterable[Any], brackets="[]", quote: Optional[str] = None, co
:param converter: the string converter to use for values
:return: the string representation
"""
def item(x):
x = to_string(x, converter=converter, context="list")
if quote is not None:
return quote + x + quote
else:
return x
s = ", ".join((item(x) for x in l))
s = ", ".join(item(x) for x in l)
if brackets is not None:
return brackets[:1] + s + brackets[-1:]
else:
return s
def to_string(x, converter: StringConverter = None, apply_converter_to_non_complex_objects=True, context=None):
"""
Converts the given object to a string, with proper handling of lists, tuples and dictionaries, optionally using a converter.
def to_string(
x,
converter: StringConverter = None,
apply_converter_to_non_complex_objects=True,
context=None,
):
"""Converts the given object to a string, with proper handling of lists, tuples and dictionaries, optionally using a converter.
The conversion also removes unwanted line breaks (as present, in particular, in sklearn's string representations).
:param x: the object to convert
@ -83,13 +92,13 @@ def to_string(x, converter: StringConverter = None, apply_converter_to_non_compl
:return: the string representation
"""
try:
if type(x) == list:
if isinstance(x, list):
return list_string(x, converter=converter)
elif type(x) == tuple:
elif isinstance(x, tuple):
return list_string(x, brackets="()", converter=converter)
elif type(x) == dict:
elif isinstance(x, dict):
return dict_string(x, brackets="{}", converter=converter)
elif type(x) == types.MethodType:
elif isinstance(x, types.MethodType):
# could be bound method of a ToStringMixin instance (which would print the repr of the instance, which can potentially cause
# an infinite recursion)
return f"Method[{x.__name__}]"
@ -100,16 +109,19 @@ def to_string(x, converter: StringConverter = None, apply_converter_to_non_compl
s = str(x)
# remove any unwanted line breaks and indentation after commas (as generated, for example, by sklearn objects)
s = reCommaWhitespacePotentiallyBreaks.sub(", ", s)
return reCommaWhitespacePotentiallyBreaks.sub(", ", s)
return s
except RecursionError as e:
except RecursionError:
log.error(f"Recursion in string conversion detected; context={context}")
raise
def object_repr(obj, member_names_or_dict: Union[List[str], Dict[str, Any]]):
if type(member_names_or_dict) == dict:
def object_repr(obj, member_names_or_dict: list[str] | dict[str, Any]):
"""Creates a string representation for the given object based on the given members.
The string takes the form "ClassName[attr1=value1, attr2=value2, ...]"
"""
if isinstance(member_names_or_dict, dict):
members_dict = member_names_or_dict
else:
members_dict = {m: to_string(getattr(obj, m)) for m in member_names_or_dict}
@ -117,9 +129,7 @@ def object_repr(obj, member_names_or_dict: Union[List[str], Dict[str, Any]]):
def or_regex_group(allowed_names: Sequence[str]):
"""
:param allowed_names: strings to include as literals in the regex
""":param allowed_names: strings to include as literals in the regex
:return: a regular expression string of the form (<name1>| ...|<nameN>), which any of the given names
"""
allowed_names = [re.escape(name) for name in allowed_names]
@ -127,6 +137,12 @@ def or_regex_group(allowed_names: Sequence[str]):
def function_name(x: Callable) -> str:
"""Attempts to retrieve the name of the given function/callable object, taking the possibility
of the function being defined via functools.partial into account.
:param x: a callable object
:return: name of the function or str(x) as a fallback
"""
if isinstance(x, functools.partial):
return function_name(x.func)
elif hasattr(x, "__name__"):
@ -136,8 +152,7 @@ def function_name(x: Callable) -> str:
class ToStringMixin:
"""
Provides implementations for ``__str__`` and ``__repr__`` which are based on the format ``"<class name>[<object info>]"`` and
"""Provides implementations for ``__str__`` and ``__repr__`` which are based on the format ``"<class name>[<object info>]"`` and
``"<class name>[id=<object id>, <object info>]"`` respectively, where ``<object info>`` is usually a list of entries of the
form ``"<name>=<value>, ..."``.
@ -174,23 +189,23 @@ class ToStringMixin:
.. automethod:: _tostring_additional_entries
.. automethod:: _tostring_exclude_private
"""
_TOSTRING_INCLUDE_ALL = "__all__"
def _tostring_class_name(self):
"""
:return: the string use for <class name> in the string representation ``"<class name>[<object info]"``
"""
""":return: the string use for <class name> in the string representation ``"<class name>[<object info]"``"""
return type(self).__qualname__
def _tostring_properties(self,
exclude: Optional[Union[str, Iterable[str]]] = None,
include: Optional[Union[str, Iterable[str]]] = None,
exclude_exceptions: Optional[List[str]] = None,
include_forced: Optional[List[str]] = None,
additional_entries: Dict[str, Any] = None,
converter: StringConverter = None) -> str:
"""
Creates a string of the class attributes, with optional exclusions/inclusions/additions.
def _tostring_properties(
self,
exclude: str | Iterable[str] | None = None,
include: str | Iterable[str] | None = None,
exclude_exceptions: list[str] | None = None,
include_forced: list[str] | None = None,
additional_entries: dict[str, Any] | None = None,
converter: StringConverter = None,
) -> str:
"""Creates a string of the class attributes, with optional exclusions/inclusions/additions.
Exclusions take precedence over inclusions.
:param exclude: attributes to be excluded
@ -201,10 +216,11 @@ class ToStringMixin:
:param converter: the string converter to use; if None, use default (which avoids infinite recursions)
:return: a string containing entry/property names and values
"""
def mklist(x):
if x is None:
return []
if type(x) == str:
if isinstance(x, str):
return [x]
return x
@ -219,17 +235,21 @@ class ToStringMixin:
if k in exclude:
return True
if self._tostring_exclude_private():
is_private = k.startswith("_")
return is_private
return k.startswith("_")
else:
return False
# determine relevant attribute dictionary
if len(include) == 1 and include[0] == self._TOSTRING_INCLUDE_ALL: # exclude semantics (include everything by default)
if (
len(include) == 1 and include[0] == self._TOSTRING_INCLUDE_ALL
): # exclude semantics (include everything by default)
attribute_dict = self.__dict__
else: # include semantics (include only inclusions)
attribute_dict = {k: getattr(self, k) for k in set(include + include_forced)
if hasattr(self, k) and k != self._TOSTRING_INCLUDE_ALL}
attribute_dict = {
k: getattr(self, k)
for k in set(include + include_forced)
if hasattr(self, k) and k != self._TOSTRING_INCLUDE_ALL
}
# apply exclusions and remove underscores from attribute names
d = {k.strip("_"): v for k, v in attribute_dict.items() if not is_excluded(k)}
@ -242,8 +262,7 @@ class ToStringMixin:
return dict_string(d, converter=converter)
def _tostring_object_info(self) -> str:
"""
Override this method to use a fully custom definition of the ``<object info>`` part in the full string
"""Override this method to use a fully custom definition of the ``<object info>`` part in the full string
representation ``"<class name>[<object info>]"`` to be generated.
As soon as this method is overridden, any property-based exclusions, inclusions, etc. will have no effect
(unless the implementation is specifically designed to make use of them - as is the default
@ -253,13 +272,16 @@ class ToStringMixin:
:return: a string containing the string to use for ``<object info>``
"""
return self._tostring_properties(exclude=self._tostring_excludes(), include=self._tostring_includes(),
exclude_exceptions=self._tostring_exclude_exceptions(), include_forced=self._tostring_includes_forced(),
additional_entries=self._tostring_additional_entries())
return self._tostring_properties(
exclude=self._tostring_excludes(),
include=self._tostring_includes(),
exclude_exceptions=self._tostring_exclude_exceptions(),
include_forced=self._tostring_includes_forced(),
additional_entries=self._tostring_additional_entries(),
)
def _tostring_excludes(self) -> List[str]:
"""
Makes the string representation exclude the returned attributes.
def _tostring_excludes(self) -> list[str]:
"""Makes the string representation exclude the returned attributes.
This method can be conveniently overridden by subclasses which can call super and extend the list returned.
This method will only have no effect if :meth:`_toStringObjectInfo` is overridden to not use its result.
@ -268,9 +290,8 @@ class ToStringMixin:
"""
return []
def _tostring_includes(self) -> List[str]:
"""
Makes the string representation include only the returned attributes (i.e. introduces inclusion semantics);
def _tostring_includes(self) -> list[str]:
"""Makes the string representation include only the returned attributes (i.e. introduces inclusion semantics);
By default, the list contains only a marker element, which is interpreted as "all attributes included".
This method can be conveniently overridden by sub-classes which can call super and extend the list returned.
@ -287,9 +308,8 @@ class ToStringMixin:
return [self._TOSTRING_INCLUDE_ALL]
# noinspection PyMethodMayBeStatic
def _tostring_includes_forced(self) -> List[str]:
"""
Defines a list of attribute names that are required to be present in the string representation, regardless of the
def _tostring_includes_forced(self) -> list[str]:
"""Defines a list of attribute names that are required to be present in the string representation, regardless of the
instance using include semantics or exclude semantics, thus facilitating added inclusions in sub-classes.
This method will have no effect if :meth:`_toStringObjectInfo` is overridden to not use its result.
@ -298,22 +318,18 @@ class ToStringMixin:
"""
return []
def _tostring_additional_entries(self) -> Dict[str, Any]:
"""
:return: a dictionary of entries to be included in the ``<object info>`` part of the string representation
"""
def _tostring_additional_entries(self) -> dict[str, Any]:
""":return: a dictionary of entries to be included in the ``<object info>`` part of the string representation"""
return {}
def _tostring_exclude_private(self) -> bool:
"""
:return: whether to exclude properties that are private (start with an underscore); explicitly included attributes
will still be considered - as will properties exempt from the rule via :meth:`toStringExcludeException`.
""":return: whether to exclude properties that are private (start with an underscore); explicitly included attributes
will still be considered - as will properties exempt from the rule via :meth:`toStringExcludeException`.
"""
return False
def _tostring_exclude_exceptions(self) -> List[str]:
"""
Defines attribute names which should not be excluded even though other rules (particularly the exclusion of private members
def _tostring_exclude_exceptions(self) -> list[str]:
"""Defines attribute names which should not be excluded even though other rules (particularly the exclusion of private members
via :meth:`_toStringExcludePrivate`) would otherwise exclude them.
:return: a list of attribute names
@ -331,8 +347,7 @@ class ToStringMixin:
return f"{self._tostring_class_name()}[{info}]"
def pprint(self, file=sys.stdout):
"""
Prints a prettily formatted string representation of the object (with line breaks and indentations)
"""Prints a prettily formatted string representation of the object (with line breaks and indentations)
to ``stdout`` or the given file.
:param file: the file to print to
@ -340,25 +355,21 @@ class ToStringMixin:
print(self.pprints(), file=file)
def pprints(self) -> str:
"""
:return: a prettily formatted string representation with line breaks and indentations
"""
""":return: a prettily formatted string representation with line breaks and indentations"""
return pretty_string_repr(self)
class _StringConverterAvoidToStringMixinRecursion(StringConverter):
"""
Avoids recursions when converting objects implementing :class:`ToStringMixin` which may contain themselves to strings.
"""Avoids recursions when converting objects implementing :class:`ToStringMixin` which may contain themselves to strings.
Use of this object prevents infinite recursions caused by a :class:`ToStringMixin` instance recursively containing itself in
either a property of another :class:`ToStringMixin`, a list or a tuple.
It handles all :class:`ToStringMixin` instances recursively encountered.
A previously handled instance is converted to a string of the form "<class name>[<<]".
"""
def __init__(self, *handled_objects: "ToStringMixin"):
"""
:param handled_objects: objects which are initially assumed to have been handled already
"""
self._handled_to_string_mixin_ids = set([id(o) for o in handled_objects])
""":param handled_objects: objects which are initially assumed to have been handled already"""
self._handled_to_string_mixin_ids = {id(o) for o in handled_objects}
def to_string(self, x) -> str:
if isinstance(x, ToStringMixin):
@ -368,11 +379,15 @@ class ToStringMixin:
self._handled_to_string_mixin_ids.add(oid)
return str(self._ToStringMixinProxy(x, self))
else:
return to_string(x, converter=self, apply_converter_to_non_complex_objects=False, context=x.__class__)
return to_string(
x,
converter=self,
apply_converter_to_non_complex_objects=False,
context=x.__class__,
)
class _ToStringMixinProxy:
"""
A proxy object which wraps a ToStringMixin to ensure that the converter is applied when creating the properties string.
"""A proxy object which wraps a ToStringMixin to ensure that the converter is applied when creating the properties string.
The proxy is to achieve that all ToStringMixin methods that aren't explicitly overwritten are bound to this proxy
(rather than the original object), such that the transitive call to _toStringProperties will call the new
implementation.
@ -392,9 +407,15 @@ class ToStringMixin:
return self.x._tostring_class_name()
def __getattr__(self, attr: str):
if attr.startswith("_tostring"): # ToStringMixin method which we may bind to use this proxy to ensure correct transitive call
if attr.startswith(
"_tostring",
): # ToStringMixin method which we may bind to use this proxy to ensure correct transitive call
method = getattr(self.x.__class__, attr)
obj = self if attr in self.TOSTRING_METHODS_TRANSITIVELY_CALLING_TOSTRINGPROPERTIES else self.x
obj = (
self
if attr in self.TOSTRING_METHODS_TRANSITIVELY_CALLING_TOSTRINGPROPERTIES
else self.x
)
return lambda *args, **kwargs: method(obj, *args, **kwargs)
else:
return getattr(self.x, attr)
@ -404,8 +425,7 @@ class ToStringMixin:
def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string=" "):
"""
Creates a pretty string representation (using indentations) from the given object/string representation (as generated, for example, via
"""Creates a pretty string representation (using indentations) from the given object/string representation (as generated, for example, via
ToStringMixin). An indentation level is added for every opening bracket.
:param s: an object or object string representation
@ -413,7 +433,7 @@ def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string="
:param indentation_string: the string which corresponds to a single indentation level
:return: a reformatted version of the input string with added indentations and line breaks
"""
if type(s) != str:
if not isinstance(s, str):
s = str(s)
indent = initial_indentation_level
result = indentation_string * indent
@ -425,7 +445,7 @@ def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string="
def take(cnt=1):
nonlocal result, i
result += s[i:i+cnt]
result += s[i : i + cnt]
i += cnt
def find_matching(j):
@ -455,9 +475,11 @@ def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string="
if i_match is not None:
k = i_match + 1
full_match = s[i:k]
take_full_match_without_break = is_quote or not("=" in full_match and "," in full_match)
take_full_match_without_break = is_quote or not (
"=" in full_match and "," in full_match
)
if take_full_match_without_break:
take(k-i)
take(k - i)
if not take_full_match_without_break:
take(1)
indent += 1
@ -465,7 +487,7 @@ def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string="
elif s[i] in "])":
take(1)
indent -= 1
elif s[i:i+2] == ", ":
elif s[i : i + 2] == ", ":
take(2)
nl()
else:
@ -475,12 +497,10 @@ def pretty_string_repr(s: Any, initial_indentation_level=0, indentation_string="
class TagBuilder:
"""
Assists in building strings made up of components that are joined via a glue string
"""
"""Assists in building strings made up of components that are joined via a glue string."""
def __init__(self, *initial_components: str, glue="_"):
"""
:param initial_components: initial components to always include at the beginning
""":param initial_components: initial components to always include at the beginning
:param glue: the glue string which joins components
"""
self.glue = glue
@ -490,8 +510,7 @@ class TagBuilder:
self.components.append(component)
def with_conditional(self, cond: bool, component: str):
"""
Conditionally adds the given component
"""Conditionally adds the given component.
:param cond: the condition
:param component: the component to add if the condition holds
@ -502,8 +521,7 @@ class TagBuilder:
return self
def with_alternative(self, cond: bool, true_component: str, false_component: str):
"""
Adds a component depending on a condition
"""Adds a component depending on a condition.
:param cond: the condition
:param true_component: the component to add if the condition holds
@ -514,7 +532,5 @@ class TagBuilder:
return self
def build(self):
"""
:return: the string (with all components joined)
"""
""":return: the string (with all components joined)"""
return self.glue.join(self.components)