Skip to content

Conversation

@jalpuche1
Copy link

@jalpuche1 jalpuche1 commented Oct 8, 2025

Pull Request Check List

Relates-to: #9161

  • Added tests for changed code.
  • Updated documentation for changed code.

Summary

This PR adds new unit tests for existing functions in src/poetry/utils/helpers.py to improve test coverage and ensure consistent behavior across supported platforms.

Covered Functions

The following helper functions are now tested:

  • merge_dicts() – verifies recursive dictionary merging
  • paths_csv() – ensures correct comma-separated string output
  • ensure_path() – validates directory existence handling
  • is_dir_writable() – confirms directory write permissions
  • pluralize() – tests correct singular/plural word handling
  • get_file_hash() – ensures deterministic hash generation
  • remove_directory() – verifies safe recursive removal
  • directory() – confirms context-manager behavior for directory iteration
  • get_real_windows_path() – tested (and auto-skipped on non-Windows systems)

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 8, 2025

Reviewer's Guide

Introduces a new test module tests/test_helpers_additional.py to cover eight helper functions from helpers.py, including object creation, file/directory operations, environment isolation, data transformation, and architecture support, thereby improving unit test coverage.

File-Level Changes

Change Details Files
Added tests for package and dependency creation
  • get_package returns correct Package object
  • get_dependency handles string-based constraints
tests/test_helpers_additional.py
Added tests for file and directory operations
  • copy_path copies files and directories correctly
  • switch_working_directory changes and restores cwd
  • with_working_directory copies src into dest and cleans up
tests/test_helpers_additional.py
Added tests for environment isolation context
  • isolated_environment sets new vars and restores original environment
tests/test_helpers_additional.py
Added tests for dictionary flattening
  • flatten_dict recursively flattens nested dictionaries into dot-notation keys
tests/test_helpers_additional.py
Added tests for architecture variant support
  • pbs_installer_supported_arch returns true for valid archs and false for invalid ones
tests/test_helpers_additional.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The tests import helpers from tests.helpers, which may shadow the production helpers module—update the import to point directly at the actual helpers package to ensure you’re testing the real code.
  • Add a test case for switch_working_directory that raises an exception inside the context to confirm the original working directory is restored on error.
  • Consider adding edge‐case tests for copy_path, such as when the destination already exists or when handling symlinks, to ensure robust behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The tests import helpers from `tests.helpers`, which may shadow the production helpers module—update the import to point directly at the actual helpers package to ensure you’re testing the real code.
- Add a test case for `switch_working_directory` that raises an exception inside the context to confirm the original working directory is restored on error.
- Consider adding edge‐case tests for `copy_path`, such as when the destination already exists or when handling symlinks, to ensure robust behavior.

## Individual Comments

### Comment 1
<location> `tests/test_helpers_additional.py:22-37` </location>
<code_context>
+    assert str(dep.pretty_constraint) == ">=1.0"
+
+
+def test_copy_path_file_and_dir(tmp_path: Path):
+    # Copy a file
+    src_file = tmp_path / "a.txt"
+    src_file.write_text("hello")
+    dest_file = tmp_path / "b.txt"
+    helpers.copy_path(src_file, dest_file)
+    assert dest_file.exists()
+    assert dest_file.read_text() == "hello"
+
+    # Copy a directory
+    src_dir = tmp_path / "src"
+    src_dir.mkdir()
+    (src_dir / "c.txt").write_text("data")
+    dest_dir = tmp_path / "dest"
+    helpers.copy_path(src_dir, dest_dir)
+    assert (dest_dir / "c.txt").exists()
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Test copying when destination already exists and error conditions.

Please extend the tests to include scenarios where the destination already exists and where the source is missing, to verify error handling and overwriting behavior.

