Skip to content

Commit 28bd4a3

Browse files
committed
Issue warnings for some deprecated attributes of modules
1 parent 19b3c08 commit 28bd4a3

File tree

3 files changed

+138
-38
lines changed

3 files changed

+138
-38
lines changed

git/__init__.py

+71-26
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@
8888

8989
__version__ = "git"
9090

91-
from typing import List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
91+
from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92+
93+
if TYPE_CHECKING:
94+
from types import ModuleType
9295

9396
from gitdb.util import to_hex_sha
9497

@@ -144,11 +147,6 @@
144147
SymbolicReference,
145148
Tag,
146149
TagReference,
147-
head, # noqa: F401 # Nonpublic. May disappear! Use git.refs.head.
148-
log, # noqa: F401 # Nonpublic. May disappear! Use git.refs.log.
149-
reference, # noqa: F401 # Nonpublic. May disappear! Use git.refs.reference.
150-
symbolic, # noqa: F401 # Nonpublic. May disappear! Use git.refs.symbolic.
151-
tag, # noqa: F401 # Nonpublic. May disappear! Use git.refs.tag.
152150
)
153151
from git.diff import ( # @NoMove
154152
INDEX,
@@ -169,21 +167,6 @@
169167
IndexEntry,
170168
IndexFile,
171169
StageType,
172-
base, # noqa: F401 # Nonpublic. May disappear! Use git.index.base.
173-
fun, # noqa: F401 # Nonpublic. May disappear! Use git.index.fun.
174-
typ, # noqa: F401 # Nonpublic. May disappear! Use git.index.typ.
175-
#
176-
# NOTE: The expression `git.util` evaluates to git.index.util, and the import
177-
# `from git import util` imports git.index.util, NOT git.util. It may not be
178-
# feasible to change this until the next major version, to avoid breaking code
179-
# inadvertently relying on it. If git.index.util really is what you want, use or
180-
# import from that name, to avoid confusion. To use the "real" git.util module,
181-
# write `from git.util import ...`, or access it as `sys.modules["git.util"]`.
182-
# (This differs from other historical indirect-submodule imports that are
183-
# unambiguously nonpublic and are subject to immediate removal. Here, the public
184-
# git.util module, even though different, makes it less discoverable that the
185-
# expression `git.util` refers to a non-public attribute of the git module.)
186-
util, # noqa: F401
187170
)
188171
from git.util import ( # @NoMove
189172
Actor,
@@ -196,7 +179,72 @@
196179
except GitError as _exc:
197180
raise ImportError("%s: %s" % (_exc.__class__.__name__, _exc)) from _exc
198181

182+
183+
# NOTE: The expression `git.util` evaluates to git.index.util and `from git import util`
184+
# imports git.index.util, NOT git.util. It may not be feasible to change this until the
185+
# next major version, to avoid breaking code inadvertently relying on it.
186+
#
187+
# - If git.index.util *is* what you want, use or import from that, to avoid confusion.
188+
#
189+
# - To use the "real" git.util module, write `from git.util import ...`, or if necessary
190+
# access it as `sys.modules["git.util"]`.
191+
#
192+
# (This differs from other indirect-submodule imports that are unambiguously non-public
193+
# and subject to immediate removal. Here, the public git.util module, though different,
194+
# makes less discoverable that the expression `git.util` refers to a non-public
195+
# attribute of the git module.)
196+
#
197+
# This had come about by a wildcard import. Now that all intended imports are explicit,
198+
# the intuitive but potentially incompatible binding occurs due to the usual rules for
199+
# Python submodule bindings. So for now we delete that and let __getattr__ handle it.
200+
#
201+
del util # type: ignore[name-defined] # noqa: F821
202+
203+
204+
def _warned_import(message: str, fullname: str) -> "ModuleType":
205+
import importlib
206+
import warnings
207+
208+
warnings.warn(message, DeprecationWarning, stacklevel=3)
209+
return importlib.import_module(fullname)
210+
211+
212+
def _getattr(name: str) -> Any:
213+
# TODO: If __version__ is made dynamic and lazily fetched, put that case right here.
214+
215+
if name == "util":
216+
return _warned_import(
217+
"The expression `git.util` and the import `from git import util` actually "
218+
"reference git.index.util, and not the git.util module accessed in "
219+
'`from git.util import XYZ` or `sys.modules["git.util"]`. This potentially '
220+
"confusing behavior is currently preserved for compatibility, but may be "
221+
"changed in the future and should not be relied on.",
222+
fullname="git.index.util",
223+
)
224+
225+
for names, prefix in (
226+
({"head", "log", "reference", "symbolic", "tag"}, "git.refs"),
227+
({"base", "fun", "typ"}, "git.index"),
228+
):
229+
if name not in names:
230+
continue
231+
232+
fullname = f"{prefix}.{name}"
233+
234+
return _warned_import(
235+
f"{__name__}.{name} is a private alias of {fullname} and subject to "
236+
f"immediate removal. Use {fullname} instead.",
237+
fullname=fullname,
238+
)
239+
240+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
241+
242+
243+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
244+
__getattr__ = _getattr
245+
199246
# { Initialize git executable path
247+
200248
GIT_OK = None
201249

202250

@@ -232,12 +280,9 @@ def refresh(path: Optional[PathLike] = None) -> None:
232280
GIT_OK = True
233281

234282

235-
# } END initialize git executable path
236-
237-
238-
#################
239283
try:
240284
refresh()
241285
except Exception as _exc:
242286
raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc
243-
#################
287+
288+
# } END initialize git executable path

git/compat.py

+37-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
AnyStr,
2424
Dict, # noqa: F401
2525
IO, # noqa: F401
26+
List,
2627
Optional,
28+
TYPE_CHECKING,
2729
Tuple, # noqa: F401
2830
Type, # noqa: F401
2931
Union,
@@ -33,7 +35,39 @@
3335
# ---------------------------------------------------------------------------
3436

3537

36-
is_win = os.name == "nt"
38+
_deprecated_platform_aliases = {
39+
"is_win": os.name == "nt",
40+
"is_posix": os.name == "posix",
41+
"is_darwin": sys.platform == "darwin",
42+
}
43+
44+
45+
def _getattr(name: str) -> Any:
46+
try:
47+
value = _deprecated_platform_aliases[name]
48+
except KeyError:
49+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
50+
51+
import warnings
52+
53+
warnings.warn(
54+
f"{__name__}.{name} and other is_<platform> aliases are deprecated. "
55+
"Write the desired os.name or sys.platform check explicitly instead.",
56+
DeprecationWarning,
57+
stacklevel=2,
58+
)
59+
return value
60+
61+
62+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
63+
__getattr__ = _getattr
64+
65+
66+
def __dir__() -> List[str]:
67+
return [*globals(), *_deprecated_platform_aliases]
68+
69+
70+
is_win: bool
3771
"""Deprecated alias for ``os.name == "nt"`` to check for native Windows.
3872
3973
This is deprecated because it is clearer to write out :attr:`os.name` or
@@ -45,7 +79,7 @@
4579
Cygwin, use ``sys.platform == "cygwin"``.
4680
"""
4781

48-
is_posix = os.name == "posix"
82+
is_posix: bool
4983
"""Deprecated alias for ``os.name == "posix"`` to check for Unix-like ("POSIX") systems.
5084
5185
This is deprecated because it clearer to write out :attr:`os.name` or
@@ -58,7 +92,7 @@
5892
(Darwin).
5993
"""
6094

61-
is_darwin = sys.platform == "darwin"
95+
is_darwin: bool
6296
"""Deprecated alias for ``sys.platform == "darwin"`` to check for macOS (Darwin).
6397
6498
This is deprecated because it clearer to write out :attr:`os.name` or

git/types.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
Any,
88
Callable,
99
Dict,
10+
List,
1011
NoReturn,
1112
Optional,
1213
Sequence as Sequence,
1314
Tuple,
1415
TYPE_CHECKING,
16+
Type,
1517
TypeVar,
1618
Union,
1719
)
@@ -127,21 +129,40 @@
127129
https://git-scm.com/docs/gitglossary#def_object_type
128130
"""
129131

130-
Lit_commit_ish = Literal["commit", "tag"]
131-
"""Deprecated. Type of literal strings identifying sometimes-commitish git object types.
132+
Lit_commit_ish: Type[Literal["commit", "tag"]]
133+
"""Deprecated. Type of literal strings identifying typically-commitish git object types.
132134
133135
Prior to a bugfix, this type had been defined more broadly. Any usage is in practice
134-
ambiguous and likely to be incorrect. Instead of this type:
136+
ambiguous and likely to be incorrect. This type has therefore been made a static type
137+
error to appear in annotations. It is preserved, with a deprecated status, to avoid
138+
introducing runtime errors in code that refers to it, but it should not be used.
139+
140+
Instead of this type:
135141
136142
* For the type of the string literals associated with :class:`Commit_ish`, use
137143
``Literal["commit", "tag"]`` or create a new type alias for it. That is equivalent to
138-
this type as currently defined.
144+
this type as currently defined (but usable in statically checked type annotations).
139145
140146
* For the type of all four string literals associated with :class:`AnyGitObject`, use
141147
:class:`GitObjectTypeString`. That is equivalent to the old definition of this type
142-
prior to the bugfix.
148+
prior to the bugfix (and is also usable in statically checked type annotations).
143149
"""
144150

151+
152+
def _getattr(name: str) -> Any:
153+
if name == "Lit_commit_ish":
154+
return Literal["commit", "tag"]
155+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
156+
157+
158+
if not TYPE_CHECKING: # Preserve static checking for undefined/misspelled attributes.
159+
__getattr__ = _getattr
160+
161+
162+
def __dir__() -> List[str]:
163+
return [*globals(), "Lit_commit_ish"]
164+
165+
145166
# Config_levels ---------------------------------------------------------
146167

147168
Lit_config_levels = Literal["system", "global", "user", "repository"]
@@ -188,12 +209,12 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception,
188209
189210
:param inp:
190211
If all members are handled, the argument for `inp` will have the
191-
:class:`~typing.Never`/:class:`~typing.NoReturn` type. Otherwise, the type will
192-
mismatch and cause a mypy error.
212+
:class:`~typing.Never`/:class:`~typing.NoReturn` type.
213+
Otherwise, the type will mismatch and cause a mypy error.
193214
194215
:param raise_error:
195-
If ``True``, will also raise :exc:`ValueError` with a general "unhandled
196-
literal" message, or the exception object passed as `exc`.
216+
If ``True``, will also raise :exc:`ValueError` with a general
217+
"unhandled literal" message, or the exception object passed as `exc`.
197218
198219
:param exc:
199220
It not ``None``, this should be an already-constructed exception object, to be

0 commit comments

Comments
 (0)