reactive-firewall / multicast

The multicast package is a Python library that simplifies sending and receiving multicast network messages. It provides classes and tools for implementing multicast communication in Python applications, making it straightforward to work with multicast sockets.
Other
2 stars 0 forks source link

Migrate to Python 3.9+ language features #134

Open reactive-firewall opened 1 month ago

reactive-firewall commented 1 month ago

Describe the solution you'd like

  1. Python 3.9+ can use f-strings instead of str().format() and should improve performance slightly
  2. python 3.9+ can use type-hinting

Describe alternatives you've considered

  1. regarding f-strings:

    • continued support of all python3 3.0-3.12 with str().format() style
    • monkey patching during build to support current and future styles
  2. regarding type-hinting

    • not using type hinting and maintaining the status quo
    • complex custom classes to wrap types and provide some checking

Additional context

  1. Documentation

    Python f-strings were introduced in Python 3.6. You can find the official documentation on f-strings in the Python 3.6 release notes under PEP 498 – Formatted String Literals.

    • These changes will require a non-MATs regression testing to be designed and implemented
    • These changes should be performance tested and documented before merging into master
    • these changes will need to be documented

    2.

    • type-hinting will need additional testing to ensure no regressions.
    • these changes will need to be documented
reactive-firewall commented 1 month ago

@coderabbitai, please refresh my memory when were python f-strings introduced and where can I find that officially documented?

Please read up on this ticket #134. Please read every python files (ie **/*.py) and search for every use of str's format function, and then generate a possible refactoring with f-strings are used instead of format for each occurrence, considering the project's usual style choices (of-course ignoring the historical pre-3.9 python support).

Finally, consider if this refactoring were to be applied and point out relevant impacts both the additive and reductive.

coderabbitai[bot] commented 1 month ago

[!TIP] For best results, initiate chat on the files or code changes.

@reactive-firewall,

Python f-strings were introduced in Python 3.6. You can find the official documentation on f-strings in the Python 3.6 release notes under PEP 498 – Formatted String Literals.

I have reviewed ticket #134 and examined all Python files for uses of str's format function. Here are possible refactorings using f-strings, considering the project's usual style choices:


1. setup.py

Original code:

raise ValueError(str(
    """[CWE-706] Access to the file {} was not expected."""
).format(filename))

Refactored code:

raise ValueError(f"[CWE-706] Access to the file {filename} was not expected.")

Original code:

with open(str("./{}").format(str(filename))) as f:

Refactored code:

with open(f"./{filename}") as f:

2. tests/test_usage.py

Original code:

str("multicast{}").format(
    str(
        test_case
    )
)

Refactored code:

f"multicast{test_case}"

3. tests/test_manifest.py

Original code:

package_prefix = str("""multicast-{}""").format(version)

Refactored code:

package_prefix = f"multicast-{version}"

4. tests/test_manifest.py

Original code:

str("{}/README.md").format(package_prefix)

Refactored code:

f"{package_prefix}/README.md"

5. tests/context.py

Original code:

print(str("python cmd used: {}").format(str(thepython)))

Refactored code:

print(f"python cmd used: {thepython}")

6. multicast/recv.py

Original code:

str("[CWE-440] module failed to import {}.").format(str(unit))

Refactored code:

f"[CWE-440] module failed to import {unit}."

7. multicast/hear.py

Original code:

print(str("{} wrote:").format(self.client_address[0]))

Refactored code:

print(f"{self.client_address[0]} wrote:")

8. multicast/hear.py

Original code:

print(str("{} SAYS: {} to {}").format(
    self.client_address[0], str(data), "ALL"
))

Refactored code:

print(f"{self.client_address[0]} SAYS: {data} to ALL")

Impacts of Refactoring

Overall, applying these refactorings should modernize the codebase, improve readability, and offer slight performance benefits, in line with your objectives outlined in ticket #134. Careful testing will ensure that the functionality remains consistent after the changes.

Let me know if you have any questions or need further assistance.

:link: Analysis chain
:checkered_flag: Scripts executed The following scripts were executed for the analysis: Script: ```shell #!/bin/bash # Find all Python files and search for uses of `str`'s `format` function rg --type py '\.format\(' -n -C 2 ``` Length of output: 14312

