From cd220585e7ad601dbf55b1464c1ce145f16f7d56 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:56:31 -0400 Subject: [PATCH 01/23] add `IPv6HTTPAdapter`, refactor `IPv4HTTPAdapter` --- ipforce/adapters.py | 50 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/ipforce/adapters.py b/ipforce/adapters.py index 326520f..8f3a252 100644 --- a/ipforce/adapters.py +++ b/ipforce/adapters.py @@ -2,7 +2,6 @@ from typing import List, Tuple from urllib3 import PoolManager from requests.adapters import HTTPAdapter -from requests.sessions import Session class IPv4HTTPAdapter(HTTPAdapter): """A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library.""" @@ -46,16 +45,45 @@ def __del__(self) -> None: if hasattr(self, "_original_getaddrinfo"): socket.getaddrinfo = self._original_getaddrinfo - @staticmethod - def get_ipv4_enforced_session() -> Session: + +class IPv6HTTPAdapter(HTTPAdapter): + """A custom HTTPAdapter that enforces the use of IPv6 for DNS resolution during HTTP(S) requests using the requests library.""" + + def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: + """ + Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv6 addresses are used. + This is necessary to ensure that the requests library uses IPv6 addresses for DNS resolution, which is required for some APIs. + :param connections: the number of connection pools to cache + :param maxsize: the maximum number of connections to save in the pool + :param block: whether the connections should block when reaching the max size + :param kwargs: additional keyword arguments for the PoolManager """ - Returns a requests.Session with IPv4HTTPAdapter mounted for both HTTP and HTTPS. - All requests made with this session will use IPv4 for DNS resolution. + self.poolmanager = PoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + socket_options=self._ipv6_socket_options(), + **kwargs + ) - :return: requests.Session object with IPv4 enforced + def _ipv6_socket_options(self) -> list: """ - session = Session() - adapter = IPv4HTTPAdapter() - session.mount("http://", adapter) - session.mount("https://", adapter) - return session + Temporarily patches socket.getaddrinfo to filter only IPv6 addresses (AF_INET6). + + :return: an empty list of socket options; DNS patching occurs here + """ + original_getaddrinfo = socket.getaddrinfo + + def ipv6_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]: + results = original_getaddrinfo(*args, **kwargs) + return [res for res in results if res[0] == socket.AF_INET6] + + self._original_getaddrinfo = socket.getaddrinfo + socket.getaddrinfo = ipv6_only_getaddrinfo + + return [] + + def __del__(self) -> None: + """Restores the original socket.getaddrinfo function upon adapter deletion.""" + if hasattr(self, "_original_getaddrinfo"): + socket.getaddrinfo = self._original_getaddrinfo From 541f6463c8c1d97d1ddddf488ee6cd4c3dd48e2f Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:57:02 -0400 Subject: [PATCH 02/23] add testcases for both adapters --- tests/test_adapters.py | 102 +++++++++++++++++++++++++++++++++++++++++ tests/test_ipv4.py | 33 +++++++++++++ tests/test_ipv6.py | 33 +++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 tests/test_adapters.py create mode 100644 tests/test_ipv4.py create mode 100644 tests/test_ipv6.py diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..6b39e84 --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,102 @@ +import unittest +import socket +from unittest.mock import patch +from ipforce.adapters import IPv4HTTPAdapter, IPv6HTTPAdapter + + +class TestIPv4Adapter(unittest.TestCase): + """Test cases for IPv4HTTPAdapter.""" + + def setUp(self): + """Set up test fixtures.""" + self.adapter = IPv4HTTPAdapter() + + def test_ipv4_socket_options(self): + """Test that IPv4 adapter filters only IPv4 addresses.""" + # Mock socket.getaddrinfo to return mixed results + mock_results = [ + (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 + (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 + (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.1', 80)), # IPv4 + ] + + with patch('socket.getaddrinfo', return_value=mock_results): + # Call the method that patches socket.getaddrinfo + self.adapter._ipv4_socket_options() + # Test that the patched function filters correctly + results = socket.getaddrinfo('example.com', 80) + self.assertEqual(len(results), 2) # Only IPv4 results + for result in results: + self.assertEqual(result[0], socket.AF_INET) + + def test_cleanup(self): + """Test that the adapter properly restores original getaddrinfo.""" + original_getaddrinfo = socket.getaddrinfo + self.adapter._ipv4_socket_options() + + # Verify it was patched + self.assertNotEqual(socket.getaddrinfo, original_getaddrinfo) + + # Clean up + self.adapter.__del__() + + # Verify it was restored + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) + + +class TestIPv6Adapter(unittest.TestCase): + """Test cases for IPv6HTTPAdapter.""" + + def setUp(self): + """Set up test fixtures.""" + self.adapter = IPv6HTTPAdapter() + + def test_ipv6_socket_options(self): + """Test that IPv6 adapter filters only IPv6 addresses.""" + # Mock socket.getaddrinfo to return mixed results + mock_results = [ + (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 + (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 + (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('2001:db8::1', 80)), # IPv6 + ] + + with patch('socket.getaddrinfo', return_value=mock_results): + # Call the method that patches socket.getaddrinfo + self.adapter._ipv6_socket_options() + + # Test that the patched function filters correctly + results = socket.getaddrinfo('example.com', 80) + self.assertEqual(len(results), 2) # Only IPv6 results + for result in results: + self.assertEqual(result[0], socket.AF_INET6) + + def test_cleanup(self): + """Test that the adapter properly restores original getaddrinfo.""" + original_getaddrinfo = socket.getaddrinfo + self.adapter._ipv6_socket_options() + + # Verify it was patched + self.assertNotEqual(socket.getaddrinfo, original_getaddrinfo) + + # Clean up + self.adapter.__del__() + + # Verify it was restored + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) + + +class TestAdapterIntegration(unittest.TestCase): + """Integration tests for both adapters.""" + + def test_both_adapters_independent(self): + """Test that both adapters can coexist without interference.""" + ipv4_adapter = IPv4HTTPAdapter() + ipv6_adapter = IPv6HTTPAdapter() + + # Both should be able to patch independently + ipv4_adapter._ipv4_socket_options() + ipv6_adapter._ipv6_socket_options() + + # Clean up both + ipv4_adapter.__del__() + ipv6_adapter.__del__() diff --git a/tests/test_ipv4.py b/tests/test_ipv4.py new file mode 100644 index 0000000..4446f0d --- /dev/null +++ b/tests/test_ipv4.py @@ -0,0 +1,33 @@ +import requests +import ipaddress +from ipforce import IPv4HTTPAdapter + + +def is_ipv4(ip: str) -> bool: + """ + Check if the given input is a valid IPv4 address. + + :param ip: input IP + """ + if not isinstance(ip, str): + return False + try: + _ = ipaddress.IPv4Address(ip) + return True + except Exception: + return False + + +def test_ipv4_adapter(): + """Test the IPv4 adapter by making a request that will only use IPv4 addresses.""" + print("Testing IPv4 Adapter...") + + # Create a session with IPv4 adapter + with requests.Session() as session: + ipv4Addapter = IPv4HTTPAdapter() + session.mount('http://', ipv4Addapter) + session.mount('https://', ipv4Addapter) + # This will only resolve to IPv4 addresses + response = session.get('https://ifconfig.co/json', timeout=10) + response.raise_for_status() + assert is_ipv4(response.json()['ip']) diff --git a/tests/test_ipv6.py b/tests/test_ipv6.py new file mode 100644 index 0000000..cb42528 --- /dev/null +++ b/tests/test_ipv6.py @@ -0,0 +1,33 @@ +import requests +import ipaddress +from ipforce import IPv6HTTPAdapter + + +def is_ipv6(ip: str) -> bool: + """ + Check if the given input is a valid IPv6 address. + + :param ip: input IP + """ + if not isinstance(ip, str): + return False + try: + _ = ipaddress.IPv6Address(ip) + return True + except Exception: + return False + + +def test_ipv6_adapter(): + """Test the IPv6 adapter by making a request that will only use IPv6 addresses.""" + print("\nTesting IPv6 Adapter...") + + # Create a session with IPv6 adapter + with requests.Session() as session: + ipv6Addapter = IPv6HTTPAdapter() + session.mount('http://', ipv6Addapter) + session.mount('https://', ipv6Addapter) + # This will only resolve to IPv6 addresses + response = session.get('https://ifconfig.co/json', timeout=10) + response.raise_for_status() + assert is_ipv6(response.json()['ip']) From 1bceb0950d207a050f1a89b52ab7a74ed426c460 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:57:36 -0400 Subject: [PATCH 03/23] minor refactoring --- ipforce/params.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ipforce/params.py b/ipforce/params.py index ce1161f..401b71f 100644 --- a/ipforce/params.py +++ b/ipforce/params.py @@ -4,4 +4,3 @@ IPFORCE_VERSION = "0.1" IPFORCE_OVERVIEW = '''OVERVIEW''' IPFORCE_REPO = "https://github.com/openscilab/ipforce" - From 04e5dd2847f948ce8cbef9fc5090e69413919a1d Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:58:00 -0400 Subject: [PATCH 04/23] update `pytest` command --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff4ada2..b1baa56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: pip install --upgrade --upgrade-strategy=only-if-needed -r test-requirements.txt - name: Test with pytest run: | - python -m pytest . --cov=ipforce --cov-report=term + python -m pytest tests/test_ipv4.py tests/test_ipv6.py tests/test_adapters.py --cov=ipforce --cov-report=term - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: From dfa3f6bfca6c23ba9e629d40006aa3abb2fecabb Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:58:10 -0400 Subject: [PATCH 05/23] expose both adapters --- ipforce/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipforce/__init__.py b/ipforce/__init__.py index d701c1d..ae9911d 100644 --- a/ipforce/__init__.py +++ b/ipforce/__init__.py @@ -1,5 +1,5 @@ """ipforce modules.""" from .params import IPFORCE_VERSION -from .adapters import IPv4HTTPAdapter +from .adapters import IPv4HTTPAdapter, IPv6HTTPAdapter __version__ = IPFORCE_VERSION From 1bc21248faa2f86316a77260255a7e5549377f50 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:58:21 -0400 Subject: [PATCH 06/23] update `README.md` --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 02ad599..a240148 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ Discord Channel -## Overview +## Overview

