44checks static typing of the code under test. (Running pytest checks dynamic behavior.)
55"""
66
7- import importlib
7+ from itertools import groupby
88from typing import Type
99
1010import pytest
11+ from typing_extensions import assert_type
1112
1213import git
1314
1415
15- def test_cannot_get_undefined () -> None :
16+ def test_cannot_access_undefined () -> None :
17+ """Accessing a bogus attribute in git remains both a dynamic and static error."""
1618 with pytest .raises (AttributeError ):
1719 git .foo # type: ignore[attr-defined]
1820
1921
2022def test_cannot_import_undefined () -> None :
23+ """Importing a bogus attribute from git remains both a dynamic and static error."""
2124 with pytest .raises (ImportError ):
2225 from git import foo # type: ignore[attr-defined] # noqa: F401
2326
2427
25- def test_util_alias_members_resolve () -> None :
26- """git.index.util members can be accessed via git.util, and mypy recognizes it."""
27- gu_tfs = git .util .TemporaryFileSwap
28- from git .index .util import TemporaryFileSwap
28+ def test_util_alias_access () -> None :
29+ """Accessing util in git works, warns, and mypy verifies it and its attributes."""
30+ # The attribute access should succeed.
31+ with pytest .deprecated_call () as ctx :
32+ util = git .util
2933
30- def accepts_tfs_type (t : Type [TemporaryFileSwap ]) -> None :
31- pass
34+ # There should be exactly one warning and it should have our util-specific message.
35+ (message ,) = [str (entry .message ) for entry in ctx ]
36+ assert "git.util" in message
37+ assert "git.index.util" in message
38+ assert "should not be relied on" in message
3239
33- def rejects_tfs_type (t : Type [git .Git ]) -> None :
34- pass
40+ # We check access through the util alias to the TemporaryFileSwap member, since it
41+ # is slightly simpler to validate and reason about than the other public members,
42+ # which are functions (specifically, higher-order functions for use as decorators).
43+ from git .index .util import TemporaryFileSwap
3544
36- # TODO: When typing_extensions is made a test dependency, use assert_type for this.
37- accepts_tfs_type (gu_tfs )
38- rejects_tfs_type (gu_tfs ) # type: ignore[arg-type]
45+ assert_type (util .TemporaryFileSwap , Type [TemporaryFileSwap ])
3946
40- assert gu_tfs is TemporaryFileSwap
47+ # This comes after the static assertion, just in case it would affect the inference.
48+ assert util .TemporaryFileSwap is TemporaryFileSwap
4149
4250
43- def test_util_alias_access_warns () -> None :
51+ def test_util_alias_import () -> None :
52+ """Importing util from git works, warns, and mypy verifies it and its attributes."""
53+ # The import should succeed.
4454 with pytest .deprecated_call () as ctx :
45- git . util
55+ from git import util
4656
47- assert len (ctx ) == 1
48- message = str (ctx [0 ].message )
57+ # There may be multiple warnings. In CPython there will be currently always be
58+ # exactly two, possibly due to the equivalent of calling hasattr to do a pre-check
59+ # prior to retrieving the attribute for actual use. However, all warnings should
60+ # have the same message, and it should be our util-specific message.
61+ (message ,) = {str (entry .message ) for entry in ctx }
4962 assert "git.util" in message
5063 assert "git.index.util" in message
5164 assert "should not be relied on" in message
5265
66+ # As above, we check access through the util alias to the TemporaryFileSwap member.
67+ from git .index .util import TemporaryFileSwap
5368
54- def test_util_alias_import_warns () -> None :
55- with pytest .deprecated_call () as ctx :
56- from git import util # noqa: F401
69+ assert_type (util .TemporaryFileSwap , Type [TemporaryFileSwap ])
5770
58- message = str (ctx [0 ].message )
59- assert "git.util" in message
60- assert "git.index.util" in message
61- assert "should not be relied on" in message
71+ # This comes after the static assertion, just in case it would affect the inference.
72+ assert util .TemporaryFileSwap is TemporaryFileSwap
6273
6374
6475# Split out util and have all its tests be separate, above.
@@ -71,12 +82,11 @@ def test_util_alias_import_warns() -> None:
7182 git .index .base ,
7283 git .index .fun ,
7384 git .index .typ ,
74- git .index .util ,
7585)
7686
7787
78- def test_private_module_alias_access_on_git_module () -> None :
79- """Private alias access works, warns, and except for util is a mypy error."""
88+ def test_private_module_alias_access () -> None :
89+ """Non-util private alias access works, warns, but is a deliberate mypy error."""
8090 with pytest .deprecated_call () as ctx :
8191 assert (
8292 git .head , # type: ignore[attr-defined]
@@ -87,21 +97,16 @@ def test_private_module_alias_access_on_git_module() -> None:
8797 git .base , # type: ignore[attr-defined]
8898 git .fun , # type: ignore[attr-defined]
8999 git .typ , # type: ignore[attr-defined]
90- git .util ,
91100 ) == _MODULE_ALIAS_TARGETS
92101
102+ # Each should have warned exactly once, and note what to use instead.
93103 messages = [str (w .message ) for w in ctx ]
94- for target , message in zip (_MODULE_ALIAS_TARGETS [: - 1 ] , messages [: - 1 ] , strict = True ):
104+ for target , message in zip (_MODULE_ALIAS_TARGETS , messages , strict = True ):
95105 assert message .endswith (f"Use { target .__name__ } instead." )
96106
97- util_message = messages [- 1 ]
98- assert "git.util" in util_message
99- assert "git.index.util" in util_message
100- assert "should not be relied on" in util_message
101107
102-
103- def test_private_module_alias_import_from_git_module () -> None :
104- """Private alias import works, warns, and except for util is a mypy error."""
108+ def test_private_module_alias_import () -> None :
109+ """Non-util private alias access works, warns, but is a deliberate mypy error."""
105110 with pytest .deprecated_call () as ctx :
106111 from git import head # type: ignore[attr-defined]
107112 from git import log # type: ignore[attr-defined]
@@ -111,7 +116,6 @@ def test_private_module_alias_import_from_git_module() -> None:
111116 from git import base # type: ignore[attr-defined]
112117 from git import fun # type: ignore[attr-defined]
113118 from git import typ # type: ignore[attr-defined]
114- from git import util
115119
116120 assert (
117121 head ,
@@ -122,17 +126,13 @@ def test_private_module_alias_import_from_git_module() -> None:
122126 base ,
123127 fun ,
124128 typ ,
125- util ,
126129 ) == _MODULE_ALIAS_TARGETS
127130
128- # FIXME: This fails because, with imports, multiple consecutive accesses may occur.
129- # In practice, with CPython, it is always exactly two accesses, the first from the
130- # equivalent of a hasattr, and the second to fetch the attribute intentionally.
131- messages = [str (w .message ) for w in ctx ]
132- for target , message in zip (_MODULE_ALIAS_TARGETS [:- 1 ], messages [:- 1 ], strict = True ):
131+ # Each import may warn multiple times. In CPython there will be currently always be
132+ # exactly two warnings per import, possibly due to the equivalent of calling hasattr
133+ # to do a pre-check prior to retrieving the attribute for actual use. However, for
134+ # each import, all messages should be the same and should note what to use instead.
135+ messages_with_duplicates = [str (w .message ) for w in ctx ]
136+ messages = [message for message , _ in groupby (messages_with_duplicates )]
137+ for target , message in zip (_MODULE_ALIAS_TARGETS , messages , strict = True ):
133138 assert message .endswith (f"Use { target .__name__ } instead." )
134-
135- util_message = messages [- 1 ]
136- assert "git.util" in util_message
137- assert "git.index.util" in util_message
138- assert "should not be relied on" in util_message
0 commit comments