refactor: re-expose API with deprecation warnings on non-public API imports

This commit is contained in:
Sigurd Spieckermann 2025-04-03 22:17:38 +02:00 committed by Sigurd Spieckermann
parent ef5ea4b212
commit 2491c0681b
15 changed files with 304 additions and 8 deletions

View File

@ -4,10 +4,25 @@ Docs: https://copier.readthedocs.io/
"""
import importlib.metadata
from typing import TYPE_CHECKING, Any
from ._main import * # noqa: F401,F403
from . import _main
from ._deprecation import deprecate_member_as_internal
if TYPE_CHECKING:
from ._main import * # noqa: F403
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"
def __getattr__(name: str) -> Any:
if not name.startswith("_") and name not in {
"run_copy",
"run_recopy",
"run_update",
}:
deprecate_member_as_internal(name, __name__)
return getattr(_main, name)

57
copier/_deprecation.py Normal file
View File

@ -0,0 +1,57 @@
"""Deprecation utilities."""
from __future__ import annotations
import warnings
_issue_note = (
"If you have any questions or concerns, please raise an issue at "
"<https://github.com/copier-org/copier/issues>."
)
def deprecate_module_as_internal(name: str) -> None:
"""Deprecate a module as internal with a warning.
Args:
name: The module name.
"""
warnings.warn(
f"Importing from `{name}` is deprecated. This module is intended for internal "
f"use only and will become inaccessible in the future. {_issue_note}",
DeprecationWarning,
stacklevel=3,
)
def deprecate_member_as_internal(member: str, module: str) -> None:
"""Deprecate a module member as internal with a warning.
Args:
member: The module member name.
module: The module name.
"""
warnings.warn(
f"Importing `{member}` from `{module}` is deprecated. This module member is "
"intended for internal use only and will become inaccessible in the future. "
f"{_issue_note}",
DeprecationWarning,
stacklevel=3,
)
def deprecate_member(member: str, module: str, new_import: str) -> None:
"""Deprecate a module member with a new import statement with a warning.
Args:
member: The module member name.
module: The module name.
new_import: The new import statement.
"""
warnings.warn(
f"Importing `{member}` from `{module}` is deprecated. Please update the import "
f"to `{new_import}`. The deprecated import will become invalid in the future. "
f"{_issue_note}",
DeprecationWarning,
stacklevel=3,
)

23
copier/cli.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _cli
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._cli import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_cli, name)

23
copier/jinja_ext.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _jinja_ext
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._jinja_ext import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_jinja_ext, name)

25
copier/main.py Normal file
View File

@ -0,0 +1,25 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _main
from copier._deprecation import (
deprecate_member,
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._main import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if name in {"run_copy", "run_recopy", "run_update"}:
deprecate_member(name, __name__, f"from copier import {name}")
elif not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_main, name)

23
copier/subproject.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _subproject
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._subproject import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_subproject, name)

23
copier/template.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _template
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._template import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_template, name)

23
copier/tools.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _tools
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._tools import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_tools, name)

23
copier/types.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _types
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._types import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_types, name)

23
copier/user_data.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _user_data
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._user_data import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_user_data, name)

23
copier/vcs.py Normal file
View File

@ -0,0 +1,23 @@
"""Deprecated: module is intended for internal use only."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from copier import _vcs
from copier._deprecation import (
deprecate_member_as_internal,
deprecate_module_as_internal,
)
if TYPE_CHECKING:
from copier._vcs import * # noqa: F403
deprecate_module_as_internal(__name__)
def __getattr__(name: str) -> Any:
if not name.startswith("_"):
deprecate_member_as_internal(name, __name__)
return getattr(_vcs, name)

View File

@ -156,6 +156,20 @@ disable_error_code = ["no-untyped-def"]
addopts = "-n auto -ra"
markers = ["impure: needs network or is not 100% reproducible"]
[tool.coverage.run]
omit = [
# Ignore deprecated modules
"copier/cli.py",
"copier/jinja_ext.py",
"copier/main.py",
"copier/subproject.py",
"copier/template.py",
"copier/tools.py",
"copier/types.py",
"copier/user_data.py",
"copier/vcs.py",
]
[tool.commitizen]
annotated_tag = true
changelog_incremental = true

View File

@ -10,6 +10,7 @@ from plumbum import local
from pydantic import ValidationError
import copier
from copier._main import Worker
from copier._template import DEFAULT_EXCLUDE, Task, Template, load_template_config
from copier._types import AnyByStrDict
from copier.errors import InvalidConfigFileError, MultipleConfigFilesError
@ -322,12 +323,12 @@ def test_multiple_config_file_error() -> None:
)
def test_flags_bad_data(data: AnyByStrDict) -> None:
with pytest.raises(ValidationError):
copier.Worker(**data)
Worker(**data)
def test_flags_extra_fails() -> None:
with pytest.raises(ValidationError):
copier.Worker( # type: ignore[call-arg]
Worker( # type: ignore[call-arg]
src_path="..",
dst_path=Path(),
i_am_not_a_member="and_i_do_not_belong_here",
@ -345,7 +346,7 @@ def is_subdict(small: dict[Any, Any], big: dict[Any, Any]) -> bool:
def test_worker_good_data(tmp_path: Path) -> None:
# This test is probably useless, as it tests the what and not the how
conf = copier.Worker("./tests/demo_data", tmp_path)
conf = Worker("./tests/demo_data", tmp_path)
assert conf._render_context()["_folder_name"] == tmp_path.name
assert conf.all_exclusions == ("exclude1", "exclude2")
assert conf.template.skip_if_exists == ["skip_if_exists1", "skip_if_exists2"]
@ -373,12 +374,12 @@ def test_worker_good_data(tmp_path: Path) -> None:
def test_worker_config_precedence(
tmp_path: Path, test_input: AnyByStrDict, expected_exclusions: tuple[str, ...]
) -> None:
conf = copier.Worker(dst_path=tmp_path, vcs_ref="HEAD", **test_input)
conf = Worker(dst_path=tmp_path, vcs_ref="HEAD", **test_input)
assert expected_exclusions == conf.all_exclusions
def test_config_data_transclusion() -> None:
config = copier.Worker("tests/demo_transclude/demo")
config = Worker("tests/demo_transclude/demo")
assert config.all_exclusions == ("exclude1", "exclude2")

View File

@ -2,7 +2,7 @@ from unittest.mock import Mock, patch
import pytest
from copier import Worker
from copier._main import Worker
from copier.errors import CopierAnswersInterrupt
from .helpers import build_file_tree

View File

@ -10,7 +10,7 @@ import yaml
from pexpect.popen_spawn import PopenSpawn
from plumbum import local
from copier import Worker
from copier._main import Worker
from copier._types import AnyByStrDict
from copier._user_data import load_answersfile_data
from copier.errors import InvalidTypeError