Repository: databrickslabs/blueprint (language: python)

Issue:
Create `ProductInfo.for_testing(klass)` factory
at the moment, it's a bit cumbersome to do

```
        product_name = make_random(4)
        product_info = ProductInfo.from_class(WorkspaceConfig)
        product_info.product_name = lambda: product_name
```


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index c144575..363633c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 <!-- FOR CONTRIBUTORS: Edit this file in Visual Studio Code with the recommended extensions, so that we update the table of contents automatically -->
 Databricks Labs Blueprint
----
+===
 
 [![python](https://img.shields.io/badge/python-3.10,%203.11,%203.12-green)](https://github.com/databrickslabs/blueprint/actions/workflows/push.yml)
 [![codecov](https://codecov.io/github/databrickslabs/blueprint/graph/badge.svg?token=x1JSVddfZa)](https://codecov.io/github/databrickslabs/blueprint) [![lines of code](https://tokei.rs/b1/github/databrickslabs/blueprint)]([https://codecov.io/github/databrickslabs/blueprint](https://github.com/databrickslabs/blueprint))
@@ -8,49 +8,53 @@ Databricks Labs Blueprint
 
 Baseline for Databricks Labs projects written in Python. Sources are validated with `mypy` and `pylint`. See [Contributing instructions](CONTRIBUTING.md) if you would like to improve this project.
 
-- [Installation](#installation)
-- [Batteries Included](#batteries-included)
-  - [Basic Terminal User Interface (TUI) Primitives](#basic-terminal-user-interface-tui-primitives)
-    - [Simple Text Questions](#simple-text-questions)
-    - [Confirming Actions](#confirming-actions)
-    - [Single Choice from List](#single-choice-from-list)
-    - [Single Choice from Dictionary](#single-choice-from-dictionary)
-    - [Multiple Choices from Dictionary](#multiple-choices-from-dictionary)
-    - [Unit Testing Prompts](#unit-testing-prompts)
-  - [Nicer Logging Formatter](#nicer-logging-formatter)
-    - [Rendering on Dark Background](#rendering-on-dark-background)
-    - [Rendering in Databricks Notebooks](#rendering-in-databricks-notebooks)
-    - [Integration With Your App](#integration-with-your-app)
-    - [Integration with `console_script` Entrypoints](#integration-with-console_script-entrypoints)
-  - [Parallel Task Execution](#parallel-task-execution)
-    - [Collecting Results](#collecting-results)
-    - [Collecting Errors from Background Tasks](#collecting-errors-from-background-tasks)
-    - [Strict Failures from Background Tasks](#strict-failures-from-background-tasks)
-  - [Application and Installation State](#application-and-installation-state)
-    - [Install Folder](#install-folder)
-    - [Detecting Current Installation](#detecting-current-installation)
-    - [Detecting Installations From All Users](#detecting-installations-from-all-users)
-    - [Saving `@dataclass` configuration](#saving-dataclass-configuration)
-    - [Saving CSV files](#saving-csv-files)
-    - [Loading `@dataclass` configuration](#loading-dataclass-configuration)
-    - [Brute-forcing `SerdeError` with `as_dict()` and `from_dict()`](#brute-forcing-serdeerror-with-as_dict-and-from_dict)
-    - [Configuration Format Evolution](#configuration-format-evolution)
-    - [Uploading Untyped Files](#uploading-untyped-files)
-    - [Listing All Files in the Install Folder](#listing-all-files-in-the-install-folder)
-    - [Unit Testing Installation State](#unit-testing-installation-state)
-    - [Assert Rewriting with PyTest](#assert-rewriting-with-pytest)
-  - [Application State Migrations](#application-state-migrations)
-  - [Building Wheels](#building-wheels)
-    - [Released Version Detection](#released-version-detection)
-    - [Unreleased Version Detection](#unreleased-version-detection)
-    - [Application Name Detection](#application-name-detection)
-    - [Publishing Wheels to Databricks Workspace](#publishing-wheels-to-databricks-workspace)
-  - [Databricks CLI's `databricks labs ...` Router](#databricks-clis-databricks-labs--router)
-    - [Account-level Commands](#account-level-commands)
-    - [Integration with Databricks Connect](#integration-with-databricks-connect)
-    - [Starting New Projects](#starting-new-projects)
-- [Notable Downstream Projects](#notable-downstream-projects)
-- [Project Support](#project-support)
+<!-- TOC -->
+* [Databricks Labs Blueprint](#databricks-labs-blueprint)
+* [Installation](#installation)
+* [Batteries Included](#batteries-included)
+  * [Basic Terminal User Interface (TUI) Primitives](#basic-terminal-user-interface-tui-primitives)
+    * [Simple Text Questions](#simple-text-questions)
+    * [Confirming Actions](#confirming-actions)
+    * [Single Choice from List](#single-choice-from-list)
+    * [Single Choice from Dictionary](#single-choice-from-dictionary)
+    * [Multiple Choices from Dictionary](#multiple-choices-from-dictionary)
+    * [Unit Testing Prompts](#unit-testing-prompts)
+  * [Nicer Logging Formatter](#nicer-logging-formatter)
+    * [Rendering on Dark Background](#rendering-on-dark-background)
+    * [Rendering in Databricks Notebooks](#rendering-in-databricks-notebooks)
+    * [Integration With Your App](#integration-with-your-app)
+    * [Integration with `console_script` Entrypoints](#integration-with-consolescript-entrypoints)
+  * [Parallel Task Execution](#parallel-task-execution)
+    * [Collecting Results](#collecting-results)
+    * [Collecting Errors from Background Tasks](#collecting-errors-from-background-tasks)
+    * [Strict Failures from Background Tasks](#strict-failures-from-background-tasks)
+  * [Application and Installation State](#application-and-installation-state)
+    * [Install Folder](#install-folder)
+    * [Detecting Current Installation](#detecting-current-installation)
+    * [Detecting Installations From All Users](#detecting-installations-from-all-users)
+    * [Saving `@dataclass` configuration](#saving-dataclass-configuration)
+    * [Saving CSV files](#saving-csv-files)
+    * [Loading `@dataclass` configuration](#loading-dataclass-configuration)
+    * [Brute-forcing `SerdeError` with `as_dict()` and `from_dict()`](#brute-forcing-serdeerror-with-asdict-and-fromdict)
+    * [Configuration Format Evolution](#configuration-format-evolution)
+    * [Uploading Untyped Files](#uploading-untyped-files)
+    * [Listing All Files in the Install Folder](#listing-all-files-in-the-install-folder)
+    * [Unit Testing Installation State](#unit-testing-installation-state)
+    * [Assert Rewriting with PyTest](#assert-rewriting-with-pytest)
+  * [Application State Migrations](#application-state-migrations)
+  * [Building Wheels](#building-wheels)
+    * [Released Version Detection](#released-version-detection)
+    * [Unreleased Version Detection](#unreleased-version-detection)
+    * [Application Name Detection](#application-name-detection)
+    * [Using `ProductInfo` with integration tests](#using-productinfo-with-integration-tests)
+    * [Publishing Wheels to Databricks Workspace](#publishing-wheels-to-databricks-workspace)
+  * [Databricks CLI's `databricks labs ...` Router](#databricks-clis-databricks-labs--router)
+    * [Account-level Commands](#account-level-commands)
+    * [Integration with Databricks Connect](#integration-with-databricks-connect)
+    * [Starting New Projects](#starting-new-projects)
+* [Notable Downstream Projects](#notable-downstream-projects)
+* [Project Support](#project-support)
+<!-- TOC -->
 
 # Installation
 
@@ -403,6 +407,8 @@ The `install_folder` method returns the path to the installation folder on Works
 is used to store typed files that are managed by the `Installation` class. [Publishing wheels](#publishing-wheels-to-databricks-workspace) 
 update the `version.json` file in the install folder.
 
+When integration testing, you may want to have a [random installation folder](#using-productinfo-with-integration-tests) for each test execution.
+
 If an `install_folder` argument is provided to the constructor of the `Installation` class, it will be used
 as the installation folder. Otherwise, the installation folder will be determined based on the current user's
 username. Specifically, the installation folder will be `/Users/{user_name}/.{product}`, where `{user_name}`
@@ -871,6 +877,7 @@ logger.info(f'Is unreleased: {is_unreleased}')
 ### Application Name Detection
 
 Library can infer the name of application by taking the directory name when `__about__.py` file is located within the current project. See [released version detection](#released-version-detection) for more details.
+[`ProductInfo.for_testing(klass)`](#using-productinfo-with-integration-tests) creates a new `ProductInfo` object with a random `product_name`.
 
 ```python
 from databricks.labs.blueprint.wheels import ProductInfo
@@ -881,6 +888,21 @@ logger.info(f'Product name is: {product_info.product_name()}')
 
 [[back to top](#databricks-labs-blueprint)]
 
+### Using `ProductInfo` with integration tests
+
+When you're integration testing your [installations](#installation), you may want to have different [installation folders](#install-folder) for each test execution. `ProductInfo.for_testing(klass)` helps you with this:
+
+```python
+from ... import ConfigurationClass
+from databricks.labs.blueprint.wheels import ProductInfo
+
+first = ProductInfo.for_testing(ConfigurationClass)
+second = ProductInfo.for_testing(ConfigurationClass)
+assert first.product_name() != second.product_name()
+```
+
+[[back to top](#databricks-labs-blueprint)]
+
 ### Publishing Wheels to Databricks Workspace
 
 Before you execute a wheel on Databricks, you have to build it and upload it. This library provides detects [released](#released-version-detection) or [unreleased](#unreleased-version-detection) version of the wheel, copies it over to a temporary folder, changes the `__about__.py` file with the right version, and builds the wheel in the temporary location, so that it's not polluted with build artifacts. `Wheels` is a context manager, so it removes all temporary files and folders ather `with` block finishes. This library is successfully used to concurrently test wheels on Shared Databricks Clusters through notebook-scoped libraries. Before you deploy the new version of the wheel, it is highly advised that you perform [application state upgrades](#application-state-migrations).
@@ -889,14 +911,13 @@ Every call `wheels.upload_to_wsfs()` updates `version.json` file in the [install
 
 ```python
 from databricks.sdk import WorkspaceClient
-from databricks.labs.blueprint.installation import Installation
-from databricks.labs.blueprint.wheels import ProductInfo, WheelsV2
+from databricks.labs.blueprint.wheels import ProductInfo
 
 w = WorkspaceClient()
 product_info = ProductInfo(__file__)
-installation = Installation(w, product_info.product_name())
+installation = product_info.current_installation(w)
 
-with WheelsV2(installation, product_info) as wheels:
+with product_info.wheels(w) as wheels:
     remote_wheel = wheels.upload_to_wsfs()
     logger.info(f'Uploaded to {remote_wheel}')
 ```
diff --git a/src/databricks/labs/blueprint/wheels.py b/src/databricks/labs/blueprint/wheels.py
index 5a7de7f..53b3530 100644
--- a/src/databricks/labs/blueprint/wheels.py
+++ b/src/databricks/labs/blueprint/wheels.py
@@ -1,6 +1,8 @@
 import inspect
 import logging
+import random
 import shutil
+import string
 import subprocess
 import sys
 import tempfile
@@ -40,14 +42,20 @@ class SingleSourceVersionError(NotImplementedError):
 class ProductInfo:
     _version_file_names = ["__about__.py", "__version__.py", "version.py"]
 
-    def __init__(self, __file: str, *, github_org: str = "databrickslabs"):
+    def __init__(self, __file: str, *, github_org: str = "databrickslabs", product_name: str | None = None):
         self._version_file = self._infer_version_file(Path(__file), self._version_file_names)
+        self._product_name = product_name
         self._github_org = github_org
 
     @classmethod
     def from_class(cls, klass: type) -> "ProductInfo":
         return cls(inspect.getfile(klass))
 
+    @classmethod
+    def for_testing(cls, klass: type) -> "ProductInfo":
+        """Create a product info for testing purposes with a random product name."""
+        return cls(inspect.getfile(klass), product_name=cls._make_random(4))
+
     def checkout_root(self):
         return find_project_root(self._version_file.as_posix())
 
@@ -75,6 +83,9 @@ def as_semver(self) -> SemVer:
         return SemVer.parse(self.version())
 
     def product_name(self) -> str:
+        """Returns the product name based on the version file folder name."""
+        if self._product_name:
+            return self._product_name
         version_file_folder = self._version_file.parent
         return version_file_folder.name.replace("_", "-")
 
@@ -109,6 +120,20 @@ def current_installation(self, ws: WorkspaceClient) -> Installation:
     def wheels(self, ws: WorkspaceClient) -> "WheelsV2":
         return WheelsV2(self.current_installation(ws), self)
 
+    @staticmethod
+    def _make_random(k) -> str:
+        """Generate a random string of fixed length"""
+        # get a random meaningful word from the system dictionary if it exists
+        system_wordlist = Path("/usr/share/dict/words")
+        if system_wordlist.exists():
+            with system_wordlist.open("r", encoding=sys.getdefaultencoding()) as f:
+                at_lest_len = [_.lower() for _ in f.read().splitlines() if len(_) > k]
+                first = random.choice(at_lest_len)
+                second = random.choice(at_lest_len)
+                return f"{first}-{second}"
+        charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
+        return "".join(random.choices(charset, k=int(k)))
+
     @staticmethod
     def _semver_and_pep440(git_detached_version: str) -> str:
         dv = SemVer.parse(git_detached_version)


Test patch:
diff --git a/tests/unit/test_wheels.py b/tests/unit/test_wheels.py
index 4113508..cf5b4b0 100644
--- a/tests/unit/test_wheels.py
+++ b/tests/unit/test_wheels.py
@@ -79,3 +79,12 @@ def test_marker_without_version_variable():
     no_version_fixture = Path(__file__).parent / "fixtures/some/__init__.py"
     with pytest.raises(SingleSourceVersionError):
         ProductInfo(no_version_fixture.as_posix())
+
+
+def test_product_info_for_testing_product_names_are_different():
+    a = ProductInfo.from_class(MockInstallation)
+    b = ProductInfo.for_testing(MockInstallation)
+    c = ProductInfo.for_testing(MockInstallation)
+    assert a.product_name() != b.product_name()
+    assert b.product_name() != c.product_name()
+    assert a.product_name() != c.product_name()