Skip to content

Commit

Permalink
Merge branch 'main' into security
Browse files Browse the repository at this point in the history
  • Loading branch information
niccokunzmann authored Jan 18, 2025
2 parents 47f59d1 + 2801ec7 commit 0bcaf62
Show file tree
Hide file tree
Showing 17 changed files with 88 additions and 53 deletions.
7 changes: 5 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ Changelog

Minor changes:

- Add a ``weekday`` attribute to ``vWeekday`` components. See `Issue 749 <https://github.com/collective/icalendar/issues/749>`_.
- Document ``vRecur`` property. See `Issue 758 <https://github.com/collective/icalendar/issues/758>`_.
- Add a ``weekday`` attribute to :class:`icalendar.prop.vWeekday` components. See `Issue 749 <https://github.com/collective/icalendar/issues/749>`_.
- Document :class:`icalendar.prop.vRecur` property. See `Issue 758 <https://github.com/collective/icalendar/issues/758>`_.
- Print failure of doctest to aid debugging.
- Improve documentation of :class:`icalendar.prop.vGeo`
- Fix tests, improve code readability, fix typing. See `Issue 766 <https://github.com/collective/icalendar/issues/766>`_ and `Issue 765 <https://github.com/collective/icalendar/issues/765>`_.

Breaking changes:

Expand All @@ -17,6 +19,7 @@ Breaking changes:
New features:

- Add :ref:`Security Policy`
- Python types in documentation now link to their documentation pages using ``intersphinx``.

Bug fixes:

Expand Down
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ files.
----

:Homepage: https://icalendar.readthedocs.io
:Community Discussions: https://github.com/collective/icalendar/discussions
:Issue Tracker: https://github.com/collective/icalendar/issues
:Code: https://github.com/collective/icalendar
:Mailing list: https://github.com/collective/icalendar/issues
:Dependencies: `python-dateutil`_ and `tzdata`_.
:License: `BSD`_

Expand Down
2 changes: 1 addition & 1 deletion bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@

def _final_version(parsed_version):
for part in parsed_version:
if (part[:1] == '*') and (part not in _final_parts):
if (part.startswith('*')) and (part not in _final_parts):
return False
return True
index = setuptools.package_index.PackageIndex(
Expand Down
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@
('index', 'icalendar', 'icalendar Documentation',
['Plone Foundation'], 1)
]

intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
}
1 change: 1 addition & 0 deletions src/icalendar/cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ def from_ical(cls, st, multiple=False):
'Found no components where exactly one is required', st))
return comps[0]

@staticmethod
def _format_error(error_description, bad_input, elipsis='[...]'):
# there's three character more in the error, ie. ' ' x2 and a ':'
max_error_length = 100 - 3
Expand Down
22 changes: 13 additions & 9 deletions src/icalendar/parser_tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Union
from typing import List, Union

SEQUENCE_TYPES = (list, tuple)
DEFAULT_ENCODING = 'utf-8'
Expand All @@ -13,13 +13,14 @@ def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes:
:return: The bytes representation of the value
"""
if isinstance(value, bytes):
value = value
return value
elif isinstance(value, str):
try:
value = value.encode(encoding)
return value.encode(encoding)
except UnicodeEncodeError:
value = value.encode('utf-8', 'replace')
return value
return value.encode('utf-8', 'replace')
else:
return value


def to_unicode(value: ICAL_TYPE, encoding='utf-8-sig') -> str:
Expand All @@ -29,13 +30,16 @@ def to_unicode(value: ICAL_TYPE, encoding='utf-8-sig') -> str:
return value
elif isinstance(value, bytes):
try:
value = value.decode(encoding)
return value.decode(encoding)
except UnicodeDecodeError:
value = value.decode('utf-8-sig', 'replace')
return value
return value.decode('utf-8-sig', 'replace')
else:
return value


def data_encode(data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING) -> bytes:
def data_encode(
data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING
) -> Union[bytes, List[bytes], dict]:
"""Encode all datastructures to the given encoding.
Currently unicode strings, dicts and lists are supported.
"""
Expand Down
40 changes: 31 additions & 9 deletions src/icalendar/prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ def __new__(cls, month:Union[str, int]):
month_index = int(month)
leap = False
else:
if not month[-1] == "L" and month[:-1].isdigit():
if month[-1] != "L" and month[:-1].isdigit():
raise ValueError(f"Invalid month: {month!r}")
month_index = int(month[:-1])
leap = True
Expand Down Expand Up @@ -1478,40 +1478,62 @@ class vGeo:
GEO:37.386013;-122.082932
Parse vGeo:
.. code-block:: pycon
>>> from icalendar.prop import vGeo
>>> geo = vGeo.from_ical('37.386013;-122.082932')
>>> geo
(37.386013, -122.082932)
Add a geo location to an event:
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> latitude = 37.386013
>>> longitude = -122.082932
>>> event.add('GEO', (latitude, longitude))
>>> event['GEO']
vGeo((37.386013, -122.082932))
"""

