kopia lustrzana https://github.com/collective/icalendar
Merge branch 'main' into security
commit
0bcaf62232
|
@ -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:
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
|
|
@ -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`_
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ if version is None and not options.accept_buildout_test_releases:
|
|||
|
||||
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(
|
||||
|
|
|
@ -43,3 +43,7 @@ man_pages = [
|
|||
('index', 'icalendar', 'icalendar Documentation',
|
||||
['Plone Foundation'], 1)
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
}
|
||||
|
|
|
@ -491,6 +491,7 @@ class Component(CaselessDict):
|
|||
'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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Union
|
||||
from typing import List, Union
|
||||
|
||||
SEQUENCE_TYPES = (list, tuple)
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -1042,7 +1042,7 @@ class vMonth(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
|
||||
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -24,9 +24,9 @@ class TestProp(unittest.TestCase):
|
|||
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"
|
||||
|
@ -45,7 +45,7 @@ class TestProp(unittest.TestCase):
|
|||
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
|
||||
|
@ -149,7 +149,7 @@ class TestProp(unittest.TestCase):
|
|||
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,
|
||||
|
@ -165,7 +165,7 @@ class TestProp(unittest.TestCase):
|
|||
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class IcalendarTestCase(unittest.TestCase):
|
|||
)
|
||||
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", ""],
|
||||
)
|
||||
|
@ -112,7 +112,7 @@ class IcalendarTestCase(unittest.TestCase):
|
|||
)
|
||||
|
||||
c = Contentline(
|
||||
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:" "MAILTO:maxm@example.com"
|
||||
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com"
|
||||
)
|
||||
self.assertEqual(
|
||||
c.parts(),
|
||||
|
@ -124,7 +124,7 @@ class IcalendarTestCase(unittest.TestCase):
|
|||
)
|
||||
self.assertEqual(
|
||||
c.to_ical().decode("utf-8"),
|
||||
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:" "MAILTO:maxm@example.com",
|
||||
"ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com",
|
||||
)
|
||||
|
||||
# and back again
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -115,10 +115,10 @@ class TestCaselessdict(unittest.TestCase):
|
|||
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")
|
||||
|
|
|
@ -12,7 +12,7 @@ class TestParserTools(unittest.TestCase):
|
|||
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")
|
||||
|
|
|
@ -13,20 +13,20 @@ class TestTools(unittest.TestCase):
|
|||
|
||||
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/content@Example.ORG" in txt)
|
||||
self.assertEqual(len(txt), length)
|
||||
self.assertIn(b"-/path/to/content@Example.ORG", txt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -20,7 +20,7 @@ from multiprocessing import Pool, cpu_count
|
|||
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
|
||||
|
||||
|
@ -30,7 +30,7 @@ from pytz import AmbiguousTimeError, NonExistentTimeError
|
|||
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:
|
||||
|
@ -123,7 +123,7 @@ def main(
|
|||
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")
|
||||
|
||||
|
|
|
@ -105,16 +105,16 @@ class TZP:
|
|||
"""
|
||||
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."""
|
||||
|
|
Ładowanie…
Reference in New Issue