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

Fix collections.ControlledDict handling of Iterators via update method #732

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

DanielYang59
Copy link
Contributor

@DanielYang59 DanielYang59 commented Dec 11, 2024

Summary by CodeRabbit

  • New Features

    • Expanded compatibility to support Python versions 3.10 through 3.13.
    • Enhanced the ControlledDict to accept zipped lists for updates.
    • Introduced a new warning class for encoding issues during file operations.
  • Bug Fixes

    • Improved error handling in the update method of ControlledDict.
    • Clarified documentation for the setdefault method.
    • Enhanced file handling logic in the zopen function to enforce explicit mode and encoding.
  • Tests

    • Added new test cases for iterator handling in ControlledDict.
    • Updated comments in Namespace tests to reflect changes in expected behavior.
    • Streamlined imports in the test suite related to encoding warnings.

Copy link

coderabbitai bot commented Dec 11, 2024

Walkthrough

The pull request introduces changes to several files, primarily focusing on enhancing Python version support in testing and project configuration. The .github/workflows/test.yml file has been updated to include Python versions 3.11 and 3.13 in the testing matrix. The pyproject.toml file's requires-python field was modified to restrict supported versions to between 3.10 and 3.13. Additionally, improvements were made to the ControlledDict class in src/monty/collections.py, specifically in error handling during updates, and new tests were added in tests/test_collections.py to enhance coverage and clarify expected behaviors.

Changes

File Change Summary
.github/workflows/test.yml Updated Python version matrix from ["3.10", "3.12"] to ["3.10", "3.11", "3.12", "3.13"].
pyproject.toml Changed requires-python from ">=3.10" to ">=3.10,<=3.13".
src/monty/collections.py Modified update method in ControlledDict for better error handling; clarified setdefault doc.
tests/test_collections.py Added tests for ControlledDict to handle zipped updates; clarified comments in test_namespace_dict.
src/monty/io.py Enhanced zopen function for file mode and encoding handling; added EncodingWarning.
tests/test_io.py Removed import of EncodingWarning from monty.io, streamlining test imports.

🐇 In the meadow, we hop with glee,
New Python versions, oh what a spree!
ControlledDict now handles with care,
Updates and tests, we joyfully share.
With each little change, our code takes flight,
Hopping towards future, all feels just right! 🌼

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


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 generate docstrings to generate docstrings for this PR. (Beta)
  • @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

codecov bot commented Dec 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 84.19%. Comparing base (26acf0b) to head (b1c9df3).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #732   +/-   ##
=======================================
  Coverage   84.19%   84.19%           
=======================================
  Files          27       27           
  Lines        1670     1670           
  Branches      314      313    -1     
=======================================
  Hits         1406     1406           
  Misses        202      202           
  Partials       62       62           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@DanielYang59
Copy link
Contributor Author

@coderabbitai review

Copy link

coderabbitai bot commented Dec 11, 2024

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 1

🧹 Outside diff range and nitpick comments (3)
tests/test_collections.py (1)

65-68: LGTM! Consider adding more iterator test cases.

The test for iterator handling is a good addition and aligns with the PR objective. However, consider enhancing test coverage with additional cases:

# Test empty iterator
dct.update(zip([], []))
assert len(dct) == previous_len  # No changes

# Test exhausted iterator
iterator = zip(["e"], [13])
list(iterator)  # exhaust it
dct.update(iterator)
assert "e" not in dct  # No changes

# Test iterator with invalid values
with pytest.raises(ValueError):
    dct.update(zip(["f"], []))  # uneven length
.github/workflows/test.yml (1)

12-12: LGTM! Consider matrix optimization.

The addition of Python 3.11 and 3.13 to the test matrix improves coverage. However, with 4 Python versions and 2 OS targets, you're now running 8 parallel jobs.

Consider if you need to test all Python versions on all OS targets. You might optimize by:

  • Testing all Python versions on Ubuntu
  • Testing only LTS Python versions (3.10, 3.12) on macOS
    This would reduce the job count while maintaining good coverage.
src/monty/collections.py (1)

Line range hint 92-104: LGTM with minor performance optimization suggestion.

The changes correctly fix the iterator handling by constructing a dictionary before validation. However, the dict construction is performed twice unnecessarily.

Consider optimizing the validation loop:

-        updates = dict(*args, **kwargs)
-        for key in dict(*args, **kwargs):
+        updates = dict(*args, **kwargs)
+        for key in updates:
             if key not in self.data and not self._allow_add:
                 raise TypeError(
                     f"Cannot add new key {key!r} using update, because add is disabled."
                 )
             elif key in self.data and not self._allow_update:
                 raise TypeError(
                     f"Cannot update key {key!r} using update, because update is disabled."
                 )
         super().update(updates)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 26acf0b and da8be8e.

📒 Files selected for processing (4)
  • .github/workflows/test.yml (1 hunks)
  • pyproject.toml (1 hunks)
  • src/monty/collections.py (2 hunks)
  • tests/test_collections.py (2 hunks)
