diff --git a/CHANGES.rst b/CHANGES.rst index c30eb3663..1b7421c3f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,10 @@ Minor changes ~~~~~~~~~~~~~ - Deprecate ``icalendar.parser.escape_string`` and ``icalendar.parser.unescape_string`` for icalendar version 8. Use ``_escape_string`` and ``_unescape_string`` internally. :issue:`1011` +- Deprecate ``icalendar.parser.q_split``, ``icalendar.parser.q_join``, and ``icalendar.parser.foldline`` for icalendar version 8. Use ``_q_split``, ``_q_join``, and ``_foldline`` internally. :issue:`1011` +- Deprecate ``icalendar.parser.dquote``, ``icalendar.parser.param_value``, ``icalendar.parser.validate_param_value``, ``icalendar.parser.rfc_6868_escape``, and ``icalendar.parser.rfc_6868_unescape`` for icalendar version 8. Use ``_dquote``, ``_param_value``, ``_validate_param_value``, ``_rfc_6868_escape``, and ``_rfc_6868_unescape`` internally. :issue:`1011` +- Deprecate ``icalendar.parser.escape_char``, ``icalendar.parser.unescape_char``, and ``icalendar.parser.validate_token`` for icalendar version 8. Use ``_escape_char``, ``_unescape_char``, and ``_validate_token`` internally. :issue:`1011` +- Deprecate ``icalendar.parser.unescape_list_or_string``, ``icalendar.parser.unescape_backslash``, ``icalendar.parser.split_on_unescaped_comma``, and ``icalendar.parser.split_on_unescaped_semicolon`` for icalendar version 8. Use ``_unescape_list_or_string``, ``_unescape_backslash``, ``_split_on_unescaped_comma``, and ``_split_on_unescaped_semicolon`` internally. :issue:`1011` - Added behavioral tests for :class:`~icalendar.cal.lazy.LazyCalendar` covering serialization round-trips, ``.todos``, ``.journals``, forward timezone references, and ``with_uid()`` substring false-positives. :issue:`1050` - Added edge case tests for :class:`~icalendar.prop.conference.Conference` parameter normalization covering string passthrough, empty list filtering, and None omission. :issue:`925` - Make icalendar an explicit editable install for clarity. :pr:`1268` diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index 31d75b3fd..8b66298f2 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -48,6 +48,8 @@ # chars. from icalendar.parser import ( Parameters, + _q_join, + _q_split, q_join, q_split, ) @@ -143,6 +145,8 @@ "TypesFactory", "__version__", "__version_tuple__", + "_q_join", + "_q_split", "is_utc", "q_join", "q_split", diff --git a/src/icalendar/cal/component.py b/src/icalendar/cal/component.py index c0893d50f..417a9e366 100644 --- a/src/icalendar/cal/component.py +++ b/src/icalendar/cal/component.py @@ -27,8 +27,8 @@ Contentline, Contentlines, Parameters, - q_join, - q_split, + _q_join, + _q_split, ) from icalendar.parser.ical.component import ComponentIcalParser from icalendar.parser_tools import DEFAULT_ENCODING @@ -372,7 +372,7 @@ def decoded(self, name: str, default: Any = _marker) -> Any: def get_inline(self, name, decode=1): """Returns a list of values (split on comma).""" - vals = [v.strip('" ') for v in q_split(self[name])] + vals = [v.strip('" ') for v in _q_split(self[name])] if decode: return [self._decode(name, val) for val in vals] return vals @@ -383,7 +383,7 @@ def set_inline(self, name, values, encode=1): """ if encode: values = [self._encode(name, value, encode=1) for value in values] - self[name] = self.types_factory["inline"](q_join(values)) + self[name] = self.types_factory["inline"](_q_join(values)) ######################### # Handling of components diff --git a/src/icalendar/caselessdict.py b/src/icalendar/caselessdict.py index f34f92cf4..031acd725 100644 --- a/src/icalendar/caselessdict.py +++ b/src/icalendar/caselessdict.py @@ -1,9 +1,10 @@ from __future__ import annotations +import warnings from collections import OrderedDict from typing import TYPE_CHECKING, Any, TypeVar -from icalendar.parser_tools import to_unicode +from icalendar.parser_tools import _to_unicode if TYPE_CHECKING: from collections.abc import Iterable, Mapping @@ -17,7 +18,7 @@ VT = TypeVar("VT") -def canonsort_keys( +def _canonsort_keys( keys: Iterable[KT], canonical_order: Iterable[KT] | None = None ) -> list[KT]: """Sort leading keys according to a canonical order. @@ -38,8 +39,8 @@ def canonsort_keys( Example: .. code-block:: pycon - >>> from icalendar.caselessdict import canonsort_keys - >>> canonsort_keys(["C", "A", "B"], ["B", "C"]) + >>> from icalendar.caselessdict import _canonsort_keys + >>> _canonsort_keys(["C", "A", "B"], ["B", "C"]) ['B', 'C', 'A'] """ canonical_map = {k: i for i, k in enumerate(canonical_order or [])} @@ -48,7 +49,26 @@ def canonsort_keys( return sorted(head, key=lambda k: canonical_map[k]) + sorted(tail) -def canonsort_items( +def canonsort_keys( + keys: Iterable[KT], canonical_order: Iterable[KT] | None = None +) -> list[KT]: + """Sort leading keys according to a canonical order. + + .. deprecated:: 7.0.0 + Use the private :func:`_canonsort_keys` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "canonsort_keys is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _canonsort_keys(keys, canonical_order) + + +def _canonsort_items( dict1: Mapping[KT, VT], canonical_order: Iterable[KT] | None = None ) -> list[tuple[KT, VT]]: """Sort items from a mapping according to a canonical key order. @@ -64,11 +84,30 @@ def canonsort_items( Example: .. code-block:: pycon - >>> from icalendar.caselessdict import canonsort_items - >>> canonsort_items({"C": 3, "A": 1, "B": 2}, ["B", "C"]) + >>> from icalendar.caselessdict import _canonsort_items + >>> _canonsort_items({"C": 3, "A": 1, "B": 2}, ["B", "C"]) [('B', 2), ('C', 3), ('A', 1)] """ - return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)] + return [(k, dict1[k]) for k in _canonsort_keys(dict1.keys(), canonical_order)] + + +def canonsort_items( + dict1: Mapping[KT, VT], canonical_order: Iterable[KT] | None = None +) -> list[tuple[KT, VT]]: + """Sort items from a mapping according to a canonical key order. + + .. deprecated:: 7.0.0 + Use the private :func:`_canonsort_items` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "canonsort_items is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _canonsort_items(dict1, canonical_order) class CaselessDict(OrderedDict): @@ -100,7 +139,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """ super().__init__(*args, **kwargs) for key, value in self.items(): - key_upper = to_unicode(key).upper() + key_upper = _to_unicode(key).upper() if key != key_upper: super().__delitem__(key) self[key_upper] = value @@ -120,7 +159,7 @@ def __getitem__(self, key: Any) -> Any: Raises: KeyError: If the key is not found. """ - key = to_unicode(key) + key = _to_unicode(key) return super().__getitem__(key.upper()) def __setitem__(self, key: Any, value: Any) -> None: @@ -130,7 +169,7 @@ def __setitem__(self, key: Any, value: Any) -> None: key: The key of the pair, case-insensitive. value: The value to associate with the key. """ - key = to_unicode(key) + key = _to_unicode(key) super().__setitem__(key.upper(), value) def __delitem__(self, key: Any) -> None: @@ -142,7 +181,7 @@ def __delitem__(self, key: Any) -> None: Raises: KeyError: If the key is not found. """ - key = to_unicode(key) + key = _to_unicode(key) super().__delitem__(key.upper()) def __contains__(self, key: Any) -> bool: @@ -154,7 +193,7 @@ def __contains__(self, key: Any) -> bool: Returns: ``True`` if the uppercased key exists, else ``False``. """ - key = to_unicode(key) + key = _to_unicode(key) return super().__contains__(key.upper()) def get(self, key: Any, default: Any = None) -> Any: @@ -167,7 +206,7 @@ def get(self, key: Any, default: Any = None) -> Any: Returns: The value for the key, if present, else the value specified by ``default``. """ - key = to_unicode(key) + key = _to_unicode(key) return super().get(key.upper(), default) def setdefault(self, key: Any, value: Any = None) -> Any: @@ -182,7 +221,7 @@ def setdefault(self, key: Any, value: Any = None) -> Any: Returns: The value for the key. """ - key = to_unicode(key) + key = _to_unicode(key) return super().setdefault(key.upper(), value) def pop(self, key: Any, default: Any = None) -> Any: @@ -195,7 +234,7 @@ def pop(self, key: Any, default: Any = None) -> Any: Returns: The removed value, or the value of ``default``. """ - key = to_unicode(key) + key = _to_unicode(key) return super().pop(key.upper(), default) def popitem(self) -> tuple[Any, Any]: @@ -220,7 +259,7 @@ def has_key(self, key: Any) -> bool: Returns: ``True`` if the key exists, else ``False``. """ - key = to_unicode(key) + key = _to_unicode(key) return super().__contains__(key.upper()) def update(self, *args: Any, **kwargs: Any) -> None: @@ -298,7 +337,7 @@ def sorted_keys(self) -> list[str]: Returns: A sorted list of keys. """ - return canonsort_keys(self.keys(), self.canonical_order) + return _canonsort_keys(self.keys(), self.canonical_order) def sorted_items(self) -> list[tuple[Any, Any]]: """Sort items according to the canonical order for this class. @@ -309,7 +348,13 @@ def sorted_items(self) -> list[tuple[Any, Any]]: Returns: A sorted list of (key, value) tuples. """ - return canonsort_items(self, self.canonical_order) + return _canonsort_items(self, self.canonical_order) -__all__ = ["CaselessDict", "canonsort_items", "canonsort_keys"] +__all__ = [ + "CaselessDict", + "_canonsort_items", + "_canonsort_keys", + "canonsort_items", + "canonsort_keys", +] diff --git a/src/icalendar/compatibility.py b/src/icalendar/compatibility.py index 4a5b95b06..e096e6ac4 100644 --- a/src/icalendar/compatibility.py +++ b/src/icalendar/compatibility.py @@ -7,8 +7,50 @@ Members will be added and removed without deprecation warnings. """ +import functools +import warnings from typing import TYPE_CHECKING + +def deprecate_for_version_8(func): + """Return a deprecated public alias for *func*. + + Wraps *func* so that every call emits a :exc:`DeprecationWarning` and + then delegates to the original implementation. The public name used in + the warning message is derived from ``func.__name__`` by stripping a + leading underscore. + + Use like this:: + + def _q_join(...): + \"\"\"docstring...\"\"\" + ... + + q_join = deprecate_for_version_8(_q_join) + + Parameters: + func: The private implementation to wrap. + + Returns: + A wrapper with the same signature and docstring as *func* that emits + a :exc:`DeprecationWarning` on every call. + """ + public_name = func.__name__.lstrip("_") + + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"{public_name} is deprecated and will be removed in icalendar 8. " + "If you are using this function externally, " + "please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + try: from typing import Self except ImportError: @@ -37,4 +79,5 @@ "Self", "TypeGuard", "TypeIs", + "deprecate_for_version_8", ] diff --git a/src/icalendar/parser/__init__.py b/src/icalendar/parser/__init__.py index 57b2938a7..22ef77ed7 100644 --- a/src/icalendar/parser/__init__.py +++ b/src/icalendar/parser/__init__.py @@ -9,6 +9,13 @@ from .content_line import Contentline, Contentlines from .parameter import ( Parameters, + _dquote, + _param_value, + _q_join, + _q_split, + _rfc_6868_escape, + _rfc_6868_unescape, + _validate_param_value, dquote, param_value, q_join, @@ -18,6 +25,10 @@ validate_param_value, ) from .property import ( + _split_on_unescaped_comma, + _split_on_unescaped_semicolon, + _unescape_backslash, + _unescape_list_or_string, split_on_unescaped_comma, split_on_unescaped_semicolon, unescape_backslash, @@ -26,8 +37,10 @@ from .string import ( _escape_char, _escape_string, + _foldline, _unescape_char, _unescape_string, + _validate_token, escape_char, escape_string, foldline, @@ -40,10 +53,23 @@ "Contentline", "Contentlines", "Parameters", + "_dquote", "_escape_char", "_escape_string", + "_foldline", + "_param_value", + "_q_join", + "_q_split", + "_rfc_6868_escape", + "_rfc_6868_unescape", + "_split_on_unescaped_comma", + "_split_on_unescaped_semicolon", + "_unescape_backslash", "_unescape_char", + "_unescape_list_or_string", "_unescape_string", + "_validate_param_value", + "_validate_token", "dquote", "escape_char", "escape_string", diff --git a/src/icalendar/parser/content_line.py b/src/icalendar/parser/content_line.py index 4f36d06e1..a123555ad 100644 --- a/src/icalendar/parser/content_line.py +++ b/src/icalendar/parser/content_line.py @@ -3,14 +3,14 @@ import re from icalendar.parser.parameter import Parameters -from icalendar.parser.property import unescape_backslash, unescape_list_or_string +from icalendar.parser.property import _unescape_backslash, _unescape_list_or_string from icalendar.parser.string import ( _escape_string, + _foldline, _unescape_string, - foldline, - validate_token, + _validate_token, ) -from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, _to_unicode UFOLD = re.compile("(\r?\n)+[ \t]") NEWLINE = re.compile(r"\r?\n") @@ -112,7 +112,7 @@ class Contentline(str): __slots__ = ("strict",) def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) assert "\n" not in value, ( "Content line can not contain unescaped new line characters." ) @@ -141,10 +141,10 @@ def from_parts( # TODO: after unicode only, remove this # Convert back to unicode, after to_ical encoded it. - name = to_unicode(name) - values = to_unicode(values) + name = _to_unicode(name) + values = _to_unicode(values) if params: - params = to_unicode(params.to_ical(sorted=sorted)) + params = _to_unicode(params.to_ical(sorted=sorted)) if params: # some parameter values can be skipped during serialization return cls(f"{name};{params}:{values}") @@ -200,7 +200,7 @@ def parts(self) -> tuple[str, Parameters, str]: name = self[:name_split] if name_split else self if not self.strict: name = re.sub(r"[ \t]+", "", name.strip()) - validate_token(name) + _validate_token(name) if not name_split or name_split + 1 == value_split: # No delimiter or empty parameter section @@ -213,11 +213,11 @@ def parts(self) -> tuple[str, Parameters, str]: param_str = _escape_string(raw_param_str) params = Parameters.from_ical(param_str, strict=self.strict) params = Parameters( - (_unescape_string(key), unescape_list_or_string(value)) + (_unescape_string(key), _unescape_list_or_string(value)) for key, value in iter(params.items()) ) # Unescape backslash sequences in values but preserve URL encoding - values = unescape_backslash(self[value_split + 1 :]) + values = _unescape_backslash(self[value_split + 1 :]) except ValueError as exc: raise ValueError( f"Content line could not be parsed into parts: '{self}': {exc}" @@ -227,7 +227,7 @@ def parts(self) -> tuple[str, Parameters, str]: @classmethod def from_ical(cls, ical, strict=False): """Unfold the content lines in an iCalendar into long content lines.""" - ical = to_unicode(ical) + ical = _to_unicode(ical) # a fold is carriage return followed by either a space or a tab return cls(UFOLD.sub("", ical), strict=strict) @@ -235,7 +235,7 @@ def to_ical(self): """Long content lines are folded so they are less than 75 characters wide. """ - return foldline(self).encode(DEFAULT_ENCODING) + return _foldline(self).encode(DEFAULT_ENCODING) class Contentlines(list[Contentline]): @@ -251,7 +251,7 @@ def to_ical(self): @classmethod def from_ical(cls, st): """Parses a string into content lines.""" - st = to_unicode(st) + st = _to_unicode(st) try: # a fold is carriage return followed by either a space or a tab unfolded = UFOLD.sub("", st) diff --git a/src/icalendar/parser/ical/component.py b/src/icalendar/parser/ical/component.py index c1371fd23..747bee1b3 100644 --- a/src/icalendar/parser/ical/component.py +++ b/src/icalendar/parser/ical/component.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, ClassVar from icalendar.parser.content_line import Contentline, Contentlines -from icalendar.parser.property import split_on_unescaped_comma +from icalendar.parser.property import _split_on_unescaped_comma from icalendar.prop import vBroken from icalendar.timezone import tzp @@ -275,7 +275,7 @@ def handle_categories( # strict and tolerant components. # CATEGORIES needs special comma handling try: - category_list = split_on_unescaped_comma(raw_value) + category_list = _split_on_unescaped_comma(raw_value) factory = self.get_factory_for_property("CATEGORIES", params) vals_inst = factory(category_list) vals_inst.params = params diff --git a/src/icalendar/parser/parameter.py b/src/icalendar/parser/parameter.py index 08a135231..fb4b89337 100644 --- a/src/icalendar/parser/parameter.py +++ b/src/icalendar/parser/parameter.py @@ -5,12 +5,14 @@ import functools import os import re +import warnings from datetime import datetime, time from typing import TYPE_CHECKING, Any, Protocol from icalendar.caselessdict import CaselessDict +from icalendar.compatibility import deprecate_for_version_8 from icalendar.error import JCalParsingError -from icalendar.parser.string import validate_token +from icalendar.parser.string import _validate_token from icalendar.parser_tools import ( DEFAULT_ENCODING, SEQUENCE_TYPES, @@ -32,7 +34,7 @@ def to_ical(self) -> bytes: ... -def param_value( +def _param_value( value: Sequence[str] | str | HasToIcal, always_quote: bool = False ) -> str: """Convert a parameter value to its iCalendar representation. @@ -50,10 +52,29 @@ def param_value( The formatted parameter value, escaped and quoted as needed. """ if isinstance(value, SEQUENCE_TYPES): - return q_join(map(rfc_6868_escape, value), always_quote=always_quote) + return _q_join(map(_rfc_6868_escape, value), always_quote=always_quote) if isinstance(value, str): - return dquote(rfc_6868_escape(value), always_quote=always_quote) - return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING))) + return _dquote(_rfc_6868_escape(value), always_quote=always_quote) + return _dquote(_rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING))) + + +def param_value( + value: Sequence[str] | str | HasToIcal, always_quote: bool = False +) -> str: + """Convert a parameter value to its iCalendar representation. + + .. deprecated:: 7.0.0 + Use the private :func:`_param_value` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "param_value is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _param_value(value, always_quote) # Could be improved @@ -63,7 +84,7 @@ def param_value( QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]') -def validate_param_value(value: str, quoted: bool = True) -> None: +def _validate_param_value(value: str, quoted: bool = True) -> None: """Validate a parameter value for unsafe characters. Checks parameter values for characters that are not allowed according to @@ -83,12 +104,29 @@ def validate_param_value(value: str, quoted: bool = True) -> None: raise ValueError(value) +def validate_param_value(value: str, quoted: bool = True) -> None: + """Validate a parameter value for unsafe characters. + + .. deprecated:: 7.0.0 + Use the private :func:`_validate_param_value` internally. For external + use, this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "validate_param_value is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + _validate_param_value(value, quoted) + + # chars presence of which in parameter value will be cause the value # to be enclosed in double-quotes QUOTABLE = re.compile("[,;:’]") # noqa: RUF001 -def dquote(val: str, always_quote: bool = False) -> str: +def _dquote(val: str, always_quote: bool = False) -> str: """Enclose parameter values in double quotes when needed. Parameter values containing special characters ``,``, ``;``, @@ -113,8 +151,25 @@ def dquote(val: str, always_quote: bool = False) -> str: return val +def dquote(val: str, always_quote: bool = False) -> str: + """Enclose parameter values in double quotes when needed. + + .. deprecated:: 7.0.0 + Use the private :func:`_dquote` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "dquote is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _dquote(val, always_quote) + + # parsing helper -def q_split(st: str, sep: str = ",", maxsplit: int = -1) -> list[str]: +def _q_split(st: str, sep: str = ",", maxsplit: int = -1) -> list[str]: """Split a string on a separator, respecting double quotes. Splits the string on the separator character, but ignores separators that @@ -133,12 +188,12 @@ def q_split(st: str, sep: str = ",", maxsplit: int = -1) -> list[str]: Examples: .. code-block:: pycon - >>> from icalendar.parser import q_split - >>> q_split('a,b,c') + >>> from icalendar.parser import _q_split + >>> _q_split('a,b,c') ['a', 'b', 'c'] - >>> q_split('a,"b,c",d') + >>> _q_split('a,"b,c",d') ['a', '"b,c"', 'd'] - >>> q_split('a;b;c', sep=';') + >>> _q_split('a;b;c', sep=';') ['a', 'b', 'c'] """ if maxsplit == 0: @@ -162,10 +217,13 @@ def q_split(st: str, sep: str = ",", maxsplit: int = -1) -> list[str]: return result -def q_join(lst: Sequence[str], sep: str = ",", always_quote: bool = False) -> str: +q_split = deprecate_for_version_8(_q_split) + + +def _q_join(lst: Sequence[str], sep: str = ",", always_quote: bool = False) -> str: """Join a list with a separator, quoting items as needed. - Joins list items with the separator, applying :func:`dquote` to each item + Joins list items with the separator, applying :func:`_dquote` to each item to add double quotes when they contain special characters. Parameters: @@ -180,13 +238,16 @@ def q_join(lst: Sequence[str], sep: str = ",", always_quote: bool = False) -> st Examples: .. code-block:: pycon - >>> from icalendar.parser import q_join - >>> q_join(['a', 'b', 'c']) + >>> from icalendar.parser import _q_join + >>> _q_join(['a', 'b', 'c']) 'a,b,c' - >>> q_join(['plain', 'has,comma']) + >>> _q_join(['plain', 'has,comma']) 'plain,"has,comma"' """ - return sep.join(dquote(itm, always_quote=always_quote) for itm in lst) + return sep.join(_dquote(itm, always_quote=always_quote) for itm in lst) + + +q_join = deprecate_for_version_8(_q_join) def single_string_parameter(func: Callable | None = None, upper=False): @@ -333,7 +394,7 @@ def to_ical(self, sorted: bool = True): # noqa: A002 check_quoteable_characters and any(c in value for c in check_quoteable_characters) ) - quoted_value = param_value(value, always_quote=always_quote) + quoted_value = _param_value(value, always_quote=always_quote) if isinstance(quoted_value, str): quoted_value = quoted_value.encode(DEFAULT_ENCODING) # CaselessDict keys are always unicode @@ -346,24 +407,24 @@ def from_ical(cls, st, strict=False): # parse into strings result = cls() - for param in q_split(st, ";"): + for param in _q_split(st, ";"): try: - key, val = q_split(param, "=", maxsplit=1) - validate_token(key) + key, val = _q_split(param, "=", maxsplit=1) + _validate_token(key) # Property parameter values that are not in quoted # strings are case insensitive. vals = [] - for v in q_split(val, ","): + for v in _q_split(val, ","): if v.startswith('"') and v.endswith('"'): v2 = v.strip('"') - validate_param_value(v2, quoted=True) - vals.append(rfc_6868_unescape(v2)) + _validate_param_value(v2, quoted=True) + vals.append(_rfc_6868_unescape(v2)) else: - validate_param_value(v, quoted=False) + _validate_param_value(v, quoted=False) if strict: - vals.append(rfc_6868_unescape(v.upper())) + vals.append(_rfc_6868_unescape(v.upper())) else: - vals.append(rfc_6868_unescape(v)) + vals.append(_rfc_6868_unescape(v)) if not vals: result[key] = val elif len(vals) == 1: @@ -523,7 +584,7 @@ def from_jcal_property(cls, jcal_property: list): RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'") -def rfc_6868_unescape(param_value: str) -> str: +def _rfc_6868_unescape(param_value: str) -> str: """Take care of :rfc:`6868` unescaping. - ^^ -> ^ @@ -541,10 +602,27 @@ def rfc_6868_unescape(param_value: str) -> str: ) +def rfc_6868_unescape(param_value: str) -> str: + """Take care of :rfc:`6868` unescaping. + + .. deprecated:: 7.0.0 + Use the private :func:`_rfc_6868_unescape` internally. For external + use, this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "rfc_6868_unescape is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _rfc_6868_unescape(param_value) + + RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"') -def rfc_6868_escape(param_value: str) -> str: +def _rfc_6868_escape(param_value: str) -> str: """Take care of :rfc:`6868` escaping. - ^ -> ^^ @@ -563,8 +641,32 @@ def rfc_6868_escape(param_value: str) -> str: ) +def rfc_6868_escape(param_value: str) -> str: + """Take care of :rfc:`6868` escaping. + + .. deprecated:: 7.0.0 + Use the private :func:`_rfc_6868_escape` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "rfc_6868_escape is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _rfc_6868_escape(param_value) + + __all__ = [ "Parameters", + "_dquote", + "_param_value", + "_q_join", + "_q_split", + "_rfc_6868_escape", + "_rfc_6868_unescape", + "_validate_param_value", "dquote", "param_value", "q_join", diff --git a/src/icalendar/parser/property.py b/src/icalendar/parser/property.py index 6efca840a..dfb80a8d0 100644 --- a/src/icalendar/parser/property.py +++ b/src/icalendar/parser/property.py @@ -1,11 +1,12 @@ """Tools for parsing properties.""" import re +import warnings from icalendar.parser.string import _unescape_string -def unescape_list_or_string(val: str | list[str]) -> str | list[str]: +def _unescape_list_or_string(val: str | list[str]) -> str | list[str]: """Unescape a value that may be a string or list of strings. Applies :func:`_unescape_string` to the value. If the value is a list, @@ -22,13 +23,31 @@ def unescape_list_or_string(val: str | list[str]) -> str | list[str]: return _unescape_string(val) +def unescape_list_or_string(val: str | list[str]) -> str | list[str]: + """Unescape a value that may be a string or list of strings. + + .. deprecated:: 7.0.0 + Use the private :func:`_unescape_list_or_string` internally. For + external use, this function is deprecated. Please contact the + maintainers if you rely on this function. + """ + warnings.warn( + "unescape_list_or_string is deprecated and will be removed in a future" + " version. If you are using this function externally, please" + " contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _unescape_list_or_string(val) + + _unescape_backslash_regex = re.compile(r"\\([\\,;:nN])") -def unescape_backslash(val: str): +def _unescape_backslash(val: str): r"""Unescape backslash sequences in iCalendar text. - Unlike :py:meth:`unescape_string`, this only handles actual backslash escapes + Unlike :py:meth:`_unescape_string`, this only handles actual backslash escapes per :rfc:`5545`, not URL encoding. This preserves URL-encoded values like ``%3A`` in URLs. @@ -39,7 +58,24 @@ def unescape_backslash(val: str): ) -def split_on_unescaped_comma(text: str) -> list[str]: +def unescape_backslash(val: str): + r"""Unescape backslash sequences in iCalendar text. + + .. deprecated:: 7.0.0 + Use the private :func:`_unescape_backslash` internally. For external + use, this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "unescape_backslash is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _unescape_backslash(val) + + +def _split_on_unescaped_comma(text: str) -> list[str]: r"""Split text on unescaped commas and unescape each part. Splits only on commas not preceded by backslash. @@ -54,14 +90,14 @@ def split_on_unescaped_comma(text: str) -> list[str]: Examples: .. code-block:: pycon - >>> from icalendar.parser import split_on_unescaped_comma - >>> split_on_unescaped_comma(r"foo\, bar,baz") + >>> from icalendar.parser.property import _split_on_unescaped_comma + >>> _split_on_unescaped_comma(r"foo\, bar,baz") ['foo, bar', 'baz'] - >>> split_on_unescaped_comma("a,b,c") + >>> _split_on_unescaped_comma("a,b,c") ['a', 'b', 'c'] - >>> split_on_unescaped_comma(r"a\,b\,c") + >>> _split_on_unescaped_comma(r"a\,b\,c") ['a,b,c'] - >>> split_on_unescaped_comma(r"Work,Personal\,Urgent") + >>> _split_on_unescaped_comma(r"Work,Personal\,Urgent") ['Work', 'Personal,Urgent'] """ if not text: @@ -79,7 +115,7 @@ def split_on_unescaped_comma(text: str) -> list[str]: i += 2 elif text[i] == ",": # Unescaped comma - split point - result.append(unescape_backslash("".join(current))) + result.append(_unescape_backslash("".join(current))) current = [] i += 1 else: @@ -87,12 +123,30 @@ def split_on_unescaped_comma(text: str) -> list[str]: i += 1 # Add final part - result.append(unescape_backslash("".join(current))) + result.append(_unescape_backslash("".join(current))) return result -def split_on_unescaped_semicolon(text: str) -> list[str]: +def split_on_unescaped_comma(text: str) -> list[str]: + r"""Split text on unescaped commas and unescape each part. + + .. deprecated:: 7.0.0 + Use the private :func:`_split_on_unescaped_comma` internally. For + external use, this function is deprecated. Please contact the + maintainers if you rely on this function. + """ + warnings.warn( + "split_on_unescaped_comma is deprecated and will be removed in a future" + " version. If you are using this function externally, please" + " contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _split_on_unescaped_comma(text) + + +def _split_on_unescaped_semicolon(text: str) -> list[str]: r"""Split text on unescaped semicolons and unescape each part. Splits only on semicolons not preceded by a backslash. @@ -108,14 +162,14 @@ def split_on_unescaped_semicolon(text: str) -> list[str]: Examples: .. code-block:: pycon - >>> from icalendar.parser import split_on_unescaped_semicolon - >>> split_on_unescaped_semicolon(r"field1\;with;field2") + >>> from icalendar.parser.property import _split_on_unescaped_semicolon + >>> _split_on_unescaped_semicolon(r"field1\;with;field2") ['field1;with', 'field2'] - >>> split_on_unescaped_semicolon("a;b;c") + >>> _split_on_unescaped_semicolon("a;b;c") ['a', 'b', 'c'] - >>> split_on_unescaped_semicolon(r"a\;b\;c") + >>> _split_on_unescaped_semicolon(r"a\;b\;c") ['a;b;c'] - >>> split_on_unescaped_semicolon(r"PO Box 123\;Suite 200;City") + >>> _split_on_unescaped_semicolon(r"PO Box 123\;Suite 200;City") ['PO Box 123;Suite 200', 'City'] """ if not text: @@ -133,7 +187,7 @@ def split_on_unescaped_semicolon(text: str) -> list[str]: i += 2 elif text[i] == ";": # Unescaped semicolon - split point - result.append(unescape_backslash("".join(current))) + result.append(_unescape_backslash("".join(current))) current = [] i += 1 else: @@ -141,12 +195,34 @@ def split_on_unescaped_semicolon(text: str) -> list[str]: i += 1 # Add final part - result.append(unescape_backslash("".join(current))) + result.append(_unescape_backslash("".join(current))) return result +def split_on_unescaped_semicolon(text: str) -> list[str]: + r"""Split text on unescaped semicolons and unescape each part. + + .. deprecated:: 7.0.0 + Use the private :func:`_split_on_unescaped_semicolon` internally. For + external use, this function is deprecated. Please contact the + maintainers if you rely on this function. + """ + warnings.warn( + "split_on_unescaped_semicolon is deprecated and will be removed in a future" + " version. If you are using this function externally, please" + " contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _split_on_unescaped_semicolon(text) + + __all__ = [ + "_split_on_unescaped_comma", + "_split_on_unescaped_semicolon", + "_unescape_backslash", + "_unescape_list_or_string", "split_on_unescaped_comma", "split_on_unescaped_semicolon", "unescape_backslash", diff --git a/src/icalendar/parser/string.py b/src/icalendar/parser/string.py index b623f60bf..d73a03d28 100644 --- a/src/icalendar/parser/string.py +++ b/src/icalendar/parser/string.py @@ -3,6 +3,7 @@ import re import warnings +from icalendar.compatibility import deprecate_for_version_8 from icalendar.parser_tools import DEFAULT_ENCODING @@ -161,8 +162,9 @@ def unescape_char(text: str | bytes) -> str | bytes | None: return _unescape_char(text) -def foldline(line: str, limit: int = 75, fold_sep: str = "\r\n ") -> str: - """Make a string folded as defined in RFC5545 +def _foldline(line: str, limit: int = 75, fold_sep: str = "\r\n ") -> str: + """Make a string folded as defined in RFC5545. + Lines of text SHOULD NOT be longer than 75 octets, excluding the line break. Long content lines SHOULD be split into a multiple line representations using a line "folding" technique. That is, a long @@ -196,6 +198,9 @@ def foldline(line: str, limit: int = 75, fold_sep: str = "\r\n ") -> str: return "".join(ret_chars) +foldline = deprecate_for_version_8(_foldline) + + def _escape_string(val: str) -> str: r"""Escape backslash sequences to URL-encoded hex values. @@ -291,7 +296,7 @@ def unescape_string(val: str) -> str: NAME = re.compile(r"[\w.-]+") -def validate_token(name: str) -> None: +def _validate_token(name: str) -> None: r"""Validate that a name is a valid iCalendar token. Checks if the name matches the :rfc:`5545` token syntax using the NAME @@ -309,11 +314,30 @@ def validate_token(name: str) -> None: raise ValueError(name) +def validate_token(name: str) -> None: + r"""Validate that a name is a valid iCalendar token. + + .. deprecated:: 7.0.0 + Use the private :func:`_validate_token` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "validate_token is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + _validate_token(name) + + __all__ = [ "_escape_char", "_escape_string", + "_foldline", "_unescape_char", "_unescape_string", + "_validate_token", "escape_char", "escape_string", "foldline", diff --git a/src/icalendar/parser_tools.py b/src/icalendar/parser_tools.py index 56111c78e..ee2ba9a78 100644 --- a/src/icalendar/parser_tools.py +++ b/src/icalendar/parser_tools.py @@ -1,9 +1,11 @@ +import warnings + SEQUENCE_TYPES = (list, tuple) DEFAULT_ENCODING = "utf-8" ICAL_TYPE = str | bytes -def from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes: +def _from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes: """Converts a value to bytes, even if it is already bytes. Parameters: @@ -24,7 +26,24 @@ def from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes: return value -def to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str: +def from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes: + """Converts a value to bytes, even if it is already bytes. + + .. deprecated:: 7.0.0 + Use the private :func:`_from_unicode` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "from_unicode is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _from_unicode(value, encoding) + + +def _to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str: """Converts a value to Unicode, even if it is already a Unicode string. Parameters: @@ -42,7 +61,24 @@ def to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str: return value -def data_encode( +def to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str: + """Converts a value to Unicode, even if it is already a Unicode string. + + .. deprecated:: 7.0.0 + Use the private :func:`_to_unicode` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "to_unicode is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _to_unicode(value, encoding) + + +def _data_encode( data: ICAL_TYPE | dict | list, encoding=DEFAULT_ENCODING ) -> bytes | list[bytes] | dict: """Encode all datastructures to the given encoding. @@ -56,16 +92,38 @@ def data_encode( if isinstance(data, str): return data.encode(encoding) if isinstance(data, dict): - return dict(map(data_encode, iter(data.items()))) + return dict(map(_data_encode, iter(data.items()))) if isinstance(data, (list, tuple)): - return list(map(data_encode, data)) + return list(map(_data_encode, data)) return data +def data_encode( + data: ICAL_TYPE | dict | list, encoding=DEFAULT_ENCODING +) -> bytes | list[bytes] | dict: + """Encode all datastructures to the given encoding. + + .. deprecated:: 7.0.0 + Use the private :func:`_data_encode` internally. For external use, + this function is deprecated. Please contact the maintainers if you + rely on this function. + """ + warnings.warn( + "data_encode is deprecated and will be removed in a future version. " + "If you are using this function externally, please contact the maintainers.", + DeprecationWarning, + stacklevel=2, + ) + return _data_encode(data, encoding) + + __all__ = [ "DEFAULT_ENCODING", "ICAL_TYPE", "SEQUENCE_TYPES", + "_data_encode", + "_from_unicode", + "_to_unicode", "data_encode", "from_unicode", "to_unicode", diff --git a/src/icalendar/prop/adr.py b/src/icalendar/prop/adr.py index 05384617c..288fe45de 100644 --- a/src/icalendar/prop/adr.py +++ b/src/icalendar/prop/adr.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class AdrFields(NamedTuple): @@ -108,10 +108,10 @@ def from_ical(ical: str | bytes) -> AdrFields: Returns: AdrFields named tuple with seven field values. """ - from icalendar.parser import split_on_unescaped_semicolon + from icalendar.parser import _split_on_unescaped_semicolon - ical = to_unicode(ical) - fields = split_on_unescaped_semicolon(ical) + ical = _to_unicode(ical) + fields = _split_on_unescaped_semicolon(ical) if len(fields) != 7: raise ValueError( f"ADR must have exactly 7 fields, got {len(fields)}: {ical}" diff --git a/src/icalendar/prop/binary.py b/src/icalendar/prop/binary.py index 874ab6dc8..8c85587e5 100644 --- a/src/icalendar/prop/binary.py +++ b/src/icalendar/prop/binary.py @@ -7,7 +7,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import to_unicode +from icalendar.parser_tools import _to_unicode class vBinary: @@ -18,7 +18,7 @@ class vBinary: obj: str def __init__(self, obj: str | bytes, params: dict[str, str] | None = None) -> None: - self.obj = to_unicode(obj) + self.obj = _to_unicode(obj) self.params = Parameters(encoding="BASE64", value="BINARY") if params: self.params.update(params) diff --git a/src/icalendar/prop/cal_address.py b/src/icalendar/prop/cal_address.py index 6893b5fd7..9e2da2870 100644 --- a/src/icalendar/prop/cal_address.py +++ b/src/icalendar/prop/cal_address.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class vCalAddress(str): @@ -66,7 +66,7 @@ def __new__( /, params: dict[str, Any] | None = None, ) -> Self: - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.params = Parameters(params) return self diff --git a/src/icalendar/prop/categories.py b/src/icalendar/prop/categories.py index aab4b4103..a3571efa6 100644 --- a/src/icalendar/prop/categories.py +++ b/src/icalendar/prop/categories.py @@ -6,7 +6,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import to_unicode +from icalendar.parser_tools import _to_unicode from icalendar.prop.text import vText @@ -57,7 +57,7 @@ def from_ical(ical: list[str] | str) -> list[str]: # Already split by Component.from_ical() return ical # Legacy: simple comma split (no escaping handled) - ical = to_unicode(ical) + ical = _to_unicode(ical) return ical.split(",") def __eq__(self, other: object) -> bool: diff --git a/src/icalendar/prop/dt/list.py b/src/icalendar/prop/dt/list.py index 2f0ee64ed..1f2a48ebc 100644 --- a/src/icalendar/prop/dt/list.py +++ b/src/icalendar/prop/dt/list.py @@ -4,7 +4,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import from_unicode +from icalendar.parser_tools import _from_unicode from .base import TimeBase from .types import vDDDTypes @@ -37,7 +37,7 @@ def __init__(self, dt_list, params: dict[str, Any] | None = None): self.dts = vddd def to_ical(self): - dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts) + dts_ical = (_from_unicode(dt.to_ical()) for dt in self.dts) return b",".join(dts_ical) @staticmethod diff --git a/src/icalendar/prop/inline.py b/src/icalendar/prop/inline.py index d09dcaa23..084d9394c 100644 --- a/src/icalendar/prop/inline.py +++ b/src/icalendar/prop/inline.py @@ -2,7 +2,7 @@ from icalendar.compatibility import Self from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, _to_unicode class vInline(str): @@ -21,7 +21,7 @@ def __new__( /, params: dict[str, Any] | None = None, ) -> Self: - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.params = Parameters(params) return self diff --git a/src/icalendar/prop/n.py b/src/icalendar/prop/n.py index 4a6846206..aea2db6ef 100644 --- a/src/icalendar/prop/n.py +++ b/src/icalendar/prop/n.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class NFields(NamedTuple): @@ -100,10 +100,10 @@ def from_ical(ical: str | bytes) -> NFields: Returns: NFields named tuple with five field values. """ - from icalendar.parser import split_on_unescaped_semicolon + from icalendar.parser import _split_on_unescaped_semicolon - ical = to_unicode(ical) - fields = split_on_unescaped_semicolon(ical) + ical = _to_unicode(ical) + fields = _split_on_unescaped_semicolon(ical) if len(fields) != 5: raise ValueError(f"N must have exactly 5 fields, got {len(fields)}: {ical}") return NFields(*fields) diff --git a/src/icalendar/prop/org.py b/src/icalendar/prop/org.py index 08736466b..1df49c626 100644 --- a/src/icalendar/prop/org.py +++ b/src/icalendar/prop/org.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class vOrg: @@ -86,10 +86,10 @@ def from_ical(ical: str | bytes) -> tuple[str, ...]: Returns: Tuple of field values with one or more fields """ - from icalendar.parser import split_on_unescaped_semicolon + from icalendar.parser import _split_on_unescaped_semicolon - ical = to_unicode(ical) - fields = split_on_unescaped_semicolon(ical) + ical = _to_unicode(ical) + fields = _split_on_unescaped_semicolon(ical) if len(fields) < 1: raise ValueError(f"ORG must have at least 1 field: {ical}") return tuple(fields) diff --git a/src/icalendar/prop/recur/frequency.py b/src/icalendar/prop/recur/frequency.py index 91b13b5c5..94fc44584 100644 --- a/src/icalendar/prop/recur/frequency.py +++ b/src/icalendar/prop/recur/frequency.py @@ -6,7 +6,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class vFrequency(str): @@ -34,7 +34,7 @@ def __new__( /, params: dict[str, Any] | None = None, ): - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) if self not in vFrequency.frequencies: raise ValueError(f"Expected frequency, got: {self}") diff --git a/src/icalendar/prop/recur/weekday.py b/src/icalendar/prop/recur/weekday.py index 9ef502e65..a4d538623 100644 --- a/src/icalendar/prop/recur/weekday.py +++ b/src/icalendar/prop/recur/weekday.py @@ -7,7 +7,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode WEEKDAY_RULE = re.compile( r"(?P[+-]?)(?P[\d]{0,2})(?P[\w]{2})$" @@ -65,7 +65,7 @@ def __new__( /, params: dict[str, Any] | None = None, ): - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) match = WEEKDAY_RULE.match(self) if match is None: diff --git a/src/icalendar/prop/text.py b/src/icalendar/prop/text.py index 94c273296..7ea1a2211 100644 --- a/src/icalendar/prop/text.py +++ b/src/icalendar/prop/text.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters, _escape_char -from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, _to_unicode class vText(str): @@ -76,7 +76,7 @@ def __new__( /, params: dict[str, Any] | None = None, ) -> Self: - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.encoding = encoding self.params = Parameters(params) diff --git a/src/icalendar/prop/uri.py b/src/icalendar/prop/uri.py index 4b32cba4c..bb90ef6dc 100644 --- a/src/icalendar/prop/uri.py +++ b/src/icalendar/prop/uri.py @@ -5,7 +5,7 @@ from icalendar.compatibility import Self from icalendar.error import JCalParsingError from icalendar.parser import Parameters -from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode +from icalendar.parser_tools import DEFAULT_ENCODING, _to_unicode class vUri(str): @@ -65,7 +65,7 @@ def __new__( /, params: dict[str, Any] | None = None, ) -> Self: - value = to_unicode(value, encoding=encoding) + value = _to_unicode(value, encoding=encoding) self = super().__new__(cls, value) self.params = Parameters(params) return self diff --git a/src/icalendar/tests/prop/test_date_and_time.py b/src/icalendar/tests/prop/test_date_and_time.py index 40108e51b..2c694edec 100644 --- a/src/icalendar/tests/prop/test_date_and_time.py +++ b/src/icalendar/tests/prop/test_date_and_time.py @@ -7,7 +7,7 @@ import pytest from icalendar import Component, vDatetime, vDDDLists, vDDDTypes, vTime -from icalendar.parser_tools import to_unicode +from icalendar.parser_tools import _to_unicode from icalendar.prop import VPROPERTY from icalendar.timezone.tzid import is_utc @@ -73,7 +73,7 @@ def test_converting_to_utc_puts_a_z_in_the_end(utc_prop: VPROPERTY): properties whose time values are specified in UTC. """ - ical = to_unicode(utc_prop.to_ical()) + ical = _to_unicode(utc_prop.to_ical()) assert ical.endswith("Z") diff --git a/src/icalendar/tests/rfc_6350_vcard/test_split_on_unescaped_semicolon.py b/src/icalendar/tests/rfc_6350_vcard/test_split_on_unescaped_semicolon.py index 31b66bf86..e49639875 100644 --- a/src/icalendar/tests/rfc_6350_vcard/test_split_on_unescaped_semicolon.py +++ b/src/icalendar/tests/rfc_6350_vcard/test_split_on_unescaped_semicolon.py @@ -4,7 +4,7 @@ import pytest -from icalendar.parser import split_on_unescaped_semicolon +from icalendar.parser import _split_on_unescaped_semicolon @pytest.mark.parametrize( @@ -48,5 +48,5 @@ ) def test_split_on_unescaped_semicolon(input_str: str, expected: list[str]) -> None: """Test splitting strings on unescaped semicolons.""" - result = split_on_unescaped_semicolon(input_str) + result = _split_on_unescaped_semicolon(input_str) assert result == expected diff --git a/src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py b/src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py index 310ac9a10..1f8e2677d 100644 --- a/src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py +++ b/src/icalendar/tests/rfc_7265_jcal/test_section_5_3_examples.py @@ -5,14 +5,14 @@ import pytest from icalendar import Calendar, Event, vText -from icalendar.parser_tools import to_unicode +from icalendar.parser_tools import _to_unicode def test_convert_coffee(calendars): """convert the unknown value property""" calendar = calendars.rfc_7265_example_2 ical = calendar.to_ical().decode() - print(to_unicode(ical)) + print(_to_unicode(ical)) assert r"X-COFFEE-DATA:Stenophylla\;Guinea\\\,Africa" in ical diff --git a/src/icalendar/tests/test_icalendar.py b/src/icalendar/tests/test_icalendar.py index 994bb4690..57cbea8e5 100644 --- a/src/icalendar/tests/test_icalendar.py +++ b/src/icalendar/tests/test_icalendar.py @@ -6,10 +6,10 @@ Contentline, Contentlines, Parameters, - dquote, - foldline, - q_join, - q_split, + _dquote, + _foldline, + _q_join, + _q_split, ) from icalendar.prop import vText @@ -243,9 +243,9 @@ def test_contentline_class(self): ) def test_fold_line(self): - assert foldline("foo") == "foo" + assert _foldline("foo") == "foo" assert ( - foldline( + _foldline( "Lorem ipsum dolor sit amet, consectetur adipiscing " "elit. Vestibulum convallis imperdiet dui posuere.", ) @@ -257,11 +257,11 @@ def test_fold_line(self): # at least just but bytes in there # porting it to "run" under python 2 & 3 makes it not much better with pytest.raises(AssertionError): - foldline("привет".encode(), limit=3) + _foldline("привет".encode(), limit=3) - assert foldline("foobar", limit=4) == "foo\r\n bar" + assert _foldline("foobar", limit=4) == "foo\r\n bar" assert ( - foldline( + _foldline( "Lorem ipsum dolor sit amet, consectetur adipiscing elit" ". Vestibulum convallis imperdiet dui posuere.", ) @@ -269,17 +269,17 @@ def test_fold_line(self): + " Vestibulum conval\r\n lis imperdiet dui posuere." ) assert ( - foldline("DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ") + _foldline("DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ") == "DESCRIPTION:АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭ\r\n ЮЯ" ) def test_value_double_quoting(self): - assert dquote("Max") == "Max" - assert dquote("Rasmussen, Max") == '"Rasmussen, Max"' - assert dquote("name:value") == '"name:value"' + assert _dquote("Max") == "Max" + assert _dquote("Rasmussen, Max") == '"Rasmussen, Max"' + assert _dquote("name:value") == '"name:value"' def test_q_split(self): - assert q_split('Max,Moller,"Rasmussen, Max"') == [ + assert _q_split('Max,Moller,"Rasmussen, Max"') == [ "Max", "Moller", '"Rasmussen, Max"', @@ -288,9 +288,10 @@ def test_q_split(self): def test_q_split_bin(self): for s in ("X-SOMETHING=ABCDE==", ",,,"): for maxsplit in range(-1, 3): - assert q_split(s, "=", maxsplit=maxsplit) == s.split("=", maxsplit) + assert _q_split(s, "=", maxsplit=maxsplit) == s.split("=", maxsplit) def test_q_join(self): assert ( - q_join(["Max", "Moller", "Rasmussen, Max"]) == 'Max,Moller,"Rasmussen, Max"' + _q_join(["Max", "Moller", "Rasmussen, Max"]) + == 'Max,Moller,"Rasmussen, Max"' ) diff --git a/src/icalendar/tests/test_issue_355_url_escaping.py b/src/icalendar/tests/test_issue_355_url_escaping.py index 206df8476..fc5a9a7b7 100644 --- a/src/icalendar/tests/test_issue_355_url_escaping.py +++ b/src/icalendar/tests/test_issue_355_url_escaping.py @@ -5,7 +5,7 @@ import pytest -from icalendar.parser import unescape_backslash +from icalendar.parser import _unescape_backslash def test_facebook_link_is_correctly_parsed(events): @@ -51,6 +51,6 @@ def test_empty_quotes(events): ) def test_unescape_backslash(input_string, expected_result, message): """Test unescape_backslash function with various inputs.""" - assert unescape_backslash(input_string) == expected_result, ( + assert _unescape_backslash(input_string) == expected_result, ( f"{message}: {input_string} -> {expected_result}" ) diff --git a/src/icalendar/tests/test_parsing.py b/src/icalendar/tests/test_parsing.py index 74793c731..a63c216e5 100644 --- a/src/icalendar/tests/test_parsing.py +++ b/src/icalendar/tests/test_parsing.py @@ -241,37 +241,37 @@ def test_unescape_char(): def test_split_on_unescaped_comma(): """Test splitting on unescaped commas.""" - from icalendar.parser import split_on_unescaped_comma + from icalendar.parser import _split_on_unescaped_comma # Simple case - assert split_on_unescaped_comma("a,b,c") == ["a", "b", "c"] + assert _split_on_unescaped_comma("a,b,c") == ["a", "b", "c"] # Escaped comma - assert split_on_unescaped_comma("a\\,b,c") == ["a,b", "c"] + assert _split_on_unescaped_comma("a\\,b,c") == ["a,b", "c"] # Multiple escaped commas - assert split_on_unescaped_comma("a\\,b\\,c") == ["a,b,c"] + assert _split_on_unescaped_comma("a\\,b\\,c") == ["a,b,c"] # Mixed - assert split_on_unescaped_comma("Work,Personal\\, Urgent") == [ + assert _split_on_unescaped_comma("Work,Personal\\, Urgent") == [ "Work", "Personal, Urgent", ] # Empty string - assert split_on_unescaped_comma("") == [""] + assert _split_on_unescaped_comma("") == [""] # Only commas - assert split_on_unescaped_comma(",,,") == ["", "", "", ""] + assert _split_on_unescaped_comma(",,,") == ["", "", "", ""] # Trailing comma - assert split_on_unescaped_comma("a,b,") == ["a", "b", ""] + assert _split_on_unescaped_comma("a,b,") == ["a", "b", ""] # Leading comma - assert split_on_unescaped_comma(",a,b") == ["", "a", "b"] + assert _split_on_unescaped_comma(",a,b") == ["", "a", "b"] # Other escaped chars - assert split_on_unescaped_comma("a\\;b,c\\nd") == ["a;b", "c\nd"] + assert _split_on_unescaped_comma("a\\;b,c\\nd") == ["a;b", "c\nd"] def test_create_a_component(): diff --git a/src/icalendar/tests/test_rfc_6868.py b/src/icalendar/tests/test_rfc_6868.py index eca5b815f..55ef97367 100644 --- a/src/icalendar/tests/test_rfc_6868.py +++ b/src/icalendar/tests/test_rfc_6868.py @@ -9,7 +9,7 @@ import pytest from icalendar.cal.calendar import Calendar -from icalendar.parser import dquote, rfc_6868_escape, rfc_6868_unescape +from icalendar.parser import _dquote, _rfc_6868_escape, _rfc_6868_unescape if TYPE_CHECKING: from icalendar import vCalAddress, vText @@ -61,7 +61,7 @@ def test_unknown_character(calendars, duplicate): ) def test_unescape(text, expected, modify): """Check unescaping.""" - result = rfc_6868_unescape(modify(text)) + result = _rfc_6868_unescape(modify(text)) assert result == modify(expected) @@ -69,7 +69,7 @@ def test_unescape(text, expected, modify): def test_unescape_newline(newline, monkeypatch): """Unescape the newline.""" monkeypatch.setattr(os, "linesep", newline) - assert rfc_6868_unescape("^n") == newline + assert _rfc_6868_unescape("^n") == newline param_values = pytest.mark.parametrize( @@ -87,9 +87,9 @@ def test_unescape_newline(newline, monkeypatch): @param_values def test_escape_rfc_6868(text, expected): """Check that we can escape the content with special characters.""" - escaped = rfc_6868_escape(text) + escaped = _rfc_6868_escape(text) assert escaped == expected, f"{escaped!r} == {expected!r}" - assert rfc_6868_escape(rfc_6868_unescape(escaped)) == expected + assert _rfc_6868_escape(_rfc_6868_unescape(escaped)) == expected @param_values @@ -100,7 +100,7 @@ def test_escaping_parameters(text, expected): param.params["RFC6868"] = text ical = cal.to_ical().decode() print(ical) - assert "X-PARAM;RFC6868=" + dquote(expected) in ical + assert "X-PARAM;RFC6868=" + _dquote(expected) in ical def test_encode_example_again(calendars): diff --git a/src/icalendar/tests/test_type_hints.py b/src/icalendar/tests/test_type_hints.py index 9b2f661c2..bc02deea2 100644 --- a/src/icalendar/tests/test_type_hints.py +++ b/src/icalendar/tests/test_type_hints.py @@ -1,4 +1,4 @@ -from icalendar.caselessdict import CaselessDict, canonsort_items, canonsort_keys +from icalendar.caselessdict import CaselessDict, _canonsort_items, _canonsort_keys def test_caselessdict_basic_behavior(): @@ -28,7 +28,7 @@ def test_canonsort_keys_basic(): keys = ["C", "A", "B"] canonical = ["B", "A"] - sorted_keys = canonsort_keys(keys, canonical) + sorted_keys = _canonsort_keys(keys, canonical) # canonical keys come first, in order assert sorted_keys[:2] == ["B", "A"] @@ -41,7 +41,7 @@ def test_canonsort_items_basic(): d = {"C": 3, "A": 1, "B": 2} canonical = ["A"] - sorted_items = canonsort_items(d, canonical) + sorted_items = _canonsort_items(d, canonical) # First item is canonical assert sorted_items[0] == ("A", 1)