Repository: TechnologyBrewery/poetry-monorepo-dependency-plugin (language: python)

Issue:
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.
Export requirements.txt without path-based monorepo dependencies
Poetry path-based dependencies defined under the monorepo dependency group will fail to be resolved upon export on any machine that is not the one on which the original export was performed. 

In order to prevent such dependency resolving failures, it is needed to extend poetry's export command such that any path-based dependencies are not included in the resultant `requirements.txt`.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 846d4bc..32b608b 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@ building and publishing archives with local path dependencies to other Poetry pr
 poetry self add poetry-monorepo-dependency-plugin
 ```
 
-If you want to activate `poetry-monorepo-dependency-plugin` for all [build][poetry-build] and
-[publish][poetry-publish] command invocations, add the following to your project's `pyproject.toml`
+If you want to activate `poetry-monorepo-dependency-plugin` for all [build][poetry-build],
+[publish][poetry-publish], and [export][poetry-export] command invocations, add the following to your project's `pyproject.toml`
 that has path dependencies to other Poetry projects:
 
 ```toml
@@ -57,6 +57,11 @@ When generating `wheel` or `sdist` archives for the `spam` project through Poetr
 `ham` project were declared as `ham = "1.2.3"`.  As a result, package metadata in archives for `spam` will shift from
 `Requires-Dist: ham @ ../ham` to `Requires-Dist: ham (==1.2.3)`
 
