Repository: benbenbang/poetry-import-plugin (language: python)

Issue:
v2 - req2toml
This project will soon introduce a breaking change.

- The core functionality will be improved
- It won't be a standalone package anymore. It will be implemented as a poetry's plugin, e.g.
``` bash
poetry req2toml -f ....
```
  like so
- The plugin name will be changed


Discussion / hints:

Fix patch:
diff --git a/Makefile b/makefile
similarity index 55%
rename from Makefile
rename to makefile
index f4dc5eb..54a8ab9 100644
--- a/Makefile
+++ b/makefile
@@ -1,15 +1,5 @@
 .EXPORT_ALL_VARIABLES:
-PYTHON_INTERPRETER = $(shell which python)
-PY_CACHE_DIR = `find . -type d -name __pycache__`
-
-# Args
-pypi_apitoken =
-
-ifeq (,$(shell which poetry))
-HAS_POETRY=False
-else
-HAS_POETRY=True
-endif
+PY_CACHE_DIR = $(shell find . -type d -name __pycache__)
 
 define cleanPyCache
 @while read pyCD; \
@@ -21,55 +11,33 @@ done <<< $(PY_CACHE_DIR)
 endef
 
 .PHONY: build
-## Build
-build_pkg:
-	@echo "Start to build pkg"
-ifeq (True,$(HAS_POETRY))
-	@poetry version $(shell git describe --tags --abbrev=0); \
+## Build package
+build:
+	@echo "building package..."
+	@poetry version $(shell git describe --tags --abbrev=0)
 	poetry build
-else
-	@export VERSION=$(shell git describe --tags --abbrev=0); \
-	$(PYTHON_INTERPRETER) setup.py build; \
-	$(PYTHON_INTERPRETER) setup.py sdist
-endif
 
 .PHONY: format
-## Run pre-commit hook to do the formating
+## Run pre-commit hook to do the formatting
 format:
-ifeq (True,$(HAS_POETRY))
-	@-poetry run pre-commit install
-	@-poetry run pre-commit run --all
-else
-	@-pre-commit install
-	@-pre-commit run --all
-endif
+	@poetry run pre-commit install
+	@poetry run pre-commit run --all
 
 .PHONY: publish
-## Tag new version and upload to pypi
-upload:
-	@poetry config pypi-token.pypi $$pypi_apitoken; \
-	poetry version $(shell git describe --tags --abbrev=0); \
+## Tag new version, build package and upload it to pypi
+publish:
+	@poetry version $(shell git describe --tags --abbrev=0); \
+	rm -rf build; \
 	poetry publish --build
 
 .PHONY: clean
-## Remove build, dist and *.egg-info directories after build or installation
+## Remove build, dist, and *.egg-info directories after build or installation
 clean:
 	@-rm -r build
 	@-rm -r dist
 	@-rm -r oqim.egg-info
 	@-$(call cleanPyCache)
 
-.PHONY: build
-## Run build_pkg, format together
-build: format build_pkg
-
-.PHONY: install
-## Install package in current env
-install: install_pkg clean
-
-## chain actions to run test and build package
-ci: test build_pkg
-
 .DEFAULT_GOAL := help
 
 help:
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 9f41c07..0000000
--- a/setup.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# standard library
-import os
-
-# pypi library
-from setuptools import find_packages, setup
-
-version = os.getenv("VERSION")
-
-if not version:
-    print("Please use `make build` to build the package. Or manually `export VERSION=xxx` and rerun")
-
-requirements = [
-    'click==7.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")'
-]
-
-setup(
-    name="req2toml",
-    version=version,
-    description="Convert requirements.txt to pyproject.toml",
-    author="Ben Chen",
-    author_email="bn@benbenbang.io",
-    python_requires=">=3.7",
-    install_requires=requirements,
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "Intended Audience :: Developers",
-        "Intended Audience :: Science/Research",
-        "License :: OSI Approved :: Apache Software License",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-    ],
-    keywords=["poetry", "python", "convert", "requirements"],
-    packages=find_packages(exclude=["doc", "tests*"]),
-    include_package_data=False,
-)