:brain: Learnings used ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/CI.md:32-32 Timestamp: 2024-09-17T06:40:42.918Z Learning: Reactive-firewall prefers to keep the word "own" in headings when emphasizing the reader's personal test. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/USAGE.md:66-66 Timestamp: 2024-09-17T03:38:28.677Z Learning: Reactive-firewall prefers to use "Caveat" as a heading instead of "Caution". ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#118 File: tests/check_pip:92-197 Timestamp: 2024-09-24T01:03:10.574Z Learning: Reactive-firewall prefers defining functions before they are called in Bash scripts to avoid errors due to Bash's sequential interpretation. ```
reactive-firewall commented 1 month ago

@coderabbitai, please refresh my memory when was python type-hinting introduced and where can I find that officially documented? Also refresh my memory how does python type-hinting work, include several examples.

Please read up on this ticket #134. Please read every python file in the project and then generate a possible opportunities where type-hinting can be introduced, considering the project's usual style choices (of-course ignoring the historical pre-3.9 python support and lack of type hinting).

Finally, consider if this refactoring were to be applied and point out relevant impacts both the additive and reductive.

coderabbitai[bot] commented 1 month ago

[!TIP] For best results, initiate chat on the files or code changes.

@reactive-firewall,

Python Type-Hinting Introduction and Official Documentation

Python type-hinting was introduced in Python 3.5 via PEP 484. The official documentation on type hinting can be found in the Python 3.5 documentation.

How Python Type-Hinting Works

Type hints allow developers to annotate the expected data types of variables, function parameters, and return values. This enhances code readability, improves IDE support, and enables static type checking using tools like mypy. Type hints do not enforce type checking at runtime but serve as a guide for developers and tools.

Examples:

  1. Function Parameters and Return Types

    def greet(name: str) -> str:
       return f"Hello, {name}"
  2. Using the typing Module for Complex Types

    from typing import List, Dict
    
    def process_data(data: List[Dict[str, int]]) -> None:
       for item in data:
           print(item['key'], item['value'])
  3. Optional Types and Union

    from typing import Optional, Union
    
    def get_user(user_id: Union[int, str]) -> Optional[Dict]:
       # Fetch user data by ID or name
       return None  # Return None if user not found

Opportunities for Introducing Type-Hinting

Based on the analysis of the project's Python files, I've identified several functions where type hints can be introduced. Here are some refactored examples:


1. setup.py

Original function:

def readFile(filename):
    # Function body...

Refactored with type hints:

def readFile(filename: str) -> str:
    # Function body...

2. setup.py

Original function:

def parse_requirements_for_install_requires(requirements_text):
    # Function body...

Refactored with type hints:

from typing import List

def parse_requirements_for_install_requires(requirements_text: str) -> List[str]:
    # Function body...

3. multicast/skt.py

Original function:

def endSocket(sock=None):
    # Function body...

Refactored with type hints:

import socket
from typing import Optional

def endSocket(sock: Optional[socket.socket] = None) -> None:
    # Function body...

4. tests/context.py

Original function:

def checkStrOrByte(theInput):
    # Function body...

Refactored with type hints:

from typing import Union, Optional

def checkStrOrByte(theInput: Optional[Union[str, bytes]]) -> Optional[Union[str, bytes]]:
    # Function body...

5. tests/profiling.py

Original function:

def do_cprofile(func):
    # Function body...

Refactored with type hints:

import functools
from typing import Callable, Any

def do_cprofile(func: Callable) -> Callable:
    # Function body...

6. multicast/__main__.py

Original function:

def main(*argv):
    # Function body...

Refactored with type hints:

from typing import Any, Tuple

def main(*argv: Any) -> Tuple[int, Any]:
    # Function body...

7. tests/test_usage.py

Original function:

def setUp(self):
    # Function body...

Refactored with type hints:

def setUp(self) -> None:
    # Function body...

Impacts of Applying Type-Hinting

Considerations


Let me know if you'd like assistance in proceeding with the refactoring or have any questions.

:link: Analysis chain @reactive-firewall, Python type-hinting was introduced in Python 3.5 via [PEP 484](https://www.python.org/dev/peps/pep-0484/). The official documentation can be found in the [Python 3.5 documentation](https://docs.python.org/3.5/library/typing.html). **How Python Type-Hinting Works** Type hints allow you to annotate your code with the expected data types of variables, function parameters, and return values. This helps improve code readability, enables better support from IDEs, and allows static type checkers like `mypy` to detect type-related errors. **Example 1: Function Parameters and Return Types** ```python def greet(name: str) -> str: return f"Hello, {name}" ``` **Example 2: Using the `typing` Module for Complex Types** ```python from typing import List, Dict def process_data(data: List[Dict[str, int]]) -> None: for item in data: print(item['key'], item['value']) ``` **Example 3: Optional Types and Type Aliases** ```python from typing import Optional, Union UserID = Union[int, str] def get_user_name(user_id: UserID) -> Optional[str]: # Fetch user name from database return None # Return None if user not found ``` --- As per ticket [#134](https://github.com/reactive-firewall/multicast/issues/134), we aim to introduce type-hinting into the codebase. To generate possible opportunities where type-hinting can be introduced, I've analyzed the project's Python files. --- Once the analysis is complete, I will provide refactoring suggestions with type hints added to function definitions, considering the project's usual style choices. Let me know if you have any questions or need further assistance.
:checkered_flag: Scripts executed The following scripts were executed for the analysis: Script: ```shell #!/bin/bash # Find all Python function definitions to identify where type hints can be added ast-grep --lang python --pattern $'def $_($_):\n $$$' ``` Length of output: 81997

