Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/recur/month.py: 74%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

66 statements  

1"""BYMONTH value type of RECUR from :rfc:`5545` and :rfc:`7529`.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any 

6from xml.etree.ElementTree import Element, SubElement 

7 

8from icalendar.error import JCalParsingError, XCalParsingError 

9from icalendar.parser import Parameters 

10from icalendar.parser.xcal.wrapper import from_xcal_wrapper 

11 

12if TYPE_CHECKING: 

13 from icalendar.compatibility import Self 

14 from icalendar.parser.xcal.base import XCalParser 

15 

16 

17class vMonth(int): 

18 """The number of the month for recurrence. 

19 

20 In :rfc:`5545`, this is just an int. 

21 In :rfc:`7529`, this can be followed by `L` to indicate a leap month. 

22 

23 .. code-block:: pycon 

24 

25 >>> from icalendar import vMonth 

26 >>> vMonth(1) # first month January 

27 vMonth('1') 

28 >>> vMonth("5L") # leap month in Hebrew calendar 

29 vMonth('5L') 

30 >>> vMonth(1).leap 

31 False 

32 >>> vMonth("5L").leap 

33 True 

34 

35 Definition from RFC: 

36 

37 .. code-block:: text 

38 

39 type-bymonth = element bymonth { 

40 xsd:positiveInteger | 

41 xsd:string 

42 } 

43 """ 

44 

45 params: Parameters 

46 

47 def __new__(cls, month: str | int, /, params: dict[str, Any] | None = None): 

48 if isinstance(month, vMonth): 

49 return cls(month.to_ical().decode()) 

50 if isinstance(month, str): 

51 # ``str.isdigit`` is True for non-ASCII digits (e.g. "١٢") that 

52 # ``int`` then either accepts as a different value or rejects, so 

53 # gate on ASCII digits to match what ``int`` parses below. 

54 if month.isascii() and month.isdigit(): 

55 month_index = int(month) 

56 leap = False 

57 else: 

58 digits = month[:-1] 

59 if ( 

60 not month 

61 or month[-1] != "L" 

62 or not (digits.isascii() and digits.isdigit()) 

63 ): 

64 raise ValueError(f"Invalid month: {month!r}") 

65 month_index = int(digits) 

66 leap = True 

67 else: 

68 leap = False 

69 month_index = int(month) 

70 self = super().__new__(cls, month_index) 

71 self.leap = leap 

72 self.params = Parameters(params) 

73 return self 

74 

75 def to_ical(self) -> bytes: 

76 """The ical representation.""" 

77 return str(self).encode("utf-8") 

78 

79 @classmethod 

80 def from_ical(cls, ical: str): 

81 return cls(ical) 

82 

83 @property 

84 def leap(self) -> bool: 

85 """Whether this is a leap month.""" 

86 return self._leap 

87 

88 @leap.setter 

89 def leap(self, value: bool) -> None: 

90 self._leap = value 

91 

92 def __repr__(self) -> str: 

93 """repr(self)""" 

94 return f"{self.__class__.__name__}({str(self)!r})" 

95 

96 def __str__(self) -> str: 

97 """str(self)""" 

98 return f"{int(self)}{'L' if self.leap else ''}" 

99 

100 @classmethod 

101 def parse_jcal_value(cls, value: Any) -> Self: 

102 """Parse a jCal value for vMonth. 

103 

104 Raises: 

105 ~error.JCalParsingError: If the value is not a valid month. 

106 """ 

107 JCalParsingError.validate_value_type(value, (str, int), cls) 

108 try: 

109 return cls(value) 

110 except ValueError as e: 

111 raise JCalParsingError( 

112 "The value must be a string or an integer.", cls, value=value 

113 ) from e 

114 

115 @classmethod 

116 @from_xcal_wrapper 

117 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self: 

118 """Parse xCal from :rfc:`6321`. 

119 

120 Parameters: 

121 xml: The XML to parse or a parser. 

122 

123 Raises: 

124 ~error.XCalParsingError: If the provided xCal is invalid. 

125 """ 

126 child = parser.parse_tag() 

127 text = child.get_xsd_token() 

128 try: 

129 return cls(text, params=params) 

130 except ValueError as e: 

131 raise XCalParsingError("Expected month number", text, child) from e 

132 

133 def to_xcal(self, element: Element) -> None: 

134 """Add the xCal representation of this property according to :rfc:`6321`.""" 

135 self.params.to_xcal(element) 

136 month_element = SubElement(element, "bymonth") 

137 month_element.text = str(self) 

138 

139 

140__all__ = ["vMonth"]