Skip to content
Merged
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
10 changes: 5 additions & 5 deletions khard/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from . import address_book # pylint: disable=unused-import # for type checking
from . import helpers
from .helpers.typing import (Date, PostAddress, StrList, convert_to_vcard,
list_to_string, string_to_date, string_to_list)
list_to_string, string_to_date, string_to_list, DEFAULT_YEAR)
from .query import AnyQuery, Query


Expand Down Expand Up @@ -450,7 +450,7 @@ def _prepare_birthday_value(self, date: Date) -> tuple[Optional[str],
return date.strip(), True
return None, False
tz = date.tzname()
if date.year == 1900 and date.month != 0 and date.day != 0 \
if date.year == DEFAULT_YEAR and date.month != 0 and date.day != 0 \
and date.hour == 0 and date.minute == 0 and date.second == 0 \
and self.version == "4.0":
fmt = '--%m%d'
Expand Down Expand Up @@ -977,7 +977,7 @@ def _format_date_object(date: Optional[Date], localize: bool) -> str:
return ""
if isinstance(date, str):
return date
if date.year == 1900 and date.month != 0 and date.day != 0 \
if date.year == DEFAULT_YEAR and date.month != 0 and date.day != 0 \
and date.hour == 0 and date.minute == 0 and date.second == 0:
return date.strftime("--%m-%d")
tz = date.tzname()
Expand Down Expand Up @@ -1075,8 +1075,8 @@ def _set_date(self, target: str, key: str, data: dict) -> None:
"with vcard version 4.0.")
if re.match(r"^--\d\d-?\d\d$", new) and self.version != "4.0":
raise ValueError(
f"{key} format --mm-dd and --mmdd only usable with "
"vcard version 4.0. You may use 1900 as placeholder, if "
f"{key} format --mm-dd and --mmdd only usable with vcard "
f"version 4.0. You may use {DEFAULT_YEAR} as placeholder, if "
"the year is unknown.")
try:
v2 = string_to_date(new)
Expand Down
4 changes: 2 additions & 2 deletions khard/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Optional, Sequence, Union

from ruamel.yaml.scalarstring import LiteralScalarString
from .typing import list_to_string, PostAddress
from .typing import list_to_string, PostAddress, DEFAULT_YEAR


YamlPostAddresses = dict[str, Union[list[dict[str, Any]], dict[str, Any]]]
Expand Down Expand Up @@ -164,7 +164,7 @@ def yaml_anniversary(anniversary: Union[str, datetime, None],
return None

if isinstance(anniversary, datetime):
if (version == "4.0" and anniversary.year == 1900
if (version == "4.0" and anniversary.year == DEFAULT_YEAR
and anniversary.month != 0
and anniversary.day != 0
and anniversary.hour == 0
Expand Down
52 changes: 42 additions & 10 deletions khard/helpers/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
PostAddress = dict[str, str]


# A default year for datetimes without one. Python does not support datetime
# objects without a year from 3.15 onwards but vCard does. We add a default
# year to the python object and detect it again when formatting the object.
# 1900 was the internally used default year of python's datetime object when it
# still did support instances without a year (before python 3.5).
DEFAULT_YEAR = 1900


@overload
def convert_to_vcard(name: str, value: StrList, constraint: type[str]) -> str: ...
@overload
Expand Down Expand Up @@ -67,20 +75,44 @@ def string_to_date(string: str) -> datetime:
:param string: the date string to parse
:returns: the parsed datetime object
"""
# try date formats --mmdd, --mm-dd, yyyymmdd, yyyy-mm-dd and datetime
# formats yyyymmddThhmmss, yyyy-mm-ddThh:mm:ss, yyyymmddThhmmssZ,
# yyyy-mm-ddThh:mm:ssZ.
for fmt in ("--%m%d", "--%m-%d", "%Y%m%d", "%Y-%m-%d", "%Y%m%dT%H%M%S",
"%Y-%m-%dT%H:%M:%S", "%Y%m%dT%H%M%SZ", "%Y-%m-%dT%H:%M:%SZ"):

# Attempt to parse the string as any of the date and time formats supported
# by Khard, as defined by the vCard and ISO 8601:2000 specifications.
# Strings which define a day-of-month but not a year are ambiguous, require
# special handling, and will be unsupported in Python >= 3.15. (They were
# already removed in ISO 8601:2004, but remain a part of vCard.)

# Ambiguous cases of a date with no year (--%m%d and --%m-%d).
try:
if string.startswith("--"):
tmp = str(DEFAULT_YEAR) + string[2:]
if "-" in string[2:]:
return datetime.strptime(tmp, "%Y%m-%d")
else:
return datetime.strptime(tmp, "%Y%m%d")
except ValueError:
pass

# Fully qualified date and time formats.
for fmt in (
"%Y%m%d",
"%Y-%m-%d",
"%Y%m%dT%H%M%S",
"%Y-%m-%dT%H:%M:%S",
"%Y%m%dT%H%M%SZ",
"%Y-%m-%dT%H:%M:%SZ",
):
try:
return datetime.strptime(string, fmt)
except ValueError:
continue # with the next format
# try datetime formats yyyymmddThhmmsstz and yyyy-mm-ddThh:mm:sstz where tz
# may look like -06:00.
continue

# Timezone formats which may contain a problematic colon.
for fmt in ("%Y%m%dT%H%M%S%z", "%Y-%m-%dT%H:%M:%S%z"):
try:
return datetime.strptime(''.join(string.rsplit(":", 1)), fmt)
return datetime.strptime("".join(string.rsplit(":", 1)), fmt)
except ValueError:
continue # with the next format
continue

# All formats tried. Date cannot be parsed.
raise ValueError
Loading