:brain: Learnings used ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/CI.md:32-32 Timestamp: 2024-09-17T06:40:42.918Z Learning: Reactive-firewall prefers to keep the word "own" in headings when emphasizing the reader's personal test. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/USAGE.md:66-66 Timestamp: 2024-09-17T03:38:28.677Z Learning: Reactive-firewall prefers to use "Caveat" as a heading instead of "Caution". ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#118 File: tests/check_pip:92-197 Timestamp: 2024-09-24T01:03:10.574Z Learning: Reactive-firewall prefers defining functions before they are called in Bash scripts to avoid errors due to Bash's sequential interpretation. ```
coderabbitai[bot] commented 1 month ago

Detailed note on simplifying and standardizing import statements as discussed in PR #148.

Referenced comment: Discussion.

This work will be tracked here and performed on a separate development branch as requested by @reactive-firewall.

coderabbitai[bot] commented 1 month ago

Reference from PR #148:

In the file tests/MulticastUDPClient.py, it is suggested to simplify import statements and exception handling.

The repeated try-except blocks for importing standard library modules add unnecessary complexity and reduce readability. Modules like sys, os, functools, socket, socketserver, and random are part of the standard library and are generally always available, making these extra checks unnecessary.

Simplifying the imports by removing the try-except blocks can enhance readability and maintainability while keeping the code portable across Python versions.

For more details, see PR #148.

reactive-firewall commented 1 month ago

also need to consider the discussion for import changes in init. Will need to ensure either way imports remain consistent.

reactive-firewall commented 1 month ago

@reactive-firewall,

1. Detailed Comment Including the Improved Implementation

As requested, here's a detailed comment including the improved implementation from our discussion:

Improved Implementation for Shutdown Mechanism

We have improved the shutdown mechanism in the McastServer class by moving the self.shutdown() call to the open_for_request() method and implementing a should_shutdown accessor. This approach avoids altering the serve_forever() method and enhances the flexibility for future subclasses.

Updated Code:

class McastServer(socketserver.UDPServer):
    def __init__(self, server_address, RequestHandlerClass):
        super().__init__(server_address, RequestHandlerClass)
        self._should_shutdown = False  # Private instance variable to control shutdown

    @property
    def should_shutdown(self):
        """Accessor for the shutdown flag; subclasses can override this."""
        return self._should_shutdown

    def open_for_request(self):
        print("open_request")
        if self.should_shutdown:
            self.shutdown()
            return
        old_socket = self.socket
        (tmp_addr, tmp_prt) = old_socket.getsockname()
        multicast.endSocket(old_socket)
        self.socket = recv.joinstep(
            [tmp_addr], tmp_prt, None, tmp_addr, multicast.genSocket()
        )

    def handle_error(self, request, client_address):
        print("handle_error")
        if request is not None and request[0] is not None and "STOP" in str(request[0]):
            self._should_shutdown = True  # Set the shutdown flag
        else:
            super(McastServer, self).handle_error(request, client_address)

Explanation:

  • Private Instance Variable _should_shutdown:

    • Used to track the shutdown state.
    • Declared as a private variable to maintain encapsulation.
  • should_shutdown Property:

    • Implemented with the @property decorator.
    • Allows subclasses to customize the shutdown condition by overriding this property if needed.
  • Modified open_for_request() Method:

    • Checks the should_shutdown property before reopening the request.
    • If should_shutdown returns True, calls self.shutdown() to stop the server.
  • Updated handle_error() Method:

    • Sets _should_shutdown to True when the shutdown condition is met (e.g., when receiving a "STOP" message).
    • Uses the superclass method for other errors.

Benefits:

  • Avoids Modifying serve_forever():

    • Keeps the server behavior consistent and leverages the existing implementation.
  • Enhances Flexibility:

    • Subclasses can override should_shutdown to implement custom shutdown logic.
  • Reduces Complexity:

    • Eliminates the need to spawn a new thread in the handle_error method, avoiding potential race conditions.

2. Explanation of the @property Decorator

What Does the @property Decorator Do?

The @property decorator is a built-in function in Python that allows you to define methods in a class that can be accessed like attributes. It enables you to add getters, setters, and deleters to manage access to instance variables, providing a way to implement encapsulation and data validation.

Key Points:

  • Encapsulation:

    • Controls how attributes are accessed and modified.
    • Allows for data hiding and abstraction.
  • Attribute-Like Access:

    • Methods decorated with @property can be accessed without parentheses, just like attributes.
  • Getters and Setters:

    • You can define a getter method using @property.
    • Use @<property_name>.setter to define a setter method.
    • Use @<property_name>.deleter to define a deleter method.

Official Documentation:

You can find detailed information in the Python documentation:

When Was the @property Introduced to Python?

  • The property() function was introduced in Python version 2.2 with the new-style classes.
  • The decorator syntax @property was introduced in Python version 2.4, as per PEP 318.

Official Documentation on Introduction:

Consistency Across Python 3 Versions:

Yes, the @property decorator remains consistent across all Python 3 versions. Since its introduction, the functionality and usage have been stable. Any code using @property in Python 3.0 will work the same way in Python 3.12.

Additional Resources:

Please let me know if you need any more information or further assistance.

🧠 Learnings used

coderabbitai[bot] commented 1 month ago

As per @reactive-firewall's request on PR #148, we should ensure safe handling of user-provided data in string formatting by using f-strings instead of . This will enhance security and readability.

reactive-firewall commented 2 weeks ago

As per PR #175 tests/context.py:

+from typing import Generator
+import logging

-@contextmanager
-def managed_process(process):
+@contextmanager
+def managed_process(process: Process, timeout: float = 3.0) -> Generator[Process, None, None]:
     """
     Context manager for safely handling multiprocessing processes.

     Args:
         process (multiprocessing.Process): The process to manage.
+        timeout (float, optional): Timeout in seconds for joining the process.
+            Defaults to 3.0.

     Yields:
         multiprocessing.Process: The managed process within the context.
     """
     try:
         yield process
     finally:
         if process.is_alive():
             process.terminate()
-            process.join(timeout=3)
+            process.join(timeout=timeout)
             if process.is_alive():
+                logging.warning(f"Process {process.name} did not terminate within {timeout}s, forcing kill")
                 process.kill()
         process.close()
reactive-firewall commented 2 weeks ago

@coderabbitai, please read up on GH Issue #134 and all of the related discussions and PR/MRs mentioned. Please also read-up on each of the following python migration guides (and related PEPs to consider during our modernization):

Goal: Document a comprehensive and specific migration strategy for modernizing the multicast project's python codebase to target python versions 3.9.20-3.13, considering opportunities to simplify code, improve performance, improve security, and enhance overall quality of the python source code.


Additional Considerations:

coderabbitai[bot] commented 2 weeks ago

[!TIP] For best results, initiate chat on the files or code changes.

@reactive-firewall,

I've reviewed GH Issue #134, the related discussions and PRs/MRs, as well as the specified Python migration guides and PEPs. Based on your goals and additional considerations, here's a comprehensive and specific migration strategy for modernizing the multicast project's Python codebase to target Python versions 3.9.20 to 3.13, focusing on simplifying code, improving performance, enhancing security, and elevating overall code quality.


Migration Strategy

1. Update Python Version and Dependencies

2. Simplify Imports and Module Management

a. Leverage Import Improvements (PEP 484, PEP 562)

b. Utilize Module Weak References

3. Enhance Data Handling with a Datagram Class

4. Adopt Modern Language Features

a. Implement f-Strings (PEP 498)

b. Introduce Type Hinting and Annotations (PEP 484, PEP 526)

5. Improve IP Address Handling with the ipaddress Module (PEP 3144)

6. Refactor Output Handling

a. Migrate from print to pprint Module

b. Use contextlib.redirect_stdout and redirect_stderr

7. Clean Up Tracebacks in Error Handling

8. Apply SOLID Design Principles

9. Additional Performance and Security Enhancements

a. Use the secrets Module for Secure Tokens (PEP 506)

b. Optimize Data Structures and Algorithms

10. Testing and Continuous Integration

11. Documentation Updates


Next Steps

  1. Create a Detailed Migration Plan:

    • Break down tasks into specific issues in GH Issue #134 and related tickets.
    • Prioritize tasks based on impact and dependencies.
  2. Establish a Migration Branch:

    • Use a feature branch (e.g., migration/python3.9) for development to avoid disrupting the main branch.
  3. Coordinate with Related Issues:

    • Align efforts with GH Issue #151 for import improvements and module weakrefs.
    • Schedule regression testing after implementing critical changes.
  4. Implement Changes Incrementally:

    • Start with low-risk refactoring like updating imports and adding type hints.
    • Gradually move to more complex changes like redesigning classes according to SOLID principles.
  5. Review and Test Thoroughly:

    • Conduct code reviews for each merge request.
    • Run automated tests and manual testing where necessary.
  6. Update Documentation Alongside Code Changes:

    • Ensure documentation stays in sync with code updates.
    • Highlight any changes that might affect end-users or contributors.
  7. Plan for a New Release:

    • Upon completion, prepare for a new major release (e.g., version 3.0.0) to reflect the significant updates.

Additional Considerations:


Please let me know if you need further details on any of these points or assistance with specific implementations.


✏️ Learnings added ``` Learnt from: reactive-firewall URL: https://github.com/reactive-firewall/multicast/issues/134 Timestamp: 2024-11-08T03:27:43.833Z Learning: When modernizing the multicast project's Python codebase, reactive-firewall prefers to consider PEPs 3155, 362, 3144, 451, 435, 443, 484, 489, 448, 475, 498, 506, 515, 526, 530, and others, targeting Python versions 3.9.20 to 3.13, with opportunities to simplify code, improve performance, enhance security, and elevate overall code quality. ``` > Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

