diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..b759b9b --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,79 @@ +# This workflow builds and publishes package distribution to PyPI + +name: Publish + +# controls when the action will run +on: + push: + branches: + - 'master' + +jobs: + publish_testpypi: + runs-on: ubuntu-latest + environment: testpypi + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install poetry 1.1.7 + uses: snok/install-poetry@v1 + with: + version: 1.1.7 + + - name: Install dependencies + run: poetry install + + - name: Test + run: | + source $(poetry env info --path)/bin/activate # activate virtual environment + pytest tests + + - name: Build + run: poetry build + + - name: Publish 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 -r testpypi + + publish_pypi: + runs-on: ubuntu-latest + environment: pypi + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install poetry 1.1.7 + uses: snok/install-poetry@v1 + with: + version: 1.1.7 + + - name: Install dependencies + run: poetry install + + - name: Test + run: | + source $(poetry env info --path)/bin/activate # activate virtual environment + pytest tests + + - name: Build + run: poetry build + + - name: Publish pypi + env: + POETRY_HTTP_BASIC_PYPI_USERNAME: __token__ + POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{secrets.PYPI_API_TOKEN}} + run: poetry publish diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..195a6ef --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,45 @@ +# This workflow tests our package + +name: Test + +# controls when the action will run +on: + pull_request: + branches: + - '*' + +jobs: + publish_testpypi: + runs-on: ubuntu-latest + environment: testpypi + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install poetry 1.1.7 + uses: snok/install-poetry@v1 + with: + version: 1.1.7 + + - name: Install dependencies + run: poetry install + + - name: Test + run: | + source $(poetry env info --path)/bin/activate # activate virtual environment + pytest tests + + - name: Build + run: poetry build + + - name: Publish testpypi dry-run + 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 -r testpypi --dry-run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da56c51 --- /dev/null +++ b/.gitignore @@ -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/ + +# IntelliJ’s project specific settings files +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9092357 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 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. diff --git a/README.md b/README.md index ab55288..2df0f2e 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ -# poetryup \ No newline at end of file +# PoetryUp + +![build](https://github.com/MousaZeidBaker/poetryup/workflows/Publish/badge.svg) +![test](https://github.com/MousaZeidBaker/poetryup/workflows/Test/badge.svg) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +![python_version](https://img.shields.io/badge/python-%3E=3.6-blue.svg) +[![pypi_v](https://img.shields.io/pypi/v/poetryup.svg)](https://pypi.org/project/poetryup) +[![pypi_dm](https://img.shields.io/pypi/dm/poetryup.svg)](https://pypi.org/project/poetryup) + +PoetryUp updates dependencies and bumps their version in the `pyproject.toml` file with respect to their version +constraint. The `poetry.lock` file will be recreated as well. PoetryUp runs +[poetry](https://github.com/python-poetry/poetry) commands, thus it's required to be installed. The difference between +running `poetry update` and `poetryup`, is that the latter also modifies the `pyproject.toml` file. + +## Usage +```shell +poetryup +``` + +## Test +Activate virtualenv & Install project dependencies +```shell +poetry shell && poetry install +``` + +Run tests +```shell +pytest tests +``` + +## Contributing +Contributions are welcome via pull requests. + +## Issues +If you encounter any problems, please file an [issue](https://github.com/MousaZeidBaker/poetryup/issues) along with a +detailed description. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..dad5fb2 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,224 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-mock" +version = "3.6.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.5.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.6" +content-hash = "c52eff49bda1a8bc14eaf49e1883fbd2dab32a33ddbedfad2dfd47dc6c542a6c" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-mock = [ + {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, + {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +zipp = [ + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5a022e3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[tool.poetry] +name = "poetryup" +version = "0.2.0" +description = "Update dependencies and bump their version in the pyproject.toml file" +authors = ["Mousa Zeid Baker"] +license = "MIT" +readme = "README.md" +homepage = "https://github.com/MousaZeidBaker/poetryup" +repository = "https://github.com/MousaZeidBaker/poetryup" +keywords=[ + "packaging", + "dependency", + "poetry", + "poetryup", +] +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.6" +toml = "^0.10.2" + +[tool.poetry.dev-dependencies] +pytest = "^6.2.5" +pytest-mock = "^3.6.1" + +[tool.poetry.scripts] +poetryup = "src.poetryup.poetryup:main" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/poetryup/__init__.py b/src/poetryup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/poetryup/poetryup.py b/src/poetryup/poetryup.py new file mode 100644 index 0000000..02c1c6b --- /dev/null +++ b/src/poetryup/poetryup.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python + +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List + +import toml +from poetryup import utils + + +@dataclass +class Dependency: + name: str + version: str + + +def _run_poetry_update() -> None: + """Run poetry update command + """ + + subprocess.run(["poetry", "update"]) + + +def _run_poetry_show() -> str: + """Run poetry show command + + Returns: + str: The output from the poetry show command + """ + + return subprocess.run( + ["poetry", "show", "--tree"], + capture_output=True + ).stdout.decode() + + +def _list_dependencies() -> List[Dependency]: + """List all top-level dependencies + + Returns: + List[Dependency]: A list of dependencies + """ + + output = _run_poetry_show() + + dependencies: List[Dependency] = [] + for line in output.split("\n"): + if re.match("^[a-zA-Z]+", line) is not None: + name, version, *_ = line.split() + dependency = Dependency(name=name, version=version) + dependencies.append(dependency) + + return dependencies + + +def _bump_versions_in_pyproject(dependencies: List[Dependency], pyproject: Dict) -> Dict: + """Bump versions in pyproject + + Args: + dependencies (List[Dependency]): A list of dependencies + pyproject (Dict): The pyproject file parsed as a dictionary + + Returns: + Dict: The updated pyproject dictionary + """ + + for dependency in dependencies: + value = utils.lookup_nested_dict( + dictionary=pyproject["tool"]["poetry"], + key=dependency.name + ) + + if value.startswith(("^", "~")): + new_version = value[0] + dependency.version + utils.update_nested_dict( + dictionary=pyproject["tool"]["poetry"], + key=dependency.name, + new_value=new_version + ) + + return pyproject + + +def poetryup(pyproject_str: str) -> str: + """Update dependencies and bump their version + Args: + pyproject_str (str): The pyproject file parsed as a string + + Returns: + str: The updated pyproject string + """ + + _run_poetry_update() + dependencies = _list_dependencies() + pyproject_dict = toml.loads(pyproject_str) + updated_pyproject_dict = _bump_versions_in_pyproject(dependencies, pyproject_dict) + + # in order to preserve the order of the pyproject file, append build-system to the end + build_system = {"build-system": updated_pyproject_dict.pop("build-system")} + updated_pyproject_str = toml.dumps(updated_pyproject_dict) + updated_pyproject_str += "\n" + toml.dumps(build_system) + + return updated_pyproject_str + + +def main(): + # read pyproject.toml file + try: + pyproject_str = Path("pyproject.toml").read_text() + except FileNotFoundError: + raise Exception("poetryup could not find a pyproject.toml file in current directory") + + updated_pyproject_str = poetryup(pyproject_str) + Path("pyproject.toml").write_text(updated_pyproject_str) + + +if __name__ == "__main__": + main() diff --git a/src/poetryup/utils.py b/src/poetryup/utils.py new file mode 100644 index 0000000..e604352 --- /dev/null +++ b/src/poetryup/utils.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +from typing import Any, Dict + + +def lookup_nested_dict(dictionary: Dict, key: str) -> Any: + """Lookup value recursively in nested dictionary + + Args: + dictionary (Dict): The dictionary to search in + key (str): The key to search for + + Returns: + Any: The value of the key if found, None otherwise + """ + + if key in dictionary: + return dictionary[key] + + for value in dictionary.values(): + if type(value) is dict: + lookup = lookup_nested_dict(value, key) + if lookup is not None: + return lookup + + return None + + +def update_nested_dict(dictionary: Dict, key: str, new_value: Any) -> bool: + """Update value in nested dictionary + + Args: + dictionary (Dict): The dictionary to search in + key (str): The key to search for + new_value (Any): The new value + + Returns: + Any: True if key found and value updated, False otherwise + """ + + if key in dictionary: + dictionary[key] = new_value + return True + + for value in dictionary.values(): + if type(value) is dict: + updated = update_nested_dict(dictionary=value, key=key, new_value=new_value) + if updated is True: + return True + + return False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..77a88a5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import pytest +from pytest_mock import MockerFixture + + +@pytest.fixture(scope="function") +def mock_poetry_commands(mocker: MockerFixture) -> None: + mocker.patch("poetryup.poetryup._run_poetry_update", return_value=None) + + return_value = "poetryup 0.2.0 Run poetry update and bump versions in pyproject.toml file" \ + "\n└── toml >=0.10.2,<0.11.0" + mocker.patch("poetryup.poetryup._run_poetry_show", return_value=return_value) diff --git a/tests/unit/fixtures/expected_pyproject/pyproject.toml b/tests/unit/fixtures/expected_pyproject/pyproject.toml new file mode 100644 index 0000000..d9a2a25 --- /dev/null +++ b/tests/unit/fixtures/expected_pyproject/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "test-poetryup" +version = "0.1.0" +description = "Test PoetryUp" +authors = [ "Mousa Zeid Baker",] + +[tool.poetry.dependencies] +python = "^3.6" +poetryup = "^0.2.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = [ "poetry-core>=1.0.0",] +build-backend = "poetry.core.masonry.api" diff --git a/tests/unit/fixtures/expected_pyproject/pyproject_with_dependency_groups.toml b/tests/unit/fixtures/expected_pyproject/pyproject_with_dependency_groups.toml new file mode 100644 index 0000000..989fc1f --- /dev/null +++ b/tests/unit/fixtures/expected_pyproject/pyproject_with_dependency_groups.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "test-poetryup" +version = "0.1.0" +description = "Test PoetryUp" +authors = [ "Mousa Zeid Baker",] + +[tool.poetry.dependencies] +python = "^3.6" + +[tool.poetry.group.main.dependencies] +poetryup = "^0.2.0" + +[tool.poetry.group.dev.dependencies] + +[build-system] +requires = [ "poetry-core>=1.0.0",] +build-backend = "poetry.core.masonry.api" diff --git a/tests/unit/fixtures/input_pyproject/pyproject.toml b/tests/unit/fixtures/input_pyproject/pyproject.toml new file mode 100644 index 0000000..a207b6a --- /dev/null +++ b/tests/unit/fixtures/input_pyproject/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "test-poetryup" +version = "0.1.0" +description = "Test PoetryUp" +authors = [ "Mousa Zeid Baker",] + +[tool.poetry.dependencies] +python = "^3.6" +poetryup = "^0.1.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = [ "poetry-core>=1.0.0",] +build-backend = "poetry.core.masonry.api" diff --git a/tests/unit/fixtures/input_pyproject/pyproject_with_dependency_groups.toml b/tests/unit/fixtures/input_pyproject/pyproject_with_dependency_groups.toml new file mode 100644 index 0000000..4327038 --- /dev/null +++ b/tests/unit/fixtures/input_pyproject/pyproject_with_dependency_groups.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "test-poetryup" +version = "0.1.0" +description = "Test PoetryUp" +authors = [ "Mousa Zeid Baker",] + +[tool.poetry.dependencies] +python = "^3.6" + +[tool.poetry.group.main.dependencies] +poetryup = "^0.1.0" + +[tool.poetry.group.dev.dependencies] + +[build-system] +requires = [ "poetry-core>=1.0.0",] +build-backend = "poetry.core.masonry.api" diff --git a/tests/unit/test_poetryup.py b/tests/unit/test_poetryup.py new file mode 100644 index 0000000..1703a58 --- /dev/null +++ b/tests/unit/test_poetryup.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path + +from poetryup.poetryup import poetryup + + +def pytest_generate_tests(metafunc) -> None: + input_pyproject_path = os.path.join(os.path.dirname(__file__), "fixtures/input_pyproject") + input_pyprojects = [file.read_text() for file in Path(input_pyproject_path).glob("*.toml")] + + expected_pyproject_path = os.path.join(os.path.dirname(__file__), "fixtures/expected_pyproject") + expected_pyprojects = [file.read_text() for file in Path(expected_pyproject_path).glob("*.toml")] + + argvalues = list(zip(input_pyprojects, expected_pyprojects)) + ids = [file.name for file in Path(input_pyproject_path).glob("*.toml")] + + metafunc.parametrize( + argnames=("input_pyproject", "expected_pyproject"), + argvalues=argvalues, + ids=ids + ) + + +def test_poetryup(input_pyproject: str, expected_pyproject: str, mock_poetry_commands) -> None: + updated_pyproject = poetryup(input_pyproject) + assert(updated_pyproject == expected_pyproject)