-IPForce is a Python library for TODO. +IPForce is a Python library that provides HTTP adapters for forcing specific IP protocol versions (IPv4 or IPv6) during HTTP requests. It's particularly useful for testing network connectivity, ensuring compatibility with specific network configurations, and controlling which IP protocol version is used for DNS resolution and connections.

@@ -55,16 +55,57 @@ - `pip install .` ### PyPI - - Check [Python Packaging User Guide](https://packaging.python.org/installing/) - `pip install ipforce==0.1` - ## Usage +### Enforce IPv4 + +```python +import requests +from ipforce import IPv4HTTPAdapter + +# Create a session that will only use IPv4 addresses +session = requests.Session() +session.mount('http://', IPv4HTTPAdapter()) +session.mount('https://', IPv4HTTPAdapter()) + +# All requests through this session will only resolve to IPv4 addresses +response = session.get('https://ifconfig.co/json') +``` + +### Enforce IPv6 + +```python +import requests +from ipforce import IPv6HTTPAdapter + +# Create a session that will only use IPv6 addresses +session = requests.Session() +session.mount('http://', IPv6HTTPAdapter()) +session.mount('https://', IPv6HTTPAdapter()) + +# All requests through this session will only resolve to IPv6 addresses +response = session.get('https://ifconfig.co/json') +``` + +### When to Use + +- **IPv4 Adapter**: Use when you need to ensure connections only use IPv4 addresses, useful for: + - Legacy systems that don't support IPv6 + - Networks with IPv4-only infrastructure + - Testing IPv4 connectivity + +- **IPv6 Adapter**: Use when you need to ensure connections only use IPv6 addresses, useful for: + - Modern networks with IPv6 infrastructure + - Testing IPv6 connectivity + - Applications requiring IPv6-specific features + +### ⚠️ Important: Thread Safety Warning -### Library +**Current adapters are NOT thread-safe!** They modify the global `socket.getaddrinfo` function, which can cause issues in multi-threaded applications. -#### Enforce IPv4 +**Future releases will include thread-safe alternatives.** ## Issues & Bug Reports From 4b51566948b6940b122187cafda297f37f4bb2a6 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 01:58:31 -0400 Subject: [PATCH 07/23] update `CHANGELOG.md` --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda311..c0cb0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- Test system +- IPv6HTTPAdapter +- IPv4HTTPAdapter ### Changed [Unreleased]: https://github.com/openscilab/ipforce/compare/TODO...v0.1 From afb92bfd3d71dc1183b7512ed7a22bc4a8c23874 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 02:12:13 -0400 Subject: [PATCH 08/23] update test --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1baa56..85fc444 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: pip install --upgrade --upgrade-strategy=only-if-needed -r test-requirements.txt - name: Test with pytest run: | - python -m pytest tests/test_ipv4.py tests/test_ipv6.py tests/test_adapters.py --cov=ipforce --cov-report=term + python -m pytest tests/test_adapters.py --cov=ipforce --cov-report=term - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: From 07eb29e7153a59d09f08560fa8f7f679834151eb Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 11 Aug 2025 02:42:06 -0400 Subject: [PATCH 09/23] update `.coveragerc` --- .coveragerc | 2 -- 1 file changed, 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index d4008ef..a624f92 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,6 @@ [run] branch = True omit = - */ipforce/cli.py - */ipforce/__main__.py */ipforce/__init__.py [report] # Regexes for lines to exclude from consideration From 34d83386e9ed47465aba39deab6c5763df362220 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 18 Aug 2025 22:48:45 -0400 Subject: [PATCH 10/23] apply suggested renamings --- README.md | 12 ++++++------ ipforce/__init__.py | 2 +- ipforce/adapters.py | 4 ++-- tests/test_adapters.py | 10 +++++----- tests/test_ipv4.py | 4 ++-- tests/test_ipv6.py | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a240148..36179f0 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,12 @@ ```python import requests -from ipforce import IPv4HTTPAdapter +from ipforce import IPv4TransportAdapter # Create a session that will only use IPv4 addresses session = requests.Session() -session.mount('http://', IPv4HTTPAdapter()) -session.mount('https://', IPv4HTTPAdapter()) +session.mount('http://', IPv4TransportAdapter()) +session.mount('https://', IPv4TransportAdapter()) # All requests through this session will only resolve to IPv4 addresses response = session.get('https://ifconfig.co/json') @@ -78,12 +78,12 @@ response = session.get('https://ifconfig.co/json') ```python import requests -from ipforce import IPv6HTTPAdapter +from ipforce import IPv6TransportAdapter # Create a session that will only use IPv6 addresses session = requests.Session() -session.mount('http://', IPv6HTTPAdapter()) -session.mount('https://', IPv6HTTPAdapter()) +session.mount('http://', IPv6TransportAdapter()) +session.mount('https://', IPv6TransportAdapter()) # All requests through this session will only resolve to IPv6 addresses response = session.get('https://ifconfig.co/json') diff --git a/ipforce/__init__.py b/ipforce/__init__.py index ae9911d..03ef52e 100644 --- a/ipforce/__init__.py +++ b/ipforce/__init__.py @@ -1,5 +1,5 @@ """ipforce modules.""" from .params import IPFORCE_VERSION -from .adapters import IPv4HTTPAdapter, IPv6HTTPAdapter +from .adapters import IPv4TransportAdapter, IPv6TransportAdapter __version__ = IPFORCE_VERSION diff --git a/ipforce/adapters.py b/ipforce/adapters.py index 8f3a252..a83d88b 100644 --- a/ipforce/adapters.py +++ b/ipforce/adapters.py @@ -3,7 +3,7 @@ from urllib3 import PoolManager from requests.adapters import HTTPAdapter -class IPv4HTTPAdapter(HTTPAdapter): +class IPv4TransportAdapter(HTTPAdapter): """A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library.""" def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: @@ -46,7 +46,7 @@ def __del__(self) -> None: socket.getaddrinfo = self._original_getaddrinfo -class IPv6HTTPAdapter(HTTPAdapter): +class IPv6TransportAdapter(HTTPAdapter): """A custom HTTPAdapter that enforces the use of IPv6 for DNS resolution during HTTP(S) requests using the requests library.""" def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: diff --git a/tests/test_adapters.py b/tests/test_adapters.py index 6b39e84..be53912 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -1,7 +1,7 @@ import unittest import socket from unittest.mock import patch -from ipforce.adapters import IPv4HTTPAdapter, IPv6HTTPAdapter +from ipforce.adapters import IPv4TransportAdapter, IPv6TransportAdapter class TestIPv4Adapter(unittest.TestCase): @@ -9,7 +9,7 @@ class TestIPv4Adapter(unittest.TestCase): def setUp(self): """Set up test fixtures.""" - self.adapter = IPv4HTTPAdapter() + self.adapter = IPv4TransportAdapter() def test_ipv4_socket_options(self): """Test that IPv4 adapter filters only IPv4 addresses.""" @@ -49,7 +49,7 @@ class TestIPv6Adapter(unittest.TestCase): def setUp(self): """Set up test fixtures.""" - self.adapter = IPv6HTTPAdapter() + self.adapter = IPv6TransportAdapter() def test_ipv6_socket_options(self): """Test that IPv6 adapter filters only IPv6 addresses.""" @@ -90,8 +90,8 @@ class TestAdapterIntegration(unittest.TestCase): def test_both_adapters_independent(self): """Test that both adapters can coexist without interference.""" - ipv4_adapter = IPv4HTTPAdapter() - ipv6_adapter = IPv6HTTPAdapter() + ipv4_adapter = IPv4TransportAdapter() + ipv6_adapter = IPv6TransportAdapter() # Both should be able to patch independently ipv4_adapter._ipv4_socket_options() diff --git a/tests/test_ipv4.py b/tests/test_ipv4.py index 4446f0d..9b740d7 100644 --- a/tests/test_ipv4.py +++ b/tests/test_ipv4.py @@ -1,6 +1,6 @@ import requests import ipaddress -from ipforce import IPv4HTTPAdapter +from ipforce import IPv4TransportAdapter def is_ipv4(ip: str) -> bool: @@ -24,7 +24,7 @@ def test_ipv4_adapter(): # Create a session with IPv4 adapter with requests.Session() as session: - ipv4Addapter = IPv4HTTPAdapter() + ipv4Addapter = IPv4TransportAdapter() session.mount('http://', ipv4Addapter) session.mount('https://', ipv4Addapter) # This will only resolve to IPv4 addresses diff --git a/tests/test_ipv6.py b/tests/test_ipv6.py index cb42528..5289049 100644 --- a/tests/test_ipv6.py +++ b/tests/test_ipv6.py @@ -1,6 +1,6 @@ import requests import ipaddress -from ipforce import IPv6HTTPAdapter +from ipforce import IPv6TransportAdapter def is_ipv6(ip: str) -> bool: @@ -24,7 +24,7 @@ def test_ipv6_adapter(): # Create a session with IPv6 adapter with requests.Session() as session: - ipv6Addapter = IPv6HTTPAdapter() + ipv6Addapter = IPv6TransportAdapter() session.mount('http://', ipv6Addapter) session.mount('https://', ipv6Addapter) # This will only resolve to IPv6 addresses From c56b95d772b065cf2a2cf98daeb6c42fbcf1daa0 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 18 Aug 2025 22:55:08 -0400 Subject: [PATCH 11/23] update `CHANGELOG.md` --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0cb0ec..65af71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.1] - 2025-xx-xx ### Added - Test system -- IPv6HTTPAdapter -- IPv4HTTPAdapter -### Changed +- IPv6TransportAdapter +- IPv4TransportAdapter -[Unreleased]: https://github.com/openscilab/ipforce/compare/TODO...v0.1 +[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.1...dev +[0.1]: https://github.com/openscilab/ipspot/compare/7128b04...v0.1 From ae9805de3582c1beb5520b96a2ce1ead0d9d9d64 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 18 Aug 2025 23:00:38 -0400 Subject: [PATCH 12/23] update `CHANGELOG.md` --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65af71e..dcb3eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,5 +11,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - IPv6TransportAdapter - IPv4TransportAdapter -[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.1...dev -[0.1]: https://github.com/openscilab/ipspot/compare/7128b04...v0.1 +[Unreleased]: https://github.com/openscilab/ipforce/compare/v0.1...dev +[0.1]: https://github.com/openscilab/ipforce/compare/7128b04...v0.1 From b64b0232b2e570f13f6bb379a1dd7aac1c983ecd Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 18 Aug 2025 23:07:10 -0400 Subject: [PATCH 13/23] fix docstring issues --- ipforce/__init__.py | 1 + ipforce/adapters.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/ipforce/__init__.py b/ipforce/__init__.py index 03ef52e..64e4c5a 100644 --- a/ipforce/__init__.py +++ b/ipforce/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ipforce modules.""" from .params import IPFORCE_VERSION from .adapters import IPv4TransportAdapter, IPv6TransportAdapter diff --git a/ipforce/adapters.py b/ipforce/adapters.py index a83d88b..34d2157 100644 --- a/ipforce/adapters.py +++ b/ipforce/adapters.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""IPForce Adapters to force IPv4 or IPv6 for requests.""" import socket from typing import List, Tuple from urllib3 import PoolManager @@ -9,6 +11,7 @@ class IPv4TransportAdapter(HTTPAdapter): def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: """ Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv4 addresses are used. + This is necessary to ensure that the requests library uses IPv4 addresses for DNS resolution, which is required for some APIs. :param connections: the number of connection pools to cache :param maxsize: the maximum number of connections to save in the pool @@ -52,6 +55,7 @@ class IPv6TransportAdapter(HTTPAdapter): def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: """ Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv6 addresses are used. + This is necessary to ensure that the requests library uses IPv6 addresses for DNS resolution, which is required for some APIs. :param connections: the number of connection pools to cache :param maxsize: the maximum number of connections to save in the pool From cf89d7a874493650bbf381e3d54e3bbc92615a22 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 19 Jan 2026 01:14:43 -0500 Subject: [PATCH 14/23] refactor IPv4 and IPv6 adapters to override send method for DNS resolution, removing init_poolmanager and socket options methods --- ipforce/adapters.py | 100 ++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 64 deletions(-) diff --git a/ipforce/adapters.py b/ipforce/adapters.py index 34d2157..4e3e828 100644 --- a/ipforce/adapters.py +++ b/ipforce/adapters.py @@ -1,93 +1,65 @@ # -*- coding: utf-8 -*- """IPForce Adapters to force IPv4 or IPv6 for requests.""" import socket -from typing import List, Tuple -from urllib3 import PoolManager +from typing import Any, List, Tuple from requests.adapters import HTTPAdapter + class IPv4TransportAdapter(HTTPAdapter): """A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library.""" - def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: - """ - Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv4 addresses are used. - - This is necessary to ensure that the requests library uses IPv4 addresses for DNS resolution, which is required for some APIs. - :param connections: the number of connection pools to cache - :param maxsize: the maximum number of connections to save in the pool - :param block: whether the connections should block when reaching the max size - :param kwargs: additional keyword arguments for the PoolManager - """ - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - socket_options=self._ipv4_socket_options(), - **kwargs - ) - - def _ipv4_socket_options(self) -> list: + def send(self, *args: list, **kwargs: dict) -> Any: """ - Temporarily patches socket.getaddrinfo to filter only IPv4 addresses (AF_INET). + Override send method to apply the monkey patch only during the request. - :return: an empty list of socket options; DNS patching occurs here + :param args: additional list arguments for the send method + :param kwargs: additional keyword arguments for the send method """ original_getaddrinfo = socket.getaddrinfo - def ipv4_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]: - results = original_getaddrinfo(*args, **kwargs) + def ipv4_only_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]: + """ + Filter getaddrinfo to return only IPv4 addresses. + + :param gargs: additional list arguments for the original_getaddrinfo function + :param gkwargs: additional keyword arguments for the original_getaddrinfo function + """ + results = original_getaddrinfo(*gargs, **gkwargs) return [res for res in results if res[0] == socket.AF_INET] - self._original_getaddrinfo = socket.getaddrinfo socket.getaddrinfo = ipv4_only_getaddrinfo - - return [] - - def __del__(self) -> None: - """Restores the original socket.getaddrinfo function upon adapter deletion.""" - if hasattr(self, "_original_getaddrinfo"): - socket.getaddrinfo = self._original_getaddrinfo + try: + response = super().send(*args, **kwargs) + finally: + socket.getaddrinfo = original_getaddrinfo + return response class IPv6TransportAdapter(HTTPAdapter): """A custom HTTPAdapter that enforces the use of IPv6 for DNS resolution during HTTP(S) requests using the requests library.""" - def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None: + def send(self, *args: list, **kwargs: dict) -> Any: """ - Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv6 addresses are used. + Override send method to apply the monkey patch only during the request. - This is necessary to ensure that the requests library uses IPv6 addresses for DNS resolution, which is required for some APIs. - :param connections: the number of connection pools to cache - :param maxsize: the maximum number of connections to save in the pool - :param block: whether the connections should block when reaching the max size - :param kwargs: additional keyword arguments for the PoolManager - """ - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - socket_options=self._ipv6_socket_options(), - **kwargs - ) - - def _ipv6_socket_options(self) -> list: - """ - Temporarily patches socket.getaddrinfo to filter only IPv6 addresses (AF_INET6). - - :return: an empty list of socket options; DNS patching occurs here + :param args: additional list arguments for the send method + :param kwargs: additional keyword arguments for the send method """ original_getaddrinfo = socket.getaddrinfo - def ipv6_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]: - results = original_getaddrinfo(*args, **kwargs) + def ipv6_only_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]: + """ + Filter getaddrinfo to return only IPv6 addresses. + + :param gargs: additional list arguments for the original_getaddrinfo function + :param gkwargs: additional keyword arguments for the original_getaddrinfo function + """ + results = original_getaddrinfo(*gargs, **gkwargs) return [res for res in results if res[0] == socket.AF_INET6] - self._original_getaddrinfo = socket.getaddrinfo socket.getaddrinfo = ipv6_only_getaddrinfo - - return [] - - def __del__(self) -> None: - """Restores the original socket.getaddrinfo function upon adapter deletion.""" - if hasattr(self, "_original_getaddrinfo"): - socket.getaddrinfo = self._original_getaddrinfo + try: + response = super().send(*args, **kwargs) + finally: + socket.getaddrinfo = original_getaddrinfo + return response From 99e7cd1708ff0d31fe5999d82eeec6bc66703e72 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 19 Jan 2026 01:14:50 -0500 Subject: [PATCH 15/23] enhance tests for IPv4 and IPv6 adapters to verify address filtering during send, cleanup after send, and restoration of socket.getaddrinfo on exceptions --- tests/test_adapters.py | 153 ++++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 56 deletions(-) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index be53912..b4a40ab 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -1,87 +1,113 @@ import unittest import socket -from unittest.mock import patch +from unittest.mock import patch, MagicMock from ipforce.adapters import IPv4TransportAdapter, IPv6TransportAdapter class TestIPv4Adapter(unittest.TestCase): - """Test cases for IPv4HTTPAdapter.""" + """Test cases for IPv4TransportAdapter.""" def setUp(self): """Set up test fixtures.""" self.adapter = IPv4TransportAdapter() - def test_ipv4_socket_options(self): - """Test that IPv4 adapter filters only IPv4 addresses.""" - # Mock socket.getaddrinfo to return mixed results + def test_ipv4_filtering_during_send(self): + """Test that IPv4 adapter filters only IPv4 addresses during send.""" mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.1', 80)), # IPv4 ] - + + original_getaddrinfo = socket.getaddrinfo + captured_results = [] + + def mock_super_send(*args, **kwargs): + # Capture the filtered results during send + captured_results.extend(socket.getaddrinfo('example.com', 80)) + return MagicMock() + with patch('socket.getaddrinfo', return_value=mock_results): - # Call the method that patches socket.getaddrinfo - self.adapter._ipv4_socket_options() - # Test that the patched function filters correctly - results = socket.getaddrinfo('example.com', 80) - self.assertEqual(len(results), 2) # Only IPv4 results - for result in results: - self.assertEqual(result[0], socket.AF_INET) - - def test_cleanup(self): - """Test that the adapter properly restores original getaddrinfo.""" + with patch.object(IPv4TransportAdapter.__bases__[0], 'send', mock_super_send): + self.adapter.send(MagicMock()) + + # Only IPv4 results should be captured + self.assertEqual(len(captured_results), 2) + for result in captured_results: + self.assertEqual(result[0], socket.AF_INET) + + def test_cleanup_after_send(self): + """Test that the adapter properly restores original getaddrinfo after send.""" original_getaddrinfo = socket.getaddrinfo - self.adapter._ipv4_socket_options() - - # Verify it was patched - self.assertNotEqual(socket.getaddrinfo, original_getaddrinfo) - - # Clean up - self.adapter.__del__() - - # Verify it was restored + + with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): + self.adapter.send(MagicMock()) + + # Verify it was restored after send + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) + + def test_cleanup_on_exception(self): + """Test that the adapter restores original getaddrinfo even if send raises.""" + original_getaddrinfo = socket.getaddrinfo + + with patch.object(IPv4TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): + with self.assertRaises(Exception): + self.adapter.send(MagicMock()) + + # Verify it was restored even after exception self.assertEqual(socket.getaddrinfo, original_getaddrinfo) class TestIPv6Adapter(unittest.TestCase): - """Test cases for IPv6HTTPAdapter.""" + """Test cases for IPv6TransportAdapter.""" def setUp(self): """Set up test fixtures.""" self.adapter = IPv6TransportAdapter() - def test_ipv6_socket_options(self): - """Test that IPv6 adapter filters only IPv6 addresses.""" - # Mock socket.getaddrinfo to return mixed results + def test_ipv6_filtering_during_send(self): + """Test that IPv6 adapter filters only IPv6 addresses during send.""" mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('2001:db8::1', 80)), # IPv6 ] - + + captured_results = [] + + def mock_super_send(*args, **kwargs): + # Capture the filtered results during send + captured_results.extend(socket.getaddrinfo('example.com', 80)) + return MagicMock() + with patch('socket.getaddrinfo', return_value=mock_results): - # Call the method that patches socket.getaddrinfo - self.adapter._ipv6_socket_options() - - # Test that the patched function filters correctly - results = socket.getaddrinfo('example.com', 80) - self.assertEqual(len(results), 2) # Only IPv6 results - for result in results: - self.assertEqual(result[0], socket.AF_INET6) - - def test_cleanup(self): - """Test that the adapter properly restores original getaddrinfo.""" + with patch.object(IPv6TransportAdapter.__bases__[0], 'send', mock_super_send): + self.adapter.send(MagicMock()) + + # Only IPv6 results should be captured + self.assertEqual(len(captured_results), 2) + for result in captured_results: + self.assertEqual(result[0], socket.AF_INET6) + + def test_cleanup_after_send(self): + """Test that the adapter properly restores original getaddrinfo after send.""" + original_getaddrinfo = socket.getaddrinfo + + with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): + self.adapter.send(MagicMock()) + + # Verify it was restored after send + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) + + def test_cleanup_on_exception(self): + """Test that the adapter restores original getaddrinfo even if send raises.""" original_getaddrinfo = socket.getaddrinfo - self.adapter._ipv6_socket_options() - - # Verify it was patched - self.assertNotEqual(socket.getaddrinfo, original_getaddrinfo) - - # Clean up - self.adapter.__del__() - - # Verify it was restored + + with patch.object(IPv6TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): + with self.assertRaises(Exception): + self.adapter.send(MagicMock()) + + # Verify it was restored even after exception self.assertEqual(socket.getaddrinfo, original_getaddrinfo) @@ -92,11 +118,26 @@ def test_both_adapters_independent(self): """Test that both adapters can coexist without interference.""" ipv4_adapter = IPv4TransportAdapter() ipv6_adapter = IPv6TransportAdapter() + original_getaddrinfo = socket.getaddrinfo + + with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): + ipv4_adapter.send(MagicMock()) + + with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): + ipv6_adapter.send(MagicMock()) + + # Verify original is still intact + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) + + def test_sequential_sends_restore_correctly(self): + """Test that multiple sequential sends properly restore getaddrinfo.""" + adapter = IPv4TransportAdapter() + original_getaddrinfo = socket.getaddrinfo - # Both should be able to patch independently - ipv4_adapter._ipv4_socket_options() - ipv6_adapter._ipv6_socket_options() + with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): + adapter.send(MagicMock()) + adapter.send(MagicMock()) + adapter.send(MagicMock()) - # Clean up both - ipv4_adapter.__del__() - ipv6_adapter.__del__() + # Verify original is still intact after multiple sends + self.assertEqual(socket.getaddrinfo, original_getaddrinfo) From 53df88b04050e315df62b45d399820092aade2d6 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 19 Jan 2026 01:18:12 -0500 Subject: [PATCH 16/23] update os image, add python 3.14, and update checkout and install python actions' version --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85fc444..64bf26a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,12 +24,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, windows-2022, macOS-13] - python-version: [3.7, 3.8, 3.9, 3.10.5, 3.11.0, 3.12.0, 3.13.0] + os: [ubuntu-22.04, windows-2022, macos-15-intel] + python-version: [3.7, 3.8, 3.9, 3.10.5, 3.11.0, 3.12.0, 3.13.0, 3.14.0] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Installation From 631fd1818e756f824dab6a65d13941b3de6c664e Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 19 Jan 2026 01:25:41 -0500 Subject: [PATCH 17/23] update GitHub Actions workflow to disable CI failure on Codecov errors + remove token (since the repo is going to be public) --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64bf26a..79df998 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,8 +46,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false if: matrix.python-version == env.TEST_PYTHON_VERSION && matrix.os == env.TEST_OS - name: Version check run: | From 5c07f9477c24681ea5be9b35bd4e4a236de26f44 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 19 Jan 2026 01:34:08 -0500 Subject: [PATCH 18/23] update name --- security.md => SECURITY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename security.md => SECURITY.md (100%) diff --git a/security.md b/SECURITY.md similarity index 100% rename from security.md rename to SECURITY.md From 1727fa9410c00ae0879f4675e9f8e4f844ef0c8b Mon Sep 17 00:00:00 2001 From: AHReccese Date: Fri, 23 Jan 2026 12:17:49 -0500 Subject: [PATCH 19/23] `README.md` updated --- README.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 36179f0..b1bcc0a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ ## Usage ### Enforce IPv4 +Use when you need to ensure connections only use IPv4 addresses, useful for legacy systems that don't support IPv6, networks with IPv4-only infrastructure, or testing IPv4 connectivity. + ```python import requests from ipforce import IPv4TransportAdapter @@ -76,6 +78,8 @@ response = session.get('https://ifconfig.co/json') ### Enforce IPv6 +Use when you need to ensure connections only use IPv6 addresses, useful for modern networks with IPv6 infrastructure, testing IPv6 connectivity, or applications requiring IPv6-specific features. + ```python import requests from ipforce import IPv6TransportAdapter @@ -89,24 +93,10 @@ session.mount('https://', IPv6TransportAdapter()) response = session.get('https://ifconfig.co/json') ``` -### When to Use - -- **IPv4 Adapter**: Use when you need to ensure connections only use IPv4 addresses, useful for: - - Legacy systems that don't support IPv6 - - Networks with IPv4-only infrastructure - - Testing IPv4 connectivity - -- **IPv6 Adapter**: Use when you need to ensure connections only use IPv6 addresses, useful for: - - Modern networks with IPv6 infrastructure - - Testing IPv6 connectivity - - Applications requiring IPv6-specific features - ### ⚠️ Important: Thread Safety Warning **Current adapters are NOT thread-safe!** They modify the global `socket.getaddrinfo` function, which can cause issues in multi-threaded applications. -**Future releases will include thread-safe alternatives.** - ## Issues & Bug Reports Just fill an issue and describe it. We'll check it ASAP! From 28d464e09045b62dcde911ea5e753d43bf32cc3f Mon Sep 17 00:00:00 2001 From: AHReccese Date: Fri, 23 Jan 2026 12:18:08 -0500 Subject: [PATCH 20/23] update namings --- tests/test_adapters.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index b4a40ab..aec48ea 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -7,12 +7,13 @@ class TestIPv4Adapter(unittest.TestCase): """Test cases for IPv4TransportAdapter.""" - def setUp(self): + def set_up(self): """Set up test fixtures.""" self.adapter = IPv4TransportAdapter() def test_ipv4_filtering_during_send(self): """Test that IPv4 adapter filters only IPv4 addresses during send.""" + self.set_up() mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 @@ -38,6 +39,7 @@ def mock_super_send(*args, **kwargs): def test_cleanup_after_send(self): """Test that the adapter properly restores original getaddrinfo after send.""" + self.set_up() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): @@ -48,6 +50,7 @@ def test_cleanup_after_send(self): def test_cleanup_on_exception(self): """Test that the adapter restores original getaddrinfo even if send raises.""" + self.set_up() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv4TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): @@ -61,12 +64,13 @@ def test_cleanup_on_exception(self): class TestIPv6Adapter(unittest.TestCase): """Test cases for IPv6TransportAdapter.""" - def setUp(self): + def set_up(self): """Set up test fixtures.""" self.adapter = IPv6TransportAdapter() def test_ipv6_filtering_during_send(self): """Test that IPv6 adapter filters only IPv6 addresses during send.""" + self.set_up() mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 @@ -91,6 +95,7 @@ def mock_super_send(*args, **kwargs): def test_cleanup_after_send(self): """Test that the adapter properly restores original getaddrinfo after send.""" + self.set_up() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): @@ -101,6 +106,7 @@ def test_cleanup_after_send(self): def test_cleanup_on_exception(self): """Test that the adapter restores original getaddrinfo even if send raises.""" + self.set_up() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv6TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): From 22abdae066dc80e8728526cfce23018044e5f98a Mon Sep 17 00:00:00 2001 From: AHReccese Date: Fri, 23 Jan 2026 12:18:16 -0500 Subject: [PATCH 21/23] update versions --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79df998..9719026 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,9 +27,9 @@ jobs: os: [ubuntu-22.04, windows-2022, macos-15-intel] python-version: [3.7, 3.8, 3.9, 3.10.5, 3.11.0, 3.12.0, 3.13.0, 3.14.0] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Installation From 3855af424a3d6b4398151160dd88359464ddf6b8 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Sun, 1 Feb 2026 14:13:07 -0500 Subject: [PATCH 22/23] update naming from `set_up` to `setup`. --- tests/test_adapters.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_adapters.py b/tests/test_adapters.py index aec48ea..659da30 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -7,13 +7,13 @@ class TestIPv4Adapter(unittest.TestCase): """Test cases for IPv4TransportAdapter.""" - def set_up(self): + def setup(self): """Set up test fixtures.""" self.adapter = IPv4TransportAdapter() def test_ipv4_filtering_during_send(self): """Test that IPv4 adapter filters only IPv4 addresses during send.""" - self.set_up() + self.setup() mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 @@ -39,7 +39,7 @@ def mock_super_send(*args, **kwargs): def test_cleanup_after_send(self): """Test that the adapter properly restores original getaddrinfo after send.""" - self.set_up() + self.setup() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv4TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): @@ -50,7 +50,7 @@ def test_cleanup_after_send(self): def test_cleanup_on_exception(self): """Test that the adapter restores original getaddrinfo even if send raises.""" - self.set_up() + self.setup() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv4TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): @@ -64,13 +64,13 @@ def test_cleanup_on_exception(self): class TestIPv6Adapter(unittest.TestCase): """Test cases for IPv6TransportAdapter.""" - def set_up(self): + def setup(self): """Set up test fixtures.""" self.adapter = IPv6TransportAdapter() def test_ipv6_filtering_during_send(self): """Test that IPv6 adapter filters only IPv6 addresses during send.""" - self.set_up() + self.setup() mock_results = [ (socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.1.1', 80)), # IPv4 (socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 80)), # IPv6 @@ -95,7 +95,7 @@ def mock_super_send(*args, **kwargs): def test_cleanup_after_send(self): """Test that the adapter properly restores original getaddrinfo after send.""" - self.set_up() + self.setup() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv6TransportAdapter.__bases__[0], 'send', return_value=MagicMock()): @@ -106,7 +106,7 @@ def test_cleanup_after_send(self): def test_cleanup_on_exception(self): """Test that the adapter restores original getaddrinfo even if send raises.""" - self.set_up() + self.setup() original_getaddrinfo = socket.getaddrinfo with patch.object(IPv6TransportAdapter.__bases__[0], 'send', side_effect=Exception("Test error")): From 33a282645cef68a0b55ad4a0488f19915ccd4611 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Sun, 1 Feb 2026 14:24:35 -0500 Subject: [PATCH 23/23] `README.md` updated --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b1bcc0a..6b286e8 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,8 @@ session.mount('https://', IPv6TransportAdapter()) response = session.get('https://ifconfig.co/json') ``` -### ⚠️ Important: Thread Safety Warning - -**Current adapters are NOT thread-safe!** They modify the global `socket.getaddrinfo` function, which can cause issues in multi-threaded applications. +> [!WARNING] +> Current adapters are NOT thread-safe! They modify the global `socket.getaddrinfo` function, which can cause issues in multi-threaded applications. ## Issues & Bug Reports