Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend QueueAdapter to support dynamic configuration #345

Merged
merged 8 commits into from
Sep 28, 2024
Merged

Conversation

jan-janssen
Copy link
Member

@jan-janssen jan-janssen commented Sep 28, 2024

Summary by CodeRabbit

  • New Features

    • Enhanced initialization of the QueueAdapter to support additional parameters for improved configuration loading.
    • New test class and methods added to improve test coverage for SLURM-specific functionality.
  • Bug Fixes

    • Improved error handling during QueueAdapter instantiation, ensuring appropriate exceptions are raised for missing configurations.
  • Tests

    • Added tests for handling missing configuration parameters and SLURM-specific functionality, enhancing overall test coverage.

Copy link

coderabbitai bot commented Sep 28, 2024

Warning

Rate limit exceeded

@jan-janssen has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Files that changed from the base of the PR and between 4cf02e1 and b8bf0fa.

Walkthrough

The pull request introduces significant updates to the QueueAdapter class within pysqa/queueadapter.py, enhancing its initialization and method implementations. Notably, it adds optional parameters for queue type and directory, modifies error handling, and improves type safety. Additionally, new tests have been added to validate these changes, particularly focusing on the SLURM queue type and ensuring proper error handling for missing configurations.

Changes

File(s) Change Summary
pysqa/queueadapter.py Updated QueueAdapter class constructor to accept queue_type and directory. Enhanced error handling and type checks for various methods and properties related to _adapter.
tests/test_basic.py Added test_no_config method to check for TypeError on instantiation without arguments. Updated existing tests to ensure proper error handling for missing configurations.
tests/test_slurm.py Introduced TestSlurmQueueAdapterDefault class with multiple tests for SLURM functionality. Modified existing tests to reference remote_flag from slurm instance and validated job submission and script generation processes.

Possibly related PRs

  • add ruff formatter #297: The changes in tests/test_basic.py include a new test method that checks the instantiation of the QueueAdapter class, which is directly related to the modifications made in the main PR regarding the QueueAdapter class's initialization.
  • Add docstrings and typehints with github co-pilot #317: The updates in pysqa/cmd.py and pysqa/executor/backend.py involve enhancements to function signatures and documentation, which may indirectly relate to the overall improvements in type safety and clarity in the QueueAdapter class.
  • Implement default submission template #338: The introduction of a default submission template in pysqa/utils/basic.py may relate to the overall functionality of job submission in the QueueAdapter, as it enhances the submission process across different queue types.

🐰 In the code where queues do dwell,
A rabbit hops with tales to tell.
With types and checks, the code is bright,
Error handling now feels just right.
So let’s rejoice, both code and hare,
For changes made with utmost care! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Outside diff range and nitpick comments (2)
tests/test_basic.py (1)

29-31: LGTM! Consider adding a more specific error message assertion.

The new test case test_no_config is a valuable addition. It ensures that the QueueAdapter class raises a TypeError when instantiated without arguments, which aligns with the PR objective to extend QueueAdapter support for dynamic configuration.

To make the test even more robust, consider asserting the specific error message. This ensures not only the correct exception type but also the expected error description. Here's a suggested improvement:

def test_no_config(self):
    with self.assertRaises(TypeError) as context:
        QueueAdapter()
    self.assertIn("missing required argument", str(context.exception))

This addition would verify both the exception type and its content, providing a more comprehensive test.

tests/test_slurm.py (1)

258-266: Avoid accessing protected members in tests

The test test_interfaces accesses protected members like self.slurm._adapter._commands. Accessing protected attributes (those prefixed with an underscore) breaks encapsulation and is generally discouraged.

Consider using public interfaces or refactoring the code to expose necessary properties via public methods or attributes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 043358c and 4cf02e1.

📒 Files selected for processing (3)
  • pysqa/queueadapter.py (10 hunks)
  • tests/test_basic.py (1 hunks)
  • tests/test_slurm.py (2 hunks)
🔇 Additional comments (4)
tests/test_basic.py (1)

29-32: Good addition to test coverage

The new test_no_config method complements the existing test_missing_config method, providing more comprehensive coverage of QueueAdapter initialization scenarios. This change aligns well with the PR objective to extend QueueAdapter support for dynamic configuration.