```suggestion
def test_copy_path_file_and_dir(tmp_path: Path):
    # Copy a file
    src_file = tmp_path / "a.txt"
    src_file.write_text("hello")
    dest_file = tmp_path / "b.txt"
    helpers.copy_path(src_file, dest_file)
    assert dest_file.exists()
    assert dest_file.read_text() == "hello"

    # Copy a file when destination already exists (should overwrite)
    dest_file.write_text("old")
    helpers.copy_path(src_file, dest_file)
    assert dest_file.read_text() == "hello"

    # Copy a directory
    src_dir = tmp_path / "src"
    src_dir.mkdir()
    (src_dir / "c.txt").write_text("data")
    dest_dir = tmp_path / "dest"
    helpers.copy_path(src_dir, dest_dir)
    assert (dest_dir / "c.txt").exists()

    # Copy a directory when destination already exists (should overwrite file inside)
    (dest_dir / "c.txt").write_text("old data")
    helpers.copy_path(src_dir, dest_dir)
    assert (dest_dir / "c.txt").read_text() == "data"

    # Error: source file does not exist
    missing_file = tmp_path / "missing.txt"
    with pytest.raises(FileNotFoundError):
        helpers.copy_path(missing_file, tmp_path / "should_not_exist.txt")

    # Error: source directory does not exist
    missing_dir = tmp_path / "missing_dir"
    with pytest.raises(FileNotFoundError):
        helpers.copy_path(missing_dir, tmp_path / "should_not_exist_dir")
```
</issue_to_address>

### Comment 2
<location> `tests/test_helpers_additional.py:52-56` </location>
<code_context>
+        assert os.environ.get(key) == val
+
+
+def test_flatten_dict_nested():
+    nested = {"a": {"b": {"c": 1}}, "x": 2}
+    flat = helpers.flatten_dict(nested)
+    assert flat["a.b.c"] == 1
+    assert flat["x"] == 2
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for edge cases in flatten_dict, such as empty dicts and non-dict values.

Add tests for empty dictionaries, non-dict values, and more deeply nested cases to improve coverage and error handling.

```suggestion
def test_flatten_dict_nested():
    nested = {"a": {"b": {"c": 1}}, "x": 2}
    flat = helpers.flatten_dict(nested)
    assert flat["a.b.c"] == 1
    assert flat["x"] == 2

def test_flatten_dict_empty():
    empty = {}
    flat = helpers.flatten_dict(empty)
    assert flat == {}

def test_flatten_dict_non_dict_value():
    # If flatten_dict expects a dict, passing a non-dict should raise an error
    with pytest.raises(TypeError):
        helpers.flatten_dict(42)

def test_flatten_dict_deeply_nested():
    nested = {"a": {"b": {"c": {"d": {"e": 5}}}}, "x": 2}
    flat = helpers.flatten_dict(nested)
    assert flat["a.b.c.d.e"] == 5
    assert flat["x"] == 2

def test_flatten_dict_mixed_types():
    mixed = {"a": {"b": [1, 2, 3]}, "x": None}
    flat = helpers.flatten_dict(mixed)
    assert flat["a.b"] == [1, 2, 3]
    assert flat["x"] is None
```
</issue_to_address>

### Comment 3
<location> `tests/test_helpers_additional.py:59-64` </location>
<code_context>
+    assert flat["x"] == 2
+
+
+def test_switch_working_directory_creates_and_restores(tmp_path: Path):
+    cwd = Path.cwd()
+    new_dir = tmp_path
+    with helpers.switch_working_directory(new_dir):
+        assert Path.cwd() == new_dir
+    assert Path.cwd() == cwd
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Test error handling for non-existent directories in switch_working_directory.

Add a test to ensure switch_working_directory raises an exception and leaves the working directory unchanged when given a non-existent directory.

```suggestion
def test_switch_working_directory_creates_and_restores(tmp_path: Path):
    cwd = Path.cwd()
    new_dir = tmp_path
    with helpers.switch_working_directory(new_dir):
        assert Path.cwd() == new_dir
    assert Path.cwd() == cwd


def test_switch_working_directory_nonexistent_dir(tmp_path: Path):
    cwd = Path.cwd()
    non_existent_dir = tmp_path / "does_not_exist"
    with pytest.raises(Exception):
        with helpers.switch_working_directory(non_existent_dir):
            pass
    assert Path.cwd() == cwd
```
</issue_to_address>

