Skip to content

Commit

Permalink
Added test coverage and refactoring
Browse files Browse the repository at this point in the history
- format() to f-string
- print to logger.info
- simplified checks
- fixed some noqa's
  • Loading branch information
rsb-23 committed Apr 1, 2024
1 parent 2e9d388 commit d9fb509
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 142 deletions.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ per-file-ignores = ["*/__init__.py: F401"]
profile = "black"
line_length = 120
multi_line_output = 3

[tool.coverage.run]
source = [ "vobject/" ]
[tool.coverage.report]
fail_under = 70.0
16 changes: 16 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from vobject.base import __behaviorRegistry as BehaviorRegistry
from vobject.base import parseLine, readComponents, textLineToContentLine
from vobject.change_tz import change_tz
from vobject.helper import indent_str
from vobject.icalendar import (
MultiDateBehavior,
PeriodBehavior,
Expand All @@ -25,6 +26,7 @@
timedeltaToString,
utc,
)
from vobject.vcard import toList

behavior_registry = BehaviorRegistry
two_hours = datetime.timedelta(hours=2)
Expand Down Expand Up @@ -926,5 +928,19 @@ def test_radicale_0827(self):
return


class TestVcardFunc(unittest.TestCase):

def test_to_list(self):
assert toList("") == [""]
assert toList("Knudson") == ["Knudson"]


class TestHelper(unittest.TestCase):

def test_indent_str(self):
assert indent_str(level=2) == " " * 6
assert indent_str(level=1, tabwidth=4) == " " * 4


if __name__ == "__main__":
unittest.main(buffer=True)
129 changes: 49 additions & 80 deletions vobject/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@

import six

from .helper import CRLF, DEBUG, SPACEORTAB, backslashEscape, deprecated, indent_str, logger
from .helper import CRLF, SPACEORTAB, backslashEscape, deprecated, indent_str, logger
from .vobject_error import NativeError, ParseError, VObjectError

backslashEscape = backslashEscape # todo: to be removed later
# Python 3 no longer has a basestring type, so....
basestring = (str, bytes)

if not isinstance(b"", type("")): # sourcery skip
unicode_type = str
else:
unicode_type = unicode # noqa
logger.name = __name__


def to_unicode(value):
Expand All @@ -29,7 +26,7 @@ def to_unicode(value):
If the argument is already a unicode string, it is returned
unchanged. Otherwise it must be a byte string and is decoded as utf8.
"""
return value if isinstance(value, unicode_type) else value.decode("utf-8")
return value if isinstance(value, str) else value.decode("utf-8")


def to_basestring(s):
Expand Down Expand Up @@ -205,12 +202,10 @@ def serialize(self, buf=None, lineLength=75, validate=True, behavior=None, *args
behavior = self.behavior

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


Expand Down Expand Up @@ -289,16 +284,13 @@ def updateTable(x):
self.singletonparams.remove("QUOTED-PRINTABLE")
if qp:
if "ENCODING" in self.params:
self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode( # noqa
self.params["ENCODING"]
)
_encoding = self.params["ENCODING"]
elif "CHARSET" in self.params:
_encoding = self.params["CHARSET"][0]
else:
if "CHARSET" in self.params:
self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode( # noqa
self.params["CHARSET"][0]
)
else:
self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode("utf-8") # noqa
_encoding = "utf-8"
# TODO: check why decoding twice?
self.value = codecs.decode(self.value.encode("utf-8"), "quoted-printable").decode(_encoding) # noqa I

@classmethod
def duplicate(cls, copyit):
Expand All @@ -320,7 +312,8 @@ def copy(self, copyit):
def __eq__(self, other):
try:
return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)
except Exception: # noqa #todo: exception not clear
except Exception as e: # noqa #todo: exception not clear
logger.critical(e)
return False

def __getattr__(self, name):
Expand Down Expand Up @@ -391,14 +384,18 @@ def __repr__(self):
def __unicode__(self):
return "<{0}{1}{2}>".format(self.name, self.params, self.valueRepr())

def prettyPrint(self, level=0, tabwidth=3):
@deprecated
def prettyPrint(self, level=0, tabwidth=3) -> None:
self.pretty_print(level=level, tabwidth=tabwidth)

def pretty_print(self, level=0, tabwidth=3) -> None:
pre = indent_str(level=level, tabwidth=tabwidth)
print(pre, f"{self.name}: {self.valueRepr()}")
logger.info(pre, f"{pre} {self.name}: {self.valueRepr()}")
if self.params:
print(pre, f"params for {self.name}:")
logger.info(pre, f"{pre} params for {self.name}:")
pre1 = indent_str(level=level + 1, tabwidth=tabwidth)
for k in self.params.keys():
print(pre1, k, self.params[k])
logger.info(f"{pre1} {k} {self.params[k]}")


class Component(VBase):
Expand Down Expand Up @@ -642,39 +639,18 @@ def __str__(self):
def __repr__(self):
return self.__str__()

@deprecated
def prettyPrint(self, level=0, tabwidth=3):
self.pretty_print(level=level, tabwidth=tabwidth)

def pretty_print(self, level=0, tabwidth=3):
pre = indent_str(level=level, tabwidth=tabwidth)
print(pre, self.name)
logger.info(f"{pre} {self.name}")
if isinstance(self, Component):
for line in self.getChildren():
line.prettyPrint(level + 1, tabwidth)


class VObjectError(Exception):
def __init__(self, msg, lineNumber=None):
self.msg = msg
if lineNumber is not None:
self.lineNumber = lineNumber

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


class ParseError(VObjectError):
pass


class ValidateError(VObjectError):
pass


class NativeError(VObjectError):
pass


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


Expand Down Expand Up @@ -769,7 +745,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!s}", lineNumber)
# Underscores are replaced with dash to work around Lotus Notes
return (
match.group("name").replace("_", "-"),
Expand Down Expand Up @@ -880,7 +856,7 @@ def getLogicalLines(fp, allowQP=True): # sourcery skip: low-code-quality
# vCard 2.1 allows parameters to be encoded without a parameter name
# False positives are unlikely, but possible.
val = logicalLine.getvalue()
if val[-1] == "=" and val.lower().find("quoted-printable") >= 0:
if val[-1] == "=" and "quoted-printable" in val.lower():
quotedPrintable = True

if logicalLine.tell() > 0:
Expand All @@ -895,10 +871,10 @@ def dquoteEscape(param):
"""
Return param, or "param" if ',' or ';' or ':' is in param.
"""
if param.find('"') >= 0:
if '"' in param:
raise VObjectError("Double quotes aren't allowed in parameter values.")
for char in ",;:": # sourcery skip # temp
if param.find(char) >= 0:
if char in param:
return f'"{param}"'
return param

Expand All @@ -910,13 +886,17 @@ def foldOneLine(outbuf, input_, lineLength=75): # sourcery skip: extract-method
TO-DO: This all seems odd. Is it still needed, especially in python3?
"""
if len(input_) < lineLength:
# Optimize for unfolded line case

