1
0
mirror of https://github.com/MousaZeidBaker/poetry-plugin-up.git synced 2025-10-05 21:32:40 +02:00

feat: initial commit

This commit is contained in:
Mousa Zeid Baker
2022-12-13 12:18:52 +01:00
commit 0a48f00b67
22 changed files with 3047 additions and 0 deletions

77
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,77 @@
# Workflow to release our package
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Release
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
steps:
- name: Release
id: release
uses: google-github-actions/release-please-action@v3
with:
# https://github.com/google-github-actions/release-please-action#configuration
release-type: python
changelog-types: >
[
{"type": "build", "section": "🏗️ Build System", "hidden": true},
{"type": "chore", "section": "🧹 Miscellaneous Chores", "hidden": true},
{"type": "ci", "section": "👷 Continuous Integration", "hidden": true},
{"type": "docs", "section": "📝 Documentation"},
{"type": "feat", "section": "🚀 Features"},
{"type": "fix", "section": "🐛 Bug Fixes"},
{"type": "perf", "section": "⚡ Performance Improvements"},
{"type": "refactor", "section": "♻️ Code Refactoring", "hidden": true},
{"type": "revert", "section": "⏪️ Reverts"},
{"type": "style", "section": "💄 Styles", "hidden": true},
{"type": "test", "section": "✅ Tests", "hidden": true}
]
include-v-in-tag: false
publish:
name: Publish
runs-on: ubuntu-latest
needs: release
# only run when a new release is created
if: ${{ needs.release.outputs.release_created }}
strategy:
fail-fast: true
matrix:
environment: [ testpypi, pypi ]
environment: ${{ matrix.environment }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 1.2.1
- name: Build
run: poetry build
- name: Publish testpypi
if: ${{ matrix.environment=='testpypi' }}
env:
POETRY_REPOSITORIES_TESTPYPI_URL: https://test.pypi.org/legacy/
POETRY_HTTP_BASIC_TESTPYPI_USERNAME: __token__
POETRY_HTTP_BASIC_TESTPYPI_PASSWORD: ${{secrets.TESTPYPI_API_TOKEN}}
run: poetry publish --repository testpypi
- name: Publish pypi
if: ${{ matrix.environment=='pypi' }}
env:
POETRY_HTTP_BASIC_PYPI_USERNAME: __token__
POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{secrets.PYPI_API_TOKEN}}
run: poetry publish

50
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,50 @@
# Workflow to test our package
name: Test
on:
pull_request:
branches:
- '*'
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 1.3.0
- name: Install dependencies
run: poetry install
- name: Lint
run: |
source $(poetry env info --path)/bin/activate
pre-commit install --install-hooks
pre-commit run --all-files
pre-commit run commitizen-branch --hook-stage push
- name: Test
run: |
source $(poetry env info --path)/bin/activate
pytest tests -vv
poetry up --help
- name: Build
run: poetry build

132
.gitignore vendored Normal file
View File

@@ -0,0 +1,132 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IntelliJs project specific settings files
.idea

46
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,46 @@
# https://pre-commit.com/#pre-commit-configyaml---top-level
default_stages:
- commit
default_install_hook_types:
- pre-commit
- commit-msg
- pre-push
exclude: pypoetry
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace # https://github.com/pre-commit/pre-commit-hooks/tree/v4.3.0#trailing-whitespace
- id: end-of-file-fixer # https://github.com/pre-commit/pre-commit-hooks/tree/v4.3.0#end-of-file-fixer
- id: check-json # https://github.com/pre-commit/pre-commit-hooks/tree/v4.3.0#check-json
- id: check-yaml # https://github.com/pre-commit/pre-commit-hooks/tree/v4.3.0#check-yaml
- id: check-toml # https://github.com/pre-commit/pre-commit-hooks/tree/v4.3.0#check-toml
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies:
- flake8-black
- flake8-isort
- repo: https://github.com/commitizen-tools/commitizen
rev: v2.37.1
hooks:
- id: commitizen
stages:
- commit-msg
- id: commitizen-branch
stages:
- push
args:
- --rev-range
- origin/master..HEAD
always_run: true
pass_filenames: false

