Coverage for src/lektor_ng/types/primitives.py: 93%
115 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-08 23:29 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-08 23:29 +0000
1import re
2from datetime import date, datetime
4from babel.dates import get_timezone
5from markupsafe import Markup
7from lektor_ng.constants import PRIMARY_ALT
8from lektor_ng.i18n import get_i18n_block
9from lektor_ng.types.base import Type
10from lektor_ng.utils import bool_from_string
13class SingleInputType(Type):
14 widget = "singleline-text"
16 def to_json(self, pad, record=None, alt=PRIMARY_ALT):
17 rv = Type.to_json(self, pad, record, alt)
18 rv["addon_label_i18n"] = get_i18n_block(self.options, "addon_label") or None
19 return rv
22class StringType(SingleInputType):
23 def value_from_raw(self, raw):
24 if raw.value is None:
25 return raw.missing_value("Missing string")
26 try:
27 return raw.value.splitlines()[0].strip()
28 except IndexError:
29 return ""
32class StringsType(Type):
33 widget = "multiline-text"
35 def value_from_raw(self, raw):
36 return [x.strip() for x in (raw.value or "").splitlines()]
39class TextType(Type):
40 widget = "multiline-text"
42 def value_from_raw(self, raw):
43 if raw.value is None:
44 return raw.missing_value("Missing text")
45 return raw.value
48class HtmlType(Type):
49 widget = "multiline-text"
51 def value_from_raw(self, raw):
52 if raw.value is None:
53 return raw.missing_value("Missing HTML")
54 return Markup(raw.value)
57class IntegerType(SingleInputType):
58 widget = "integer"
60 def value_from_raw(self, raw):
61 if raw.value is None:
62 return raw.missing_value("Missing integer value")
63 try:
64 return int(raw.value.strip())
65 except ValueError:
66 try:
67 return int(float(raw.value.strip()))
68 except ValueError:
69 return raw.bad_value("Not an integer")
72class FloatType(SingleInputType):
73 widget = "float"
75 def value_from_raw(self, raw):
76 if raw.value is None:
77 return raw.missing_value("Missing float value")
78 try:
79 return float(raw.value.strip())
80 except ValueError:
81 return raw.bad_value("Not an integer")
84class BooleanType(Type):
85 widget = "checkbox"
87 def to_json(self, pad, record=None, alt=PRIMARY_ALT):
88 rv = Type.to_json(self, pad, record, alt)
89 rv["checkbox_label_i18n"] = get_i18n_block(self.options, "checkbox_label")
90 return rv
92 def value_from_raw(self, raw):
93 if raw.value is None:
94 return raw.missing_value("Missing boolean")
95 val = bool_from_string(raw.value.strip().lower())
96 if val is None:
97 return raw.bad_value("Bad boolean value")
98 return val
101class DateType(SingleInputType):
102 widget = "datepicker"
104 def value_from_raw(self, raw):
105 if raw.value is None:
106 return raw.missing_value("Missing date")
107 try:
108 return date(*map(int, raw.value.split("-")))
109 except Exception:
110 return raw.bad_value("Bad date format")
113class DateTimeType(SingleInputType):
114 widget = "singleline-text"
116 def value_from_raw(self, raw):
117 if raw.value is None:
118 return raw.missing_value("Missing datetime")
120 # The previous version of this code allowed a timezone name, followed by a zone
121 # offset. In that case the zone name would be ignored (unless the combined zone
122 # name, including the offset, matched an IANA zone key). For the sake of
123 # backwards compatibility we do the same here.
124 m = re.match(
125 r"""
126 (?P<datetime>
127 \d{4} - \d\d? - \d\d? # YY-MM-DD
128 \s+ \d\d? : \d\d (?P<seconds> :\d\d )? # HH:MM[:SS]
129 )
130 (?: \s+
131 (?P<timezone>
132 # Long timezone keys, and — on Windows — names containing
133 # certain characters (those that are not allowed in filenames)
134 # give zoneinfo gas.
135 # https://github.com/python/cpython/issues/96463
136 [^<>:"|?*\x00-\x1f]{,100}?
137 (?P<zoneoffset> [-+] \d\d (?: :? \d\d ){1,2} )? # ±HHMM[SS]
138 )
139 )?
140 \Z""",
141 raw.value.strip(),
142 re.DOTALL | re.VERBOSE,
143 )
144 if m is None:
145 return raw.bad_value("Bad datetime format")
146 timezone, zoneoffset = m.group("timezone", "zoneoffset")
147 tz = None
148 if timezone is not None:
149 try:
150 tz = get_timezone(timezone)
151 zoneoffset = None
152 # NB: under py39 on windows with tzdata, zoneinfo.ZoneInfo can raise
153 # ValueError
154 except (LookupError, ValueError):
155 if zoneoffset is None:
156 return raw.bad_value(f"Unknown timezone {timezone!r}")
158 dt = m["datetime"]
159 fmt = "%Y-%m-%d %H:%M"
160 if m["seconds"] is not None:
161 fmt += ":%S"
162 if zoneoffset is not None:
163 dt += f" {zoneoffset}"
164 fmt += " %z"
165 try:
166 result = datetime.strptime(dt, fmt)
167 except ValueError:
168 return raw.bad_value("Invalid datetime")
170 if tz is None:
171 return result
173 # as of babel 2.12, get_timezone can return either a pytz timezone
174 # or a zoneinfo timezone
175 assert result.tzinfo is None
176 if hasattr(tz, "localize"): # pytz
177 return tz.localize(result)
178 return result.replace(tzinfo=tz)