Skip to content

Commit

Permalink
Refactor (#12)
Browse files Browse the repository at this point in the history
* Add update.Updater

* Add update.Options

* Add update.Action

* Fix misplaced @classmethod

* Use update.Action

* Remove misplaced blank line

* Add missing type annotation for Action.message

* Add PackageUpdater

* Eliminate Action

* Refactor using Action pattern

* Minor cleanup

* Minor cleanup in PackageUpdater.__init__

* Add update.Actions

* Check PackageUpdater.required outside of class

* Move PackageUpdater.show() outside of class

* Move dry_run check outside of PackageUpdater

* Fix Rollback.required

* Fix Commit access of PackageUpdater attributes

* Fix ANN201 (Missing return type annotation for public function)

* Fix missing docstrings

D101 Missing docstring in public class
D102 Missing docstring in public method
D105 Missing docstring in magic method
D107 Missing docstring in __init__

* Disable ANN102 (Missing type annotation for cls in classmethod)

* Use poetry-up as documentation title

* Fix access of PullRequest.required

* Fix Action method name {__run__ => __call__}

* Eliminate redundant stubs for console test

* Remove redundant pass in Action.__call__

* Add package fixture

* Use package fixture in poetry tests

* Add tests for update module

* Update API documentation
  • Loading branch information
cjolowicz committed Apr 10, 2020
1 parent 5aec142 commit edbbb00
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
select = ANN,B,B9,BLK,C,D,DAR,E,F,I,N,RST,S,W
ignore = ANN101,E203,E501,W503
ignore = ANN101,ANN102,E203,E501,W503
max-line-length = 80
max-complexity = 10
application-import-names = poetry_up,tests
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime


project = "Poetry Up"
project = "poetry-up"
author = "Claudio Jolowicz"
copyright = f"{datetime.now().year}, {author}"
extensions = [
Expand Down
7 changes: 7 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ poetry_up.poetry

.. automodule:: poetry_up.poetry
:members:


poetry_up.update
----------------

.. automodule:: poetry_up.update
:members:
80 changes: 14 additions & 66 deletions src/poetry_up/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,7 @@

import click

from . import __version__, git, github, poetry


program_name = "poetry-up"


def show_package(package: poetry.Package) -> None:
"""Print message describing the package upgrade."""
message = "{}: {} → {}".format(
click.style(package.name, fg="bright_green"),
click.style(package.old_version, fg="blue"),
click.style(package.new_version, fg="red"),
)
click.echo(message)
from . import __version__, update


@click.command() # noqa: C901
Expand Down Expand Up @@ -78,55 +65,16 @@ def main( # noqa: C901
if cwd is not None:
os.chdir(cwd)

if not git.is_clean():
raise click.ClickException("Working tree is not clean")

switch = commit or push or pull_request
original_branch = git.current_branch()

for package in poetry.show_outdated():
if packages and package.name not in packages:
continue

show_package(package)

if dry_run:
continue

branch = f"{program_name}/{package.name}-{package.new_version}"
title = f"Upgrade to {package.name} {package.new_version}"
description = (
f"Upgrade {package.name} from {package.old_version}"
f" to {package.new_version}"
)
message = f"{title}\n\n{description}\n"

if switch:
git.switch(branch, create=not git.branch_exists(branch), location=upstream)

poetry.update(package.name, lock=not install)

if not git.is_clean(["pyproject.toml", "poetry.lock"]) and commit:
git.add(["pyproject.toml", "poetry.lock"])
git.commit(message=message)

if switch and git.resolve_branch(branch) == git.resolve_branch(upstream):
click.echo(
f"Skipping {package.name} {package.new_version}"
" (Poetry refused upgrade)"
)

git.switch(original_branch)
git.remove_branch(branch)

continue

if push:
mr = git.MergeRequest(title, description) if merge_request else None
git.push(remote, branch, merge_request=mr)

if pull_request and not github.pull_request_exists(branch):
github.create_pull_request(title, description)

if original_branch != git.current_branch():
git.switch(original_branch)
options = update.Options(
install,
commit,
push,
merge_request,
pull_request,
upstream,
remote,
dry_run,
packages,
)
updater = update.Updater(options)
updater.run()
249 changes: 249 additions & 0 deletions src/poetry_up/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
"""Update module."""
from dataclasses import dataclass
from typing import Tuple

import click

from . import git, github, poetry


program_name = "poetry-up"


@dataclass
class Options:
"""Options for the update operation."""

install: bool
commit: bool
push: bool
merge_request: bool
pull_request: bool
upstream: str
remote: str
dry_run: bool
packages: Tuple[str, ...]


class Action:
"""Base class for actions."""

def __init__(self, updater: "PackageUpdater") -> None:
"""Constructor."""
self.updater = updater

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return True

def __call__(self) -> None:
"""Run the action."""


class Switch(Action):
"""Switch to the update branch."""

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return (
self.updater.options.commit
or self.updater.options.push
or self.updater.options.pull_request
)

def __call__(self) -> None:
"""Run the action."""
git.switch(
self.updater.branch,
create=not git.branch_exists(self.updater.branch),
location=self.updater.options.upstream,
)


class Update(Action):
"""Update the package using Poetry."""

def __call__(self) -> None:
"""Run the action."""
poetry.update(self.updater.package.name, lock=not self.updater.options.install)


class Commit(Action):
"""Create a Git commit for the update."""

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return self.updater.options.commit and not git.is_clean(
["pyproject.toml", "poetry.lock"]
)

def __call__(self) -> None:
"""Run the action."""
git.add(["pyproject.toml", "poetry.lock"])
git.commit(message=f"{self.updater.title}\n\n{self.updater.description}\n")


class Rollback(Action):
"""Rollback an attempted package update."""

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return self.updater.actions.switch.required and (
git.resolve_branch(self.updater.branch)
== git.resolve_branch(self.updater.options.upstream)
)

def __call__(self) -> None:
"""Run the action."""
click.echo(
f"Skipping {self.updater.package.name} {self.updater.package.new_version}"
" (Poetry refused upgrade)"
)

git.switch(self.updater.original_branch)
git.remove_branch(self.updater.branch)


class Push(Action):
"""Push the update branch to the remote repository."""

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return self.updater.options.push

def __call__(self) -> None:
"""Run the action."""
merge_request = (
git.MergeRequest(self.updater.title, self.updater.description)
if self.updater.options.merge_request
else None
)
git.push(
self.updater.options.remote,
self.updater.branch,
merge_request=merge_request,
)


class PullRequest(Action):
"""Open a pull request for the update branch."""

@property
def required(self) -> bool:
"""Return True if the action needs to run."""
return self.updater.options.pull_request and not github.pull_request_exists(
self.updater.branch
)

def __call__(self) -> None:
"""Run the action."""
github.create_pull_request(self.updater.title, self.updater.description)


@dataclass
class Actions:
"""Actions for a package update."""

switch: Switch
update: Update
commit: Commit
rollback: Rollback
push: Push
pull_request: PullRequest

@classmethod
def create(cls, updater: "PackageUpdater") -> "Actions":
"""Create the package update actions."""
return cls(
Switch(updater),
Update(updater),
Commit(updater),
Rollback(updater),
Push(updater),
PullRequest(updater),
)


class PackageUpdater:
"""Update a package."""

def __init__(
self, package: poetry.Package, options: Options, original_branch: str
) -> None:
"""Constructor."""
self.package = package
self.options = options
self.original_branch = original_branch

self.branch = f"{program_name}/{package.name}-{package.new_version}"
self.title = f"Upgrade to {package.name} {package.new_version}"
self.description = (
f"Upgrade {package.name} from {package.old_version}"
f" to {package.new_version}"
)

self.actions = Actions.create(self)

@property
def required(self) -> bool:
"""Return True if the package needs to be updated."""
return not self.options.packages or self.package.name in self.options.packages

def run(self) -> None:
"""Run the package update."""
if self.actions.switch.required:
self.actions.switch()

self.actions.update()

if self.actions.commit.required:
self.actions.commit()

if self.actions.rollback.required:
self.actions.rollback()
return

if self.actions.push.required:
self.actions.push()

if self.actions.pull_request.required:
self.actions.pull_request()

def show(self) -> None:
"""Print information about the package update."""
message = "{}: {} → {}".format(
click.style(self.package.name, fg="bright_green"),
click.style(self.package.old_version, fg="blue"),
click.style(self.package.new_version, fg="red"),
)
click.echo(message)


class Updater:
"""Update packages."""

def __init__(self, options: Options) -> None:
"""Constructor."""
self.options = options

def run(self) -> None:
"""Run the package updates."""
if not git.is_clean():
raise click.ClickException("Working tree is not clean")

original_branch = git.current_branch()

for package in poetry.show_outdated():
updater = PackageUpdater(package, self.options, original_branch)
if updater.required:
updater.show()
if not self.options.dry_run:
updater.run()

if original_branch != git.current_branch():
git.switch(original_branch)
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import pytest

from poetry_up import poetry


@contextlib.contextmanager
def working_directory(directory: Path) -> Iterator[None]:
Expand Down Expand Up @@ -44,3 +46,9 @@ def repository(shared_datadir: Path, tmp_path: Path) -> Iterator[Path]:
)
with working_directory(local_repository):
yield local_repository


@pytest.fixture
def package() -> poetry.Package:
"""Package to be upgraded."""
return poetry.Package("marshmallow", "3.0.0", "3.5.1")
3 changes: 0 additions & 3 deletions tests/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,6 @@ def test_main_removes_branch_on_refused_upgrade(
repository: Path,
stub_poetry_show_outdated: None,
stub_poetry_update_noop: None,
stub_git_push: None,
stub_pull_request_exists: None,
stub_create_pull_request: None,
) -> None:
"""It removes the branch if the upgrade was refused."""
runner.invoke(console.main)
Expand Down

0 comments on commit edbbb00

Please sign in to comment.