Skip to content

Commit

Permalink
Increase test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
SmileyChris committed Feb 9, 2022
1 parent a850cff commit 49060c4
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 47 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ build/
qrcode.egg-info/
.pytest_cache/
.idea/
cov.xml
3 changes: 1 addition & 2 deletions qrcode/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,10 @@ def __init__(self, num, shift):
if not num: # pragma: no cover
raise Exception(f"{len(num)}/{shift}")

offset = 0
for offset in range(len(num)):
if num[offset] != 0:
break
else:
offset += 1

self.num = num[offset:] + [0] * shift

Expand Down
45 changes: 17 additions & 28 deletions qrcode/console_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
When stdout is a tty the QR Code is printed to the terminal and when stdout is
a pipe to a file an image is written. The default image format is PNG.
"""
from ctypes import cast
import optparse
import os
import sys
from typing import Dict, Iterable, Optional, Set, Type
from typing import Dict, Iterable, NoReturn, Optional, Set, Type

import qrcode
from qrcode.image.base import BaseImage, DrawerAliases
Expand Down Expand Up @@ -41,7 +42,13 @@ def main(args=None):
from pkg_resources import get_distribution

version = get_distribution("qrcode").version
parser = optparse.OptionParser(usage=__doc__.strip(), version=version)
parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version)

# Wrap parser.error in a typed NoReturn method for better typing.
def raise_error(msg: str) -> NoReturn:
parser.error(msg)
raise # pragma: no cover

parser.add_option(
"--factory",
help="Full python path to the image factory class to "
Expand Down Expand Up @@ -83,8 +90,7 @@ def main(args=None):
try:
image_factory = get_factory(module)
except ValueError as e:
parser.error(str(e))
image_factory = None
raise_error(str(e))
else:
image_factory = None

Expand All @@ -97,10 +103,7 @@ def main(args=None):
data = args[0]
data = data.encode(errors="surrogateescape")
else:
# Use sys.stdin.buffer if available (Python 3) avoiding
# UnicodeDecodeErrors.
stdin_buffer = getattr(sys.stdin, "buffer", sys.stdin)
data = stdin_buffer.read()
data = sys.stdin.buffer.read()
if opts.optimize is None:
qr.add_data(data)
else:
Expand All @@ -119,33 +122,19 @@ def main(args=None):
aliases: Optional[DrawerAliases] = getattr(
qr.image_factory, "drawer_aliases", None
)
if aliases and opts.factory_drawer:
if opts.factory_drawer:
if not aliases:
parser.error(f"The selected factory has no drawer aliases.")
raise_error(f"The selected factory has no drawer aliases.")
if opts.factory_drawer not in aliases:
parser.error(
raise_error(
f"{opts.factory_drawer} factory drawer not found. Expected {commas(aliases)}"
)
drawer_cls, drawer_kwargs = aliases[opts.factory_drawer]
kwargs["module_drawer"] = drawer_cls(**drawer_kwargs)
elif opts.factory == "svg-circles":
from qrcode.image.styles.moduledrawers.svg import SvgCircleDrawer

kwargs["module_drawer"] = SvgCircleDrawer()
img = qr.make_image(**kwargs)

sys.stdout.flush()
# Use sys.stdout.buffer if available (Python 3), avoiding
# UnicodeDecodeErrors.
stdout_buffer = getattr(sys.stdout, "buffer", None)
if not stdout_buffer:
if sys.platform == "win32": # pragma: no cover
import msvcrt

msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
stdout_buffer = sys.stdout

img.save(stdout_buffer)
img.save(sys.stdout.buffer)


def get_factory(module: str) -> Type[BaseImage]:
Expand All @@ -161,7 +150,7 @@ def get_drawer_help() -> str:
for alias, module in default_factories.items():
try:
image = get_factory(module)
except ImportError:
except ImportError: # pragma: no cover
continue
aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None)
if not aliases:
Expand All @@ -184,5 +173,5 @@ def commas(items: Iterable[str], joiner="or") -> str:
return f"{', '.join(items[:-1])} {joiner} {items[-1]}"


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
24 changes: 15 additions & 9 deletions qrcode/image/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ def drawrect_context(self, row: int, col: int, qr: "QRCode"):
"""
Draw a single rectangle of the QR code given the surrounding context
"""
raise NotImplementedError("BaseImage.drawrect_context")
raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover

def process(self):
"""
Processes QR code after completion
"""
raise NotImplementedError("BaseImage.drawimage")
raise NotImplementedError("BaseImage.drawimage") # pragma: no cover

@abc.abstractmethod
def save(self, stream, kind=None):
Expand Down Expand Up @@ -106,13 +106,13 @@ def is_eye(self, row: int, col: int):


class BaseImageWithDrawer(BaseImage):
default_drawer_class: "Type[QRModuleDrawer]"
default_drawer_class: Type[QRModuleDrawer]
drawer_aliases: DrawerAliases = {}

def get_default_module_drawer(self) -> "QRModuleDrawer":
def get_default_module_drawer(self) -> QRModuleDrawer:
return self.default_drawer_class()

def get_default_eye_drawer(self) -> "QRModuleDrawer":
def get_default_eye_drawer(self) -> QRModuleDrawer:
return self.default_drawer_class()

needs_context = True
Expand All @@ -123,17 +123,23 @@ def get_default_eye_drawer(self) -> "QRModuleDrawer":
def __init__(
self,
*args,
module_drawer: "QRModuleDrawer" = None,
eye_drawer: "QRModuleDrawer" = None,
module_drawer: Union[QRModuleDrawer, str] = None,
eye_drawer: Union[QRModuleDrawer, str] = None,
**kwargs,
):
self.module_drawer = module_drawer or self.get_default_module_drawer()
self.module_drawer = self.get_drawer(module_drawer) or self.get_default_module_drawer()
# The eye drawer can be overridden by another module drawer as well,
# but you have to be more careful with these in order to make the QR
# code still parseable
self.eye_drawer = eye_drawer or self.get_default_eye_drawer()
self.eye_drawer = self.get_drawer(eye_drawer) or self.get_default_eye_drawer()
super().__init__(*args, **kwargs)

def get_drawer(self, drawer: Union[QRModuleDrawer, str, None]) -> Optional[QRModuleDrawer]:
if not isinstance(drawer, str):
return drawer
drawer_cls, kwargs = self.drawer_aliases[drawer]
return drawer_cls(**kwargs)

def init_new_image(self):
self.module_drawer.initialize(img=self)
self.eye_drawer.initialize(img=self)
Expand Down
6 changes: 6 additions & 0 deletions qrcode/tests/test_qrcode_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ def test_render_svg_with_background(self):
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=SvgImageWhite)
img.save(io.BytesIO())

def test_svg_circle_drawer(self):
qr = qrcode.QRCode()
qr.add_data(UNICODE_TEXT)
img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle")
img.save(io.BytesIO())
42 changes: 34 additions & 8 deletions qrcode/tests/test_script.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import io
import os
import sys
import unittest
from tempfile import mkdtemp
from unittest import mock

from qrcode.console_scripts import main
from qrcode.console_scripts import main, commas


def bad_read():
Expand All @@ -31,13 +32,11 @@ def test_piped(self, mock_stdout):

@mock.patch("os.isatty", lambda *args: True)
@mock.patch("qrcode.main.QRCode.print_ascii")
def test_stdin(self, mock_print_ascii):
mock_stdin = mock.Mock(sys.stdin)
stdin_buffer = getattr(mock_stdin, "buffer", mock_stdin)
stdin_buffer.read.return_value = "testtext"
with mock.patch("sys.stdin", mock_stdin):
main([])
self.assertTrue(stdin_buffer.read.called)
@mock.patch("sys.stdin")
def test_stdin(self, mock_stdin, mock_print_ascii):
mock_stdin.buffer.read.return_value = "testtext"
main([])
self.assertTrue(mock_stdin.buffer.read.called)
mock_print_ascii.assert_called_with(tty=True)

@mock.patch("os.isatty", lambda *args: True)
Expand Down Expand Up @@ -66,7 +65,34 @@ def test_factory(self, mock_stdout):
def test_bad_factory(self, mock_stderr):
self.assertRaises(SystemExit, main, "testtext --factory fish".split())

@mock.patch.object(sys, "argv", "qr testtext output".split())
def test_sys_argv(self):
main()

def test_output(self):
tmpfile = os.path.join(self.tmpdir, "test.png")
main(["testtext", "--output", tmpfile])
os.remove(tmpfile)

@mock.patch("sys.stderr", new_callable=io.StringIO)
def test_factory_drawer_none(self, mock_stderr):
with self.assertRaises(SystemExit):
main("testtext --factory pil --factory-drawer nope".split())
self.assertIn("The selected factory has no drawer aliases", mock_stderr.getvalue())

@mock.patch("sys.stderr", new_callable=io.StringIO)
def test_factory_drawer_bad(self, mock_stderr):
with self.assertRaises(SystemExit):
main("testtext --factory svg --factory-drawer sobad".split())
self.assertIn("sobad factory drawer not found", mock_stderr.getvalue())

@mock.patch("sys.stderr", new_callable=io.StringIO)
def test_factory_drawer(self, mock_stderr):
main("testtext --factory svg --factory-drawer circle".split())

def test_commas(self):
self.assertEqual(commas([]), "")
self.assertEqual(commas(['A']), "A")
self.assertEqual(commas('AB'), "A or B")
self.assertEqual(commas("ABC"), "A, B or C")
self.assertEqual(commas("ABC", joiner="and"), "A, B and C")
7 changes: 7 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ ignore = E203,W503
[coverage:run]
source = qrcode

[coverage:report]
exclude_lines =
pragma: no cover
@overload
if (typing\.)?TYPE_CHECKING:

[zest.releaser]
less-zeros = yes
version-levels = 2
Expand All @@ -80,3 +86,4 @@ prereleaser.middle =

[tool:pytest]
filterwarnings = module
addopts = --cov-report term-missing:skip-covered

0 comments on commit 49060c4

Please sign in to comment.