def __init__(self, geo):
def __init__(self, geo: tuple[float|str|int, float|str|int]):
"""Create a new vGeo from a tuple of (latitude, longitude).
Raises:
ValueError: if geo is not a tuple of (latitude, longitude)
"""
try:
latitude, longitude = (geo[0], geo[1])
latitude = float(latitude)
longitude = float(longitude)
except Exception:
raise ValueError('Input must be (float, float) for '
'latitude and longitude')
except Exception as e:
raise ValueError("Input must be (float, float) for "
"latitude and longitude") from e
self.latitude = latitude
self.longitude = longitude
self.params = Parameters()

def to_ical(self):
return f'{self.latitude};{self.longitude}'
return f"{self.latitude};{self.longitude}"

@staticmethod
def from_ical(ical):
try:
latitude, longitude = ical.split(';')
latitude, longitude = ical.split(";")
return (float(latitude), float(longitude))
except Exception:
raise ValueError(f"Expected 'float;float' , got: {ical}")
except Exception as e:
raise ValueError(f"Expected 'float;float' , got: {ical}") from e

def __eq__(self, other):
return self.to_ical() == other.to_ical()

def __repr__(self):
"""repr(self)"""
return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))"

class vUTCOffset:
"""UTC Offset
Expand Down
4 changes: 2 additions & 2 deletions src/icalendar/tests/fuzzed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def fuzz_calendar_v1(
cal = [cal]
for c in cal:
if should_walk:
for event in cal.walk("VEVENT"):
for event in c.walk("VEVENT"):
event.to_ical()
else:
cal.to_ical()
c.to_ical()
10 changes: 5 additions & 5 deletions src/icalendar/tests/prop/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def test_prop_vDDDLists(self):
from icalendar.prop import vDDDLists

dt_list = vDDDLists.from_ical("19960402T010000Z")
self.assertTrue(isinstance(dt_list, list))
self.assertIsInstance(dt_list, list)
self.assertEqual(len(dt_list), 1)
self.assertTrue(isinstance(dt_list[0], datetime))
self.assertIsInstance(dt_list[0], datetime)
self.assertEqual(str(dt_list[0]), "1996-04-02 01:00:00+00:00")

p = "19960402T010000Z,19960403T010000Z,19960404T010000Z"
Expand All @@ -45,7 +45,7 @@ def test_prop_vDDDLists(self):
self.assertEqual(dt_list.to_ical(), b"20000101T000000,20001111T000000")

instance = vDDDLists([])
self.assertFalse(instance == "value")
self.assertNotEqual(instance, "value")

def test_prop_vDate(self):
from icalendar.prop import vDate
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_prop_vRecur(self):
self.assertEqual(vRecur(r).to_ical(), b"FREQ=DAILY;COUNT=10;INTERVAL=2")

r = vRecur.from_ical(
"FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;" "BYHOUR=8,9;BYMINUTE=30"
"FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;BYHOUR=8,9;BYMINUTE=30"
)
self.assertEqual(
r,
Expand All @@ -165,7 +165,7 @@ def test_prop_vRecur(self):

self.assertEqual(
vRecur(r).to_ical(),
b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;" b"BYMONTH=1",
b"FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;BYMONTH=1",
)

r = vRecur.from_ical("FREQ=WEEKLY;INTERVAL=1;BYWEEKDAY=TH")
Expand Down
2 changes: 1 addition & 1 deletion src/icalendar/tests/prop/test_windows_to_olson_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_windows_timezone(tzp):
"""Test that the timezone is mapped correctly to olson."""
dt = vDatetime.from_ical("20170507T181920", "Eastern Standard Time")
expected = tzp.localize(datetime(2017, 5, 7, 18, 19, 20), "America/New_York")
assert dt.tzinfo == dt.tzinfo
assert dt.tzinfo == expected.tzinfo
assert dt == expected


Expand Down
6 changes: 3 additions & 3 deletions src/icalendar/tests/test_icalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_long_lines(self):
)
self.assertEqual(
Contentlines.from_ical(
"A faked\r\n long line\r\nAnd another " "lin\r\n\te that is folded\r\n"
"A faked\r\n long line\r\nAnd another lin\r\n\te that is folded\r\n"
),
["A faked long line", "And another line that is folded", ""],
)
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_contentline_class(self):
)

c = Contentline(
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:" "MAILTO:[email protected]"
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:[email protected]"
)
self.assertEqual(
c.parts(),
Expand All @@ -124,7 +124,7 @@ def test_contentline_class(self):
)
self.assertEqual(
c.to_ical().decode("utf-8"),
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:" "MAILTO:[email protected]",
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:[email protected]",
)

# and back again
Expand Down
2 changes: 1 addition & 1 deletion src/icalendar/tests/test_issue_722_generate_vtimezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_conversion_converges(tzp, tzid):
tzinfo2 = generated1.to_tz()
generated2 = Timezone.from_tzinfo(tzinfo2, "test-generated")
tzinfo3 = generated2.to_tz()
generated3 = Timezone.from_tzinfo(tzinfo2, "test-generated")
generated3 = Timezone.from_tzinfo(tzinfo3, "test-generated")
# pprint(generated1.get_transitions())
# pprint(generated2.get_transitions())
assert_components_equal(generated1, generated2)
Expand Down
4 changes: 2 additions & 2 deletions src/icalendar/tests/test_unit_caselessdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ def test_CaselessDict(self):
self.assertEqual(ncd.get("key1"), "val1")
self.assertEqual(ncd.get("key3", "NOT FOUND"), "val3")
self.assertEqual(ncd.get("key4", "NOT FOUND"), "NOT FOUND")
self.assertTrue("key4" in ncd)
self.assertIn("key4", ncd)

del ncd["key4"]
self.assertFalse("key4" in ncd)
self.assertNotIn("key4", ncd)

ncd.update({"key5": "val5", "KEY6": "val6", "KEY5": "val7"})
self.assertEqual(ncd["key6"], "val6")
Expand Down
2 changes: 1 addition & 1 deletion src/icalendar/tests/test_unit_parser_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_parser_tools_to_unicode(self):
self.assertEqual(to_unicode(b"\xc6\xb5"), "\u01b5")
self.assertEqual(to_unicode(b"\xc6\xb5", encoding="ascii"), "\u01b5")
self.assertEqual(to_unicode(1), 1)
self.assertEqual(to_unicode(None), None)
self.assertIsNone(to_unicode(None))

def test_parser_tools_from_unicode(self):
self.assertEqual(from_unicode("\u01b5", encoding="ascii"), b"\xc6\xb5")
Expand Down
12 changes: 6 additions & 6 deletions src/icalendar/tests/test_unit_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ def test_tools_UIDGenerator(self):

txt = uid.to_ical()
length = 15 + 1 + 16 + 1 + 11
self.assertTrue(len(txt) == length)
self.assertTrue(b"@example.com" in txt)
self.assertEqual(len(txt), length)
self.assertIn(b"@example.com", txt)

# You should at least insert your own hostname to be more compliant
uid = g.uid("Example.ORG")
txt = uid.to_ical()
self.assertTrue(len(txt) == length)
self.assertTrue(b"@Example.ORG" in txt)
self.assertEqual(len(txt), length)
self.assertIn(b"@Example.ORG", txt)

# You can also insert a path or similar
uid = g.uid("Example.ORG", "/path/to/content")
txt = uid.to_ical()
self.assertTrue(len(txt) == length)
self.assertTrue(b"-/path/to/[email protected]" in txt)
self.assertEqual(len(txt), length)
self.assertIn(b"-/path/to/[email protected]", txt)


@pytest.mark.parametrize(
Expand Down
6 changes: 3 additions & 3 deletions src/icalendar/timezone/equivalent_timezone_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pathlib import Path
from pprint import pprint
from time import time
from typing import Callable, NamedTuple, Optional
from typing import Callable, NamedTuple, Optional, Any, Tuple, List

from zoneinfo import ZoneInfo, available_timezones

Expand All @@ -30,7 +30,7 @@
def check(dt, tz:tzinfo):
return (dt, tz.utcoffset(dt))

def checks(tz:tzinfo) -> tuple:
def checks(tz:tzinfo) -> List[Tuple[Any, Optional[timedelta]]]:
result = []
for dt in DTS:
try:
Expand Down Expand Up @@ -123,7 +123,7 @@ def generate_tree(
with file.open("w") as f:
f.write(f"'''This file is automatically generated by {Path(__file__).name}'''\n")
f.write("import datetime\n\n")
f.write(f"\nlookup = ")
f.write("\nlookup = ")
pprint(lookup, stream=f)
f.write("\n\n__all__ = ['lookup']\n")

Expand Down
14 changes: 7 additions & 7 deletions src/icalendar/timezone/tzp.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ def clean_timezone_id(self, tzid: str) -> str:
"""
return tzid.strip("/")

def timezone(self, id: str) -> Optional[datetime.tzinfo]:
def timezone(self, tz_id: str) -> Optional[datetime.tzinfo]:
"""Return a timezone with an id or None if we cannot find it."""
_unclean_id = id
id = self.clean_timezone_id(id)
tz = self.__provider.timezone(id)
_unclean_id = tz_id
tz_id = self.clean_timezone_id(tz_id)
tz = self.__provider.timezone(tz_id)
if tz is not None:
return tz
if id in WINDOWS_TO_OLSON:
tz = self.__provider.timezone(WINDOWS_TO_OLSON[id])
return tz or self.__provider.timezone(_unclean_id) or self.__tz_cache.get(id)
if tz_id in WINDOWS_TO_OLSON:
tz = self.__provider.timezone(WINDOWS_TO_OLSON[tz_id])
return tz or self.__provider.timezone(_unclean_id) or self.__tz_cache.get(tz_id)

def uses_pytz(self) -> bool:
"""Whether we use pytz at all."""
Expand Down

0 comments on commit 0bcaf62

Please sign in to comment.