diff --git a/CHANGES.rst b/CHANGES.rst index d7c30360..17bd2eaf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,11 @@ Changelog Minor changes: -- Add a ``weekday`` attribute to ``vWeekday`` components. See `Issue 749 `_. -- Document ``vRecur`` property. See `Issue 758 `_. +- Add a ``weekday`` attribute to :class:`icalendar.prop.vWeekday` components. See `Issue 749 `_. +- Document :class:`icalendar.prop.vRecur` property. See `Issue 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 `_ and `Issue 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: diff --git a/README.rst b/README.rst index d68a19cc..0ff9a6c8 100644 --- a/README.rst +++ b/README.rst @@ -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`_ diff --git a/bootstrap.py b/bootstrap.py index 6f5193e0..b6312c31 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -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( diff --git a/docs/conf.py b/docs/conf.py index eb0321a8..eb28cd28 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,3 +43,7 @@ ('index', 'icalendar', 'icalendar Documentation', ['Plone Foundation'], 1) ] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index bae86bd8..ebd60efc 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -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 diff --git a/src/icalendar/parser_tools.py b/src/icalendar/parser_tools.py index 28b22fc5..6e8ba38b 100644 --- a/src/icalendar/parser_tools.py +++ b/src/icalendar/parser_tools.py @@ -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. """ diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index c826584c..b86bcd77 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -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 @@ -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 diff --git a/src/icalendar/tests/fuzzed/__init__.py b/src/icalendar/tests/fuzzed/__init__.py index 17048e6d..2b7e1e4c 100644 --- a/src/icalendar/tests/fuzzed/__init__.py +++ b/src/icalendar/tests/fuzzed/__init__.py @@ -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() diff --git a/src/icalendar/tests/prop/test_unit.py b/src/icalendar/tests/prop/test_unit.py index 87a725b8..ce478119 100644 --- a/src/icalendar/tests/prop/test_unit.py +++ b/src/icalendar/tests/prop/test_unit.py @@ -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" @@ -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 @@ -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, @@ -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") diff --git a/src/icalendar/tests/prop/test_windows_to_olson_mapping.py b/src/icalendar/tests/prop/test_windows_to_olson_mapping.py index 2202c589..489d0d23 100644 --- a/src/icalendar/tests/prop/test_windows_to_olson_mapping.py +++ b/src/icalendar/tests/prop/test_windows_to_olson_mapping.py @@ -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 diff --git a/src/icalendar/tests/test_icalendar.py b/src/icalendar/tests/test_icalendar.py index 36314aee..531eb907 100644 --- a/src/icalendar/tests/test_icalendar.py +++ b/src/icalendar/tests/test_icalendar.py @@ -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", ""], ) @@ -112,7 +112,7 @@ def test_contentline_class(self): ) 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 @@ def test_contentline_class(self): ) 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 diff --git a/src/icalendar/tests/test_issue_722_generate_vtimezone.py b/src/icalendar/tests/test_issue_722_generate_vtimezone.py index 31e7f27f..173a0295 100644 --- a/src/icalendar/tests/test_issue_722_generate_vtimezone.py +++ b/src/icalendar/tests/test_issue_722_generate_vtimezone.py @@ -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) diff --git a/src/icalendar/tests/test_unit_caselessdict.py b/src/icalendar/tests/test_unit_caselessdict.py index a3126ee9..fefbc4d2 100644 --- a/src/icalendar/tests/test_unit_caselessdict.py +++ b/src/icalendar/tests/test_unit_caselessdict.py @@ -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") diff --git a/src/icalendar/tests/test_unit_parser_tools.py b/src/icalendar/tests/test_unit_parser_tools.py index 8eea7900..8f3f378e 100644 --- a/src/icalendar/tests/test_unit_parser_tools.py +++ b/src/icalendar/tests/test_unit_parser_tools.py @@ -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") diff --git a/src/icalendar/tests/test_unit_tools.py b/src/icalendar/tests/test_unit_tools.py index b396e55f..e0d8db41 100644 --- a/src/icalendar/tests/test_unit_tools.py +++ b/src/icalendar/tests/test_unit_tools.py @@ -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/content@Example.ORG" in txt) + self.assertEqual(len(txt), length) + self.assertIn(b"-/path/to/content@Example.ORG", txt) @pytest.mark.parametrize( diff --git a/src/icalendar/timezone/equivalent_timezone_ids.py b/src/icalendar/timezone/equivalent_timezone_ids.py index 63da737e..f4b79dda 100644 --- a/src/icalendar/timezone/equivalent_timezone_ids.py +++ b/src/icalendar/timezone/equivalent_timezone_ids.py @@ -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 @@ -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: @@ -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") diff --git a/src/icalendar/timezone/tzp.py b/src/icalendar/timezone/tzp.py index 39dbb9ca..677d6e99 100644 --- a/src/icalendar/timezone/tzp.py +++ b/src/icalendar/timezone/tzp.py @@ -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."""