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
1 change: 0 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ disable=
unspecified-encoding,
unused-argument,
# C
consider-using-f-string,
import-outside-toplevel,
invalid-name,
missing-docstring,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_icalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,11 @@ def test_regexes():
"""
Test regex patterns
"""
assert re.findall(vobject.base.patterns["name"], "12foo-bar:yay") == ["12foo-bar", "yay"]
assert re.findall(vobject.base.patterns["safe_char"], 'a;b"*,cd') == ["a", "b", "*", "c", "d"]
assert re.findall(vobject.base.patterns["qsafe_char"], 'a;b"*,cd') == ["a", ";", "b", "*", ",", "c", "d"]
assert re.findall(vobject.base.P_NAME, "12foo-bar:yay") == ["12foo-bar", "yay"]
assert re.findall(vobject.base.P_SAFE_CHAR, 'a;b"*,cd') == ["a", "b", "*", "c", "d"]
assert re.findall(vobject.base.P_QSAFE_CHAR, 'a;b"*,cd') == ["a", ";", "b", "*", ",", "c", "d"]
assert re.findall(
vobject.base.patterns["param_value"], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE
vobject.base.P_PARAM_VALUE, '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE # black hack
) == ['"quoted"', "", "not-quoted", "", "start", "", "after-illegal-quote", ""]

match = vobject.base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"')
Expand Down
142 changes: 59 additions & 83 deletions vobject/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ def transformToNative(self):
e.lineNumber = lineNumber
raise

msg = "In transformToNative, unhandled exception on line {0}: {1}: {2}"
msg = msg.format(lineNumber, sys.exc_info()[0], sys.exc_info()[1])
msg = msg + " (" + str(self_orig) + ")"
msg = (
f"In transformToNative, unhandled exception on line {lineNumber}: {sys.exc_info()[0]}:"
f" {sys.exc_info()[1]} ({self_orig})"
)
raise ParseError(msg, lineNumber)

def transformFromNative(self):
Expand All @@ -185,8 +186,10 @@ def transformFromNative(self):
e.lineNumber = lineNumber
raise

msg = "In transformFromNative, unhandled exception on line {0} {1}: {2}"
msg = msg.format(lineNumber, sys.exc_info()[0], sys.exc_info()[1])
msg = (
f"In transformFromNative, unhandled exception on line {lineNumber} {sys.exc_info()[0]}:"
f" {sys.exc_info()[1]}"
)
raise NativeError(msg, lineNumber)
else:
return self
Expand All @@ -208,11 +211,11 @@ def serialize(self, buf=None, lineLength=75, validate=True, behavior=None, *args

if behavior:
if DEBUG:
logger.debug("serializing {0!s} with behavior {1!s}".format(self.name, behavior))
logger.debug("serializing %s with behavior %s", self.name, self.behavior)
return behavior.serialize(self, buf, lineLength, validate, *args, **kwargs)
else:
if DEBUG:
logger.debug("serializing {0!s} without behavior".format(self.name))
logger.debug("serializing %s without behavior", self.name)
return defaultSerialize(self, buf, lineLength)


Expand Down Expand Up @@ -385,15 +388,15 @@ def valueRepr(self):

def __str__(self):
try:
return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())
return f"<{self.name}{self.params}{self.valueRepr()}>"
except UnicodeEncodeError:
return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr().encode("utf-8"))
return f"<{self.name}{self.params}{self.valueRepr().encode('utf-8')}>"

def __repr__(self):
return self.__str__()

def __unicode__(self):
return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())
return f"<{self.name}{self.params}{self.valueRepr()}>"

def prettyPrint(self, level=0, tabwidth=3):
pre = " " * level * tabwidth
Expand Down Expand Up @@ -642,9 +645,9 @@ def transformChildrenFromNative(self, clearBehavior=True):

def __str__(self):
if self.name:
return "<{0}| {1}>".format(self.name, self.getSortedChildren())
return f"<{self.name}| {self.getSortedChildren()}>"
else:
return "<*unnamed*| {0}>".format(self.getSortedChildren())
return f"<*unnamed*| {self.getSortedChildren()}>"

