Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: a check_reverse_relations method #336

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
- '3.11'
- '3.10'
- '3.9'
- '3.8'
- '3.7'
#- '3.8'
#- '3.7'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down
2 changes: 1 addition & 1 deletion caldav/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import vobject.icalendar

__version__ = "1.3.6"
__version__ = "1.4.0dev"

from .davclient import DAVClient
from .objects import *
Expand Down
51 changes: 50 additions & 1 deletion caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,6 @@ def _handle_relations(self, uid, ical_data):
## TODO: think more through this - is `save_foo` better than `add_foo`?
## `save_foo` should not be used for updating existing content on the
## calendar!

add_event = save_event
add_todo = save_todo
add_journal = save_journal
Expand Down Expand Up @@ -1709,10 +1708,24 @@ class CalendarObjectResource(DAVObject):
event, a todo-item, a journal entry, or a free/busy entry
"""

## There is also STARTTOFINISH, STARTTOSTART and FINISHTOFINISH in RFC9253,
## those do not seem to have any reverse
## (FINISHTOSTART and STARTTOFINISH may seem like reverse relations, but
## as I read the RFC, FINISHTOSTART seems like the reverse of DEPENDS-ON)
## (STARTTOSTART and FINISHTOFINISH may also seem like symmetric relations,
## meaning they are their own reverse, but as I read the RFC they are
## asymmetric)
RELTYPE_REVERSER: ClassVar = {
"PARENT": "CHILD",
"CHILD": "PARENT",
"SIBLING": "SIBLING",
## this is how Tobias Brox inteprets RFC9253:
"DEPENDS-ON": "FINISHTOSTART",
"FINISHTOSTART": "DEPENDENT",
## next/first is a special case, linked list
## it needs special handling when length of list<>2
"NEXT": "FIRST",
"FIRST": "NEXT",
}

_ENDPARAM = None
Expand Down Expand Up @@ -1827,6 +1840,8 @@ def set_relation(
if set_reverse:
other = self.parent.object_by_uid(uid)
if set_reverse:
## TODO: special handling of NEXT/FIRST.
## STARTTOFINISH does not have any equivalent "reverse".
reltype_reverse = self.RELTYPE_REVERSER[reltype]
other.set_relation(other=self, reltype=reltype_reverse, set_reverse=False)

Expand Down Expand Up @@ -1870,6 +1885,10 @@ def get_relatives(

TODO: this is partially overlapped by plann.lib._relships_by_type
in the plann tool. Should consolidate the code.

TODO: should probably return some kind of object instead of a weird dict structure.
(but due to backward compatibility requirement, such an object should behave like
the current dict)
"""
ret = defaultdict(set)
relations = self.icalendar_component.get("RELATED-TO", [])
Expand All @@ -1895,6 +1914,36 @@ def get_relatives(
raise
return ret

def check_reverse_relations(self, pdb: bool = False) -> list:
"""
Goes through all relations and verifies that the return relation is set
Returns a list of objects missing a reverse (or an empty list if everything is OK)
"""
ret = []
relations = self.get_relatives()
for reltype in relations:
for other in relations[reltype]:
revreltype = self.RELTYPE_REVERSER[reltype]
## TODO: special case FIRST/NEXT needs special handling
other_relations = other.get_relatives(
fetch_objects=False, reltypes={revreltype}
)
if (
not str(self.icalendar_component["uid"])
in other_relations[revreltype]
):
if pdb:
import pdb

pdb.set_trace()
ret.append((other, revreltype))
return ret

## TODO: fix this (and consolidate with _handle_relations / set_relation?)
# def ensure_reverse_relations(self):
# missing_relations = self.check_reverse_relations()
# ...

def _get_icalendar_component(self, assert_one=False):
"""Returns the icalendar subcomponent - which should be an
Event, Journal, Todo or FreeBusy from the icalendar class
Expand Down
18 changes: 18 additions & 0 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,24 @@ def testCreateChildParent(self):
assert len(foo["PARENT"]) == 1
foo = parent_.get_relatives(relfilter=lambda x: x.params.get("GAP"))

## verify the check_reverse_relations method (TODO: move to a separate test)
assert parent_.check_reverse_relations() == []
assert child_.check_reverse_relations() == []
assert grandparent_.check_reverse_relations() == []

## My grandchild is also my child ... that sounds fishy
grandparent_.set_relation(child, reltype="CHILD", set_reverse=False)

## The check_reverse should tell that something is amiss
missing_parent = grandparent_.check_reverse_relations()
assert len(missing_parent) == 1
assert missing_parent[0][0].icalendar_component["uid"] == "ctuid2"
assert missing_parent[0][1] == "PARENT"
## But only when run on the grandparent. The child is blissfully
## unaware who the second parent is (even if reloading it).
child_.load()
assert child_.check_reverse_relations() == []

def testSetDue(self):
self.skip_on_compatibility_flag("read_only")

Expand Down
Loading