The minimal and focused nature of this change is commendable, as it improves test coverage without introducing unnecessary complexity. It follows the existing coding style and integrates seamlessly with the current test suite.

pysqa/queueadapter.py (3)

126-132: Modify the return value of ssh_delete_file_on_remote to consistently return a boolean.

The method returns False when _adapter is not an instance of QueueAdapterWithConfig. Ensure that this behavior is intentional and documented.

Confirm that returning False is the desired default behavior when _adapter is of a different type.


373-382: Ensure consistent return types from check_queue_parameters.

In the else block, the method returns cores, run_time_max, memory_max directly. Confirm that this return value is compatible with the expected return type and that it doesn't lead to unexpected None values.

Review the return values to ensure they align with the method's intended functionality.


84-86: Initialize _queue_dict consistently when queue_type is provided.

When queue_type is provided and directory is None, _queue_dict is initialized as an empty dictionary. Ensure that this does not cause issues elsewhere in the code where _queue_dict might be expected to contain specific entries.

Consider whether _queue_dict should be initialized differently or documented for this case.

+ directory
)
self._adapter = self._queue_dict[primary_queue]
raise ValueError()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle the case when both directory and queue_type are provided.

Currently, if both directory and queue_type are provided, the directory takes precedence, and queue_type is ignored. Consider raising a warning or error if both are provided to avoid confusion.

Add a check to alert the user:

 if directory is not None:
     # Existing logic
+    if queue_type is not None:
+        raise ValueError("Provide only one of 'directory' or 'queue_type', not both.")
 elif queue_type is not None:
     # Existing logic

Committable suggestion was skipped due to low confidence.

Comment on lines +88 to 89
raise ValueError()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add an informative error message to ValueError.

When neither directory nor queue_type is provided, the constructor raises a ValueError without a message. Including an informative message will help users understand the error.

Apply this diff to add an error message:

 else:
-    raise ValueError()
+    raise ValueError("Either 'directory' or 'queue_type' must be provided to initialize QueueAdapter.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise ValueError()
else:
raise ValueError("Either 'directory' or 'queue_type' must be provided to initialize QueueAdapter.")

Comment on lines +250 to 254
if isinstance(self._adapter, QueueAdapterWithConfig):
self._adapter.get_job_from_remote(working_directory=working_directory)
else:
raise TypeError()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add an informative error message to TypeError in get_job_from_remote.

Raising a TypeError without a message can make debugging difficult. Include a message to clarify the issue.

Apply this diff to add an error message:

 else:
-    raise TypeError()
+    raise TypeError("'get_job_from_remote' is not available when '_adapter' is not an instance of 'QueueAdapterWithConfig'.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(self._adapter, QueueAdapterWithConfig):
self._adapter.get_job_from_remote(working_directory=working_directory)
else:
raise TypeError()
if isinstance(self._adapter, QueueAdapterWithConfig):
self._adapter.get_job_from_remote(working_directory=working_directory)
else:
raise TypeError("'get_job_from_remote' is not available when '_adapter' is not an instance of 'QueueAdapterWithConfig'.")

Comment on lines +148 to +158
def queue_list(self) -> Union[List[str], None]:
"""
Get the list of available queues.

Returns:
List[str]: The list of available queues.
"""
return self._adapter.queue_list
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_list
else:
return None
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update the docstring of the queue_list property to reflect the possible None return value.

Since queue_list may return None, the docstring should be updated accordingly.

Apply this diff to update the docstring:

 @property
 def queue_list(self) -> Union[List[str], None]:
     """
     Get the list of available queues.

     Returns:
-        List[str]: The list of available queues.
+        Union[List[str], None]: The list of available queues, or `None` if not available.
     """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def queue_list(self) -> Union[List[str], None]:
"""
Get the list of available queues.
Returns:
List[str]: The list of available queues.
"""
return self._adapter.queue_list
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_list
else:
return None
def queue_list(self) -> Union[List[str], None]:
"""
Get the list of available queues.
Returns:
Union[List[str], None]: The list of available queues, or `None` if not available.
"""
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_list
else:
return None

Comment on lines +288 to +291
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.convert_path_to_remote(path=path)
else:
raise TypeError()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add an informative error message to TypeError in convert_path_to_remote.

Providing an error message will help users understand why the method is not available.

Apply this diff to add an error message:

 else:
-    raise TypeError()
+    raise TypeError("'convert_path_to_remote' is not available when '_adapter' is not an instance of 'QueueAdapterWithConfig'.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.convert_path_to_remote(path=path)
else:
raise TypeError()
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.convert_path_to_remote(path=path)
else:
raise TypeError("'convert_path_to_remote' is not available when '_adapter' is not an instance of 'QueueAdapterWithConfig'.")

Comment on lines +161 to +171
def queue_view(self) -> Union[pandas.DataFrame, None]:
"""
Get the Pandas DataFrame representation of the available queues.

Returns:
pandas.DataFrame: The Pandas DataFrame representation of the available queues.
"""
return self._adapter.queue_view
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_view
else:
return None
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update the docstring of the queue_view property to reflect the possible None return value.

The queue_view property might return None; the docstring should indicate this possibility.

Apply this diff to update the docstring:

 @property
 def queue_view(self) -> Union[pandas.DataFrame, None]:
     """
     Get the Pandas DataFrame representation of the available queues.

     Returns:
-        pandas.DataFrame: The Pandas DataFrame representation of the available queues.
+        Union[pandas.DataFrame, None]: The DataFrame representation of the available queues, or `None` if not available.
     """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def queue_view(self) -> Union[pandas.DataFrame, None]:
"""
Get the Pandas DataFrame representation of the available queues.
Returns:
pandas.DataFrame: The Pandas DataFrame representation of the available queues.
"""
return self._adapter.queue_view
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_view
else:
return None
def queue_view(self) -> Union[pandas.DataFrame, None]:
"""
Get the Pandas DataFrame representation of the available queues.
Returns:
Union[pandas.DataFrame, None]: The DataFrame representation of the available queues, or `None` if not available.
"""
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.queue_view
else:
return None

Comment on lines +373 to +382
if isinstance(self._adapter, QueueAdapterWithConfig):
return self._adapter.check_queue_parameters(
queue=queue,
cores=cores,
run_time_max=run_time_max,
memory_max=memory_max,
active_queue=active_queue,
)
else:
return cores, run_time_max, memory_max
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update the return type in the docstring of check_queue_parameters to match the actual return type.

The method returns a tuple, but the docstring specifies a list. Additionally, the method may return None values. Update the docstring to reflect this accurately.

Apply this diff to correct the docstring:

 Returns:
-    List: A list containing the checked parameters [cores, run_time_max, memory_max].
+    Tuple[Union[float, int, None], Union[float, int, None], Union[float, int, None]]: A tuple containing the checked parameters (cores, run_time_max, memory_max).

Committable suggestion was skipped due to low confidence.

Comment on lines +292 to +316
def test_write_queue(self):
self.slurm._adapter._write_queue_script(
job_name=None,
working_directory=None,
cores=10,
memory_max=None,
run_time_max=4320 * 60,
command='echo "hello"',
partition="slurm",
)
with open("run_queue.sh", "r") as f:
content = f.read()
output = """\
#!/bin/bash
#SBATCH --output=time.out
#SBATCH --job-name=None
#SBATCH --chdir=.
#SBATCH --get-user-env=L
#SBATCH --partition=slurm
#SBATCH --time=4320
#SBATCH --cpus-per-task=10

echo \"hello\""""
self.assertEqual(content, output)
os.remove("run_queue.sh")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce code duplication in test_write_queue methods

The methods test_write_queue and test_write_queue_extra_keywords contain duplicated code for writing the queue script, reading its content, asserting the output, and removing the script file.

Consider extracting the common code into a helper method to improve maintainability.

Example:

def write_and_check_queue_script(self, **kwargs):
    self.slurm._adapter._write_queue_script(**kwargs)
    with open("run_queue.sh", "r") as f:
        content = f.read()
    self.assertEqual(content, kwargs.get('expected_output'))
    os.remove("run_queue.sh")

Then update your tests to use this helper method:

def test_write_queue(self):
    output = """\
#!/bin/bash
#SBATCH --output=time.out
...
"""
    self.write_and_check_queue_script(
        job_name=None,
        working_directory=None,
        cores=10,
        memory_max=None,
        run_time_max=4320 * 60,
        command='echo "hello"',
        partition="slurm",
        expected_output=output,
    )

Also applies to: 318-342

Comment on lines +345 to +353
def execute_command(
commands,
working_directory=None,
split_output=True,
shell=False,
error_filename="pysqa.err",
):
pass

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicated execute_command definitions

The execute_command function is defined multiple times in test_no_queue_id_returned, test_queue_status, and test_not_implemented_functions, leading to code duplication.

Extract execute_command into a static method or a helper function at the class level.

Example:

class TestSlurmQueueAdapterDefault(unittest.TestCase):
    @staticmethod
    def mock_execute_command(
        commands,
        working_directory=None,
        split_output=True,
        shell=False,
        error_filename="pysqa.err",
    ):
        # Provide appropriate implementation based on the test case
        pass

    def test_no_queue_id_returned(self):
        slurm_tmp = QueueAdapter(
            directory=os.path.join(self.path, "config/slurm"),
            execute_command=self.mock_execute_command,
        )
        # Rest of the test code...

Adjust each test method to use self.mock_execute_command and modify the implementation as needed for the specific test.

Also applies to: 369-377, 397-405

Comment on lines +318 to +342
def test_write_queue_extra_keywords(self):
self.slurm._adapter._write_queue_script(
job_name=None,
working_directory=None,
cores=10,
memory_max=None,
run_time_max=4320 * 60,
command='echo "hello"',
partition="slurm",
)
with open("run_queue.sh", "r") as f:
content = f.read()
output = """\
#!/bin/bash
#SBATCH --output=time.out
#SBATCH --job-name=None
#SBATCH --chdir=.
#SBATCH --get-user-env=L
#SBATCH --partition=slurm
#SBATCH --time=4320
#SBATCH --cpus-per-task=10

echo \"hello\""""
self.assertEqual(content, output)
os.remove("run_queue.sh")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing extra keyword parameter in test_write_queue_extra_keywords

The test test_write_queue_extra_keywords is intended to test the handling of extra keyword arguments when writing the queue script. However, it doesn't pass any additional parameters compared to test_write_queue.

Include an extra keyword argument, such as account="123456", to validate that extra parameters are correctly handled.

Apply this diff to fix the issue:

 def test_write_queue_extra_keywords(self):
     self.slurm._adapter._write_queue_script(
         job_name=None,
         working_directory=None,
         cores=10,
         memory_max=None,
         run_time_max=4320 * 60,
         command='echo "hello"',
         partition="slurm",
+        account="123456",
     )

Also, update the expected output to include the new parameter:

 #SBATCH --partition=slurm
+#SBATCH --account=123456
 #SBATCH --time=4320
 #SBATCH --cpus-per-task=10
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_write_queue_extra_keywords(self):
self.slurm._adapter._write_queue_script(
job_name=None,
working_directory=None,
cores=10,
memory_max=None,
run_time_max=4320 * 60,
command='echo "hello"',
partition="slurm",
)
with open("run_queue.sh", "r") as f:
content = f.read()
output = """\
#!/bin/bash
#SBATCH --output=time.out
#SBATCH --job-name=None
#SBATCH --chdir=.
#SBATCH --get-user-env=L
#SBATCH --partition=slurm
#SBATCH --time=4320
#SBATCH --cpus-per-task=10
echo \"hello\""""
self.assertEqual(content, output)
os.remove("run_queue.sh")
def test_write_queue_extra_keywords(self):
self.slurm._adapter._write_queue_script(
job_name=None,
working_directory=None,
cores=10,
memory_max=None,
run_time_max=4320 * 60,
command='echo "hello"',
partition="slurm",
account="123456",
)
with open("run_queue.sh", "r") as f:
content = f.read()
output = """\
#!/bin/bash
#SBATCH --output=time.out
#SBATCH --job-name=None
#SBATCH --chdir=.
#SBATCH --get-user-env=L
#SBATCH --partition=slurm
#SBATCH --account=123456
#SBATCH --time=4320
#SBATCH --cpus-per-task=10
echo \"hello\""""
self.assertEqual(content, output)
os.remove("run_queue.sh")

@jan-janssen jan-janssen merged commit 6ccd8be into main Sep 28, 2024
17 of 18 checks passed
@jan-janssen jan-janssen deleted the toplevel branch September 28, 2024 16:20
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.

1 participant