🧠 Learnings used ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#0 File: :0-0 Timestamp: 2024-10-12T00:41:41.731Z Learning: For future scripts, reactive-firewall prefers to use only POSIX built-ins (like `printf` instead of `echo`) to align with CEP-8 conventions. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#0 File: :0-0 Timestamp: 2024-10-21T04:40:19.258Z Learning: For future reviews, remember that reactive-firewall prefers to keep un-mapped exit codes as they are until they are ready to be mapped. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#0 File: :0-0 Timestamp: 2024-10-12T03:22:28.902Z Learning: For future scripts, reactive-firewall prefers to keep the shebang line with an extra space (e.g., `#! /bin/bash`) as per project style. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/CI.md:32-32 Timestamp: 2024-10-08T21:59:30.537Z Learning: Reactive-firewall prefers to keep the word "own" in headings when emphasizing the reader's personal test. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/CI.md:32-32 Timestamp: 2024-09-17T06:40:42.918Z Learning: Reactive-firewall prefers to keep the word "own" in headings when emphasizing the reader's personal test. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#0 File: :0-0 Timestamp: 2024-10-08T03:15:05.171Z Learning: For future reviews, remember that when drafting or updating CEP documents, it's important to adhere to the project's standard format and include all relevant sections such as Motivation, Detailed Explanation, Implementation Guidelines, Impact, and References. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#0 File: :0-0 Timestamp: 2024-10-08T21:59:30.537Z Learning: For future reviews, remember that when drafting or updating CEP documents, it's important to adhere to the project's standard format and include all relevant sections such as Motivation, Detailed Explanation, Implementation Guidelines, Impact, and References. ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/USAGE.md:66-66 Timestamp: 2024-10-08T21:59:30.537Z Learning: Reactive-firewall prefers to use "Caveat" as a heading instead of "Caution". ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#110 File: docs/USAGE.md:66-66 Timestamp: 2024-09-17T03:38:28.677Z Learning: Reactive-firewall prefers to use "Caveat" as a heading instead of "Caution". ``` --- ``` Learnt from: reactive-firewall PR: reactive-firewall/multicast#118 File: tests/check_pip:92-197 Timestamp: 2024-10-08T21:59:30.537Z Learning: Reactive-firewall prefers defining functions before they are called in Bash scripts to avoid errors due to Bash's sequential interpretation. ```