111
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,111 @@
# Contributing
Contributions are welcome via pull requests. Please make sure to install git
hooks which enforces certain rules and linting.
## Getting started
Install dependencies & activate virtual env
```shell
poetry install --sync && poetry shell
```
Install git hooks
```shell
pre-commit install --install-hooks --overwrite
```
Run pre-commit hooks against all files
```shell
pre-commit run --all-files
```
Run tests
```shell
pytest tests
```
Install current project from branch
```shell
poetry add git+https://github.com/MousaZeidBaker/poetry-plugin-up.git#branch-name
```
## Commit message
Commit messages **MUST** follow [Conventional
Commits](https://www.conventionalcommits.org/) specification.
```
<type>(<scope>): <description>
│ │ │
│ │ └─ Description: Short summary in present tense. Not capitalized. No period at the end.
│ │
│ └─ Scope: Optional contextual information
└─ Type: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test
[optional body]
[optional footer(s)]
```
**Commit type** must be one of following:
- **build**: Changes that affect the build system or external dependencies
- **chore**: Other changes that don't modify src or test files
- **ci**: Changes to our CI configuration files and scripts
- **docs**: Documentation only changes
- **feat**: A new feature
- **fix**: A bug fix
- **perf**: A code change that improves performance
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **revert**: Reverts a previous commit
- **style**: Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, etc)
- **test**: Adding missing tests or correcting existing tests
### Automated releases
A fully automated release process is implemented using [Release
Please](https://github.com/googleapis/release-please). The **commit type**
determines the next [semantic version](https://semver.org/), see following
examples:
- `fix:` represents a bug fix which correlates with a PATCH bump
- `feat:` represents a new feature which correlates with a MINOR bump
- `feat!:`, or `fix!:`, `refactor!:`, etc., represents a breaking change
(indicated by the `!`) which correlates with a MAJOR bump
One can manually set the version number by adding `Release-As: x.y.z` to the
**commit body**, but this should not be needed.
### How to change a commit message?
Amend the most recent commit
```shell
git commit --amend -m "fix: new message"
```
Force push the changes if already pushed to remote
```shell
git push --force-with-lease origin EXAMPLE-BRANCH
```
Amend older or multiple commits with interactive rebase
- use the `git rebase -i HEAD~N` command to display a list of the last `N`
commits in your default text editor
- replace `pick` with `reword` for each commit message that needs to be changed
- save the changes and close the editor
- for each chosen commit, a new editor will open, change the commit message,
save the file, and close the editor
- force push the changes, if already pushed to remote, with `git push
--force-with-lease origin EXAMPLE-BRANCH`

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Mousa Zeid Baker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# Poetry Plugin: up
![release](https://github.com/MousaZeidBaker/poetry-plugin-up/actions/workflows/release.yaml/badge.svg)
[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
![python_version](https://img.shields.io/badge/Python-%3E=3.7-blue)
![poetry_version](https://img.shields.io/badge/Poetry-%3E=1.2-blue)
This package is a plugin that updates dependencies and bumps their versions in
`pyproject.toml` file. The version constraints are respected, unless the
`--latest` flag is passed, in which case dependencies are updated to latest
available compatible versions.
This plugin provides similar features as the existing `update` command with
additional features.
## Installation
The easiest way to install the `up` plugin is via the `self add` command of
Poetry.
```shell
poetry self add poetry-plugin-up
```
If you used `pipx` to install Poetry you can add the plugin via the `pipx
inject` command.
```shell
pipx inject poetry poetry-plugin-up
```
Otherwise, if you used `pip` to install Poetry you can add the plugin packages
via the `pip install` command.
```shell
pip install poetry-plugin-up
```
## Usage
The plugin provides an `up` command to update dependencies
```shell
poetry up --help
```
Update dependencies
```shell
poetry up
```
Update dependencies to latest available compatible versions
```shell
poetry up --latest
```
Update the `foo` and `bar` packages
```shell
poetry up foo bar
```
Update packages only in the `main` group
```shell
poetry up --only main
```
Update packages but ignore the `dev` group
```shell
poetry up --without dev
```
## Contributing
Contributions are welcome! See the [Contributing Guide](https://github.com/MousaZeidBaker/poetry-plugin-up/blob/master/CONTRIBUTING.md).
## Issues
If you encounter any problems, please file an
[issue](https://github.com/MousaZeidBaker/poetry-plugin-up/issues) along with a
detailed description.

1444
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

48
pyproject.toml Normal file
View File

@@ -0,0 +1,48 @@
[tool.poetry]
name = "poetry-plugin-up"
version = "0.0.0"
description = "Poetry plugin that updates dependencies and bumps their versions in pyproject.toml file"
authors = ["Mousa Zeid Baker"]
packages = [
{ include = "poetry_plugin_up", from = "src" },
]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/MousaZeidBaker/poetry-plugin-up"
repository = "https://github.com/MousaZeidBaker/poetry-plugin-up"
keywords=[
"packaging",
"dependency",
"poetry",
"update",
"upgrade",
]
classifiers=[
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
include = ["LICENSE"]
[tool.poetry.dependencies]
python = "^3.7"
poetry = "^1.2.0"
[tool.poetry.group.dev.dependencies]
pre-commit = "^2.20.0"
pytest = "^7.2.0"
pytest-mock = "^3.10.0"
[tool.poetry.plugins."poetry.application.plugin"]
up = "poetry_plugin_up.plugin:UpApplicationPlugin"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 80
[tool.isort]
profile = "black"

3
setup.cfg Normal file
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 80
exclude = pypoetry

View File

View File

@@ -0,0 +1,199 @@
from typing import Any, Dict, Iterable, List
from cleo.helpers import argument, option
from poetry.console.commands.installer_command import InstallerCommand
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.packages.package import Package
from poetry.version.version_selector import VersionSelector
from tomlkit import dumps
from tomlkit.toml_document import TOMLDocument
class UpCommand(InstallerCommand):
name = "up"
description = (
"Update dependencies and bump versions in <comment>pyproject.toml</>"
)
arguments = [
argument(
name="packages",
description="The packages to update.",
optional=True,
multiple=True,
)
]
options = [
*InstallerCommand._group_dependency_options(),
option(
long_name="latest",
short_name=None,
description="Update to latest available compatible versions.",
),
option(
long_name="no-install",
short_name=None,
description="Do not install dependencies, only refresh "
"<comment>pyproject.toml</> and <comment>poetry.lock</>.",
),
option(
long_name="dry-run",
short_name=None,
description="Output bumped <comment>pyproject.toml</> but do not "
"execute anything.",
),
]
def handle(self) -> int:
only_packages = self.argument("packages")
latest = self.option("latest")
no_install = self.option("no-install")
dry_run = self.option("dry-run")
selector = VersionSelector(self.poetry.pool)
pyproject_content = self.poetry.file.read()
for group in self.get_groups():
for dependency in group.dependencies:
self.handle_dependency(
dependency=dependency,
latest=latest,
only_packages=only_packages,
pyproject_content=pyproject_content,
selector=selector,
)
if dry_run:
self.line(dumps(pyproject_content))
return 0
# write new content to pyproject.toml
self.poetry.file.write(pyproject_content)
if no_install:
# update lock file
self.call(name="lock", args="--no-update")
return 0
# update dependencies
self.call(name="update")
return 0
def get_groups(self) -> Iterable[DependencyGroup]:
"""Returns activated dependency groups"""
for group in self.activated_groups:
yield self.poetry.package.dependency_group(group)
def handle_dependency(
self,
dependency: Dependency,
latest: bool,
only_packages: List[str],
pyproject_content: TOMLDocument,
selector: VersionSelector,
) -> None:
"""Handles a dependency"""
if not self.is_bumpable(dependency, only_packages, latest):
return
target_package_version = dependency.pretty_constraint
if latest:
target_package_version = "*"
candidate: Package = selector.find_best_candidate(
package_name=dependency.name,
target_package_version=target_package_version,
allow_prereleases=dependency.allows_prereleases(),
source=dependency.source_name,
)
if candidate is None:
self.line(f"No new version for '{dependency.name}'")
return
new_version = "^" + candidate.pretty_version
if not latest:
if dependency.pretty_constraint[0] == "~":
new_version = "~" + candidate.pretty_version
elif dependency.pretty_constraint[:2] == ">=":
new_version = ">=" + candidate.pretty_version
self.bump_version_in_pyproject_content(
dependency=dependency,
new_version=new_version,
pyproject_content=pyproject_content,
)
@staticmethod
def is_bumpable(
dependency: Dependency,
only_packages: List[str],
latest: bool,
) -> bool:
"""Determines if a dependency can be bumped in pyproject.toml"""
if dependency.source_type in ["git", "file", "directory"]:
return False
if dependency.name in ["python"]:
return False
if only_packages and dependency.name not in only_packages:
return False
if not latest:
constraint = dependency.pretty_constraint
if constraint[0].isdigit():
# pinned
return False
if constraint[0] == "*":
# wildcard
return False
if constraint[0] == "<" and constraint[1].isdigit():
# less than
return False
if constraint[0] == ">" and constraint[1].isdigit():
# greater than
return False
if constraint[:2] == "<=":
# less than or equal to
return False
if constraint[:2] == "!=":
# inequality
return False
if len(constraint.split(",")) > 1:
# multiple requirements e.g. '>=1.0.0, <2.0.0'
return False
return True
@staticmethod
def bump_version_in_pyproject_content(
dependency: Dependency,
new_version: str,
pyproject_content: TOMLDocument,
) -> None:
"""Bumps versions in pyproject content (pyproject.toml)"""
poetry_content: Dict[str, Any] = pyproject_content["tool"]["poetry"]
for group in dependency.groups:
# find section to modify
section = {}
if group == "main":
section = poetry_content.get("dependencies", {})
elif group == "dev" and "dev-dependencies" in poetry_content:
# take account for the old `dev-dependencies` section
section = poetry_content.get("dev-dependencies", {})
else:
section = (
poetry_content.get("group", {})
.get(group, {})
.get("dependencies", {})
)
# modify section
if isinstance(section.get(dependency.pretty_name), str):
section[dependency.pretty_name] = new_version
elif "version" in section.get(dependency.pretty_name, {}):
section[dependency.pretty_name]["version"] = new_version

View File

@@ -0,0 +1,12 @@
from poetry.plugins.application_plugin import ApplicationPlugin
from poetry_plugin_up.command import UpCommand
def factory():
return UpCommand()
class UpApplicationPlugin(ApplicationPlugin):
def activate(self, application):
application.command_loader.register_factory("up", factory)

0
tests/__init__.py Normal file
View File

88
tests/conftest.py Normal file
View File

@@ -0,0 +1,88 @@
from pathlib import Path
from typing import List
import pytest
from cleo.testers.application_tester import ApplicationTester
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.packages.package import Package
from poetry.core.pyproject.toml import PyProjectTOML
from poetry.factory import Factory
from poetry.poetry import Poetry
from pytest import TempPathFactory
from tomlkit.toml_document import TOMLDocument
from tests.helpers import TestApplication, TestUpCommand
@pytest.fixture(scope="function")
def project_path() -> Path:
return Path(__file__).parent / "fixtures" / "simple_project"
@pytest.fixture(scope="function")
def pyproject_content(project_path: Path) -> TOMLDocument:
path = project_path / "pyproject.toml"
return PyProjectTOML(path).file.read()
@pytest.fixture(scope="function")
def expected_pyproject_content(project_path: Path) -> TOMLDocument:
path = project_path / "expected_pyproject.toml"
return PyProjectTOML(path).file.read()
@pytest.fixture(scope="function")
def tmp_pyproject_path(
tmp_path_factory: TempPathFactory,
pyproject_content: TOMLDocument,
) -> Path:
tmp_pyproject_path = (
tmp_path_factory.mktemp("simple_project") / "pyproject.toml"
)
tmp_pyproject_path.write_text(pyproject_content.as_string())
return tmp_pyproject_path
@pytest.fixture(scope="function")
def app_tester(tmp_pyproject_path: Path) -> ApplicationTester:
poetry = Factory().create_poetry(tmp_pyproject_path)
app = TestApplication(poetry)
return ApplicationTester(app)
@pytest.fixture
def poetry(project_path: Path) -> Poetry:
return Factory().create_poetry(project_path)
@pytest.fixture
def up_cmd_tester(poetry: Poetry) -> TestUpCommand:
return TestUpCommand(poetry)
@pytest.fixture
def groups(poetry: Poetry) -> List[DependencyGroup]:
return poetry.package._dependency_groups.values()
@pytest.fixture
def packages() -> List[Package]:
return [
Package(name="foo", version="2.2.2"),
Package(name="bar", version="2.2.2"),
Package(name="baz", version="2.2.2"),
Package(name="corge", version="2.2.2"),
Package(name="grault", version="2.2.2"),
Package(name="garply", version="2.2.2"),
Package(name="waldo", version="2.2.2"),
Package(name="fred", version="2.2.2"),
Package(name="plugh", version="2.2.2"),
Package(name="xyzzy", version="2.2.2"),
Package(name="nacho", version="2.2.2"),
Package(name="thud", version="2.2.2"),
Package(name="foobar", version="2.2.2"),
Package(name="foobaz", version="2.2.2"),
Package(name="fooqux", version="2.2.2"),
Package(name="fooquux", version="2.2.2"),
Package(name="foo-corge", version="2.2.2"),
]

102
tests/e2e/test_e2e.py Normal file
View File

@@ -0,0 +1,102 @@
from pathlib import Path
from typing import List
from cleo.testers.application_tester import ApplicationTester
from poetry.core.packages.package import Package
from poetry.core.pyproject.toml import PyProjectTOML
from pytest_mock import MockerFixture
def test_command(
app_tester: ApplicationTester,
packages: List[Package],
mocker: MockerFixture,
project_path: Path,
tmp_pyproject_path: Path,
) -> None:
command_call = mocker.patch(
"poetry.console.commands.command.Command.call",
return_value=0,
)
mocker.patch(
"poetry.version.version_selector.VersionSelector.find_best_candidate",
side_effect=packages,
)
path = project_path / "expected_pyproject.toml"
expected = PyProjectTOML(path).file.read()
assert app_tester.execute("up") == 0
assert PyProjectTOML(tmp_pyproject_path).file.read() == expected
command_call.assert_called_once_with(name="update")
def test_command_with_latest(
app_tester: ApplicationTester,
packages: List[Package],
mocker: MockerFixture,
project_path: Path,
tmp_pyproject_path: Path,
) -> None:
command_call = mocker.patch(
"poetry.console.commands.command.Command.call",
return_value=0,
)
mocker.patch(
"poetry.version.version_selector.VersionSelector.find_best_candidate",
side_effect=packages,
)
path = project_path / "expected_pyproject_with_latest.toml"
expected = PyProjectTOML(path).file.read()
assert app_tester.execute("up --latest") == 0
assert PyProjectTOML(tmp_pyproject_path).file.read() == expected
command_call.assert_called_once_with(name="update")
def test_command_with_dry_run(
app_tester: ApplicationTester,
packages: List[Package],
mocker: MockerFixture,
tmp_pyproject_path: Path,
) -> None:
command_call = mocker.patch(
"poetry.console.commands.command.Command.call",
return_value=0,
)
mocker.patch(
"poetry.version.version_selector.VersionSelector.find_best_candidate",
side_effect=packages,
)
expected = PyProjectTOML(tmp_pyproject_path).file.read()
assert app_tester.execute("up --dry-run") == 0
# assert pyproject.toml file not modified
assert PyProjectTOML(tmp_pyproject_path).file.read() == expected
command_call.assert_not_called()
def test_command_with_no_install(
app_tester: ApplicationTester,
packages: List[Package],
mocker: MockerFixture,
project_path: Path,
tmp_pyproject_path: Path,
) -> None:
command_call = mocker.patch(
"poetry.console.commands.command.Command.call",
return_value=0,
)
mocker.patch(
"poetry.version.version_selector.VersionSelector.find_best_candidate",
side_effect=packages,
)
path = project_path / "expected_pyproject.toml"
expected = PyProjectTOML(path).file.read()
assert app_tester.execute("up --no-install") == 0
assert PyProjectTOML(tmp_pyproject_path).file.read() == expected
command_call.assert_called_once_with(name="lock", args="--no-update")

View File

@@ -0,0 +1,30 @@
[tool.poetry]
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = ["Mousa Zeid Baker"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.7"
foo = "^2.2.2"
bar = "^2.2.2"
baz = {version = "^2.2.2", extras = ["qux", "quux"]}
corge = {version = "^2.2.2", optional = true}
grault = {version = "^2.2.2", allow-prereleases = true}
garply = {path = "./"}
waldo = {git = "https://example.com/test/project.git"}
[tool.poetry.group.dev.dependencies]
fred = "1.1.1"
plugh = "^2.2.2"
xyzzy = "~2.2.2"
nacho = "<1.1.1"
thud = ">1.1.1"
[tool.poetry.group.docs.dependencies]
foobar = "<=1.1.1"
foobaz = ">=2.2.2"
fooqux = "!=1.1.1"
fooquux = "*"
Foo_Corge = "^2.2.2"

View File

@@ -0,0 +1,30 @@
[tool.poetry]
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = ["Mousa Zeid Baker"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.7"
foo = "^2.2.2"
bar = "^2.2.2"
baz = {version = "^2.2.2", extras = ["qux", "quux"]}
corge = {version = "^2.2.2", optional = true}
grault = {version = "^2.2.2", allow-prereleases = true}
garply = {path = "./"}
waldo = {git = "https://example.com/test/project.git"}
[tool.poetry.group.dev.dependencies]
fred = "^2.2.2"
plugh = "^2.2.2"
xyzzy = "^2.2.2"
nacho = "^2.2.2"
thud = "^2.2.2"
[tool.poetry.group.docs.dependencies]
foobar = "^2.2.2"
foobaz = "^2.2.2"
fooqux = "^2.2.2"
fooquux = "^2.2.2"
Foo_Corge = "^2.2.2"

View File

@@ -0,0 +1,30 @@
[tool.poetry]
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = ["Mousa Zeid Baker"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.7"
foo = "^1.1.1"
bar = "^1.1.1"
baz = {version = "^1.1.1", extras = ["qux", "quux"]}
corge = {version = "^1.1.1", optional = true}
grault = {version = "^1.1.1", allow-prereleases = true}
garply = {path = "./"}
waldo = {git = "https://example.com/test/project.git"}
[tool.poetry.group.dev.dependencies]
fred = "1.1.1"
plugh = "^1.1.1"
xyzzy = "~1.1.1"
nacho = "<1.1.1"
thud = ">1.1.1"
[tool.poetry.group.docs.dependencies]
foobar = "<=1.1.1"
foobaz = ">=1.1.1"
fooqux = "!=1.1.1"
fooquux = "*"
Foo_Corge = "^1.1.1"

23
tests/helpers.py Normal file
View File

@@ -0,0 +1,23 @@
from typing import Any
from poetry.console.application import Application
from poetry.poetry import Poetry
from poetry_plugin_up.command import UpCommand
class TestUpCommand(UpCommand):
def __init__(self, poetry: Poetry) -> None:
super().__init__()
self._poetry = poetry
__test__ = False
def line(self, data: Any):
print(data)
class TestApplication(Application):
def __init__(self, poetry: Poetry) -> None:
super().__init__()
self._poetry = poetry

View File

@@ -0,0 +1,98 @@
from unittest.mock import Mock
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.package import Package
from pytest_mock import MockerFixture
from tomlkit import parse
from tests.helpers import TestUpCommand
def test_handle_dependency(
up_cmd_tester: TestUpCommand,
mocker: MockerFixture,
) -> None:
dependency = Dependency(
name="foo",
constraint="^1.0",
groups=["main"],
)
new_version = "2.0.0"
package = Package(
name=dependency.name,
version=new_version,
)
content = parse("")
selector = Mock()
selector.find_best_candidate = Mock(return_value=package)
bump_version_in_pyproject_content = mocker.patch(
"poetry_plugin_up.command.UpCommand.bump_version_in_pyproject_content",
return_value=None,
)
up_cmd_tester.handle_dependency(
dependency=dependency,
latest=False,
only_packages=[],
pyproject_content=content,
selector=selector,
)
selector.find_best_candidate.assert_called_once_with(
package_name=dependency.name,
target_package_version=dependency.pretty_constraint,
allow_prereleases=dependency.allows_prereleases(),
source=dependency.source_name,
)
bump_version_in_pyproject_content.assert_called_once_with(
dependency=dependency,
new_version=f"^{new_version}",
pyproject_content=content,
)
def test_handle_dependency_with_latest(
up_cmd_tester: TestUpCommand,
mocker: MockerFixture,
) -> None:
dependency = Dependency(
name="foo",
constraint="^1.0",
groups=["main"],
)
new_version = "2.0.0"
package = Package(
name=dependency.name,
version=new_version,
)
content = parse("")
selector = Mock()
selector.find_best_candidate = Mock(return_value=package)
bump_version_in_pyproject_content = mocker.patch(
"poetry_plugin_up.command.UpCommand.bump_version_in_pyproject_content",
return_value=None,
)
up_cmd_tester.handle_dependency(
dependency=dependency,
latest=True,
only_packages=[],
pyproject_content=content,
selector=selector,
)
selector.find_best_candidate.assert_called_once_with(
package_name=dependency.name,
target_package_version="*",
allow_prereleases=dependency.allows_prereleases(),
source=dependency.source_name,
)
bump_version_in_pyproject_content.assert_called_once_with(
dependency=dependency,
new_version=f"^{new_version}",
pyproject_content=content,
)

414
tests/unit/test_unit.py Normal file
View File

@@ -0,0 +1,414 @@
from poetry.core.packages.dependency import Dependency
from tomlkit import parse
from tests.helpers import TestUpCommand
def test_bump_version_in_pyproject_content(
up_cmd_tester: TestUpCommand,
) -> None:
dependencies = [
Dependency(
name="foo",
constraint="^1.0",
groups=["main"],
),
Dependency(
name="bar",
constraint="^1.0",
groups=["main"],
optional=True,
),
Dependency(
name="baz",
constraint="^1.0",
groups=["dev"],
),
]
content = parse(
"""
[tool.poetry.dependencies]
python = "^3.7"
foo = "^1.0"
bar = { version = "^1.1", optional = true }
[tool.poetry.group.dev.dependencies]
baz = "^1.2"
"""
)
for dependency in dependencies:
new_version = "^1.9"
up_cmd_tester.bump_version_in_pyproject_content(
dependency=dependency,
new_version=new_version,
pyproject_content=content,
)
poetry_content = content["tool"]["poetry"]
assert poetry_content["dependencies"]["foo"] == new_version
assert poetry_content["dependencies"]["bar"]["version"] == new_version
assert poetry_content["group"]["dev"]["dependencies"]["baz"] == new_version
def test_bump_version_in_pyproject_content_with_old_dev_dependencies(
up_cmd_tester: TestUpCommand,
) -> None:
dependencies = [
Dependency(
name="foo",
constraint="^1.0",
groups=["main"],
),
Dependency(
name="bar",
constraint="^1.0",
groups=["main"],
optional=True,
),
Dependency(
name="baz",
constraint="^1.0",
groups=["dev"],
),
]
content = parse(
"""
[tool.poetry.dependencies]
python = "^3.7"
foo = "^1.0"
bar = { version = "^1.1", optional = true }
[tool.poetry.dev-dependencies]
baz = "^1.2"
"""
)
for dependency in dependencies:
new_version = "^1.9"
up_cmd_tester.bump_version_in_pyproject_content(
dependency=dependency,
new_version=new_version,
pyproject_content=content,
)
poetry_content = content["tool"]["poetry"]
assert poetry_content["dependencies"]["foo"] == new_version
assert poetry_content["dependencies"]["bar"]["version"] == new_version
assert poetry_content["dev-dependencies"]["baz"] == new_version
def test_is_bumpable_is_false_when_source_type_is_git(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="*",
source_type="git",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_source_type_is_file(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="*",
source_type="file",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_source_type_is_directory(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="*",
source_type="directory",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_name_is_python(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(name="python", constraint="^1.2.3")
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_dependency_not_in_only_packages(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(name="foo", constraint="^1.2.3")
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=["bar"],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_pinned(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_wildcard(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="*",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_less_than(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="<1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_greater_than(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint=">1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_less_than_or_equal(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="<=1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_inequality(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="!=1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_false_when_version_multiple_requirements(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint=">=1.2.3, <2.0.0",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is False
def test_is_bumpable_is_true_when_version_caret(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="^1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_tilde(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="~1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_greater_than_or_equal(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint=">=1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_tilde_pep440(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="~=1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=False,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_pinned_and_latest(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=True,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_wildcard_and_latest(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="*",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=True,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_less_than_and_latest(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="<1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=True,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_greater_than_and_latest(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint=">1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=True,
)
assert is_bumpable is True
def test_is_bumpable_is_true_when_version_less_than_or_equal_and_latest(
up_cmd_tester: TestUpCommand,
) -> None:
dependency = Dependency(
name="foo",
constraint="<=1.2.3",
)
is_bumpable = up_cmd_tester.is_bumpable(
dependency=dependency,
only_packages=[],
latest=True,
)
assert is_bumpable is True