diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0154e83..6696769 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,14 @@ --- repos: - - repo: https://github.com/asottile/reorder_python_imports - rev: v3.12.0 - hooks: - - id: reorder-python-imports - - repo: https://github.com/psf/black - rev: 23.12.0 - hooks: - - id: black - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: check-byte-order-marker - - id: trailing-whitespace - - id: end-of-file-fixer +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.7 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/caldav/__init__.py b/caldav/__init__.py index 107a085..17dde0e 100644 --- a/caldav/__init__.py +++ b/caldav/__init__.py @@ -11,11 +11,10 @@ "You need to install the `build` package and do a `python -m build` to get caldav.__version__ set correctly" ) from .davclient import DAVClient -from .objects import * ## This should go away in version 2.0. TODO: fix some system for deprecation notices ## TODO: this should go away in some future version of the library. ## How to make deprecation notices? -from .objects import * +from .objects import * ## This should go away in version 2.0. TODO: fix some system for deprecation notices # Silence notification of no default logging handler log = logging.getLogger("caldav") diff --git a/caldav/davclient.py b/caldav/davclient.py index 7d6b475..0502d20 100644 --- a/caldav/davclient.py +++ b/caldav/davclient.py @@ -2,14 +2,14 @@ import logging import sys from types import TracebackType +from typing import TYPE_CHECKING from typing import Any -from typing import cast from typing import Dict from typing import List from typing import Optional from typing import Tuple -from typing import TYPE_CHECKING from typing import Union +from typing import cast from urllib.parse import unquote import requests @@ -27,17 +27,21 @@ from caldav.lib.python_utilities import to_wire from caldav.lib.url import URL from caldav.objects import Calendar -from caldav.objects import log from caldav.objects import Principal +from caldav.objects import log from caldav.requests import HTTPBearerAuth +from .elements.base import BaseElement + if TYPE_CHECKING: pass if sys.version_info < (3, 9): - from typing import Iterable, Mapping + from typing import Iterable + from typing import Mapping else: - from collections.abc import Iterable, Mapping + from collections.abc import Iterable + from collections.abc import Mapping if sys.version_info < (3, 11): from typing_extensions import Self @@ -653,9 +657,7 @@ def request( log.debug("using proxy - %s" % (proxies)) log.debug( - "sending request - method={0}, url={1}, headers={2}\nbody:\n{3}".format( - method, str(url_obj), combined_headers, to_normal_str(body) - ) + f"sending request - method={method}, url={str(url_obj)}, headers={combined_headers}\nbody:\n{to_normal_str(body)}" ) try: @@ -763,16 +765,16 @@ def request( with NamedTemporaryFile(prefix="caldavcomm", delete=False) as commlog: commlog.write(b"=" * 80 + b"\n") - commlog.write(f"{datetime.datetime.now():%FT%H:%M:%S}".encode("utf-8")) + commlog.write(f"{datetime.datetime.now():%FT%H:%M:%S}".encode()) commlog.write(b"\n====>\n") - commlog.write(f"{method} {url}\n".encode("utf-8")) + commlog.write(f"{method} {url}\n".encode()) commlog.write( b"\n".join(to_wire(f"{x}: {headers[x]}") for x in headers) ) commlog.write(b"\n\n") commlog.write(to_wire(body)) commlog.write(b"<====\n") - commlog.write(f"{response.status} {response.reason}".encode("utf-8")) + commlog.write(f"{response.status} {response.reason}".encode()) commlog.write( b"\n".join( to_wire(f"{x}: {response.headers[x]}") for x in response.headers diff --git a/caldav/elements/cdav.py b/caldav/elements/cdav.py index 6df4926..01c100d 100644 --- a/caldav/elements/cdav.py +++ b/caldav/elements/cdav.py @@ -5,10 +5,11 @@ from typing import ClassVar from typing import Optional +from caldav.lib.namespace import ns + from .base import BaseElement from .base import NamedBaseElement from .base import ValuedBaseElement -from caldav.lib.namespace import ns utc_tz = timezone.utc diff --git a/caldav/elements/dav.py b/caldav/elements/dav.py index feb092c..fb7d436 100644 --- a/caldav/elements/dav.py +++ b/caldav/elements/dav.py @@ -1,9 +1,10 @@ #!/usr/bin/env python from typing import ClassVar +from caldav.lib.namespace import ns + from .base import BaseElement from .base import ValuedBaseElement -from caldav.lib.namespace import ns # Operations diff --git a/caldav/elements/ical.py b/caldav/elements/ical.py index ffae7e1..1578eaf 100644 --- a/caldav/elements/ical.py +++ b/caldav/elements/ical.py @@ -1,9 +1,10 @@ #!/usr/bin/env python from typing import ClassVar -from .base import ValuedBaseElement from caldav.lib.namespace import ns +from .base import ValuedBaseElement + # Properties class CalendarColor(ValuedBaseElement): diff --git a/caldav/lib/url.py b/caldav/lib/url.py index 9feff4a..4224dd9 100644 --- a/caldav/lib/url.py +++ b/caldav/lib/url.py @@ -2,12 +2,12 @@ import sys import urllib.parse from typing import Any -from typing import cast from typing import Optional from typing import Union +from typing import cast from urllib.parse import ParseResult -from urllib.parse import quote from urllib.parse import SplitResult +from urllib.parse import quote from urllib.parse import unquote from urllib.parse import urlparse from urllib.parse import urlunparse diff --git a/caldav/objects.py b/caldav/objects.py index 13cc740..4c0101e 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -7,6 +7,7 @@ release. I think it makes sense moving the CalendarObjectResource class hierarchy into a separate file) """ + import re import sys import uuid @@ -15,17 +16,17 @@ class hierarchy into a separate file) from datetime import datetime from datetime import timedelta from datetime import timezone +from typing import TYPE_CHECKING from typing import Any from typing import List from typing import Optional from typing import Set from typing import Tuple -from typing import TYPE_CHECKING from typing import TypeVar from typing import Union from urllib.parse import ParseResult -from urllib.parse import quote from urllib.parse import SplitResult +from urllib.parse import quote from urllib.parse import unquote import icalendar @@ -35,15 +36,18 @@ class hierarchy into a separate file) from lxml.etree import _Element from vobject.base import VBase -from .elements.base import BaseElement -from .elements.cdav import CalendarData -from .elements.cdav import CompFilter from caldav.lib.python_utilities import to_normal_str from caldav.lib.python_utilities import to_unicode from caldav.lib.python_utilities import to_wire +from .elements.base import BaseElement +from .elements.cdav import CalendarData +from .elements.cdav import CompFilter + try: - from typing import ClassVar, Optional, Union + from typing import ClassVar + from typing import Optional + from typing import Union TimeStamp = Optional[Union[date, datetime]] except: @@ -51,8 +55,10 @@ class hierarchy into a separate file) import logging -from caldav.elements import cdav, dav -from caldav.lib import error, vcal +from caldav.elements import cdav +from caldav.elements import dav +from caldav.lib import error +from caldav.lib import vcal from caldav.lib.url import URL if TYPE_CHECKING: @@ -61,12 +67,21 @@ class hierarchy into a separate file) from .davclient import DAVClient if sys.version_info < (3, 9): - from typing import Callable, Container, Iterable, Iterator, Sequence - - from typing_extensions import DefaultDict, Literal + from typing import Callable + from typing import Container + from typing import DefaultDict + from typing import Iterable + from typing import Iterator + from typing import Sequence + + from typing_extensions import Literal else: from collections import defaultdict as DefaultDict - from collections.abc import Callable, Container, Iterable, Iterator, Sequence + from collections.abc import Callable + from collections.abc import Container + from collections.abc import Iterable + from collections.abc import Iterator + from collections.abc import Sequence from typing import Literal if sys.version_info < (3, 11): @@ -631,7 +646,8 @@ def get_vcal_address(self) -> "vCalAddress": """ Returns the principal, as an icalendar.vCalAddress object """ - from icalendar import vCalAddress, vText + from icalendar import vCalAddress + from icalendar import vText cn = self.get_display_name() ids = self.calendar_user_address_set() @@ -2236,7 +2252,8 @@ def add_attendee( role=REQ-PARTICIPANT schedule-agent is not set """ - from icalendar import vCalAddress, vText + from icalendar import vCalAddress + from icalendar import vText if isinstance(attendee, Principal): attendee_obj = attendee.get_vcal_address() @@ -2575,9 +2592,7 @@ def has_component(self): or (self._icalendar_instance and self.icalendar_component) ) and self.data.count("BEGIN:VEVENT") + self.data.count( "BEGIN:VTODO" - ) + self.data.count( - "BEGIN:VJOURNAL" - ) > 0 + ) + self.data.count("BEGIN:VJOURNAL") > 0 def __str__(self) -> str: return "%s: %s" % (self.__class__.__name__, self.url) @@ -2867,7 +2882,7 @@ def _next(self, ts=None, i=None, dtstart=None, rrule=None, by=None, no_count=Tru rrule = i["RRULE"] if not dtstart: if by is True or ( - by is None and any((x for x in rrule if x.startswith("BY"))) + by is None and any(x for x in rrule if x.startswith("BY")) ): if "DTSTART" in i: dtstart = i["DTSTART"].dt diff --git a/docs/source/conf.py b/docs/source/conf.py index 50f2702..51782b1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # python-caldav documentation build configuration file, created by # sphinx-quickstart on Thu Jun 3 10:47:52 2010. diff --git a/examples/basic_usage_examples.py b/examples/basic_usage_examples.py index bf404e5..df345d4 100644 --- a/examples/basic_usage_examples.py +++ b/examples/basic_usage_examples.py @@ -168,7 +168,7 @@ def read_modify_event_demo(event): event.data = event.data ## So this will not affect the event anymore: icalendar_component["summary"] = "do the needful" - assert not "do the needful" in event.data + assert "do the needful" not in event.data ## The mofifications are still only saved locally in memory - ## let's save it to the server: diff --git a/examples/scheduling_examples.py b/examples/scheduling_examples.py index 4b591ad..5067647 100644 --- a/examples/scheduling_examples.py +++ b/examples/scheduling_examples.py @@ -10,7 +10,6 @@ from caldav import DAVClient from caldav import error - ############### ### SETUP START ### rfc6638_users should be a list with three dicts containing credential details. diff --git a/pyproject.toml b/pyproject.toml index 539fb29..8a3a979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -37,7 +36,7 @@ dependencies = [ "icalendar;python_version!='3.8'", ] dynamic = ["version"] - +requires-python = ">=3.8" [project.optional-dependencies] test = [ "pytest", @@ -60,3 +59,7 @@ include-package-data = true [tool.setuptools.packages.find] exclude = ["tests"] namespaces = false + +[tool.ruff.lint] +extend-select = ["UP", "I"] +isort.force-single-line = true diff --git a/setup.py b/setup.py deleted file mode 100755 index 1b5eca2..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -import setuptools - -setuptools.setup() diff --git a/tests/_test_absolute.py b/tests/_test_absolute.py index 39cb83a..241e976 100644 --- a/tests/_test_absolute.py +++ b/tests/_test_absolute.py @@ -1,10 +1,9 @@ -# encoding: utf-8 import datetime import caldav -class TestRadicale(object): +class TestRadicale: SUMMARIES = set( ( "Godspeed You! Black Emperor at " "Cirque Royal / Koninklijk Circus", @@ -35,7 +34,7 @@ def test_eventslist(self): assert dtstart == self.DTSTART -class TestTryton(object): +class TestTryton: def setup(self): URL = "http://admin:admin@localhost:9080/caldav/Calendars/Test" self.client = caldav.DAVClient(URL) diff --git a/tests/conf.py b/tests/conf.py index 01b9754..4bd97e5 100644 --- a/tests/conf.py +++ b/tests/conf.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- ## YOU SHOULD MOST LIKELY NOT EDIT THIS FILE! ## Make a conf_private.py for personal configuration. ## Check conf_private.py.EXAMPLE import logging +from typing import Any +from typing import Optional from caldav.davclient import DAVClient @@ -38,7 +39,8 @@ pass try: - from .conf_private import xandikos_host, xandikos_port + from .conf_private import xandikos_host + from .conf_private import xandikos_port except ImportError: xandikos_host = "localhost" xandikos_port = 8993 ## random port above 8000 @@ -49,11 +51,12 @@ import xandikos test_xandikos = True - except: + except ImportError: test_xandikos = False try: - from .conf_private import radicale_host, radicale_port + from .conf_private import radicale_host + from .conf_private import radicale_port except ImportError: radicale_host = "localhost" radicale_port = 5232 ## default radicale host @@ -65,7 +68,7 @@ import radicale test_radicale = True - except: + except ImportError: test_radicale = False try: @@ -144,7 +147,7 @@ ) -def client(idx=None, **kwargs): +def client(idx: Optional[int] = None, **kwargs: Any) -> DAVClient: if idx is None and not kwargs: return client(0) elif idx is not None and not kwargs and caldav_servers: @@ -160,10 +163,11 @@ def client(idx=None, **kwargs): if bad_param in kwargs: kwargs.pop(bad_param) for kw in kwargs: - if not kw in CONNKEYS: + if kw not in CONNKEYS: logging.critical( - "unknown keyword %s in connection parameters. All compatibility flags should now be sent as a separate list, see conf_private.py.EXAMPLE. Ignoring." - % kw + "unknown keyword %s in connection parameters. " + "All compatibility flags should now be sent as a separate list, see conf_private.py.EXAMPLE. Ignoring.", + kw, ) kwargs.pop(kw) return DAVClient(**kwargs) diff --git a/tests/proxy.py b/tests/proxy.py index b74b543..9d77418 100644 --- a/tests/proxy.py +++ b/tests/proxy.py @@ -13,6 +13,7 @@ * Added custom logging methods * Added code to make this a standalone application """ + import ftplib import getopt import logging.handlers @@ -28,7 +29,6 @@ from time import sleep from types import CodeType from types import FrameType -from urllib import parse from urllib.parse import urlparse from urllib.parse import urlunparse @@ -68,7 +68,7 @@ def _connect_to(self, netloc, soc): ) try: soc.connect(host_port) - except socket.error as arg: + except OSError as arg: try: msg = arg[1] except: @@ -248,7 +248,7 @@ def handler(signo, frame): def daemonize(logger): - class DevNull(object): + class DevNull: def __init__(self): self.fd = os.open("/dev/null", os.O_WRONLY) @@ -361,7 +361,7 @@ def main(): threading.activeCount(), ) req_count = 0 - except select.error as e: + except OSError as e: if e[0] == 4 and run_event.isSet(): pass else: diff --git a/tests/test_caldav.py b/tests/test_caldav.py index b3c6254..fe5afbd 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- """ Tests here communicate with third party servers and/or internal ad-hoc instances of Xandikos and Radicale, dependent on the @@ -7,15 +6,14 @@ Tests that do not require communication with a working caldav server belong in test_caldav_unit.py """ + import codecs import logging import random -import sys import tempfile import threading import time import uuid -from collections import namedtuple from datetime import date from datetime import datetime from datetime import timedelta @@ -27,6 +25,18 @@ import vobject from requests.packages import urllib3 +from caldav.elements import cdav +from caldav.elements import dav +from caldav.elements import ical +from caldav.lib import error +from caldav.lib.python_utilities import to_local +from caldav.lib.python_utilities import to_str +from caldav.objects import DAVObject +from caldav.objects import Event +from caldav.objects import FreeBusy +from caldav.objects import Principal +from caldav.objects import Todo + from . import compatibility_issues from .conf import caldav_servers from .conf import client @@ -41,36 +51,21 @@ from .conf import xandikos_port from .proxy import NonThreadingHTTPServer from .proxy import ProxyHandler -from caldav.davclient import DAVClient -from caldav.davclient import DAVResponse -from caldav.elements import cdav -from caldav.elements import dav -from caldav.elements import ical -from caldav.lib import error -from caldav.lib import url -from caldav.lib.python_utilities import to_local -from caldav.lib.python_utilities import to_str -from caldav.lib.url import URL -from caldav.objects import Calendar -from caldav.objects import CalendarSet -from caldav.objects import DAVObject -from caldav.objects import Event -from caldav.objects import FreeBusy -from caldav.objects import Principal -from caldav.objects import Todo if test_xandikos: import asyncio import aiohttp import aiohttp.web - from xandikos.web import XandikosApp, XandikosBackend + from xandikos.web import XandikosApp + from xandikos.web import XandikosBackend if test_radicale: - import radicale.config + import socket + import radicale + import radicale.config import radicale.server - import socket from urllib.parse import urlparse @@ -455,7 +450,7 @@ len(rfc6638_users) < 3, reason="need at least three users in rfc6638_users to be set in order to run this test", ) -class TestScheduling(object): +class TestScheduling: """Testing support of RFC6638. TODO: work in progress. Stalled a bit due to lack of proper testing accounts. I haven't managed to get this test to pass at any systems yet, but I believe the problem is not on the library side. * icloud: cannot really test much with only one test account @@ -537,7 +532,7 @@ def testInviteAndRespond(self): ## principals[1] should have one new inbox item new_inbox_items = [] for item in self.principals[1].schedule_inbox().get_items(): - if not item.url in inbox_items: + if item.url not in inbox_items: new_inbox_items.append(item) assert len(new_inbox_items) == 1 ## ... and the new inbox item should be an invite request @@ -553,7 +548,7 @@ def testInviteAndRespond(self): ## calendar invite was accepted new_inbox_items = [] for item in self.principals[0].schedule_inbox().get_items(): - if not item.url in inbox_items: + if item.url not in inbox_items: new_inbox_items.append(item) assert len(new_inbox_items) == 1 assert new_inbox_items[0].is_invite_reply() @@ -570,7 +565,7 @@ def testInviteAndRespond(self): ## inbox/outbox? -class RepeatedFunctionalTestsBaseClass(object): +class RepeatedFunctionalTestsBaseClass: """This is a class with functional tests (tests that goes through basic functionality and actively communicates with third parties) that we want to repeat for all configured caldav_servers. @@ -1227,7 +1222,7 @@ def testSync(self): if not self.check_compatibility_flag("fragile_sync_tokens"): assert len(list(updated)) == 0 assert len(list(deleted)) == 1 - assert not obj.url in my_objects.objects_by_url() + assert obj.url not in my_objects.objects_by_url() if self.check_compatibility_flag("time_based_sync_tokens"): time.sleep(1) @@ -1691,7 +1686,7 @@ def testSearchTodos(self): def testWrongPassword(self): if ( - not "password" in self.server_params + "password" not in self.server_params or not self.server_params["password"] or self.server_params["password"] == "any-password-seems-to-work" ): diff --git a/tests/test_caldav_unit.py b/tests/test_caldav_unit.py index d795756..28b5ce3 100644 --- a/tests/test_caldav_unit.py +++ b/tests/test_caldav_unit.py @@ -1,11 +1,11 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- """ Rule: None of the tests in this file should initiate any internet communication, and there should be no dependencies on a working caldav server for the tests in this file. We use the Mock class when needed to emulate server communication. """ + import pickle from datetime import date from datetime import datetime @@ -16,15 +16,11 @@ import icalendar import lxml.etree import pytest -import vobject -import caldav from caldav import Calendar from caldav import CalendarObjectResource from caldav import CalendarSet -from caldav import DAVObject from caldav import Event -from caldav import FreeBusy from caldav import Journal from caldav import Principal from caldav import Todo @@ -32,9 +28,7 @@ from caldav.davclient import DAVResponse from caldav.elements import cdav from caldav.elements import dav -from caldav.elements import ical from caldav.lib import error -from caldav.lib import url from caldav.lib.python_utilities import to_normal_str from caldav.lib.python_utilities import to_wire from caldav.lib.url import URL @@ -199,7 +193,7 @@ def testOne(self): start=datetime(1998, 10, 10), end=datetime(1998, 12, 12) ) assert len(self.yearly.icalendar_instance.subcomponents) == 1 - assert not "RRULE" in self.yearly.icalendar_component + assert "RRULE" not in self.yearly.icalendar_component assert "UID" in self.yearly.icalendar_component assert "RECURRENCE-ID" in self.yearly.icalendar_component @@ -265,8 +259,8 @@ def testRequestNonAscii(self, mocked): assert response.tree is None response = client.put( - "/foo/møøh/bar".encode("utf-8"), - "bringebærsyltetøy 北京 пиво".encode("utf-8"), + "/foo/møøh/bar".encode(), + "bringebærsyltetøy 北京 пиво".encode(), {}, ) assert response.status == 200 @@ -312,7 +306,8 @@ def testEmptyXMLNoContentLength(self, mocked): mocked().status_code = 200 mocked().headers = {"Content-Type": "text/xml"} mocked().content = "" - client = DAVClient(url="AsdfasDF").request("/") + client = DAVClient(url="AsdfasDF") + client.request("/") @mock.patch("caldav.davclient.requests.Session.request") def testNonValidXMLNoContentLength(self, mocked): @@ -991,11 +986,8 @@ def testHugeTreeParam(self): # assert type(e) == lxml.etree.XMLSyntaxError davclient.huge_tree = True - try: - DAVResponse(resp, davclient=davclient) - assert True - except: - assert False + + DAVResponse(resp, davclient=davclient) def testFailedQuery(self): """ @@ -1266,16 +1258,11 @@ def testFilters(self): ) ) ) - # print(filter) + print(filter) crash = cdav.CompFilter() - value = None - try: - value = str(crash) - except: - pass - if value is not None: - raise Exception("This should have crashed") + with pytest.raises(Exception, match="name attribute must be defined"): + str(crash) def test_calendar_comp_class_by_data(self): calendar = Calendar() diff --git a/tests/test_cdav.py b/tests/test_cdav.py index e9ba39a..4d6bc93 100644 --- a/tests/test_cdav.py +++ b/tests/test_cdav.py @@ -2,8 +2,8 @@ import tzlocal -from caldav.elements.cdav import _to_utc_date_string from caldav.elements.cdav import CalendarQuery +from caldav.elements.cdav import _to_utc_date_string try: import zoneinfo @@ -16,7 +16,7 @@ def test_element(): cq = CalendarQuery() assert str(cq).startswith("=4 [testenv] -deps = --editable .[test] -commands = coverage run -m pytest - +usedevelop = true +extras = test +deps = + radicale: radicale + xandikos: xandikos +commands = + coverage: coverage run -m pytest [] + !coverage: pytest [] [testenv:docs] deps = sphinx commands =