🔇 Additional comments (2)
tests/test_collections.py (1)

164-167: LGTM! Clear test case for Namespace update behavior.

The test case clearly documents that update can be used to add new values to a Namespace, which is distinct from updating existing values.

src/monty/collections.py (1)

Line range hint 92-104: Verify fix against pymatgen issue.

Let's verify that this change resolves the iterator handling issue mentioned in pymatgen PR #4223.

✅ Verification successful

Iterator handling in ControlledDict.update() is properly tested and working

Looking at the test coverage in tests/test_collections.py, specifically the test_update_allowed method, there is explicit testing for iterator handling:

# Test Iterator handling
dct.update(zip(["c", "d"], [11, 12]))
assert dct["c"] == 11

The implementation in the review correctly handles iterators by using dict(*args, **kwargs) to process the input before updating, and the test suite verifies this behavior. The changes appear to properly resolve any iterator-related issues.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for related test cases and usage patterns in pymatgen

# Look for ControlledDict usage with iterators in tests
rg -A 5 "ControlledDict.*update.*zip\(" || \
rg -A 5 "ControlledDict.*update.*iter\(" || \
rg -A 5 "ControlledDict.*update.*map\("

# Look for any failing tests related to ControlledDict
rg -B 2 -A 5 "def test.*ControlledDict.*fail"

Length of output: 174


Script:

#!/bin/bash
# Let's try a broader search to understand the context and test coverage

# Search for ControlledDict tests
rg -B 2 -A 5 "class.*Test.*ControlledDict"

# Look for update method tests specifically
rg -B 2 -A 5 "test.*update" src/monty/collections.py

# Search for any test files that might contain ControlledDict tests
fd "test.*\.py" | xargs rg "ControlledDict"

# Look for the actual implementation to understand the context better
ast-grep --pattern 'class ControlledDict {
  $$$
  update($$$) {
    $$$
  }
  $$$
}'

Length of output: 1272


Script:

#!/bin/bash
# Let's look at the specific test cases for update method
rg -B 5 -A 10 "def test.*update" tests/test_collections.py

# Also check the actual test file content to understand the test coverage
cat tests/test_collections.py

# Look for any related issues or PRs in the commit history
git log --grep="ControlledDict.*update" --grep="pymatgen.*4223" -i

Length of output: 7848

pyproject.toml Show resolved Hide resolved
@DanielYang59 DanielYang59 marked this pull request as ready for review December 11, 2024 15:11
@DanielYang59 DanielYang59 force-pushed the fix-namespace-dict-with-iterator branch from 0aeb657 to d54bde4 Compare December 12, 2024 10:56
@DanielYang59 DanielYang59 marked this pull request as draft December 13, 2024 11:56
@DanielYang59 DanielYang59 marked this pull request as ready for review December 13, 2024 12:03
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: 0

🔭 Outside diff range comments (1)
src/monty/io.py (1)

Line range hint 337-463: Enhance FileLock security and robustness

The FileLock implementation could benefit from additional security measures and robustness improvements.

Consider these security enhancements:

     def acquire(self) -> None:
+        # Ensure atomic creation of lock file
+        lock_dir = os.path.dirname(self.lockfile)
+        if not os.path.exists(lock_dir):
+            os.makedirs(lock_dir, mode=0o755, exist_ok=True)
+
         start_time = time.time()
         while True:
             try:
-                self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                # Use restrictive permissions for the lock file
+                old_umask = os.umask(0o077)
+                try:
+                    self.fd = os.open(
+                        self.lockfile,
+                        os.O_CREAT | os.O_EXCL | os.O_RDWR,
+                        mode=0o600,
+                    )
+                finally:
+                    os.umask(old_umask)
                 break

This change:

  1. Ensures the lock directory exists
  2. Uses restrictive permissions for the lock file
  3. Properly handles umask restoration
🧹 Nitpick comments (3)
src/monty/io.py (3)

Line range hint 61-90: Enhance warning messages structure and consistency

The warning messages are well-implemented but could benefit from some improvements:

Consider applying these enhancements:

-            "We strongly discourage using a default `mode`, it would be "
-            f"set to `r` now but would not be allowed after {_deadline}",
+            f"Default mode 'r' is deprecated and will be removed after {_deadline}. "
+            "Please explicitly specify the file mode.",
             FutureWarning,
             stacklevel=2,
         )

-            "We strongly discourage using implicit binary/text `mode`, "
-            f"and this would not be allowed after {_deadline}. "
-            "I.e. you should pass t/b in `mode`.",
+            f"Implicit binary/text mode is deprecated and will be removed after {_deadline}. "
+            "Please explicitly include 'b' or 't' in the mode string.",
             FutureWarning,
             stacklevel=2,
         )

-            "We strongly encourage explicit `encoding`, "
-            "and we would use UTF-8 by default as per PEP 686",
+            "No encoding specified. Using UTF-8 by default (PEP 686). "
+            "Please explicitly specify the encoding for text mode operations.",

