Repository: DavidVujic/python-polylith

Issue:
poly libs and poly check: optionally return error for library version mismatch
**Is your feature request related to a problem? Please describe.**
Adding an option to the `poly libs` and `poly check` commands to return an error code when there are different versions of libraries specified across the repo.

**Describe the solution you'd like**
Adding an option to the commands (in the same way as `--strict`) that will return an exit code 1 if any library version is different in any of the projects.


**Additional context**
Ideas coming from the conversation in the Discussions section: https://github.com/DavidVujic/python-polylith/discussions/225

Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=234&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=234&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=234)



Patch:
diff --git a/bases/polylith/cli/core.py b/bases/polylith/cli/core.py
index 7e0d079..56f9cfc 100644
--- a/bases/polylith/cli/core.py
+++ b/bases/polylith/cli/core.py
@@ -63,17 +63,21 @@ def check_command(
 
     cli_options = {
         "verbose": verbose,
+        "short": False,
         "quiet": quiet,
         "strict": strict,
         "alias": str.split(alias, ",") if alias else [],
     }
 
     filtered_projects = filtered_projects_data(only_projects_data, directory)
-    projects_data = enriched_with_lock_files_data(filtered_projects, verbose)
+    enriched_projects = enriched_with_lock_files_data(filtered_projects, verbose)
 
-    results = {commands.check.run(root, ns, p, cli_options) for p in projects_data}
+    results = {commands.check.run(root, ns, p, cli_options) for p in enriched_projects}
+    libs_result = commands.check.check_libs_versions(
+        filtered_projects, all_projects_data, cli_options
+    )
 
-    if not all(results):
+    if not all(results) or not libs_result:
         raise Exit(code=1)
 
 
diff --git a/bases/polylith/cli/options.py b/bases/polylith/cli/options.py
index e76bbf1..d250f9f 100644
--- a/bases/polylith/cli/options.py
+++ b/bases/polylith/cli/options.py
@@ -9,8 +9,9 @@ directory = Option(
 
 short = Option(help="Print short view.")
 short_workspace = Option(help="Display Workspace Info adjusted for many projects.")
+
 strict = Option(
-    help="More strict checks when matching name of third-party libraries and imports"
+    help="More strict checks when matching name and version of third-party libraries and imports."
 )
 
 verbose = Option(help="More verbose output.")
diff --git a/components/polylith/commands/check.py b/components/polylith/commands/check.py
index de0acfd..5a8fe40 100644
--- a/components/polylith/commands/check.py
+++ b/components/polylith/commands/check.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Set
+from typing import List, Set
 
 from polylith import check, distributions, libs
 
@@ -27,7 +27,32 @@ def collect_known_aliases(project_data: dict, options: dict) -> Set[str]:
     deps = project_data["deps"]
     library_alias = options["alias"]
 
-    return distributions.known_aliases_and_sub_dependencies(deps, library_alias, options)
+    return distributions.known_aliases_and_sub_dependencies(
+        deps, library_alias, options
+    )
+
+
+def check_libs_versions(
+    projects_data: List[dict], all_projects_data: List[dict], options: dict
+) -> bool:
+    is_strict = options["strict"]
+    is_quiet = options["quiet"]
+
+    if not is_strict:
+        return True
+
+    development_data = next(p for p in all_projects_data if p["type"] == "development")
+
+    libraries = libs.report.libs_with_different_versions(
+        development_data, projects_data
+    )
+
+    if not is_quiet:
+        libs.report.print_libs_with_different_versions(
+            libraries, development_data, projects_data, options
+        )
+
+    return False if libraries else True
 
 
 def run(root: Path, ns: str, project_data: dict, options: dict) -> bool:
diff --git a/components/polylith/libs/report.py b/components/polylith/libs/report.py
index a486152..cb2e710 100644
--- a/components/polylith/libs/report.py
+++ b/components/polylith/libs/report.py
@@ -179,10 +179,14 @@ def libs_in_projects_table(
     return table
 
 
+def flattened_lib_names(projects_data: List[dict]) -> Set[str]:
+    return {k for proj in projects_data for k, _v in proj["deps"]["items"].items()}
+
+
 def print_libs_in_projects(
     development_data: dict, projects_data: List[dict], options: dict
 ) -> None:
-    flattened = {k for proj in projects_data for k, _v in proj["deps"]["items"].items()}
+    flattened = flattened_lib_names(projects_data)
 
     if not flattened:
         return
@@ -193,3 +197,41 @@ def print_libs_in_projects(
 
     console.print(Padding("[data]Library versions in projects[/]", (1, 0, 0, 0)))
     console.print(table, overflow="ellipsis")
+
+
+def has_different_version(
+    lib: str, development_data: dict, projects_data: List[dict]
+) -> bool:
+    proj_versions = [get_version(lib, p) for p in projects_data]
+    dev_version = get_version(lib, development_data)
+
+    return not is_same_version(proj_versions + [dev_version])
+
+
+def libs_with_different_versions(
+    development_data: dict, projects_data: List[dict]
+) -> Set[str]:
+    flattened = flattened_lib_names(projects_data)
+    return {
+        f
+        for f in flattened
+        if has_different_version(f, development_data, projects_data)
+    }
+
+
+def print_libs_with_different_versions(
+    libraries: Set[str],
+    development_data: dict,
+    projects_data: List[dict],
+    options: dict,
+) -> None:
+    if not libraries:
+        return
+
+    table = libs_in_projects_table(development_data, projects_data, libraries, options)
+
+    console = Console(theme=theme.poly_theme)
+    console.print(
+        Padding("[data]Different library versions in projects[/]", (1, 0, 0, 0))
+    )
+    console.print(table, overflow="ellipsis")
diff --git a/components/polylith/poetry/commands/check.py b/components/polylith/poetry/commands/check.py
index 7d8215a..3559c9f 100644
--- a/components/polylith/poetry/commands/check.py
+++ b/components/polylith/poetry/commands/check.py
@@ -9,7 +9,7 @@ from polylith.poetry import internals
 command_options = [
     option(
         long_name="strict",
-        description="More strict checks when matching name of third-party libraries and imports",
+        description="More strict checks when matching name and version of third-party libraries and imports.",
         flag=True,
     ),
     option(
@@ -27,18 +27,11 @@ class CheckCommand(Command):
 
     options = command_options
 
-    def print_report(self, root: Path, ns: str, project_data: dict) -> bool:
+    def print_report(
+        self, root: Path, ns: str, project_data: dict, options: dict
+    ) -> bool:
         path = project_data["path"]
         name = project_data["name"]
-        dists_fn = partial(internals.distributions, None, root)
-
-        options = {
-            "verbose": True if self.option("verbose") else False,
-            "quiet": True if self.option("quiet") else False,
-            "strict": True if self.option("strict") else False,
-            "alias": self.option("alias") or [],
-            "dists_fn": dists_fn,
-        }
 
         try:
             third_party_libs = internals.find_third_party_libs(self.poetry, path)
@@ -64,6 +57,20 @@ class CheckCommand(Command):
             self.poetry, directory, only_projects_data
         )
 
-        results = {self.print_report(root, ns, data) for data in projects_data}
+        dists_fn = partial(internals.distributions, None, root)
+        options = {
+            "verbose": True if self.option("verbose") else False,
+            "short": False,
+            "quiet": True if self.option("quiet") else False,
+            "strict": True if self.option("strict") else False,
+            "alias": self.option("alias") or [],
+            "dists_fn": dists_fn,
+        }
+
+        results = {self.print_report(root, ns, data, options) for data in projects_data}
+
+        libs_result = commands.check.check_libs_versions(
+            projects_data, all_projects_data, options
+        )
 
-        return 0 if all(results) else 1
+        return 0 if all(results) and libs_result else 1
diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml
index 31b5e52..c360715 100644
--- a/projects/poetry_polylith_plugin/pyproject.toml
+++ b/projects/poetry_polylith_plugin/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "poetry-polylith-plugin"
-version = "1.24.0"
+version = "1.25.0"
 description = "A Poetry plugin that adds tooling support for the Polylith Architecture"
 authors = ["David Vujic"]
 homepage = "https://davidvujic.github.io/python-polylith-docs/"
diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml
index 6d9ebd7..f04b87f 100644
--- a/projects/polylith_cli/pyproject.toml
+++ b/projects/polylith_cli/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "polylith-cli"
-version = "1.11.0"
+version = "1.12.0"
 description = "Python tooling support for the Polylith Architecture"
 authors = ['David Vujic']
 homepage = "https://davidvujic.github.io/python-polylith-docs/"


Test patch:
diff --git a/test/components/polylith/libs/test_report.py b/test/components/polylith/libs/test_report.py
index 27926d7..737dc70 100644
--- a/test/components/polylith/libs/test_report.py
+++ b/test/components/polylith/libs/test_report.py
@@ -98,3 +98,35 @@ def test_calculate_diff_strict_should_identify_close_match_for_dash_and_low_dash
     res = report.calculate_diff(brick_imports, third_party_libs, True)
 
     assert len(res) == 0
+
+
+def test_libs_versions_diff():
+    dev_data = {"deps": {"items": {"rich": "13.*"}}}
+    projects_data = [{"name": "one", "deps": {"items": {"rich": "13.*"}}}]
+
+    assert report.libs_with_different_versions(dev_data, projects_data) == set()
+
+
+def test_libs_versions_diff_should_return_libs_with_different_versions():
+    dev_data = {"deps": {"items": {"rich": "13.*"}}}
+
+    proj_one = {"name": "one", "deps": {"items": {"rich": "13.*"}}}
+    proj_two = {"name": "two", "deps": {"items": {"rich": "11.*"}}}
+    projects_data = [proj_one, proj_two]
+
+    res = report.libs_with_different_versions(dev_data, projects_data)
+
+    assert res == {"rich"}
+
+
+def test_libs_versions_diff_should_only_return_libs_with_different_versions():
+    dev_data = {"deps": {"items": {"rich": "13.*"}}}
+
+    proj_one = {"name": "one", "deps": {"items": {"rich": "13.*"}}}
+    proj_two = {"name": "two", "deps": {"items": {}}}
+
+    projects_data = [proj_one, proj_two]
+
+    res = report.libs_with_different_versions(dev_data, projects_data)
+
+    assert res == set()