def __repr__(self):
return self.__str__()
Expand All @@ -665,7 +668,7 @@ def __init__(self, msg, lineNumber=None):

def __str__(self):
if hasattr(self, "lineNumber"):
return "At line {0!s}: {1!s}".format(self.lineNumber, self.msg)
return f"At line {self.lineNumber}: {self.msg}"
else:
return repr(self.msg)

Expand All @@ -686,76 +689,55 @@ class NativeError(VObjectError):

# --------- Parsing functions and parseLine regular expressions ----------------

patterns = {}

# Note that underscore is not legal for names, it's included because
# Lotus Notes uses it
patterns["name"] = "[a-zA-Z0-9_-]+" # 1*(ALPHA / DIGIT / "-")
patterns["safe_char"] = '[^";:,]'
patterns["qsafe_char"] = '[^"]'
P_NAME = "[a-zA-Z0-9_-]+" # 1*(ALPHA / DIGIT / "-")
P_SAFE_CHAR = '[^";:,]'
P_QSAFE_CHAR = '[^"]'

# the combined Python string replacement and regex syntax is a little confusing;
# remember that {foobar} is replaced with patterns['foobar'], so for instance
# param_value is any number of safe_chars or any number of qsaf_chars surrounded
# by double quotes.

patterns["param_value"] = ' "{qsafe_char!s} * " | {safe_char!s} * '.format(**patterns)

P_PARAM_VALUE = f' "{P_QSAFE_CHAR} * " | {P_SAFE_CHAR} * '

# get a tuple of two elements, one will be empty, the other will have the value
patterns["param_value_grouped"] = (
"""
" ( {qsafe_char!s} * )" | ( {safe_char!s} + )
""".format(
**patterns
)
)
P_PARAM_VALUE_GROUPED = f' " ( {P_QSAFE_CHAR} * )" | ( {P_SAFE_CHAR} + ) '