### Comment 4
<location> `tests/test_helpers_additional.py:6` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 5
<location> `tests/test_helpers_additional.py:48-49` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid loops in tests. ([`no-loop-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-loop-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

### Comment 6
<location> `tests/test_helpers_additional.py:83-84` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid loops in tests. ([`no-loop-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-loop-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

### Comment 7
<location> `tests/test_helpers_additional.py:87-88` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid loops in tests. ([`no-loop-in-tests`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/no-loop-in-tests))

<details><summary>Explanation</summary>Avoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:
* loops
* conditionals

Some ways to fix this:

* Use parametrized tests to get rid of the loop.
* Move the complex logic into helpers.
* Move the complex part into pytest fixtures.

> Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / [Don't Put Logic in Tests](https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests)
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 22 to 37
def test_copy_path_file_and_dir(tmp_path: Path):
# Copy a file
src_file = tmp_path / "a.txt"
src_file.write_text("hello")
dest_file = tmp_path / "b.txt"
helpers.copy_path(src_file, dest_file)
assert dest_file.exists()
assert dest_file.read_text() == "hello"

# Copy a directory
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "c.txt").write_text("data")
dest_dir = tmp_path / "dest"
helpers.copy_path(src_dir, dest_dir)
assert (dest_dir / "c.txt").exists()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Test copying when destination already exists and error conditions.

Please extend the tests to include scenarios where the destination already exists and where the source is missing, to verify error handling and overwriting behavior.

Suggested change
def test_copy_path_file_and_dir(tmp_path: Path):
# Copy a file
src_file = tmp_path / "a.txt"
src_file.write_text("hello")
dest_file = tmp_path / "b.txt"
helpers.copy_path(src_file, dest_file)
assert dest_file.exists()
assert dest_file.read_text() == "hello"
# Copy a directory
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "c.txt").write_text("data")
dest_dir = tmp_path / "dest"
helpers.copy_path(src_dir, dest_dir)
assert (dest_dir / "c.txt").exists()
def test_copy_path_file_and_dir(tmp_path: Path):
# Copy a file
src_file = tmp_path / "a.txt"
src_file.write_text("hello")
dest_file = tmp_path / "b.txt"
helpers.copy_path(src_file, dest_file)
assert dest_file.exists()
assert dest_file.read_text() == "hello"
# Copy a file when destination already exists (should overwrite)
dest_file.write_text("old")
helpers.copy_path(src_file, dest_file)
assert dest_file.read_text() == "hello"
# Copy a directory
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "c.txt").write_text("data")
dest_dir = tmp_path / "dest"
helpers.copy_path(src_dir, dest_dir)
assert (dest_dir / "c.txt").exists()
# Copy a directory when destination already exists (should overwrite file inside)
(dest_dir / "c.txt").write_text("old data")
helpers.copy_path(src_dir, dest_dir)
assert (dest_dir / "c.txt").read_text() == "data"
# Error: source file does not exist
missing_file = tmp_path / "missing.txt"
with pytest.raises(FileNotFoundError):
helpers.copy_path(missing_file, tmp_path / "should_not_exist.txt")
# Error: source directory does not exist
missing_dir = tmp_path / "missing_dir"
with pytest.raises(FileNotFoundError):
helpers.copy_path(missing_dir, tmp_path / "should_not_exist_dir")

Copy link
Member

@radoering radoering left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests for utils/helpers.py should be in tests/utils/test_helpers.py. Please move them there instead of creating a new test file.

Many of the tests are just smoke tests, but I suppose it is a start and better than nothing.

assert "," in result


def test_ensure_path_creates_directory(tmp_path: Path) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure_path does not create a directory. It just raises an error if the directory/file does not exist. Here, the different cases should be tested.

assert result.is_dir()


def test_is_dir_writable_returns_true(tmp_path: Path) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here a parametrized test would make sense.

assert helpers.pluralize(3, "file") == "files"


def test_get_file_hash(tmp_path: Path) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, it probably makes more sense to write a file (bytes) and check for the fixed hash.


# directory() is a context manager, not an iterator
with helpers.directory(tmp_path) as path:
assert path == tmp_path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we should check the cwd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants