import hashlib
import re
import wtforms.widgets.core
from decimal import Decimal
from bs4 import BeautifulSoup
from unidecode import unidecode
from typing import overload, Any, TYPE_CHECKING
if TYPE_CHECKING:
from onegov.form import Form
from typing import Self
from wtforms.fields.core import UnboundField
[docs]
_unwanted_characters = re.compile(r'[^a-zA-Z0-9]+')
[docs]
original_html_params = wtforms.widgets.core.html_params
[docs]
def as_internal_id(label: str) -> str:
clean = unidecode(label).strip(' "\'').lower()
clean = _unwanted_characters.sub('_', clean)
return clean
[docs]
def get_fields_from_class(
cls: type['Form']
) -> list[tuple[str, 'UnboundField[Any]']]:
# often times FormMeta will have already calculated the fields
# and stored them on the class, so we only need to calculate
# them fresh if this attribute is None
if cls._unbound_fields is not None:
return cls._unbound_fields
# FIXME: this is transcribed from FormMeta.__call__, so it is
# a little fragile, perhaps we can come up with a way
# to safely call it regardless of what __new__/__init__
# on cls looks like, so we can re-use their code.
fields = [
(name, field)
for name in dir(cls)
if not name.startswith('_')
and hasattr((field := getattr(cls, name)), '_formfield')
]
fields.sort(key=lambda x: (x[1].creation_counter, x[0]))
return fields
# FIXME: What about html entities? i.e. &.*;
[docs]
class decimal_range: # noqa: N801
""" Implementation of Python's range builtin using decimal values instead
of integers.
"""
[docs]
__slots__ = ('start', 'stop', 'step', 'current')
def __init__(
self,
start: float | Decimal,
stop: float | Decimal,
step: str | float | Decimal | None = None
):
if step is None:
step = 1 if start <= stop else -1
self.start = self.current = Decimal(start)
[docs]
self.stop = Decimal(stop)
[docs]
self.step = Decimal(step)
assert self.step != Decimal(0)
[docs]
def __repr__(self) -> str:
if (
(self.start <= self.stop and self.step == Decimal('1.0'))
or (self.start >= self.stop and self.step == Decimal('-1.0'))
):
return f"decimal_range('{self.start}', '{self.stop}')"
return f"decimal_range('{self.start}', '{self.stop}', '{self.step}')"
[docs]
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return False
return (
self.start, self.stop, self.step
) == (
other.start, other.stop, other.step
)
[docs]
def __iter__(self) -> 'Self':
return self
[docs]
def __next__(self) -> Decimal:
result, self.current = self.current, self.current + self.step
if self.step > 0 and result >= self.stop:
raise StopIteration
if self.step < 0 and result <= self.stop:
raise StopIteration
return result
[docs]
def hash_definition(definition: str) -> str:
return hashlib.new( # nosec:B324
'md5',
definition.encode('utf-8'),
usedforsecurity=False
).hexdigest()
@overload
[docs]
def path_to_filename(path: None) -> None: ...
@overload
def path_to_filename(path: str) -> str: ...
def path_to_filename(path: str | None) -> str | None:
if not path:
return None
if not isinstance(path, str):
raise ValueError
if '/' in path:
return path.rsplit('/', 1)[-1]
if '\\' in path:
return path.rsplit('\\', 1)[-1]
return path
[docs]
def remove_empty_links(text: str) -> str:
# Find links with no text or other tags
# only br tags and/or whitespaces
soup = BeautifulSoup(str(text), 'html.parser')
for link in soup.find_all('a'):
if not any(
tag.name != 'br' and (
tag.name or not tag.isspace()
) for tag in link.contents
):
if all(tag.name == 'br' for tag in link.contents):
link.replace_with(
BeautifulSoup('<br/>', 'html.parser')
)
else:
link.decompose()
return str(soup)