+Additionally, to address [path dependencies](https://python-poetry.org/docs/dependency-specification/#path-dependencies) not being portable, this plugin provides
+the ability to extend Poetry's [export][poetry-export] command using the `export-without-path-deps` command. This command will exclude path dependencies from being written to intermediate `requirements.txt` export. When installing the exported 
+`requirements.txt` on another machine and/or Docker container local Paths dependencies can not be resolved and therefore can not 
+be installed so they are removed. 
+
 ### Command line mode
 
 If you need greater control over when `poetry-monorepo-dependency-plugin` is activated, this plugin exposes new `build-rewrite-path-deps`
@@ -68,6 +73,11 @@ any configuration defined in the project's `pyproject.toml` `[tool.poetry-monore
 poetry build-rewrite-path-deps --version-pinning-strategy=semver
 ```
 
+To generate a `requirements.txt` without path dependencies use the `export-without-path-deps`. For example:
+```commandline
+poetry export-without-path-deps -f requirements.txt --output requirements.txt
+```
+
 ### Configuration
 
 The following configuration options are supported within your project's `pyproject.toml` configuration:
@@ -113,5 +123,6 @@ the package to PyPI.
 
 [poetry]: https://python-poetry.org/
 [poetry-build]: https://python-poetry.org/docs/cli/#build
+[poetry-export]: https://python-poetry.org/docs/cli/#export
 [poetry-publish]: https://python-poetry.org/docs/cli/#publish
 [mit_licence]: http://dan.mit-license.org/
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 2b6d905..8797370 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/TechnologyBrewery/poetry-monorepo-dependency-pl
 [tool.poetry.dependencies]
 python = "^3.8"
 poetry = ">=1.6"
+poetry-plugin-export = ">=1.5.0"
 cleo = ">=2.0.1"
 
 [tool.poetry.group.dev.dependencies]
diff --git a/src/poetry_monorepo_dependency_plugin/path_dependency_remover.py b/src/poetry_monorepo_dependency_plugin/path_dependency_remover.py
new file mode 100644
index 0000000..de508d6
--- /dev/null
+++ b/src/poetry_monorepo_dependency_plugin/path_dependency_remover.py
@@ -0,0 +1,53 @@
+import typing
+
+from cleo.io.io import IO as cleoIO
+import cleo.io.outputs.output
+from poetry.core.pyproject.toml import PyProjectTOML
+from poetry.core.constraints.version import Version
+from poetry.core.packages.dependency import Dependency
+from poetry.core.packages.directory_dependency import DirectoryDependency
+from poetry.core.packages.dependency_group import DependencyGroup
+
+
+class PathDependencyRemover:
+    """
+    Exposes core functionality for gathering a pyproject.toml's path dependencies,
+    determining if they are Poetry projects, and if so, extracting the corresponding
+    dependency.
+    """
+
+    def update_dependency_group(
+        self,
+        io: cleoIO,
+        pyproject: PyProjectTOML,
+        dependency_group: DependencyGroup,
+    ) -> None:
+        """
+        Removes all path dependencies to Poetry projects.
+
+        :param io: instance of Cleo IO that may be used for logging diagnostic output during
+        plugin execution
+        :param pyproject: encapsulates the pyproject.toml of the current project for which
+        path dependencies will be rewritten
+        :param dependency_group: specifies the dependency group from which to pin path
+        dependencies, this will usually be "main"
+        :return: none
+        """
+        io.write_line(
+            "Updating dependency constraints...",
+            verbosity=cleo.io.outputs.output.Verbosity.DEBUG,
+        )
+
+        for dependency in dependency_group.dependencies:
+            if not isinstance(
+                dependency,
+                DirectoryDependency,
+            ):
+                continue
+
+            io.write_line(
+                f"  • Removing {dependency.name} path dependency)",
+                verbosity=cleo.io.outputs.output.Verbosity.DEBUG,
+            )
+
+            dependency_group.remove_dependency(dependency.name)
diff --git a/src/poetry_monorepo_dependency_plugin/plugin.py b/src/poetry_monorepo_dependency_plugin/plugin.py
index 0e71aba..8216473 100644
--- a/src/poetry_monorepo_dependency_plugin/plugin.py
+++ b/src/poetry_monorepo_dependency_plugin/plugin.py
@@ -11,8 +11,10 @@
 from cleo.helpers import option
 from poetry.console.commands.build import BuildCommand
 from poetry.console.commands.publish import PublishCommand
+from poetry_plugin_export.command import ExportCommand
 
 from .path_dependency_rewriter import PathDependencyRewriter
+from .path_dependency_remover import PathDependencyRemover
 
 _version_pinning_strategy = option(
     "version-pinning-strategy",
@@ -75,10 +77,26 @@ def handle(self) -> int:
         return super().handle()
 
 
+class ExportWithoutPathDepsCommand(ExportCommand):
+    name = "export-without-path-deps"
+    description = (
+        "Extends the 'export' command to generate exports in which path dependencies to "
+        "other Poetry projects are removed from package dependencies."
+    )
+
+    def handle(self) -> int:
+        path_dependency_remover = PathDependencyRemover()
+        path_dependency_remover.update_dependency_group(
+            self.io, self.poetry.pyproject, self.poetry.package.dependency_group("main")
+        )
+        return super().handle()
+
+
 class MonorepoDependencyPlugin(poetry.plugins.application_plugin.ApplicationPlugin):
     COMMANDS = (
         BuildCommand,
         PublishCommand,
+        ExportCommand,
     )
 
     def __init__(self):
@@ -92,6 +110,9 @@ def activate(self, application: poetry.console.application.Application):
         application.command_loader.register_factory(
             "publish-rewrite-path-deps", lambda: PublishWithVersionedPathDepsCommand()
         )
+        application.command_loader.register_factory(
+            "export-without-path-deps", lambda: ExportWithoutPathDepsCommand()
+        )
 
         try:
             local_poetry_proj_config = application.poetry.pyproject.data
@@ -121,18 +142,32 @@ def event_listener(
         event_name: str,
         dispatcher: cleo.events.event_dispatcher.EventDispatcher,
     ) -> None:
+
         if not isinstance(event.command, self.COMMANDS):
             return
 
-        path_dependency_writer = PathDependencyRewriter(
-            self.plugin_config["version-pinning-strategy"]
-        )
-        path_dependency_writer.update_dependency_group(
-            event.io,
-            self.poetry.pyproject,
-            self.poetry.package.dependency_group("main"),
+        event.io.write_line(
+            "Intercepting the command: " + str(event.command.__class__),
+            verbosity=cleo.io.outputs.output.Verbosity.DEBUG,
         )
 
+        if isinstance(event.command, ExportCommand):
+            path_dependency_remover = PathDependencyRemover()
+            path_dependency_remover.update_dependency_group(
+                event.io,
+                self.poetry.pyproject,
+                self.poetry.package.dependency_group("main"),
+            )
+        else:
+            path_dependency_writer = PathDependencyRewriter(
+                self.plugin_config["version-pinning-strategy"]
+            )
+            path_dependency_writer.update_dependency_group(
+                event.io,
+                self.poetry.pyproject,
+                self.poetry.package.dependency_group("main"),
+            )
+
 
 def _default_plugin_config() -> Mapping:
     """


Test patch:
diff --git a/tests/features/remove-path-dependencies.feature b/tests/features/remove-path-dependencies.feature
new file mode 100644
index 0000000..612967d
--- /dev/null
+++ b/tests/features/remove-path-dependencies.feature
@@ -0,0 +1,15 @@
+Feature: Remove path dependencies from Poetry projects
+
+  Scenario Outline: Remove project dependencies with paths 
+    Given a project with a local path dependencies to other Poetry projects
+    When the project is exported using the plugin's command-line mode
+    Then the path dependencies for "<dependency name>" are removed from poetry dependencies
+    Examples:
+      | dependency name
+      | spam
+      | spam
+      | spam
+      | ham
+      | ham
+      | ham
+      | eggs
diff --git a/tests/features/rewrite-path-dependencies.feature b/tests/features/rewrite-path-dependencies.feature
index 7680481..8145177 100644
--- a/tests/features/rewrite-path-dependencies.feature
+++ b/tests/features/rewrite-path-dependencies.feature
@@ -12,4 +12,4 @@ Feature: Re-write path dependencies to Poetry projects as versioned package depe
       | ham             | mixed                    | 4.5.6              |
       | ham             | exact                    | 4.5.6              |
       | ham             | semver                   | ^4.5.6             |
-      | eggs            | mixed                    | >=1.0rc4,<1.0.1    |
+      | eggs            | mixed                    | >=1.0rc4,<1.0.1    |
\ No newline at end of file
diff --git a/tests/features/steps/remove_path_dependencies_steps.py b/tests/features/steps/remove_path_dependencies_steps.py
new file mode 100644
index 0000000..7091551
--- /dev/null
+++ b/tests/features/steps/remove_path_dependencies_steps.py
@@ -0,0 +1,38 @@
+import unittest.mock
+import unittest
+from pathlib import Path
+
+import cleo.io.io
+import poetry.core.factory
+from behave import *
+import nose.tools as nt
+
+from poetry_monorepo_dependency_plugin.path_dependency_remover import (
+    PathDependencyRemover,
+)
+
+
+@when("the project is exported using the plugin's command-line mode")
+def step_impl(context):
+    path_dependency_remover = PathDependencyRemover()
+    mock_io = unittest.mock.create_autospec(cleo.io.io.IO)
+    path_dependency_remover.update_dependency_group(
+        mock_io,
+        context.project_with_local_deps.pyproject,
+        context.project_with_local_deps.package.dependency_group("main"),
+    )
+
+
+@then(
+    'the path dependencies for "{dependency_name}" are removed from poetry dependencies'
+)
+def step_impl(context, dependency_name):
+    mydependencies = context.project_with_local_deps.package.dependency_group(
+        "main"
+    ).dependencies
+
+    nt.assert_not_in(
+        dependency_name,
+        mydependencies,
+        f"Found the path dependency {dependency_name}",
+    )