From f698dbdce6a8948ef623d6c6f5be8230524b8651 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 29 Feb 2024 15:44:10 +1100 Subject: [PATCH] chore: migrate from flat to src layout The src layout helps prevent a number of issues during development. A discussion by Python's packaging group of the pros can be found at https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/. Fundamentally, it prevents `import pact` from finding the local `pact/` directory, and ensures that the package is installed correctly (typically as an editable installation). Signed-off-by: JP-Ellis --- .gitignore | 6 +- .gitmodules | 2 +- .pre-commit-config.yaml | 6 +- hatch_build.py | 41 +++++----- pyproject.toml | 81 ++++++++++++------- {pact => src/pact}/__init__.py | 0 {pact => src/pact}/broker.py | 0 {pact => src/pact}/cli/__init__.py | 0 {pact => src/pact}/cli/verify.py | 0 {pact => src/pact}/constants.py | 0 {pact => src/pact}/consumer.py | 0 {pact => src/pact}/http_proxy.py | 0 {pact => src/pact}/matchers.py | 0 {pact => src/pact}/message_consumer.py | 0 {pact => src/pact}/message_pact.py | 0 {pact => src/pact}/message_provider.py | 0 {pact => src/pact}/pact.py | 0 {pact => src/pact}/provider.py | 0 {pact => src/pact}/v3/__init__.py | 0 {pact => src/pact}/v3/_ffi.pyi | 0 {pact => src/pact}/v3/ffi.py | 0 {pact => src/pact}/v3/pact.py | 0 {pact => src/pact}/v3/py.typed | 0 {pact => src/pact}/verifier.py | 0 {pact => src/pact}/verify_wrapper.py | 0 .../__init__.py | 0 .../conftest.py | 0 .../definition | 0 .../test_v1_consumer.py | 7 +- .../test_v2_consumer.py | 7 +- .../test_v3_consumer.py | 4 +- .../test_v4_consumer.py | 4 +- .../util/__init__.py | 0 .../util/consumer.py | 4 +- 34 files changed, 98 insertions(+), 64 deletions(-) rename {pact => src/pact}/__init__.py (100%) rename {pact => src/pact}/broker.py (100%) rename {pact => src/pact}/cli/__init__.py (100%) rename {pact => src/pact}/cli/verify.py (100%) rename {pact => src/pact}/constants.py (100%) rename {pact => src/pact}/consumer.py (100%) rename {pact => src/pact}/http_proxy.py (100%) rename {pact => src/pact}/matchers.py (100%) rename {pact => src/pact}/message_consumer.py (100%) rename {pact => src/pact}/message_pact.py (100%) rename {pact => src/pact}/message_provider.py (100%) rename {pact => src/pact}/pact.py (100%) rename {pact => src/pact}/provider.py (100%) rename {pact => src/pact}/v3/__init__.py (100%) rename {pact => src/pact}/v3/_ffi.pyi (100%) rename {pact => src/pact}/v3/ffi.py (100%) rename {pact => src/pact}/v3/pact.py (100%) rename {pact => src/pact}/v3/py.typed (100%) rename {pact => src/pact}/verifier.py (100%) rename {pact => src/pact}/verify_wrapper.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/__init__.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/conftest.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/definition (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v1_consumer.py (98%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v2_consumer.py (98%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v3_consumer.py (97%) rename tests/v3/{compatiblity_suite => compatibility_suite}/test_v4_consumer.py (97%) rename tests/v3/{compatiblity_suite => compatibility_suite}/util/__init__.py (100%) rename tests/v3/{compatiblity_suite => compatibility_suite}/util/consumer.py (99%) diff --git a/.gitignore b/.gitignore index f93aa99ac..763596319 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ ################################################################################ ## Pact Python Specific ################################################################################ -pact/bin -pact/data +src/pact/bin +src/pact/data # Version is determined from the VCS -pact/__version__.py +src/pact/__version__.py ################################################################################ ## Standard Templates diff --git a/.gitmodules b/.gitmodules index fa1a87278..ebf9afa34 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "compatibility-suite"] - path = tests/v3/compatiblity_suite/definition + path = tests/v3/compatibility_suite/definition url = ../pact-compatibility-suite.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d59569216..ff7e6d229 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,8 +42,8 @@ repos: hooks: - id: ruff # Exclude python files in pact/** and tests/**, except for the - # files in pact/v3/** and tests/v3/**. - exclude: ^(pact|tests)/(?!v3/).*\.py$ + # files in src/pact/v3/** and tests/v3/**. + exclude: ^(src/pact|tests)/(?!v3/).*\.py$ args: [--fix, --exit-non-zero-on-fix] - id: ruff-format exclude: ^(pact|tests)/(?!v3/).*\.py$ @@ -63,7 +63,7 @@ repos: entry: hatch run mypy language: system types: [python] - exclude: ^(pact|tests)/(?!v3/).*\.py$ + exclude: ^(src/pact|tests)/(?!v3/).*\.py$ stages: [pre-push] - repo: https://github.com/igorshubovych/markdownlint-cli diff --git a/hatch_build.py b/hatch_build.py index 303d98611..3baec67cb 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -27,7 +27,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags -ROOT_DIR = Path(__file__).parent.resolve() +PACT_ROOT_DIR = Path(__file__).parent.resolve() / "src" / "pact" # Latest version available at: # https://github.com/pact-foundation/pact-ruby-standalone/releases @@ -73,7 +73,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 def clean(self, versions: list[str]) -> None: # noqa: ARG002 """Clean up any files created by the build hook.""" for subdir in ["bin", "lib", "data"]: - shutil.rmtree(ROOT_DIR / "pact" / subdir, ignore_errors=True) + shutil.rmtree(PACT_ROOT_DIR / subdir, ignore_errors=True) def initialize( self, @@ -107,7 +107,7 @@ def pact_bin_install(self, version: str) -> None: """ Install the Pact standalone binaries. - The binaries are installed in `pact/bin`, and the relevant version for + The binaries are installed in `src/pact/bin`, and the relevant version for the current operating system is determined automatically. Args: @@ -188,23 +188,28 @@ def _pact_bin_extract(self, artifact: Path) -> None: """ Extract the Pact binaries. - The upstream distributables contain a lot of files which are not needed - for this library. This function ensures that only the files in - `pact/bin` are extracted to avoid unnecessary bloat. + The binaries in the `bin` directory require the underlying Ruby runtime + to be present, which is included in the `lib` directory. Args: artifact: The path to the downloaded artifact. """ - if str(artifact).endswith(".zip"): - with zipfile.ZipFile(artifact) as f: - f.extractall(ROOT_DIR) # noqa: S202 - - if str(artifact).endswith(".tar.gz"): - with tarfile.open(artifact) as f: - f.extractall(ROOT_DIR) # noqa: S202 - - # Cleanup the extract `README.md` - (ROOT_DIR / "pact" / "README.md").unlink() + with tempfile.TemporaryDirectory() as tmpdir: + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + f.extractall(tmpdir) # noqa: S202 + + for d in ["bin", "lib"]: + if (PACT_ROOT_DIR / d).is_dir(): + shutil.rmtree(PACT_ROOT_DIR / d) + shutil.copytree( + Path(tmpdir) / "pact" / d, + PACT_ROOT_DIR / d, + ) def pact_lib_install(self, version: str) -> None: """ @@ -415,7 +420,7 @@ def _pact_lib_cffi(self, includes: list[str]) -> None: library_dirs=[str(self.tmpdir)], ) output = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir))) - shutil.copy(output, ROOT_DIR / "pact" / "v3") + shutil.copy(output, PACT_ROOT_DIR / "v3") def _download(self, url: str) -> Path: """ @@ -431,7 +436,7 @@ def _download(self, url: str) -> Path: The path to the downloaded artifact. """ filename = url.split("/")[-1] - artifact = ROOT_DIR / "pact" / "data" / filename + artifact = PACT_ROOT_DIR / "data" / filename artifact.parent.mkdir(parents=True, exist_ok=True) if not artifact.exists(): diff --git a/pyproject.toml b/pyproject.toml index dbf3fea54..ac01fd8cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,15 +100,33 @@ build-backend = "hatchling.build" source = "vcs" [tool.hatch.build.hooks.vcs] -version-file = "pact/__version__.py" - -[tool.hatch.build] -include = ["**/py.typed", "**/*.md", "LICENSE", "pact/**/*.py", "pact/**/*.pyi"] +version-file = "src/pact/__version__.py" + +[tool.hatch.build.targets.sdist] +include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", + + # Metadata + "*.md", + "LICENSE", +] [tool.hatch.build.targets.wheel] -# Ignore the data files in the wheel as their contents are already included -# in the package. -artifacts = ["pact/bin/*", "pact/lib/*", "pact/v3/_ffi.*"] +packages = ["/src/pact"] +include = [ + # Source + "/src/pact/**/*.py", + "/src/pact/**/*.pyi", + "/src/pact/**/py.typed", +] +artifacts = [ + "/src/pact/bin/*", # Ruby executables + "/src/pact/lib/*", # Ruby library + "/src/pact/v3/_ffi.*", # Rust library +] [tool.hatch.build.targets.wheel.hooks.custom] @@ -135,8 +153,8 @@ format = "ruff format {args}" test = "pytest tests/ {args}" example = "pytest examples/ {args}" all = ["format", "lint", "typecheck", "test", "example"] -docs = ["mkdocs serve {args}"] -docs-build = ["mkdocs build {args}"] +docs = "mkdocs serve {args}" +docs-build = "mkdocs build {args}" # Test environment for running unit tests. This automatically tests against all # supported Python versions. @@ -151,6 +169,7 @@ python = ["3.8", "3.9", "3.10", "3.11", "3.12"] ################################################################################ [tool.pytest.ini_options] +pythonpath = "." addopts = [ "--import-mode=importlib", "--cov-config=pyproject.toml", @@ -173,6 +192,10 @@ markers = [ ## Coverage ################################################################################ +[tool.coverage.paths] +pact = ["/src/pact"] +tests = ["/examples", "/tests"] + [tool.coverage.report] exclude_lines = [ "if __name__ == .__main__.:", # Ignore non-runnable code @@ -191,25 +214,25 @@ target-version = "py38" # TODO: Remove the explicity extend-exclude once astral-sh/ruff#6262 is fixed. # https://github.com/pact-foundation/pact-python/issues/458 extend-exclude = [ - # "pact/*.py", - # "pact/cli/*.py", - # "tests/*.py", - # "tests/cli/*.py", - "pact/__init__.py", - "pact/__version__.py", - "pact/broker.py", - "pact/cli/*.py", - "pact/constants.py", - "pact/consumer.py", - "pact/http_proxy.py", - "pact/matchers.py", - "pact/message_consumer.py", - "pact/message_pact.py", - "pact/message_provider.py", - "pact/pact.py", - "pact/provider.py", - "pact/verifier.py", - "pact/verify_wrapper.py", + # "src/pact/*.py", + # "src/pact/cli/*.py", + # "src/tests/*.py", + # "src/tests/cli/*.py", + "src/pact/__init__.py", + "src/pact/__version__.py", + "src/pact/broker.py", + "src/pact/cli/*.py", + "src/pact/constants.py", + "src/pact/consumer.py", + "src/pact/http_proxy.py", + "src/pact/matchers.py", + "src/pact/message_consumer.py", + "src/pact/message_pact.py", + "src/pact/message_provider.py", + "src/pact/pact.py", + "src/pact/provider.py", + "src/pact/verifier.py", + "src/pact/verify_wrapper.py", "tests/__init__.py", "tests/cli/*.py", "tests/conftest.py", @@ -264,7 +287,7 @@ docstring-code-format = true ################################################################################ [tool.mypy] -exclude = '^(pact|tests)/(?!v3).+\.py$' +exclude = '^(src/pact|tests)/(?!v3).+\.py$' ################################################################################ ## CI Build Wheel diff --git a/pact/__init__.py b/src/pact/__init__.py similarity index 100% rename from pact/__init__.py rename to src/pact/__init__.py diff --git a/pact/broker.py b/src/pact/broker.py similarity index 100% rename from pact/broker.py rename to src/pact/broker.py diff --git a/pact/cli/__init__.py b/src/pact/cli/__init__.py similarity index 100% rename from pact/cli/__init__.py rename to src/pact/cli/__init__.py diff --git a/pact/cli/verify.py b/src/pact/cli/verify.py similarity index 100% rename from pact/cli/verify.py rename to src/pact/cli/verify.py diff --git a/pact/constants.py b/src/pact/constants.py similarity index 100% rename from pact/constants.py rename to src/pact/constants.py diff --git a/pact/consumer.py b/src/pact/consumer.py similarity index 100% rename from pact/consumer.py rename to src/pact/consumer.py diff --git a/pact/http_proxy.py b/src/pact/http_proxy.py similarity index 100% rename from pact/http_proxy.py rename to src/pact/http_proxy.py diff --git a/pact/matchers.py b/src/pact/matchers.py similarity index 100% rename from pact/matchers.py rename to src/pact/matchers.py diff --git a/pact/message_consumer.py b/src/pact/message_consumer.py similarity index 100% rename from pact/message_consumer.py rename to src/pact/message_consumer.py diff --git a/pact/message_pact.py b/src/pact/message_pact.py similarity index 100% rename from pact/message_pact.py rename to src/pact/message_pact.py diff --git a/pact/message_provider.py b/src/pact/message_provider.py similarity index 100% rename from pact/message_provider.py rename to src/pact/message_provider.py diff --git a/pact/pact.py b/src/pact/pact.py similarity index 100% rename from pact/pact.py rename to src/pact/pact.py diff --git a/pact/provider.py b/src/pact/provider.py similarity index 100% rename from pact/provider.py rename to src/pact/provider.py diff --git a/pact/v3/__init__.py b/src/pact/v3/__init__.py similarity index 100% rename from pact/v3/__init__.py rename to src/pact/v3/__init__.py diff --git a/pact/v3/_ffi.pyi b/src/pact/v3/_ffi.pyi similarity index 100% rename from pact/v3/_ffi.pyi rename to src/pact/v3/_ffi.pyi diff --git a/pact/v3/ffi.py b/src/pact/v3/ffi.py similarity index 100% rename from pact/v3/ffi.py rename to src/pact/v3/ffi.py diff --git a/pact/v3/pact.py b/src/pact/v3/pact.py similarity index 100% rename from pact/v3/pact.py rename to src/pact/v3/pact.py diff --git a/pact/v3/py.typed b/src/pact/v3/py.typed similarity index 100% rename from pact/v3/py.typed rename to src/pact/v3/py.typed diff --git a/pact/verifier.py b/src/pact/verifier.py similarity index 100% rename from pact/verifier.py rename to src/pact/verifier.py diff --git a/pact/verify_wrapper.py b/src/pact/verify_wrapper.py similarity index 100% rename from pact/verify_wrapper.py rename to src/pact/verify_wrapper.py diff --git a/tests/v3/compatiblity_suite/__init__.py b/tests/v3/compatibility_suite/__init__.py similarity index 100% rename from tests/v3/compatiblity_suite/__init__.py rename to tests/v3/compatibility_suite/__init__.py diff --git a/tests/v3/compatiblity_suite/conftest.py b/tests/v3/compatibility_suite/conftest.py similarity index 100% rename from tests/v3/compatiblity_suite/conftest.py rename to tests/v3/compatibility_suite/conftest.py diff --git a/tests/v3/compatiblity_suite/definition b/tests/v3/compatibility_suite/definition similarity index 100% rename from tests/v3/compatiblity_suite/definition rename to tests/v3/compatibility_suite/definition diff --git a/tests/v3/compatiblity_suite/test_v1_consumer.py b/tests/v3/compatibility_suite/test_v1_consumer.py similarity index 98% rename from tests/v3/compatiblity_suite/test_v1_consumer.py rename to tests/v3/compatibility_suite/test_v1_consumer.py index 3710db730..084429630 100644 --- a/tests/v3/compatiblity_suite/test_v1_consumer.py +++ b/tests/v3/compatibility_suite/test_v1_consumer.py @@ -7,8 +7,11 @@ import pytest from pytest_bdd import given, parsers, scenario -from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, request_n_is_made_to_the_mock_server_with_the_following_changes, diff --git a/tests/v3/compatiblity_suite/test_v2_consumer.py b/tests/v3/compatibility_suite/test_v2_consumer.py similarity index 98% rename from tests/v3/compatiblity_suite/test_v2_consumer.py rename to tests/v3/compatibility_suite/test_v2_consumer.py index 61f0d02c9..cd1447cfe 100644 --- a/tests/v3/compatiblity_suite/test_v2_consumer.py +++ b/tests/v3/compatibility_suite/test_v2_consumer.py @@ -6,8 +6,11 @@ from pytest_bdd import given, parsers, scenario -from tests.v3.compatiblity_suite.util import InteractionDefinition, parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import ( + InteractionDefinition, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( a_response_is_returned, request_n_is_made_to_the_mock_server, request_n_is_made_to_the_mock_server_with_the_following_changes, diff --git a/tests/v3/compatiblity_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py similarity index 97% rename from tests/v3/compatiblity_suite/test_v3_consumer.py rename to tests/v3/compatibility_suite/test_v3_consumer.py index bffb78706..b7014d6f7 100644 --- a/tests/v3/compatiblity_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -10,8 +10,8 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatiblity_suite.util import parse_markdown_table -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) diff --git a/tests/v3/compatiblity_suite/test_v4_consumer.py b/tests/v3/compatibility_suite/test_v4_consumer.py similarity index 97% rename from tests/v3/compatiblity_suite/test_v4_consumer.py rename to tests/v3/compatibility_suite/test_v4_consumer.py index fd43866fd..7b7ab019d 100644 --- a/tests/v3/compatiblity_suite/test_v4_consumer.py +++ b/tests/v3/compatibility_suite/test_v4_consumer.py @@ -9,8 +9,8 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatiblity_suite.util import string_to_int -from tests.v3.compatiblity_suite.util.consumer import ( +from tests.v3.compatibility_suite.util import string_to_int +from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) diff --git a/tests/v3/compatiblity_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py similarity index 100% rename from tests/v3/compatiblity_suite/util/__init__.py rename to tests/v3/compatibility_suite/util/__init__.py diff --git a/tests/v3/compatiblity_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py similarity index 99% rename from tests/v3/compatiblity_suite/util/consumer.py rename to tests/v3/compatibility_suite/util/consumer.py index 2488b0cd9..f9834187d 100644 --- a/tests/v3/compatiblity_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -15,7 +15,7 @@ from yarl import URL from pact.v3 import Pact -from tests.v3.compatiblity_suite.util import ( +from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, parse_markdown_table, string_to_int, @@ -27,7 +27,7 @@ from pathlib import Path from pact.v3.pact import HttpInteraction, PactServer - from tests.v3.compatiblity_suite.util import InteractionDefinition + from tests.v3.compatibility_suite.util import InteractionDefinition logger = logging.getLogger(__name__)