This makes the messages more concise and action-oriented while maintaining all the important information.


Line range hint 132-169: Enhance type safety in line ending detection

The function handles different file types well, but we can improve type safety and error handling.

Consider these improvements:

 def _get_line_ending(
     file: str
     | Path
     | io.TextIOWrapper
     | io.BufferedReader
     | gzip.GzipFile
     | bz2.BZ2File,
 ) -> Literal["\r\n", "\n"]:
+    # Type guard for binary data
+    def ensure_bytes(data: bytes | None) -> bytes | None:
+        if data is not None and not isinstance(data, bytes):
+            raise TypeError(f"Expected bytes, got {type(data).__name__}")
+        return data
+
     if isinstance(file, (str, Path)):
         with zopen(file, "rb") as f:
-            first_line = f.readline()
+            first_line = ensure_bytes(f.readline())
     elif isinstance(file, io.TextIOWrapper):
-        first_line = file.buffer.readline()
+        first_line = ensure_bytes(file.buffer.readline())

Line range hint 171-334: Improve error handling and documentation in reverse reading functions

The reverse reading implementations are well-optimized but could benefit from better error handling and documentation.

  1. Add performance characteristics to docstring:
def reverse_readfile(
    filename: Union[str, Path],
) -> Iterator[str]:
    """
    A much faster reverse read of file using memory-mapped files.

    Performance characteristics:
    - O(1) memory usage for regular files using mmap
    - O(n) memory usage for compressed files
    - Best for files > 1MB due to mmap overhead
    """
  1. Add explicit error handling for memory mapping failures:
             try:
                 filemap = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ)
-            except ValueError:
+            except (ValueError, OSError) as e:
                 warnings.warn(
-                    "trying to mmap an empty file.",
+                    f"Failed to memory-map file: {e}. Falling back to standard reading.",
                     stacklevel=2
                 )
                 return
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c110169 and b1c9df3.

📒 Files selected for processing (1)
  • src/monty/io.py (1 hunks)
🔇 Additional comments (1)
src/monty/io.py (1)

Line range hint 1-463: Verify security implications of file operations

Let's verify the security implications of the file operations across the codebase.

✅ Verification successful

Let me analyze the security implications based on the results. The file operations appear to be well-implemented with proper security considerations:

  1. File operations use proper error handling and secure practices:

    • zopen uses explicit encoding (UTF-8) and enforces explicit mode specification
    • File locks implement proper cleanup in __del__ and context managers
    • Memory-mapped operations have proper bounds checking
  2. Encoding handling is consistent:

    • All decode operations explicitly specify UTF-8
    • Binary/text mode handling is strictly enforced with warnings
    • Line ending detection is robust across platforms
  3. OS operations are limited and secure:

    • os.open is only used with proper flags for atomic file locking
    • os.makedirs uses exist_ok parameter to prevent race conditions
    • No unsafe chmod/chown operations found

Let me verify one final aspect regarding path traversal protection:


Let me fix the regex and check directory traversal one more time:


File operations are securely implemented with proper safeguards

The verification reveals that the file operations in src/monty/io.py are secure:

  1. Path handling is properly sanitized:

    • Uses os.path.abspath for file lock paths
    • Employs pathlib.Path for safe path manipulation
    • No direct path traversal vulnerabilities found
  2. File operations have proper security controls:

    • Enforces explicit encoding (UTF-8) and mode specification
    • Implements proper cleanup in context managers
    • Uses atomic operations for file locking
    • Memory-mapped operations have bounds checking
  3. Directory operations are protected:

    • No unsafe directory traversal patterns in the codebase
    • Path joining is done safely using os.path.join and Path
    • Proper error handling for file operations

The review comment's security concerns are addressed by the existing implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential security issues in file operations

# Check for unsafe file operations
echo "Checking for potentially unsafe file operations..."
rg -n "os\.(open|makedirs|chmod|chown)" --type py

# Check for proper encoding handling
echo "Checking for potential encoding issues..."
rg -n "\.decode\(|\.encode\(" --type py

# Check for proper error handling in file operations
echo "Checking for file operation error handling..."
ast-grep --pattern 'try {
  $$$
  open($_, $_)
  $$$
}'

Length of output: 4167


Script:

#!/bin/bash
# Check for path sanitization
echo "Checking path handling..."
rg -n "os\.path\.(abspath|normpath|realpath)" --type py

# Check for directory traversal prevention
echo "Checking directory traversal prevention..."
rg -n "(\.\.\/|\.\.\\\\)" --type py

Length of output: 1443


Script:

#!/bin/bash
# Check for directory traversal patterns with proper escaping
echo "Checking directory traversal patterns..."
rg -n '\.\./' --type py

# Check for path joining operations
echo "Checking path joining methods..."
rg -n "os\.path\.join|Path\(" --type py -A 2

Length of output: 15039

@DanielYang59
Copy link
Contributor Author

Haven't looked closely yet, but the macos failure might be related to: actions/setup-python#981

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