Skip to content

Commit 022e4b7

Browse files
committed
Push coverage to 100%
1 parent 1639494 commit 022e4b7

8 files changed

Lines changed: 118 additions & 26 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ dev = [
5555
show_contexts = true
5656

5757
[tool.coverage.report]
58-
# fail_under = 100
58+
fail_under = 100
5959
show_missing = true
6060
skip_covered = true
6161

@@ -64,6 +64,7 @@ branch = true
6464
relative_files = true
6565
dynamic_context = "test_function"
6666
omit = [
67+
"src/classify/__main__.py",
6768
"src/classify/contrib/*",
6869
"tests/dummy_class.py",
6970
]

src/classify/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class NotAClassError(Exception):
2+
pass

src/classify/library.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import builtins
22
import collections
33
import inspect
4-
import os
54
import pydoc
65
import sys
76
from collections.abc import Generator
87

98
from attrs import Factory, frozen
109

10+
from .exceptions import NotAClassError
11+
1112

1213
@frozen
1314
class Attribute:
@@ -64,12 +65,7 @@ def get_members(obj):
6465
]
6566

6667

67-
def classify(obj: object, name=None) -> Class:
68-
if not inspect.isclass(obj):
69-
prefix = name if name else "Input"
70-
msg = f"{prefix} doesn't look like a class, please specify the path to a class"
71-
raise TypeError(msg)
72-
68+
def classify(obj: object) -> Class:
7369
# flatten the MRO of the given class and flip the order so it's the first
7470
# non-object class first
7571
mro = [cls for cls in reversed(inspect.getmro(obj)) if cls is not builtins.object]
@@ -139,10 +135,12 @@ def build_methods(methods) -> Generator[Method, None, None]:
139135

140136
def build(thing) -> Class:
141137
"""Build a dictionary mapping of a class."""
142-
if "django" in thing:
143-
os.environ["DJANGO_SETTINGS_MODULE"] = "classify.contrib.django.settings"
144-
145138
sys.path.insert(0, "")
146139

147-
obj, name = pydoc.resolve(thing) # ty: ignore[not-iterable]
148-
return classify(obj, name)
140+
obj, _ = pydoc.resolve(thing) # ty: ignore[not-iterable]
141+
# TODO: what is name here??
142+
143+
if not inspect.isclass(obj):
144+
raise NotAClassError
145+
146+
return classify(obj)

src/classify/main.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from rich.syntax import DEFAULT_THEME
88

99
from . import renderers
10+
from .exceptions import NotAClassError
1011
from .library import build
1112
from .renderers import Renderer
1213

@@ -38,25 +39,29 @@
3839
def run(
3940
klass, console_theme, django_settings, renderer: Renderer, output_path, port, serve
4041
) -> None:
41-
if django_settings:
42-
os.environ["DJANGO_SETTINGS_MODULE"] = django_settings
42+
os.environ["DJANGO_SETTINGS_MODULE"] = django_settings
4343

4444
try:
4545
structure = build(klass)
4646
except (ImportError, pydoc.ErrorDuringImport):
47-
sys.stderr.write(f"Could not import: {sys.argv[1]}\n")
47+
sys.stderr.write(f"Could not import: {klass}\n")
48+
sys.exit(1)
49+
except NotAClassError:
50+
sys.stderr.write(
51+
f"{klass} doesn't look like a class, please specify the path to a class\n"
52+
)
4853
sys.exit(1)
4954

5055
match renderer:
5156
case Renderer.CONSOLE:
5257
renderers.to_console(structure, console_theme)
5358
case Renderer.HTML:
5459
renderers.to_html(structure, output_path, serve, port)
55-
case Renderer.PAGER:
60+
case Renderer.PAGER: # pragma: no branch
61+
# unclear why coverage thinks run() doesn't return, so marking as
62+
# no branch for now
5663
renderers.to_pager(structure, console_theme)
5764

58-
sys.exit(0)
59-
6065

61-
if __name__ == "__main__":
66+
if __name__ == "__main__": # pragma: no cover
6267
run()

src/classify/renderers/string.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,9 @@ def methods(methods):
3131
content += "\n"
3232
return content
3333

34-
def parents(parents):
35-
return ", ".join([p.__name__ for p in parents])
36-
3734
content = declaration(structure.name, structure.parents)
3835
content += "\n"
39-
if docstring:
40-
content += docstring(structure.docstring)
36+
content += docstring(structure.docstring) if docstring else ""
4137
content += attributes(structure.attributes)
4238
content += "\n"
4339
content += methods(structure.methods)

tests/dummy_class.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ def three(self):
77

88

99
class DummyClass(DummyParent):
10+
"""The main testing class"""
11+
12+
some_attribute = 7
13+
1014
def __init__(self):
1115
super().__init__()
1216

tests/test_library.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
("cls", "expected"),
1010
[
1111
(DummyParent, ["__dict__", "__weakref__", "one", "three"]),
12-
(DummyClass, ["__init__", "one", "two"]),
12+
(DummyClass, ["__init__", "one", "some_attribute", "two"]),
1313
],
1414
ids=["parent", "child"],
1515
)

tests/test_main.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from click.testing import CliRunner
5+
6+
from classify.main import run
7+
8+
9+
@pytest.mark.parametrize(
10+
"invocation",
11+
[
12+
["tests.dummy_class.DummyClass"],
13+
["tests.dummy_class.DummyClass", "--renderer", "console"],
14+
],
15+
ids=["default-renderer", "console-renderer"],
16+
)
17+
def test_run(invocation):
18+
runner = CliRunner()
19+
20+
result = runner.invoke(run, invocation)
21+
22+
assert result.exit_code == 0
23+
assert "DummyClass" in result.output
24+
25+
26+
def test_run_with_html_renderer():
27+
runner = CliRunner()
28+
29+
result = runner.invoke(
30+
run,
31+
["tests.dummy_class.DummyClass", "--renderer", "html"],
32+
)
33+
34+
assert result.exit_code == 0
35+
assert result.output.startswith("Wrote:")
36+
37+
38+
def test_run_with_html_renderer_and_output_set():
39+
runner = CliRunner()
40+
41+
result = runner.invoke(
42+
run,
43+
[
44+
"tests.dummy_class.DummyClass",
45+
"--renderer",
46+
"html",
47+
"--output",
48+
"output",
49+
],
50+
)
51+
52+
assert result.exit_code == 0
53+
assert result.output == "Wrote: output/classify.html\n"
54+
55+
output = Path("output/classify.html").read_text()
56+
assert "DummyClass" in output
57+
58+
59+
def test_run_with_pager_renderer():
60+
runner = CliRunner()
61+
62+
result = runner.invoke(run, ["tests.dummy_class.DummyClass", "--renderer", "pager"])
63+
64+
assert result.exit_code == 0
65+
assert "DummyClass" in result.output
66+
67+
68+
def test_run_with_unknown_path():
69+
runner = CliRunner()
70+
71+
result = runner.invoke(run, ["unknown"])
72+
73+
assert result.exit_code == 1
74+
assert result.output == "Could not import: unknown\n"
75+
76+
77+
def test_run_without_a_class():
78+
runner = CliRunner()
79+
80+
result = runner.invoke(run, ["tests.dummy_class"])
81+
82+
assert result.exit_code == 1
83+
assert (
84+
result.output
85+
== "tests.dummy_class doesn't look like a class, please specify the path to a class\n"
86+
)

0 commit comments

Comments
 (0)