Skip to content

Commit

Permalink
Set up Github Actions (#1)
Browse files Browse the repository at this point in the history
* Create python-tests.yml

* Setup Github Actions

* Added comments
  • Loading branch information
ag14774 authored Sep 24, 2024
1 parent 84a373f commit aeb8f7c
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 303 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry update
poetry install
- name: Run pre-commit tests
run: |
poetry run pre-commit run --all-files
- name: Test with pytest
run: |
poetry run pytest
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
File renamed without changes.
379 changes: 146 additions & 233 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion poetry_monoranger_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""Copyright (C) 2024 GlaxoSmithKline plc"""
"""Copyright (C) 2024 GlaxoSmithKline plc"""
6 changes: 4 additions & 2 deletions poetry_monoranger_plugin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module defines the configuration class for the Monoranger plugin.
"""

from __future__ import annotations

from dataclasses import dataclass
Expand All @@ -17,9 +18,10 @@ class MonorangerConfig:
monorepo_root (str): Path to the root of the monorepo. Defaults to "../".
version_rewrite_rule (Literal['==', '~', '^', '>=,<']): Rule for version rewriting. Defaults to "^".
"""

enabled: bool = False
monorepo_root: str = "../"
version_rewrite_rule: Literal['==', '~', '^', '>=,<'] = "^"
version_rewrite_rule: Literal["==", "~", "^", ">=,<"] = "^"

@classmethod
def from_dict(cls, d: dict[str, Any]):
Expand All @@ -32,4 +34,4 @@ def from_dict(cls, d: dict[str, Any]):
MonorangerConfig: An instance of MonorangerConfig with values populated from the dictionary.
"""
d = {k.replace("-", "_"): v for k, v in d.items()}
return cls(**d)
return cls(**d)
12 changes: 7 additions & 5 deletions poetry_monoranger_plugin/lock_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
(`lock`, `install`, `update`) to support monorepo setups. It ensures these commands behave as if they
were run from the monorepo root directory, maintaining a shared lockfile.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
Expand All @@ -20,13 +21,13 @@
from poetry_monoranger_plugin.config import MonorangerConfig



class LockModifier:
"""Modifies Poetry commands (`lock`, `install`, `update`) for monorepo support.
Ensures these commands behave as if they were run from the monorepo root directory
even when run from a subdirectory, thus maintaining a shared lockfile.
"""

def __init__(self, plugin_conf: MonorangerConfig):
self.plugin_conf = plugin_conf

Expand All @@ -41,15 +42,16 @@ def execute(self, event: ConsoleCommandEvent):
event (ConsoleCommandEvent): The triggering event.
"""
command = event.command
assert isinstance(command, (LockCommand, InstallCommand, UpdateCommand)), \
f"{self.__class__.__name__} can only be used for `poetry lock`, `poetry install`, and `poetry update` commands"
assert isinstance(
command, (LockCommand, InstallCommand, UpdateCommand)
), f"{self.__class__.__name__} can only be used for `poetry lock`, `poetry install`, and `poetry update` commands"

io = event.io
io.write_line("<info>Running command from monorepo root directory</info>")

monorepo_root = (command.poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)

installer = Installer(
io,
command.env,
Expand All @@ -59,6 +61,6 @@ def execute(self, event: ConsoleCommandEvent):
monorepo_root_poetry.config,
disable_cache=monorepo_root_poetry.disable_cache,
)

command.set_poetry(monorepo_root_poetry)
command.set_installer(installer)
45 changes: 25 additions & 20 deletions poetry_monoranger_plugin/monorepo_adder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module defines classes and methods to modify the behavior of Poetry's add and remove commands for monorepo support.
"""

from __future__ import annotations

import copy
Expand All @@ -19,13 +20,14 @@
from tomlkit.toml_document import TOMLDocument

from poetry_monoranger_plugin.config import MonorangerConfig


class DummyInstaller(Installer):
"""A dummy installer that overrides the run method and disables it
Note: For more details, refer to the docstring of `MonorepoAdderRemover`.
"""

@classmethod
def from_installer(cls, installer: Installer):
"""Creates a DummyInstaller instance from an existing Installer instance.
Expand All @@ -48,7 +50,7 @@ def run(self):
int: Always returns 0.
"""
return 0


class MonorepoAdderRemover:
"""A class to modify the behavior of Poetry's add and remove commands for monorepo support.
Expand All @@ -59,22 +61,23 @@ class MonorepoAdderRemover:
Under normal circumstances, the add/remove commands modify the per-project lockfile, and if it
was modified successfully, *only then* the pyproject.toml file is updated. This leaves the
pyproject.toml in a good state in case the lockfile generation/dependency resolution fails.
However, in a monorepo setup, we want to maintain a single lockfile for all the projects in the
monorepo. This means that the add/remove commands should not generate a per-project lockfile.The
monorepo. This means that the add/remove commands should not generate a per-project lockfile.The
purpose of the DummyInstaller is to disable the installation part of the add/remove commands
and just allow the add/remove to directly modify the pyproject.toml file without generating a
per-project lockfile.
After the pyproject.toml file is modified, we can update the root lockfile by creating a new
After the pyproject.toml file is modified, we can update the root lockfile by creating a new
Installer and executing the steps that are normally executed by the add/remove command. Since
with this approach the pyproject.toml file is modified before lockfile updating, we need to
with this approach the pyproject.toml file is modified before lockfile updating, we need to
ensure that the changes are rolled back in case of an error during the lockfile update.
"""

def __init__(self, plugin_conf: MonorangerConfig):
self.plugin_conf = plugin_conf
self.pre_add_pyproject: None|TOMLDocument = None
self.pre_add_pyproject: None | TOMLDocument = None

def execute(self, event: ConsoleCommandEvent):
"""Replaces the installer with a dummy installer to disable the installation part of the add/remove commands.
Expand All @@ -87,19 +90,20 @@ def execute(self, event: ConsoleCommandEvent):
event (ConsoleCommandEvent): The event that triggered the command.
"""
command = event.command
assert isinstance(command, (AddCommand, RemoveCommand)), \
f"{self.__class__.__name__} can only be used for `poetry add` and `poetry remove` command"
assert isinstance(
command, (AddCommand, RemoveCommand)
), f"{self.__class__.__name__} can only be used for `poetry add` and `poetry remove` command"

# Create a copy of the poetry object to prevent the command from modifying the original poetry object
poetry = Poetry.__new__(Poetry)
poetry.__dict__.update(command.poetry.__dict__)
command.set_poetry(poetry)

self.pre_add_pyproject = copy.deepcopy(poetry.file.read())

installer = DummyInstaller.from_installer(command.installer)
command.set_installer(installer)

def post_execute(self, event: ConsoleTerminateEvent):
"""Handles the post-execution steps for the add or remove command, including rolling back changes if needed.
Expand All @@ -111,12 +115,13 @@ def post_execute(self, event: ConsoleTerminateEvent):
event (ConsoleTerminateEvent): The event that triggered the command termination.
"""
command = event.command
assert isinstance(command, (AddCommand, RemoveCommand)), \
f"{self.__class__.__name__} can only be used for `poetry add` and `poetry remove` command"

assert isinstance(
command, (AddCommand, RemoveCommand)
), f"{self.__class__.__name__} can only be used for `poetry add` and `poetry remove` command"

io = event.io
poetry = command.poetry

if self.pre_add_pyproject and (poetry.file.read() == self.pre_add_pyproject):
return

Expand All @@ -141,10 +146,10 @@ def post_execute(self, event: ConsoleTerminateEvent):
installer.whitelist([poetry.package.name])

status = installer.run()

if status != 0 and not command.option("dry-run") and self.pre_add_pyproject is not None:
io.write_line("<error>An error occurred during the installation. Rolling back changes...</error>")
assert isinstance(self.pre_add_pyproject, TOMLDocument)
poetry.file.write(self.pre_add_pyproject)
event.set_exit_code(status)

event.set_exit_code(status)
23 changes: 13 additions & 10 deletions poetry_monoranger_plugin/path_rewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This module defines the PathRewriter class, which modifies the behavior of the Poetry build command to
rewrite directory dependencies to their pinned versions.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast
Expand All @@ -23,6 +24,7 @@

class PathRewriter:
"""A class to handle the rewriting of directory dependencies in a Poetry project."""

def __init__(self, plugin_conf: MonorangerConfig):
self.plugin_conf: MonorangerConfig = plugin_conf

Expand All @@ -33,22 +35,23 @@ def execute(self, event: ConsoleCommandEvent):
event (ConsoleCommandEvent): The triggering event.
"""
command = event.command
assert isinstance(command, BuildCommand), \
f"{self.__class__.__name__} can only be used with the `poetry build` command"
assert isinstance(
command, BuildCommand
), f"{self.__class__.__name__} can only be used with the `poetry build` command"

io = event.io
poetry = command.poetry

main_deps_group = poetry.package.dependency_group(MAIN_GROUP)
directory_deps = [dep for dep in main_deps_group.dependencies if isinstance(dep, DirectoryDependency)]

for dependency in directory_deps:
try:
pinned = self._pin_dependency(poetry, dependency)
except (RuntimeError, ValueError) as e:
io.write_line(f"<fg=yellow>Could not pin dependency {dependency.name}: {str(e)}</>")
continue

main_deps_group.remove_dependency(dependency.name)
main_deps_group.add_dependency(pinned)

Expand All @@ -67,17 +70,17 @@ def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
ValueError: If the version rewrite rule is invalid.
"""
pyproject_file = poetry.pyproject_path.parent / dependency.path / "pyproject.toml"

if not pyproject_file.exists():
raise RuntimeError(f"Could not find pyproject.toml in {dependency.path}")

dep_pyproject: PyProjectTOML = PyProjectTOML(pyproject_file)

if not dep_pyproject.is_poetry_project():
raise RuntimeError(f"Directory {dependency.path} is not a valid poetry project")

name = cast(str, dep_pyproject.poetry_config["name"])
version = cast(str, dep_pyproject.poetry_config["version"])
version = cast(str, dep_pyproject.poetry_config["version"])
if self.plugin_conf.version_rewrite_rule in ["~", "^"]:
pinned_version = f"{self.plugin_conf.version_rewrite_rule}{version}"
elif self.plugin_conf.version_rewrite_rule == "==":
Expand All @@ -88,5 +91,5 @@ def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
pinned_version = f">={version},<{next_patch_version}"
else:
raise ValueError(f"Invalid version rewrite rule: {self.plugin_conf.version_rewrite_rule}")
return Dependency(name, pinned_version, groups=dependency.groups)

return Dependency(name, pinned_version, groups=dependency.groups)
Loading

0 comments on commit aeb8f7c

Please sign in to comment.