def outbuf_write(msg) -> None:
try:
outbuf.write(bytes(input_, "UTF-8"))
except Exception: # noqa #todo: exception not clear
outbuf.write(bytes(msg, "UTF-8"))
except TypeError:
# fall back on py2 syntax
outbuf.write(input_)
outbuf.write(msg)

if len(input_) < lineLength:
# Optimize for unfolded line case
outbuf_write(input_)

else:
# Look for valid utf8 range and write that out
Expand All @@ -929,28 +909,17 @@ def foldOneLine(outbuf, input_, lineLength=75): # sourcery skip: extract-method
s = decoded[start] # take one char
size = len(to_basestring(s)) # calculate it's size in bytes
if counter + size > lineLength:
try:
outbuf.write(bytes("\r\n ", "UTF-8"))
except Exception: # noqa #todo: exception not clear
# fall back on py2 syntax
outbuf.write("\r\n ")
outbuf_write("\r\n ")

counter = 1 # one for space

if str is unicode_type:
outbuf.write(to_unicode(s)) # noqa # todo: check and remove noqa
else:
# fall back on py2 syntax
outbuf.write(s.encode("utf-8"))
outbuf_write(s)

written += size
counter += size
start += 1
try:
outbuf.write(bytes("\r\n", "UTF-8"))
except Exception: # noqa #todo: exception not clear
# fall back on py2 syntax
outbuf.write("\r\n")

outbuf_write("\r\n")


def defaultSerialize(obj, buf, lineLength):
Expand Down Expand Up @@ -1092,7 +1061,7 @@ def readComponents(streamOrString, validate=False, transform=True, ignoreUnreada
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()!s} was never closed", n)
yield stack.pop()

except ParseError as e:
Expand All @@ -1112,7 +1081,7 @@ def readOne(stream, validate=False, transform=True, ignoreUnreadable=False, allo


@deprecated
def registerBehavior(behavior, name=None, default=False, id=None): # noqa
def registerBehavior(behavior, name=None, default=False, id=None): # noqa D
return register_behavior(behavior, name, default, _id=id)


Expand All @@ -1137,7 +1106,7 @@ def register_behavior(behavior, name=None, default=False, _id=None):


@deprecated
def getBehavior(name, id=None): # noqa
def getBehavior(name, id=None): # noqa D
return get_behavior(name, _id=id)


Expand All @@ -1159,7 +1128,7 @@ def get_behavior(name, _id=None):


@deprecated
def newFromBehavior(name, id=None): # noqa
def newFromBehavior(name, id=None): # noqa D
return new_from_behavior(name=name, _id=id)


Expand All @@ -1170,7 +1139,7 @@ def new_from_behavior(name, _id=None):
name = name.upper()
behavior = get_behavior(name, _id)
if behavior is None:
raise VObjectError("No behavior found named {0!s}".format(name))
raise VObjectError(f"No behavior found named {name!s}")
obj = Component(name) if behavior.isComponent else ContentLine(name, [], "")
obj.behavior = behavior
obj.isNative = False
Expand Down
5 changes: 2 additions & 3 deletions vobject/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
logger = logging.getLogger(__name__)
if not logging.getLogger().handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter("%(name)s %(levelname)s %(message)s")
formatter = logging.Formatter("%(name)s:%(lineno)d %(levelname)s %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.ERROR) # Log errors
DEBUG = False # Don't waste time on debug calls
logger.setLevel(logging.INFO) # modify log levels here

# ----------------------------------- Constants --------------------------------
CR = "\r"
Expand Down
Loading

0 comments on commit d9fb509

Please sign in to comment.