Skip to content

Commit 3cf5054

Browse files
authored
Merge pull request #168 from seddonym/misc-tweaks
Misc tweaks
2 parents 340a169 + 8d8759d commit 3cf5054

File tree

8 files changed

+182
-35
lines changed

8 files changed

+182
-35
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
run: python -m tox
4545

4646
benchmarks:
47-
runs-on: ubuntu-latest
47+
runs-on: ubuntu-22.04
4848
steps:
4949
- uses: actions/checkout@v3
5050

docs/usage.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ Methods for analysing direct imports
179179

180180
.. py:function:: ImportGraph.count_imports()
181181
182-
:return: The number of direct imports in the graph.
182+
:return: The number of imports in the graph. For backward compatibility reasons, ``count_imports`` does not actually
183+
return the number of imports, but the number of dependencies between modules.
184+
So if a module is imported twice from the same module, it will only be counted once.
183185
:rtype: Integer.
184186

185187
Methods for analysing import chains

tests/benchmarking/test_benchmarking.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
from pathlib import Path
44
from grimp.adaptors.graph import ImportGraph
5+
import grimp
56

67

78
@pytest.fixture(scope="module")
@@ -17,6 +18,19 @@ def large_graph():
1718
return graph
1819

1920

21+
def test_build_django(benchmark):
22+
"""
23+
Benchmarks building a graph of real package - in this case Django.
24+
"""
25+
fn = lambda: grimp.build_graph("django")
26+
if hasattr(benchmark, "pendantic"):
27+
# Running with pytest-benchmark
28+
benchmark.pedantic(fn, rounds=3)
29+
else:
30+
# Running with codspeed.
31+
benchmark(fn)
32+
33+
2034
def test_top_level_large_graph(large_graph, benchmark):
2135
benchmark(
2236
lambda: large_graph.find_illegal_dependencies_for_layers(

tests/unit/adaptors/graph/test_chains.py

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ def test_find_downstream_modules(module, as_package, expected_result):
2525

2626
graph.add_module(external, is_squashed=True)
2727

28-
graph.add_import(imported=a, importer=b)
29-
graph.add_import(imported=a, importer=c)
30-
graph.add_import(imported=c, importer=d)
31-
graph.add_import(imported=d, importer=e)
32-
graph.add_import(imported=f, importer=b)
33-
graph.add_import(imported=f, importer=g)
34-
graph.add_import(imported=external, importer=d)
28+
graph.add_import(importer=b, imported=a)
29+
graph.add_import(importer=c, imported=a)
30+
graph.add_import(importer=d, imported=c)
31+
graph.add_import(importer=e, imported=d)
32+
graph.add_import(importer=b, imported=f)
33+
graph.add_import(importer=g, imported=f)
34+
graph.add_import(importer=d, imported=external)
3535

3636
assert expected_result == graph.find_downstream_modules(module, as_package=as_package)
3737

@@ -41,6 +41,7 @@ def test_find_downstream_modules(module, as_package, expected_result):
4141
(
4242
("foo.d", False, {"foo.d.c", "foo.a"}),
4343
("foo.b.g", False, set()),
44+
# Note: foo.d.c is not included in the upstreams because that's internal to the package.
4445
("foo.d", True, {"foo.a", "foo.a.f", "foo.b.g"}),
4546
("foo.b.g", True, set()),
4647
("bar", True, {"foo.a.f", "foo.b.g"}),
@@ -56,13 +57,13 @@ def test_find_upstream_modules(module, as_package, expected_result):
5657

5758
graph.add_module(external, is_squashed=True)
5859

59-
graph.add_import(imported=a, importer=b)
60-
graph.add_import(imported=a, importer=c)
61-
graph.add_import(imported=c, importer=d)
62-
graph.add_import(imported=d, importer=e)
63-
graph.add_import(imported=f, importer=b)
64-
graph.add_import(imported=g, importer=f)
65-
graph.add_import(imported=f, importer=external)
60+
graph.add_import(importer=b, imported=a)
61+
graph.add_import(importer=c, imported=a)
62+
graph.add_import(importer=d, imported=c)
63+
graph.add_import(importer=e, imported=d)
64+
graph.add_import(importer=b, imported=f)
65+
graph.add_import(importer=f, imported=g)
66+
graph.add_import(importer=external, imported=f)
6667

6768
assert expected_result == graph.find_upstream_modules(module, as_package=as_package)
6869

@@ -169,6 +170,44 @@ def test_demonstrate_nondeterminism_of_equal_chains(self):
169170

170171

171172
class TestFindShortestChains:
173+
@pytest.mark.parametrize(
174+
"importer, imported",
175+
(
176+
("green", "green.one"),
177+
("green.one", "green"),
178+
),
179+
)
180+
def test_modules_with_shared_descendants_raises_value_error_when_as_packages_true(
181+
self, importer: str, imported: str
182+
):
183+
graph = ImportGraph()
184+
graph.add_module(importer)
185+
graph.add_module(imported)
186+
187+
with pytest.raises(ValueError, match="Modules have shared descendants."):
188+
graph.find_shortest_chains(importer=importer, imported=imported, as_packages=True)
189+
190+
@pytest.mark.parametrize(
191+
"importer, imported",
192+
(
193+
("green", "green.one"),
194+
("green.one", "green"),
195+
),
196+
)
197+
def test_modules_with_shared_descendants_allowed_when_as_packages_false(
198+
self, importer: str, imported: str
199+
):
200+
graph = ImportGraph()
201+
middle = "middle"
202+
graph.add_import(importer=importer, imported=middle)
203+
graph.add_import(importer=middle, imported=imported)
204+
205+
result = graph.find_shortest_chains(
206+
importer=importer, imported=imported, as_packages=False
207+
)
208+
209+
assert result == {(importer, middle, imported)}
210+
172211
@pytest.mark.parametrize("as_packages", (False, True))
173212
def test_top_level_import(self, as_packages: bool):
174213
graph = ImportGraph()
@@ -340,6 +379,25 @@ def test_long_indirect_import(self, as_packages: bool, expected_result: Set[Tupl
340379

341380
assert result == expected_result
342381

382+
def test_chains_within_packages_are_not_included(self):
383+
graph = ImportGraph()
384+
385+
graph.add_module("importer_package")
386+
graph.add_module("imported_package")
387+
388+
# Chain via importer package.
389+
graph.add_import(importer="importer_package.one", imported="importer_package.two")
390+
graph.add_import(importer="importer_package.two", imported="importer_package.three")
391+
graph.add_import(importer="importer_package.three", imported="imported_package.four")
392+
graph.add_import(importer="imported_package.four", imported="imported_package.five")
393+
graph.add_import(importer="imported_package.five", imported="imported_package.six")
394+
395+
result = graph.find_shortest_chains(
396+
importer="importer_package", imported="imported_package"
397+
)
398+
399+
assert result == {("importer_package.three", "imported_package.four")}
400+
343401
def test_chains_via_importer_package_dont_stop_longer_chains_being_included(self):
344402
graph = ImportGraph()
345403

@@ -501,6 +559,9 @@ def test_doesnt_change_import_count(self, as_packages: bool):
501559
# Importer is child of imported (but doesn't import). This doesn't
502560
# make sense if as_packages is True, so it should raise an exception.
503561
("b.two", "b", True, ValueError()),
562+
# Importer is child of imported (but doesn't import). This doesn't
563+
# make sense if as_packages is True, so it should raise an exception.
564+
("b", "b.two", True, ValueError()),
504565
# Importer's child imports imported's child (b.two.green -> a.one.green).
505566
("b.two", "a.one", True, True),
506567
# Importer's grandchild directly imports imported's grandchild

tests/unit/adaptors/graph/test_hierarchy.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest # type: ignore
22

33
from grimp.adaptors.graph import ImportGraph
4+
from grimp.exceptions import ModuleNotPresent
45

56

67
@pytest.mark.parametrize(
@@ -18,6 +19,14 @@ def test_find_children(module, expected_result):
1819
assert expected_result == graph.find_children(module)
1920

2021

22+
def test_find_children_raises_exception_for_missing_module():
23+
graph = ImportGraph()
24+
graph.add_module("foo.a.one")
25+
26+
with pytest.raises(ModuleNotPresent):
27+
graph.find_children("foo.a")
28+
29+
2130
def test_find_children_raises_exception_for_squashed_module():
2231
graph = ImportGraph()
2332
module = "foo"
@@ -28,6 +37,18 @@ def test_find_children_raises_exception_for_squashed_module():
2837
graph.find_children(module)
2938

3039

40+
def test_adding_same_child_module_twice_does_not_corrupt_hierarchy():
41+
graph = ImportGraph()
42+
graph.add_module("mypackage.blue")
43+
graph.add_module("mypackage.blue.alpha")
44+
graph.add_module("mypackage.blue") # Add for second time.
45+
graph.add_module("mypackage.blue.beta")
46+
47+
result = graph.find_children("mypackage.blue")
48+
49+
assert result == {"mypackage.blue.alpha", "mypackage.blue.beta"}
50+
51+
3152
@pytest.mark.parametrize(
3253
"module, expected_result",
3354
(
@@ -56,3 +77,41 @@ def test_find_descendants_raises_exception_for_squashed_module():
5677

5778
with pytest.raises(ValueError, match="Cannot find descendants of a squashed module."):
5879
graph.find_descendants(module)
80+
81+
82+
def test_find_descendants_works_with_gaps():
83+
graph = ImportGraph()
84+
graph.add_module("mypackage.foo")
85+
# We do not add "mypackage.foo.blue" - there's a gap.
86+
graph.add_module("mypackage.foo.blue.alpha")
87+
graph.add_module("mypackage.foo.blue.alpha.one")
88+
graph.add_module("mypackage.foo.blue.alpha.two")
89+
graph.add_module("mypackage.foo.blue.beta.three")
90+
graph.add_module("mypackage.bar.green.alpha")
91+
92+
result = graph.find_descendants("mypackage.foo")
93+
94+
assert result == {
95+
"mypackage.foo.blue.alpha",
96+
"mypackage.foo.blue.alpha.one",
97+
"mypackage.foo.blue.alpha.two",
98+
"mypackage.foo.blue.beta.three",
99+
}
100+
101+
102+
def test_find_descendants_works_if_modules_added_in_different_order():
103+
graph = ImportGraph()
104+
graph.add_module("mypackage.foo")
105+
graph.add_module("mypackage.foo.blue.alpha")
106+
graph.add_module("mypackage.foo.blue.alpha.one")
107+
graph.add_module("mypackage.bar.green.beta")
108+
# Add the middle item in the hierarchy last.
109+
graph.add_module("mypackage.foo.blue")
110+
111+
result = graph.find_descendants("mypackage.foo")
112+
113+
assert result == {
114+
"mypackage.foo.blue",
115+
"mypackage.foo.blue.alpha",
116+
"mypackage.foo.blue.alpha.one",
117+
}

tests/unit/adaptors/graph/test_manipulation.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,28 @@
44
from grimp.exceptions import ModuleNotPresent
55

66

7-
def test_add_module():
8-
graph = ImportGraph()
9-
module = "foo"
10-
11-
graph.add_module(module)
7+
class TestAddModule:
8+
def test_add_module(self):
9+
graph = ImportGraph()
10+
module = "foo"
1211

13-
assert graph.modules == {module}
12+
graph.add_module(module)
1413

14+
assert graph.modules == {module}
1515

16-
class TestRemoveModule:
17-
def test_removes_module_from_modules(self):
16+
def test_add_module_does_not_add_ancestors_too(self):
1817
graph = ImportGraph()
19-
a, b = {"mypackage.blue", "mypackage.green"}
18+
module = "mypackage.foo.bar"
2019

21-
graph.add_module(a)
22-
graph.add_module(b)
23-
graph.add_import(importer=a, imported=b)
20+
graph.add_module(module)
21+
22+
assert graph.modules == {"mypackage.foo.bar"}
2423

25-
graph.remove_module(b)
26-
assert graph.modules == {a}
2724

25+
class TestRemoveModule:
2826
def test_removes_module_removes_import_details_for_imported(self):
2927
graph = ImportGraph()
30-
a, b, c = {"mypackage.blue", "mypackage.green", "mypackage.yellow"}
28+
a, b, c = "mypackage.blue", "mypackage.green", "mypackage.yellow"
3129

3230
graph.add_import(
3331
importer=a,
@@ -53,9 +51,20 @@ def test_removes_module_removes_import_details_for_imported(self):
5351
}
5452
]
5553

54+
def test_removes_module_from_modules(self):
55+
graph = ImportGraph()
56+
a, b = "mypackage.blue", "mypackage.green"
57+
58+
graph.add_module(a)
59+
graph.add_module(b)
60+
graph.add_import(importer=a, imported=b)
61+
62+
graph.remove_module(b)
63+
assert graph.modules == {a}
64+
5665
def test_removes_module_removes_import_details_for_importer(self):
5766
graph = ImportGraph()
58-
a, b, c = {"mypackage.blue", "mypackage.green", "mypackage.yellow"}
67+
a, b, c = "mypackage.blue", "mypackage.green", "mypackage.yellow"
5968

6069
graph.add_import(
6170
importer=b,
@@ -83,7 +92,7 @@ def test_removes_module_removes_import_details_for_importer(self):
8392

8493
def test_removing_non_existent_module_doesnt_error(self):
8594
graph = ImportGraph()
86-
a, b = {"mypackage.blue", "mypackage.green"}
95+
a, b = "mypackage.blue", "mypackage.green"
8796

8897
graph.add_module(a)
8998
graph.add_module(b)
@@ -174,7 +183,7 @@ def test_removes_from_modules(self):
174183

175184
def test_removes_from_import_details(self):
176185
graph = ImportGraph()
177-
a, b, c = {"mypackage.blue", "mypackage.green", "mypackage.yellow"}
186+
a, b, c = "mypackage.blue", "mypackage.green", "mypackage.yellow"
178187

179188
graph.add_import(
180189
importer=a,

tox.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ deps =
2525
pytest-cov==5.0.0
2626
pytest-benchmark==4.0.0
2727
# External packages to attempt to build the graph from.
28-
django
28+
Django==4.2.17 # N.B. Django 5 doesn't support Python 3.9.
2929
flask==3.0.3
3030
requests==2.32.3
3131
sqlalchemy==2.0.35
@@ -59,6 +59,7 @@ deps =
5959
pytest==7.4.4
6060
PyYAML==6.0.1
6161
pytest-benchmark==4.0.0
62+
Django==5.1.1
6263
commands =
6364
{posargs:pytest --benchmark-only --benchmark-autosave}
6465

@@ -74,6 +75,7 @@ deps =
7475
pytest==7.4.4
7576
pyyaml==6.0.1
7677
pytest-codspeed==2.2.1
78+
Django==5.1.1
7779
commands =
7880
{posargs:pytest --codspeed}
7981

0 commit comments

Comments
 (0)