# get a parameter and its values, without any saved groups
patterns["param"] = (
r"""
; (?: {name!s} ) # parameter name
P_PARAM = rf"""
; (?: {P_NAME} ) # parameter name
(?:
(?: = (?: {param_value!s} ) )? # 0 or more parameter values, multiple
(?: , (?: {param_value!s} ) )* # parameters are comma separated
(?: = (?: {P_PARAM_VALUE} ) )? # 0 or more parameter values, multiple
(?: , (?: {P_PARAM_VALUE} ) )* # parameters are comma separated
)*
""".format(
**patterns
)
)
"""

# get a parameter, saving groups for name and value (value still needs parsing)
patterns["params_grouped"] = (
r"""
; ( {name!s} )
P_PARAMS_GROUPED = rf"""
; ( {P_NAME} )

(?: =
(
(?: (?: {param_value!s} ) )? # 0 or more parameter values, multiple
(?: , (?: {param_value!s} ) )* # parameters are comma separated
(?: (?: {P_PARAM_VALUE} ) )? # 0 or more parameter values, multiple
(?: , (?: {P_PARAM_VALUE} ) )* # parameters are comma separated
)
)?
""".format(
**patterns
)
)
"""

# get a full content line, break it up into group, name, parameters, and value
patterns["line"] = (
r"""
^ ((?P<group> {name!s})\.)?(?P<name> {name!s}) # name group
(?P<params> ;?(?: {param!s} )* ) # params group (may be empty)
P_LINE = rf"""
^ ((?P<group> {P_NAME})\.)?(?P<name> {P_NAME}) # name group
(?P<params> ;?(?: {P_PARAM} )* ) # params group (may be empty)
: (?P<value> .* )$ # value group
""".format(
**patterns
)
)
"""


' "%(qsafe_char)s*" | %(safe_char)s* ' # what is this line?? - never assigned?

param_values_re = re.compile(patterns["param_value_grouped"], re.VERBOSE)
params_re = re.compile(patterns["params_grouped"], re.VERBOSE)
line_re = re.compile(patterns["line"], re.DOTALL | re.VERBOSE)
param_values_re = re.compile(P_PARAM_VALUE_GROUPED, re.VERBOSE)
params_re = re.compile(P_PARAMS_GROUPED, re.VERBOSE)
line_re = re.compile(P_LINE, re.DOTALL | re.VERBOSE)
begin_re = re.compile("BEGIN", re.IGNORECASE)


Expand Down Expand Up @@ -783,7 +765,7 @@ def parseLine(line, lineNumber=None):
"""
match = line_re.match(line)
if match is None:
raise ParseError("Failed to parse line: {0!s}".format(line), lineNumber)
raise ParseError(f"Failed to parse line: {line}", lineNumber)
# Underscores are replaced with dash to work around Lotus Notes
return (
match.group("name").replace("_", "-"),
Expand All @@ -795,23 +777,19 @@ def parseLine(line, lineNumber=None):

# logical line regular expressions

patterns["lineend"] = r"(?:\r\n|\r|\n|$)"
patterns["wrap"] = r"{lineend!s} [\t ]".format(**patterns)
patterns["logicallines"] = (
r"""
P_LINEEND = r"(?:\r\n|\r|\n|$)"
P_WRAP = rf"{P_LINEEND} [\t ]"
P_LOGICALLINES = rf"""
(
(?: [^\r\n] | {wrap!s} )*
{lineend!s}
)
""".format(
**patterns
)
(?: [^\r\n] | {P_WRAP} )*
{P_LINEEND}
)
"""

patterns["wraporend"] = r"({wrap!s} | {lineend!s} )".format(**patterns)
P_WRAPOREND = rf"({P_WRAP} | {P_LINEEND} )"

wrap_re = re.compile(patterns["wraporend"], re.VERBOSE)
logical_lines_re = re.compile(patterns["logicallines"], re.VERBOSE)
wrap_re = re.compile(P_WRAPOREND, re.VERBOSE)
logical_lines_re = re.compile(P_LOGICALLINES, re.VERBOSE)

testLines = """
Line 0 text
Expand Down Expand Up @@ -975,12 +953,12 @@ def defaultSerialize(obj, buf, lineLength):
else:
groupString = obj.group + "."
if obj.useBegin:
foldOneLine(outbuf, "{0}BEGIN:{1}".format(groupString, obj.name), lineLength)
foldOneLine(outbuf, f"{groupString}BEGIN:{obj.name}", lineLength)
for child in obj.getSortedChildren():
# validate is recursive, we only need to validate once
child.serialize(outbuf, lineLength, validate=False)
if obj.useBegin:
foldOneLine(outbuf, "{0}END:{1}".format(groupString, obj.name), lineLength)
foldOneLine(outbuf, f"{groupString}END:{obj.name}", lineLength)

elif isinstance(obj, ContentLine):
startedEncoded = obj.encoded
Expand All @@ -996,13 +974,13 @@ def defaultSerialize(obj, buf, lineLength):
for key in keys:
paramstr = ",".join(dquoteEscape(p) for p in obj.params[key])
try:
s.write(";{0}={1}".format(key, paramstr))
s.write(f";{key}={paramstr}")
except (UnicodeDecodeError, UnicodeEncodeError):
s.write(";{0}={1}".format(key, paramstr.encode("utf-8")))
s.write(f";{key}={paramstr.encode('utf-8')}")
try:
s.write(":{0}".format(obj.value))
s.write(f":{obj.value}")
except (UnicodeDecodeError, UnicodeEncodeError):
s.write(":{0}".format(obj.value.encode("utf-8")))
s.write(f":{obj.value.encode('utf-8')}")
if obj.behavior and not startedEncoded:
obj.behavior.decode(obj)
foldOneLine(outbuf, s.getvalue(), lineLength)
Expand Down Expand Up @@ -1082,8 +1060,7 @@ def readComponents(streamOrString, validate=False, transform=True, ignoreUnreada
stack.top().setProfile(vline.value)
elif vline.name == "END":
if len(stack) == 0:
err = "Attempted to end the {0} component but it was never opened"
raise ParseError(err.format(vline.value), n)
raise ParseError(f"Attempted to end the {vline.value} component but it was never opened", n)

if vline.value.upper() == stack.topName(): # START matches END
if len(stack) == 1:
Expand All @@ -1102,15 +1079,14 @@ def readComponents(streamOrString, validate=False, transform=True, ignoreUnreada
else:
stack.modifyTop(stack.pop())
else:
err = "{0} component wasn't closed"
raise ParseError(err.format(stack.topName()), n)
raise ParseError(f"{stack.topName()} component wasn't closed", n)
else:
stack.modifyTop(vline) # not a START or END line
if stack.top():
if stack.topName() is None:
logger.warning("Top level component was never named")
elif stack.top().useBegin:
raise ParseError("Component {0!s} was never closed".format((stack.topName())), n)
raise ParseError(f"Component {stack.topName()} was never closed", n)
yield stack.pop()

except ParseError as e:
Expand Down Expand Up @@ -1173,7 +1149,7 @@ def newFromBehavior(name, id_=None):
name = name.upper()
behavior = getBehavior(name, id_)
if behavior is None:
raise VObjectError("No behavior found named {0!s}".format(name))
raise VObjectError(f"No behavior found named {name}")
if behavior.isComponent:
obj = Component(name)
else:
Expand Down
12 changes: 4 additions & 8 deletions vobject/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ def validate(cls, obj, raiseException=False, complainUnrecognized=False):

"""
if not cls.allowGroup and obj.group is not None:
err = "{0} has a group, but this object doesn't support groups".format(obj)
raise base.VObjectError(err)
raise base.VObjectError(f"{obj} has a group, but this object doesn't support groups")
if isinstance(obj, base.ContentLine):
return cls.lineValidate(obj, raiseException, complainUnrecognized)
elif isinstance(obj, base.Component):
Expand All @@ -90,18 +89,15 @@ def validate(cls, obj, raiseException=False, complainUnrecognized=False):
for key, val in cls.knownChildren.items():
if count.get(key, 0) < val[0]:
if raiseException:
m = "{0} components must contain at least {1} {2}"
raise base.ValidateError(m.format(cls.name, val[0], key))
raise base.ValidateError(f"{cls.name} components must contain at least {val[0]} {key}")
return False
if val[1] and count.get(key, 0) > val[1]:
if raiseException:
m = "{0} components cannot contain more than {1} {2}"
raise base.ValidateError(m.format(cls.name, val[1], key))
raise base.ValidateError(f"{cls.name} components cannot contain more than {val[1]} {key}")
return False
return True
else:
err = "{0} is not a Component or Contentline".format(obj)
raise base.VObjectError(err)
raise base.VObjectError(f"{obj} is not a Component or Contentline")

@classmethod
def lineValidate(cls, line, raiseException, complainUnrecognized):
Expand Down
8 changes: 2 additions & 6 deletions vobject/hcalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ def out(s):
# TODO: Spec says we should handle when dtstart isn't included

out(
'<abbr class="dtstart", title="{0!s}">{1!s}</abbr>\r\n'.format(
dtstart.strftime(machine), dtstart.strftime(timeformat)
)
f'<abbr class="dtstart", title="{dtstart.strftime(machine)}">{dtstart.strftime(timeformat)}</abbr>\r\n'
)

# DTEND
Expand All @@ -108,9 +106,7 @@ def out(s):
human = dtend - timedelta(days=1)

out(
'- <abbr class="dtend", title="{0!s}">{1!s}</abbr>\r\n'.format(
dtend.strftime(machine), human.strftime(timeformat)
)
f'- <abbr class="dtend", title="{dtend.strftime(machine)}">{human.strftime(timeformat)}</abbr>\r\n'
)

# LOCATION
Expand Down
Loading