Test patch:
diff --git a/tests/.coveragerc b/tests/.coveragerc
index 2fdc68d..e69de29 100644
--- a/tests/.coveragerc
+++ b/tests/.coveragerc
@@ -1,15 +0,0 @@
-[run]
-omit =
-    # Testing scripts
-    tests/*
-
-    # Specific files
-    */__init__.py
-    */setup.py
-    req2toml/console.py
-
-source = ./req2toml
-
-[report]
-exclude_lines =
-    def inpty
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..02d0b4b
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,1 @@
+pytest_plugins = ["tests.fixtures.pyproject"]
diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/pyproject.py b/tests/fixtures/pyproject.py
new file mode 100644
index 0000000..44ada49
--- /dev/null
+++ b/tests/fixtures/pyproject.py
@@ -0,0 +1,37 @@
+# standard library
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+# pypi library
+import pytest
+from pytest_mock import MockerFixture
+
+if TYPE_CHECKING:
+    # pypi library
+    from pytest import TempPathFactory
+
+
+@pytest.fixture
+def pyproject_toml_raw():
+    return """[tool.poetry]
+name = "poetry-import"
+version = "2.0.0"
+description = "Convert requirements.txt to pyproject.toml"
+authors = ["Ben Chen <benbenbang@github.com>"]
+license = "Apache-2.0"
+readme = "README.md"
+repository = "https://github.com/benbenbang/poetry-import"
+keywords = ["poetry", "pyproject", "toml", "requirements", "import"]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+"""
+
+
+@pytest.fixture
+def pyproject_toml(pyproject_toml_raw, tmp_path: "TempPathFactory", mocker: "MockerFixture"):
+    folder = Path(f"{tmp_path}")
+    toml_path = folder / "pyproject.toml"
+    toml_path.write_text(pyproject_toml_raw)
+    mocker.patch.dict("os.environ", PYPROJECT_CUSTOM_PATH=f"{toml_path}")
+    return toml_path
diff --git a/tests/mixin.py b/tests/mixin.py
index 0666432..e69de29 100644
--- a/tests/mixin.py
+++ b/tests/mixin.py
@@ -1,118 +0,0 @@
-# standard library
-import os
-from pathlib import Path
-from subprocess import PIPE, Popen
-from tempfile import mkstemp
-
-# pypi library
-from click import Command, Context
-
-__all__ = ["CaseMixin", "TempfileMixin"]
-
-
-class CaseMixin:
-    ctx = Context(Command("Test"))
-
-    def set_read_requirements_cases(self):
-        # Paths Setup
-        self.right_path_txt = Path("./tmp/right_path.txt")
-        self.wrong_path_txt = Path("./tmp/not_found.txt")
-        self.support_fmt_file = Path("./tmp/right_path.txt")
-        self.unsupport_fmt_file = Path("./tmp/right_path.md")
-
-        # Contents Setup
-        self.formatted_content = """
-numpy==1.16; (python<=3.6)
-numpy>1.19.0; (python>3.6)
-pandas==0.25.2
-        """
-        self.unformatted_content = """numpy==1.16\nnumpy>1.19.0\npandas==0.25.2; (numpy<=1.17)\nmatplotlib"""
-        self.messy_content = """numpy==1.16; (python<=3.6)
-    numpy>1.19.0; (python>3.6)
-        scikit-learn==0.17.1;\n\tjoblib=0.16
-\n\n\n\n\n\n\n\t\tpandas==0.25.2
-        """
-
-    def set_toml_at_root_cases(self):
-        self.toml_present_path = "./tmp/pyproject.toml"
-        self.toml_not_present_path = "./tmp/pyproject_not_present.toml"
-
-    def set_in_pty_cases(self):
-        self.return_zero_cmd1 = ["echo", "Hello", "world"]
-        self.return_zero_cmd2 = "echo Hello world"
-        self.return_error_cmd = ["echo Hello world"]
-
-
-class TempfileMixin:
-    def setUp(self):
-        Path("./tmp").mkdir(exist_ok=True)
-
-    def tearDown(self):
-        if hasattr(self, "tmpPath"):
-            self._delete_tempfile(self.tmpPath)
-
-        if Path("./tmp").is_dir():
-            try:
-                Path("./tmp").rmdir()
-            except OSError:
-                print("Unable to delete ./tmp folder due to none empty. Going to remove by rm -rf")
-                proc = Popen(["rm", "-rf", "./tmp"])
-                proc.communicate()
-
-    def _get_splitted_path(self, path):
-        return (
-            "/".join([f"{p}" for p in reversed([p for p in path.parents])]),
-            path.stem,
-            path.suffix,
-        )
-
-    def _create_tempfile(self, path, content):
-        dirname, prefix, ext = self._get_splitted_path(path)
-        _, tmpPath = mkstemp(dir=dirname, prefix=prefix, suffix=ext)
-        with open(tmpPath, "w+") as file:
-            file.write(content)
-
-        return tmpPath
-
-    def _delete_tempfile(self, path):
-        os.unlink(path)
-
-    def _create_toml(self, path, content):
-        Path(path).touch()
-        with open(path, "w+") as file:
-            file.write(content)
-
-    def _create_temp_nested_dir(self):
-        self.nestedDir = Path("./tmp") / "nested_tmp"
-        self.nestedDir.mkdir(exist_ok=True)
-
-    def _delete_temp_nested_dir(self):
-        self.nestedDir.rmdir()
-
-    def _git_init(self):
-        try:
-            os.chdir("./tmp")
-            proc = Popen(["git", "init"], stdout=PIPE, stderr=PIPE)
-            result = next((r.decode for r in proc.communicate() if r), "Unable to create Git Repo")
-            os.chdir("..")
-            assert result != "Unable to create Git Repo"
-            ok = True
-        except AssertionError:
-            print("Unable to init git. Abort!")
-            ok = False
-
-        return ok
-
-    def _rm_git(self):
-        try:
-            os.chdir("./tmp")
-            proc = Popen(["rm", "-rf", ".git"], stdout=PIPE, stderr=PIPE)
-            os.chdir("..")
-            result = [r.decode() for r in proc.communicate() if r]
-            assert len(result) == 0
-            ok = True
-        except AssertionError:
-            print("Unable to delete git. Abort!")
-            ok = False
-
-        return ok
diff --git a/tests/test_command.py b/tests/test_command.py
new file mode 100644
index 0000000..5f86d65
--- /dev/null
+++ b/tests/test_command.py
@@ -0,0 +1,129 @@
+# standard library
+from pathlib import Path
+from typing import TYPE_CHECKING
+from unittest.mock import MagicMock, mock_open, patch
+
+# pypi library
+import pytest
+
+# poetry-import library
+from poetry_import.command import ImportReqCommand
+
+if TYPE_CHECKING:
+    # pypi library
+    from pytest_mock import MockerFixture
+
+
+@pytest.fixture
+def command():
+    mock_command = ImportReqCommand()
+    mock_command._poetry = MagicMock()  # Mock the poetry instance
+    return mock_command
+
+
+@pytest.mark.unittests
+def test_parse_group_specifications(command):
+    # Test case 1: Simple case with one file going to the root group
+    group_specs = ["a.txt"]
+    files = ["a.txt"]
+    expected_file_group_map = {"root": ["a.txt"]}
+    expected_constraints_file = None
+    result, constraints_file = command._parse_group_specifications(group_specs, files)
+    assert result == expected_file_group_map and constraints_file == expected_constraints_file, "Test Case 1 Failed"
+
+    # Test case 2: Handling constraints file
+    group_specs = ["-c", "constraints.txt", "a.txt", "-g", "dev", "ipython", "ruff", "-g", "tests", "b.txt"]
+    files = ["a.txt", "b.txt"]
+    expected_file_group_map = {"root": ["a.txt"], "dev": ["ipython", "ruff"], "tests": ["b.txt"]}
+    expected_constraints_file = "constraints.txt"
+    result, constraints_file = command._parse_group_specifications(group_specs, files)
+    assert result == expected_file_group_map and constraints_file == expected_constraints_file, "Test Case 2 Failed"
+
+    # Test case 3: Complex case with mixed file and direct dependency listings
+    group_specs = ["a.txt", "-g", "dev", "ipython", "ruff", "-g", "tests", "b.txt"]
+    files = ["a.txt", "b.txt"]
+    expected_file_group_map = {"root": ["a.txt"], "dev": ["ipython", "ruff"], "tests": ["b.txt"]}
+    expected_constraints_file = None
+    result, constraints_file = command._parse_group_specifications(group_specs, files)
+    assert result == expected_file_group_map and constraints_file == expected_constraints_file, "Test Case 3 Failed"
+
+
+@pytest.mark.unittests
+def test_parse_requirements_file(command, mocker: "MockerFixture"):
+    mocker.patch("pathlib.Path.open", new_callable=mock_open, read_data="flask==1.0\nDjango==3.0")
+
+    dependencies = command._parse_requirements_file("requirements.txt")
+    assert dependencies == ["flask==1.0", "Django==3.0"], "Failed to parse requirements file correctly"
+
+
+@pytest.mark.unittests
+def test_update_or_install_dependencies(command):
+    # Setup the command options as if they were parsed from command line input
+    command.option = MagicMock(side_effect=lambda x: x == "update")
+
+    with patch.object(command.poetry, "run") as mock_run:
+        command._update_or_install_dependencies()
+        mock_run.assert_called_once_with("update")  # Assert that poetry.run was called once with 'update'
+
+    # Test with 'install' option
+    command.option = MagicMock(side_effect=lambda x: x == "install")
+    with patch.object(command.poetry, "run") as mock_run:
+        command._update_or_install_dependencies()
+        mock_run.assert_called_once_with("install")  # Assert that poetry.run was called once with 'install'
+
+    # Test with no update/install (should not call poetry.run)
+    command.option = MagicMock(return_value=False)
+    with patch.object(command.poetry, "run") as mock_run:
+        command._update_or_install_dependencies()
+        mock_run.assert_not_called()  # Assert that poetry.run was not called
+
+
+@pytest.mark.unittests
+def test_integrate_dependencies(command, pyproject_toml, pyproject_toml_raw):
+    mocked_file = Path(pyproject_toml)
+    expect = (
+        pyproject_toml_raw
+        + """
+[tool.poetry.group.dev.dependencies]
+flask = "1.0"
+Django = "3.0"
+"""
+    )
+
+    dependencies = ["flask==1.0", "Django==3.0"]
+    group = "dev"
+    command._integrate_dependencies(group, dependencies)
+
+    # Ensure the file was written to (updated)
+    assert mocked_file.read_text() == expect
+
+
+@pytest.mark.unittests
+def test_apply_constraints(command):
+    dependencies = ["flask", "Django"]
+    constraints = {"flask": "==1.1"}
+    expected = ["flask==1.1", "Django"]
+    result = command._apply_constraints(dependencies, constraints)
+    assert result == expected, "Constraints were not applied correctly"
+
+
+@pytest.mark.unittests
+def test_command_full_flow(command, mocker: "MockerFixture"):
+    mocker.patch("pathlib.Path.open", new_callable=mock_open, read_data="flask==1.0\nDjango==3.0")
+
+    # Setup command options and arguments
+    command.argument = MagicMock(return_value=["requirements.txt"])
+    command.option = MagicMock(side_effect=lambda x: {"group": ["-g", "dev", "requirements.txt"]}.get(x, False))
+
+    # Mock _parse_group_specifications to simplify command line parsing logic in the test
+    command._parse_group_specifications = MagicMock(return_value=({"dev": ["requirements.txt"]}, None))
+    command._parse_requirements_file = MagicMock(return_value=["flask==1.0", "Django==3.0"])
+    command._integrate_dependencies = MagicMock()
+    command._update_or_install_dependencies = MagicMock()
+
+    # Run the command handle
+    command.handle()
+
+    # Assertions to check if the dependencies were integrated correctly
+    command._integrate_dependencies.assert_called_once_with("dev", ["flask==1.0", "Django==3.0"])
+    command._update_or_install_dependencies.assert_called_once()
diff --git a/tests/test_read_requirements.py b/tests/test_read_requirements.py
index 8e3d1e8..e69de29 100644
--- a/tests/test_read_requirements.py
+++ b/tests/test_read_requirements.py
@@ -1,76 +0,0 @@
-# standard library
-from unittest import TestCase, expectedFailure
-
-# req2toml plugin
-from req2toml.utils import read_requirments
-from tests.mixin import CaseMixin, TempfileMixin
-
-
-class TestReadRequirmentsFormattedContent(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req1_from_right_path_with_txt(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.right_path_txt, self.formatted_content)
-        reqs = read_requirments(self.ctx, self.tmpPath)
-
-        self.assertEqual(len(reqs), 3)
-        for req in reqs:
-            for r in req.split(";"):
-                self.assertIn(r, self.formatted_content)
-
-
-class TestReadRequirmentsUnformattedContent(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req2_from_right_path_with_txt(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.right_path_txt, self.unformatted_content)
-        reqs = read_requirments(self.ctx, self.tmpPath)
-
-        self.assertEqual(len(reqs), 4)
-        for req in reqs:
-            for r in req.split(";"):
-                self.assertIn(r, self.unformatted_content)
-
-
-class TestReadRequirmentsMessyContent(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req3_from_right_path_with_txt(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.right_path_txt, self.messy_content)
-        reqs = read_requirments(self.ctx, self.tmpPath)
-
-        self.assertEqual(len(reqs), 5)
-        for req in reqs:
-            for r in req.split(";"):
-                self.assertIn(r, self.messy_content)
-
-
-class TestReadRequirmentsRstExt(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req_from_right_path_with_rst(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.support_fmt_file, self.formatted_content)
-        reqs = read_requirments(self.ctx, self.tmpPath)
-
-        self.assertEqual(len(reqs), 3)
-        for req in reqs:
-            for r in req.split(";"):
-                self.assertIn(r, self.formatted_content)
-
-
-@expectedFailure
-class TestReadRequirmentsFileNotFound(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req_from_wrong_path_with_txt(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.wrong_path_txt, self.messy_content)
-        read_requirments(self.ctx, self.wrong_path_txt)
-
-
-@expectedFailure
-class TestReadRequirmentsMarkdownExt(CaseMixin, TempfileMixin, TestCase):
-    def test_read_req_from_right_path_with_rst(self):
-        self.set_read_requirements_cases()
-
-        self.tmpPath = self._create_tempfile(self.unsupport_fmt_file, self.formatted_content)
-        read_requirments(self.ctx, self.tmpPath)
diff --git a/tests/test_toml_at_root.py b/tests/test_toml_at_root.py
index cfd3081..e69de29 100644
--- a/tests/test_toml_at_root.py
+++ b/tests/test_toml_at_root.py
@@ -1,92 +0,0 @@
-# standard library
-import os
-from unittest import TestCase
-
-# req2toml plugin
-from req2toml.utils import toml_at_root
-from tests.mixin import CaseMixin, TempfileMixin
-
-pyproject_dot_toml = """[tool.poetry]
-name = "req2toml"
-version = "x.x.x"
-description = "Convert requirements.txt to pyproject.toml"
-license = "Apache-2.0"
-classifiers = [
-    "Development Status :: 3 - Prod",
-    "Intended Audience :: Poetry Users",
-    "License :: OSI Approved :: MIT License",
-    "Programming Language :: Python :: 3.7",
-    "Programming Language :: Python :: 3.8",
-]
-
-[tool.poetry.dependencies]
-python = "^3.7"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
-"""
-
-
-class TestTomlPresent(CaseMixin, TempfileMixin, TestCase):
-    # Unable to create or delete git repo at the test env
-    # Abort but return 0 to pass the test
-    RETURN_ON_GIT_INIT = RETURN_ON_GIT_DELETE = 0
-
-    def test_toml_present_from_root(self):
-        self.set_toml_at_root_cases()
-        self._create_toml(self.toml_present_path, pyproject_dot_toml)
-
-        ok = self._git_init()
-        if not ok:
-            return self.RETURN_ON_GIT_INIT
-
-        os.chdir("./tmp")
-        self.assertTrue(toml_at_root())
-        os.chdir("..")
-
-        ok = self._rm_git()
-        if not ok:
-            return self.RETURN_ON_GIT_DELETE
-
-        self._delete_tempfile(self.toml_present_path)
-
-    def test_toml_present_from_nested_dir(self):
-        self.set_toml_at_root_cases()
-        self._create_toml(self.toml_present_path, pyproject_dot_toml)
-        self._create_temp_nested_dir()
-
-        ok = self._git_init()
-        if not ok:
-            return self.RETURN_ON_GIT_INIT
-
-        os.chdir(self.nestedDir)
-        self.assertTrue(toml_at_root())
-        os.chdir("../..")
-
-        self._delete_temp_nested_dir()
-        ok = self._rm_git()
-        if not ok:
-            return self.RETURN_ON_GIT_DELETE
-
-        self._delete_tempfile(self.toml_present_path)
-
-
-class TestTomlNotPresent(CaseMixin, TempfileMixin, TestCase):
-    def test_toml_not_present(self):
-        self.set_toml_at_root_cases()
-        self._create_toml(self.toml_not_present_path, pyproject_dot_toml)
-
-        ok = self._git_init()
-        if not ok:
-            return self.RETURN_ON_GIT_INIT  # type: ignore
-
-        os.chdir("./tmp")
-        self.assertFalse(toml_at_root())
-        os.chdir("..")
-
-        ok = self._rm_git()
-        if not ok:
-            return self.RETURN_ON_GIT_DELETE  # type: ignore
-
-        self._delete_tempfile(self.toml_not_present_path)