From 32f64c692f52e660294dd7bde9de7801aa67d0a8 Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 11:54:15 +0200 Subject: [PATCH 1/6] feat: add name & group flags to include specific dependencies --- src/poetryup/core/pyproject.py | 12 ++++- src/poetryup/main.py | 11 +++- tests/unit/test_pyproject.py | 93 ++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/poetryup/core/pyproject.py b/src/poetryup/core/pyproject.py index 896b3f1..b2642fb 100644 --- a/src/poetryup/core/pyproject.py +++ b/src/poetryup/core/pyproject.py @@ -205,12 +205,16 @@ def update_dependencies( self, latest: bool = False, skip_exact: bool = False, + name: List[str] = [], + group: List[str] = [], ) -> None: """Update dependencies and bump their version in pyproject Args: latest: Whether to update dependencies to their latest version skip_exact: Whether to skip dependencies with an exact version + name: The dependency names to include + group: The dependency groups to include """ if latest: @@ -221,7 +225,13 @@ def update_dependencies( groups = {} for dependency in self.dependencies: if skip_exact and dependency.constraint == Constraint.EXACT: - # skip dependencies with an exact version + # skip dep with an exact version + continue + if name and dependency.name not in name: + # skip dep whom name is NOT in the provided name list + continue + if group and dependency.group not in group: + # skip dep whom group is NOT in the provided group list continue if isinstance(dependency.version, str): groups[dependency.group] = groups.get( diff --git a/src/poetryup/main.py b/src/poetryup/main.py index cbf2892..7867505 100644 --- a/src/poetryup/main.py +++ b/src/poetryup/main.py @@ -4,6 +4,7 @@ import os import subprocess from pathlib import Path +from typing import List import typer @@ -24,6 +25,14 @@ def poetryup( default=False, help="Whether to skip dependencies with an exact version.", ), + name: List[str] = typer.Option( + default=[], + help="The dependency names to include.", + ), + group: List[str] = typer.Option( + default=[], + help="The dependency groups to include.", + ), ): """Update dependencies and bump their version in pyproject.toml file""" @@ -35,7 +44,7 @@ def poetryup( ) pyproject = Pyproject(pyproject_str) - pyproject.update_dependencies(latest, skip_exact) + pyproject.update_dependencies(latest, skip_exact, name, group) Path("pyproject.toml").write_text(pyproject.dumps()) # refresh the lock file after changes in pyproject.toml subprocess.run(["poetry", "lock", "--no-update"]) diff --git a/tests/unit/test_pyproject.py b/tests/unit/test_pyproject.py index 0cbb334..1b75c6c 100644 --- a/tests/unit/test_pyproject.py +++ b/tests/unit/test_pyproject.py @@ -96,6 +96,99 @@ def test_update_dependencies_latest( mock.assert_has_calls(calls) +def test_update_dependencies_latest_skip_exact( + mock_poetry_commands, + mocker: MockerFixture, +) -> None: + pyproject = Pyproject(pyproject_str) + mock = mocker.patch.object( + pyproject, + "_Pyproject__run_poetry_add", + return_value=None, + ) + pyproject.update_dependencies(latest=True, skip_exact=True) + + calls = [ + call( + packages=["poetryup@latest"], + group="default", + ), + call( + packages=[ + "poetryup_caret@latest", + "poetryup_tilde@latest", + "poetryup_wildcard@latest", + "poetryup_inequality_greater_than@latest", + "poetryup_inequality_greater_than_or_equal@latest", + "poetryup_inequality_less_than@latest", + "poetryup_inequality_less_than_or_equal@latest", + "poetryup_inequality_not_equal@latest", + # poetryup_exact should not be on the call + "poetryup_multiple_requirements@latest", + "poetryup_underscore@latest", + "Poetryup_Capital@latest", + ], + group="main", + ), + ] + mock.assert_has_calls(calls) + + +def test_update_dependencies_latest_with_specific_group( + mock_poetry_commands, + mocker: MockerFixture, +) -> None: + pyproject = Pyproject(pyproject_str) + mock = mocker.patch.object( + pyproject, + "_Pyproject__run_poetry_add", + return_value=None, + ) + pyproject.update_dependencies(latest=True, group=["main"]) + + calls = [ + call( + packages=[ + "poetryup_caret@latest", + "poetryup_tilde@latest", + "poetryup_wildcard@latest", + "poetryup_inequality_greater_than@latest", + "poetryup_inequality_greater_than_or_equal@latest", + "poetryup_inequality_less_than@latest", + "poetryup_inequality_less_than_or_equal@latest", + "poetryup_inequality_not_equal@latest", + "poetryup_exact@latest", + "poetryup_multiple_requirements@latest", + "poetryup_underscore@latest", + "Poetryup_Capital@latest", + ], + group="main", + ), + ] + mock.assert_has_calls(calls) + + +def test_update_dependencies_latest_with_specific_name( + mock_poetry_commands, + mocker: MockerFixture, +) -> None: + pyproject = Pyproject(pyproject_str) + mock = mocker.patch.object( + pyproject, + "_Pyproject__run_poetry_add", + return_value=None, + ) + pyproject.update_dependencies(latest=True, name=["poetryup"]) + + calls = [ + call( + packages=["poetryup@latest"], + group="default", + ), + ] + mock.assert_has_calls(calls) + + def test_search_dependency( mock_poetry_commands, ) -> None: From 2a38530b981c7ddbb4de43a0b62c27002141dc44 Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 12:03:50 +0200 Subject: [PATCH 2/6] bump project version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a2e840..b152293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetryup" -version = "0.7.2" +version = "0.8.0" description = "Update dependencies and bump their version in the pyproject.toml file" authors = ["Mousa Zeid Baker"] packages = [ From 2c8fc81b16e8190ced38ccb591526ac157f04a74 Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 14:50:00 +0200 Subject: [PATCH 3/6] fix: name & group flags --- src/poetryup/core/pyproject.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/poetryup/core/pyproject.py b/src/poetryup/core/pyproject.py index b2642fb..2b4820f 100644 --- a/src/poetryup/core/pyproject.py +++ b/src/poetryup/core/pyproject.py @@ -250,6 +250,16 @@ def update_dependencies( # bump versions in pyproject table = self.pyproject["tool"]["poetry"] for dependency in self.bumped_dependencies: + if skip_exact and dependency.constraint == Constraint.EXACT: + # skip dep with an exact version + continue + if name and dependency.name not in name: + # skip dep whom name is NOT in the provided name list + continue + if group and dependency.group not in group: + # skip dep whom group is NOT in the provided group list + continue + if dependency.group == "default": table["dependencies"][dependency.name] = dependency.version elif ( From 7df8646eddf79a5e366f953c471f69381cacf99c Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 16:25:32 +0200 Subject: [PATCH 4/6] feat: add filter function --- src/poetryup/core/pyproject.py | 74 ++++++++++++++++-------- src/poetryup/main.py | 4 +- tests/unit/test_pyproject.py | 102 ++++++++++++++++++++++++++++----- 3 files changed, 141 insertions(+), 39 deletions(-) diff --git a/src/poetryup/core/pyproject.py b/src/poetryup/core/pyproject.py index 2b4820f..bbc688b 100644 --- a/src/poetryup/core/pyproject.py +++ b/src/poetryup/core/pyproject.py @@ -201,10 +201,45 @@ def search_dependency( if dependency.name == name or dependency.normalized_name == name: return dependency + def filter_dependencies( + self, + dependencies: List[Dependency], + without_constraint: List[Constraint] = [], + name: List[str] = [], + group: List[str] = [], + ) -> Union[Dependency, None]: + """Search for a dependency by name given a list of dependencies + + Args: + dependencies: A list of dependencies to filter + without_constraint: The dependency constraints to ignore + name: The dependency names to include + group: The dependency groups to include + + Returns: + A list of dependencies + """ + + if without_constraint: + # remove deps whom constraint is in the provided constraint list + dependencies = [ + x + for x in dependencies + if x.constraint not in without_constraint + ] + if name: + # remove deps whom name is NOT in the provided name list + dependencies = [x for x in dependencies if x.name in name] + if group: + # remove deps whom group is NOT in the provided group list + dependencies = [x for x in dependencies if x.group in group] + + return dependencies + def update_dependencies( self, latest: bool = False, - skip_exact: bool = False, + without_constraint: List[Constraint] = [], name: List[str] = [], group: List[str] = [], ) -> None: @@ -212,27 +247,24 @@ def update_dependencies( Args: latest: Whether to update dependencies to their latest version - skip_exact: Whether to skip dependencies with an exact version + without_constraint: The dependency constraints to ignore name: The dependency names to include group: The dependency groups to include """ if latest: logging.info("Updating dependencies to their latest version") + dependencies = self.filter_dependencies( + self.dependencies, + without_constraint, + name, + group, + ) # sort dependencies into their groups and add them at once in order # to avoid version solver error in case dependencies depend on each # other groups = {} - for dependency in self.dependencies: - if skip_exact and dependency.constraint == Constraint.EXACT: - # skip dep with an exact version - continue - if name and dependency.name not in name: - # skip dep whom name is NOT in the provided name list - continue - if group and dependency.group not in group: - # skip dep whom group is NOT in the provided group list - continue + for dependency in dependencies: if isinstance(dependency.version, str): groups[dependency.group] = groups.get( dependency.group, [] @@ -248,18 +280,14 @@ def update_dependencies( self.__run_poetry_update() # bump versions in pyproject + bumped_dependencies = self.filter_dependencies( + self.bumped_dependencies, + without_constraint, + name, + group, + ) table = self.pyproject["tool"]["poetry"] - for dependency in self.bumped_dependencies: - if skip_exact and dependency.constraint == Constraint.EXACT: - # skip dep with an exact version - continue - if name and dependency.name not in name: - # skip dep whom name is NOT in the provided name list - continue - if group and dependency.group not in group: - # skip dep whom group is NOT in the provided group list - continue - + for dependency in bumped_dependencies: if dependency.group == "default": table["dependencies"][dependency.name] = dependency.version elif ( diff --git a/src/poetryup/main.py b/src/poetryup/main.py index 7867505..bcca587 100644 --- a/src/poetryup/main.py +++ b/src/poetryup/main.py @@ -9,6 +9,7 @@ import typer from poetryup.core.pyproject import Pyproject +from poetryup.models.dependency import Constraint logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO").upper()) @@ -44,7 +45,8 @@ def poetryup( ) pyproject = Pyproject(pyproject_str) - pyproject.update_dependencies(latest, skip_exact, name, group) + without_constraint = [Constraint.EXACT] if skip_exact else [] + pyproject.update_dependencies(latest, without_constraint, name, group) Path("pyproject.toml").write_text(pyproject.dumps()) # refresh the lock file after changes in pyproject.toml subprocess.run(["poetry", "lock", "--no-update"]) diff --git a/tests/unit/test_pyproject.py b/tests/unit/test_pyproject.py index 1b75c6c..a915aad 100644 --- a/tests/unit/test_pyproject.py +++ b/tests/unit/test_pyproject.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from poetryup.core.pyproject import Pyproject +from poetryup.models.dependency import Constraint, Dependency pyproject_str = Path( os.path.join( @@ -106,7 +107,10 @@ def test_update_dependencies_latest_skip_exact( "_Pyproject__run_poetry_add", return_value=None, ) - pyproject.update_dependencies(latest=True, skip_exact=True) + pyproject.update_dependencies( + latest=True, + without_constraint=[Constraint.EXACT], + ) calls = [ call( @@ -193,28 +197,24 @@ def test_search_dependency( mock_poetry_commands, ) -> None: pyproject = Pyproject(pyproject_str) - name = pyproject.dependencies[0].name - assert ( - pyproject.search_dependency( - pyproject.dependencies, - name, - ) - is not None + expected = pyproject.dependencies[0] + actual = pyproject.search_dependency( + pyproject.dependencies, + expected.name, ) + assert actual == expected def test_search_dependency_by_normalized_name( mock_poetry_commands, ) -> None: pyproject = Pyproject(pyproject_str) - normalized_name = pyproject.dependencies[0].normalized_name - assert ( - pyproject.search_dependency( - pyproject.dependencies, - normalized_name, - ) - is not None + expected = pyproject.dependencies[0] + actual = pyproject.search_dependency( + pyproject.dependencies, + expected.normalized_name, ) + assert actual == expected def test_search_dependency_non_existent( @@ -230,6 +230,78 @@ def test_search_dependency_non_existent( ) +def test_filter_dependencies_without_constraint( + mock_poetry_commands, +) -> None: + dependencies = [ + Dependency( + name="poetryup_exact", + version="0.1.0", + group="default", + ), + Dependency( + name="poetryup_caret", + version="^0.1.0", + group="dev", + ), + ] + pyproject = Pyproject(pyproject_str) + expected = dependencies[1] + actual = pyproject.filter_dependencies( + dependencies, + without_constraint=[Constraint.EXACT], + ) + assert actual == [expected] + + +def test_filter_dependencies_name( + mock_poetry_commands, +) -> None: + dependencies = [ + Dependency( + name="poetryup_exact", + version="0.1.0", + group="default", + ), + Dependency( + name="poetryup_caret", + version="^0.1.0", + group="dev", + ), + ] + pyproject = Pyproject(pyproject_str) + expected = dependencies[0] + actual = pyproject.filter_dependencies( + dependencies, + name=[expected.name], + ) + assert actual == [expected] + + +def test_filter_dependencies_group( + mock_poetry_commands, +) -> None: + dependencies = [ + Dependency( + name="poetryup_exact", + version="0.1.0", + group="default", + ), + Dependency( + name="poetryup_caret", + version="^0.1.0", + group="dev", + ), + ] + pyproject = Pyproject(pyproject_str) + expected = dependencies[0] + actual = pyproject.filter_dependencies( + dependencies, + group=[expected.group], + ) + assert actual == [expected] + + def test_dumps( mock_poetry_commands, ) -> None: From 7435e8e3b8437cfb370d3547eded3b404b795160 Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 18:50:55 +0200 Subject: [PATCH 5/6] docs: update README --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 0e1f05d..64404f1 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,33 @@ is that the latter also modifies the `pyproject.toml` file. ![poetryup_demo](https://raw.githubusercontent.com/MousaZeidBaker/poetryup/master/media/poetryup_demo.gif) ## Usage + +Show help message and exit ```shell poetryup --help ``` +Update all dependencies with respect to their version constraints specified in the +`pyproject.toml` file +```shell +poetryup +``` + +Update all dependencies to their latest available version +```shell +poetryup --latest +``` + +Update dependencies in the `default` and `dev` group to their latest available version +```shell +poetryup --latest --group defaut --group dev +``` + +Update the `foo` and `bar` dependencies to their latest available version +```shell +poetryup --latest --name foo --name bar +``` + ## Automate Dependency Updates with GitHub Actions Use PoetryUp with GitHub actions to automate the process of updating dependencies, for reference see this project's [workflow From 4241efbf7a12bef6b957aef01e5ca9f92919d95e Mon Sep 17 00:00:00 2001 From: Mousa Zeid Baker Date: Tue, 14 Jun 2022 19:02:49 +0200 Subject: [PATCH 6/6] fix: change list arguments to plural --- src/poetryup/core/pyproject.py | 54 +++++++++++++++++----------------- tests/unit/test_pyproject.py | 18 ++++++------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/poetryup/core/pyproject.py b/src/poetryup/core/pyproject.py index bbc688b..6313cbd 100644 --- a/src/poetryup/core/pyproject.py +++ b/src/poetryup/core/pyproject.py @@ -204,73 +204,73 @@ def search_dependency( def filter_dependencies( self, dependencies: List[Dependency], - without_constraint: List[Constraint] = [], - name: List[str] = [], - group: List[str] = [], + without_constraints: List[Constraint] = [], + names: List[str] = [], + groups: List[str] = [], ) -> Union[Dependency, None]: """Search for a dependency by name given a list of dependencies Args: dependencies: A list of dependencies to filter - without_constraint: The dependency constraints to ignore - name: The dependency names to include - group: The dependency groups to include + without_constraints: The dependency constraints to ignore + names: The dependency names to include + groups: The dependency groups to include Returns: A list of dependencies """ - if without_constraint: + if without_constraints: # remove deps whom constraint is in the provided constraint list dependencies = [ x for x in dependencies - if x.constraint not in without_constraint + if x.constraint not in without_constraints ] - if name: + if names: # remove deps whom name is NOT in the provided name list - dependencies = [x for x in dependencies if x.name in name] - if group: + dependencies = [x for x in dependencies if x.name in names] + if groups: # remove deps whom group is NOT in the provided group list - dependencies = [x for x in dependencies if x.group in group] + dependencies = [x for x in dependencies if x.group in groups] return dependencies def update_dependencies( self, latest: bool = False, - without_constraint: List[Constraint] = [], - name: List[str] = [], - group: List[str] = [], + without_constraints: List[Constraint] = [], + names: List[str] = [], + groups: List[str] = [], ) -> None: """Update dependencies and bump their version in pyproject Args: latest: Whether to update dependencies to their latest version - without_constraint: The dependency constraints to ignore - name: The dependency names to include - group: The dependency groups to include + without_constraints: The dependency constraints to ignore + names: The dependency names to include + groups: The dependency groups to include """ if latest: logging.info("Updating dependencies to their latest version") dependencies = self.filter_dependencies( self.dependencies, - without_constraint, - name, - group, + without_constraints, + names, + groups, ) # sort dependencies into their groups and add them at once in order # to avoid version solver error in case dependencies depend on each # other - groups = {} + dependency_groups = {} for dependency in dependencies: if isinstance(dependency.version, str): - groups[dependency.group] = groups.get( + dependency_groups[dependency.group] = dependency_groups.get( dependency.group, [] ) + [f"{dependency.name}@latest"] - for group, packages in groups.items(): + for group, packages in dependency_groups.items(): self.__run_poetry_add( packages=packages, group=group, @@ -282,9 +282,9 @@ def update_dependencies( # bump versions in pyproject bumped_dependencies = self.filter_dependencies( self.bumped_dependencies, - without_constraint, - name, - group, + without_constraints, + names, + groups, ) table = self.pyproject["tool"]["poetry"] for dependency in bumped_dependencies: diff --git a/tests/unit/test_pyproject.py b/tests/unit/test_pyproject.py index a915aad..860dee2 100644 --- a/tests/unit/test_pyproject.py +++ b/tests/unit/test_pyproject.py @@ -109,7 +109,7 @@ def test_update_dependencies_latest_skip_exact( ) pyproject.update_dependencies( latest=True, - without_constraint=[Constraint.EXACT], + without_constraints=[Constraint.EXACT], ) calls = [ @@ -148,7 +148,7 @@ def test_update_dependencies_latest_with_specific_group( "_Pyproject__run_poetry_add", return_value=None, ) - pyproject.update_dependencies(latest=True, group=["main"]) + pyproject.update_dependencies(latest=True, groups=["main"]) calls = [ call( @@ -182,7 +182,7 @@ def test_update_dependencies_latest_with_specific_name( "_Pyproject__run_poetry_add", return_value=None, ) - pyproject.update_dependencies(latest=True, name=["poetryup"]) + pyproject.update_dependencies(latest=True, names=["poetryup"]) calls = [ call( @@ -248,8 +248,8 @@ def test_filter_dependencies_without_constraint( pyproject = Pyproject(pyproject_str) expected = dependencies[1] actual = pyproject.filter_dependencies( - dependencies, - without_constraint=[Constraint.EXACT], + dependencies=dependencies, + without_constraints=[Constraint.EXACT], ) assert actual == [expected] @@ -272,8 +272,8 @@ def test_filter_dependencies_name( pyproject = Pyproject(pyproject_str) expected = dependencies[0] actual = pyproject.filter_dependencies( - dependencies, - name=[expected.name], + dependencies=dependencies, + names=[expected.name], ) assert actual == [expected] @@ -296,8 +296,8 @@ def test_filter_dependencies_group( pyproject = Pyproject(pyproject_str) expected = dependencies[0] actual = pyproject.filter_dependencies( - dependencies, - group=[expected.group], + dependencies=dependencies, + groups=[expected.group], ) assert actual == [expected]