Skip to content

Commit

Permalink
fix(#19): also update the pyproject.toml in the tar.gz
Browse files Browse the repository at this point in the history
  • Loading branch information
gerbenoostra committed Jan 20, 2025
1 parent 24cc6f3 commit 2a3d2e9
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 5 deletions.
79 changes: 74 additions & 5 deletions poetry_plugin_mono_repo_deps/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import Any, Dict, List, TypeVar, cast

from cleo.events.console_event import ConsoleEvent
from cleo.events.console_events import COMMAND
from cleo.events.console_events import COMMAND, TERMINATE
from cleo.events.console_terminate_event import ConsoleTerminateEvent
from cleo.events.event import Event
from cleo.events.event_dispatcher import EventDispatcher
from cleo.io.io import IO
Expand All @@ -16,6 +17,7 @@
from poetry.poetry import Poetry
from poetry.repositories.lockfile_repository import LockfileRepository
from poetry.utils.helpers import merge_dicts
from tomlkit import TOMLDocument

T = TypeVar("T")

Expand Down Expand Up @@ -83,12 +85,14 @@ def load_config(poetry: Poetry) -> Config | None:
class MonoRepoDepsPlugin(ApplicationPlugin):
def __init__(self) -> None:
super().__init__()
self._original_toml_data: TOMLDocument | None = None

def activate(self, application: Application) -> None:
self._application = application
dispatcher = application.event_dispatcher
if dispatcher is not None:
dispatcher.add_listener(COMMAND, self.handle_command)
dispatcher.add_listener(TERMINATE, self.handle_terminate)
else: # pragma: no cover
pass

Expand Down Expand Up @@ -128,6 +132,8 @@ def handle_command(self, event: Event, _event_name: str, _dispatcher: EventDispa

# for build
self.update_locked_repository(io, config)
if command.name != "export":
self.update_pyproject_toml(config)
# for export
self.update_lock_data(config)
return None
Expand Down Expand Up @@ -160,10 +166,67 @@ def update_lock_data(self, config: Config) -> None:
"""Updates the lockers internal lock data, necessary for commands like `export`"""
poetry = self._application.poetry
locked_packages = cast(List[Dict[str, Any]], poetry._locker.lock_data["package"])

for info in locked_packages:
if is_to_be_replaced_package_lock(config, info):
modify_locked_package_to_named(config, info, locked_packages)

def update_pyproject_toml(self, config: Config) -> None:
"""Updates the pyproject.toml file, necessary for commands like `build`"""
poetry = self._application.poetry
pyproject = poetry.pyproject

toml_data: TOMLDocument = pyproject.data
self._original_toml_data = deepcopy(toml_data)
poetry_config = pyproject.poetry_config
# used to retrieve the current version of the package
locked_packages = cast(List[Dict[str, Any]], poetry._locker.lock_data["package"])
# update all possible dependency sections in the pyproject.toml
update_locked_dependencies(config, poetry_config.get("dependencies", {}), locked_packages)
update_locked_dependencies(config, poetry_config.get("dev-dependencies", {}), locked_packages)
if "group" in poetry_config:
for group_config in poetry_config["group"].values():
update_locked_dependencies(config, group_config.get("dependencies", {}), locked_packages)
# don't need to assign it back to pyproject.data, as we've modified the data structure in place
# writes the modified pyproject to disk, will be restored after the command by `restore_pyproject_toml`
pyproject.save()

def restore_pyproject_toml(self) -> None:
"""Restores the pyproject.toml file, necessary for commands like `build`"""
toml_data = self._original_toml_data
if toml_data is None:
# we apparently didn't save it
return
pyproject = self._application.poetry.pyproject
pyproject._toml_document = toml_data
pyproject.save()
self._original_toml_data = None

def handle_terminate(self, event: Event, _event_name: str, _dispatcher: EventDispatcher) -> None:
try:
poetry = self._application.poetry
except RuntimeError:
# should only happen if poetry runs outside poetry folder structure
# as we modify poetry lock files, the plugin can only work in poetry folders
# and is only relevant for commands that work in poetry folders
# thus, either the command is not relevant
# or, the command would fail itself
return

config = load_config(poetry)
if config is None: # pragma: no cover
return

event = cast(ConsoleTerminateEvent, event) # because we listen to TERMINATEs
command = event.command
if command.name not in config.commands:
# Skipped for export, but that's handled by restoration
# which only restores if we modified the pyproject.toml file
return
# for build
self.restore_pyproject_toml()
return None


def is_to_be_replaced_package_lock(config: Config, locked_package_data: dict[str, Any]) -> bool:
source = locked_package_data.get("source", {})
Expand Down Expand Up @@ -240,10 +303,16 @@ def modify_locked_package_to_named(
if is_to_be_replaced_package_lock(config, info):
_modify_locked_package_to_named(info)
# remove path and develop from dependencies of dependencies
for dep_name, dep in info.get("dependencies", {}).items():
if is_to_be_replaced_dependency_lock(config, dep):
dep_version = get_current_locked_version(all_locked_packages, dep_name, "*")
_modify_locked_dependency_to_named(dep, dep_version)
update_locked_dependencies(config, info.get("dependencies", {}), all_locked_packages)


def update_locked_dependencies(
config: Config, dependencies: dict[str, Any], all_locked_packages: list[dict[str, Any]]
) -> None:
for dep_name, dep in dependencies.items():
if is_to_be_replaced_dependency_lock(config, dep):
dep_version = get_current_locked_version(all_locked_packages, dep_name, "*")
_modify_locked_dependency_to_named(dep, dep_version)


def get_current_locked_version(locked_packaged: list[dict[str, Any]], name: str, default: str) -> str:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_commands_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,37 @@ def test_build_artifact(fixture_simple_a: Path, module_dir: str) -> None:
pkg_info_content = list(b.decode("utf-8").strip(os.linesep) for b in pkg_info_bytes)
validate_package_metadata(setup, pkg_info_content)

pyproject_stream = tar.extractfile(f"{package_name}-0.0.1/pyproject.toml")
assert pyproject_stream is not None
with pyproject_stream:
pyproject_bytes = pyproject_stream.readlines()
pyproject_content = list(b.decode("utf-8").strip(os.linesep) for b in pyproject_bytes)
validate_pyproject_content(setup, pyproject_content)

# verifying that we didn't modify the original pyproject.toml
with open("pyproject.toml") as f:
original_content = f.readlines()
setup_disabled = TestSetup(enabled=False, deps=setup.deps, transitive_deps=setup.transitive_deps)
validate_pyproject_content(setup_disabled, original_content)


def validate_pyproject_content(setup: TestSetup, content: list[str]) -> None:
_logger.info("Validating pyproject content:")
_logger.info("\n".join(content))
# path dependency should be replaced by named dependency
for dep in setup.deps or []:
dep_line_candidates = [line for line in content if line.strip().startswith(f"{dep.name} = ")]
# we expect only one in our test setups
# (could fail if we would reuse the lib name, for example as extras identifier)
# (therefore we've constructed the pyproject.toml's to not reuse the same name)
assert len(dep_line_candidates) == 1
dep_line = dep_line_candidates[0]
if setup.enabled:
assert dep.version in dep_line
dep.export_line
else:
assert dep.version not in dep_line


def validate_package_metadata(setup: TestSetup, content: list[str]) -> None:
_logger.info("Validating package metadata:")
Expand Down

0 comments on commit 2a3d2e9

Please sign in to comment.