From c568d7394767ba97c86768b4d3cb8cf6d3cc850a Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 16 Sep 2025 17:43:12 -0700 Subject: [PATCH 1/5] Update deprecated urllib APIs --- tests/external_httplib/test_urllib.py | 59 ++++++++++++-------------- tests/external_httplib/test_urllib2.py | 1 - 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/tests/external_httplib/test_urllib.py b/tests/external_httplib/test_urllib.py index 8d9dd1820..f6b0aa4fa 100644 --- a/tests/external_httplib/test_urllib.py +++ b/tests/external_httplib/test_urllib.py @@ -12,14 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - import pytest -try: - import urllib.request as urllib -except: - import urllib +from urllib.request import urlopen as urlopen, urlretrieve from testing_support.external_fixtures import cache_outgoing_headers, insert_incoming_headers from testing_support.fixtures import cat_enabled @@ -29,16 +24,19 @@ from newrelic.api.background_task import background_task +# Since Python 3.3, `urllib.URLopener()` has been deprecated in favor of +# `urllib.request.urlopen`. In Python 3.14, `urllib.URLopener()` will be +# removed. `urllib.request.urlopen` corresponds to the old `urllib2.urlopen` @pytest.fixture(scope="session") def metrics(server): - scoped = [(f"External/localhost:{server.port}/urllib/", 1)] + scoped = [(f"External/localhost:{server.port}/urllib2/", 1)] rollup = [ ("External/all", 1), ("External/allOther", 1), (f"External/localhost:{server.port}/all", 1), - (f"External/localhost:{server.port}/urllib/", 1), + (f"External/localhost:{server.port}/urllib2/", 1), ] return scoped, rollup @@ -53,8 +51,8 @@ def test_urlopener_http_request(server, metrics): ) @background_task(name="test_urllib:test_urlopener_http_request") def _test(): - opener = urllib.URLopener() - opener.open(f"http://localhost:{server.port}/") + with urlopen(f"http://localhost:{server.port}/") as response: + assert response.status == 200 _test() @@ -68,23 +66,20 @@ def test_urlopener_https_request(server, metrics): ) @background_task(name="test_urllib:test_urlopener_https_request") def _test(): - opener = urllib.URLopener() - try: - opener.open(f"https://localhost:{server.port}/") - except Exception: - pass + with urlopen(f"http://localhost:{server.port}/") as response: + assert response.status == 200 _test() def test_urlopener_http_request_with_port(server): - scoped = [(f"External/localhost:{server.port}/urllib/", 1)] + scoped = [(f"External/localhost:{server.port}/urllib2/", 1)] rollup = [ ("External/all", 1), ("External/allOther", 1), (f"External/localhost:{server.port}/all", 1), - (f"External/localhost:{server.port}/urllib/", 1), + (f"External/localhost:{server.port}/urllib2/", 1), ] @validate_transaction_metrics( @@ -95,18 +90,18 @@ def test_urlopener_http_request_with_port(server): ) @background_task(name="test_urllib:test_urlopener_http_request_with_port") def _test(): - opener = urllib.URLopener() - opener.open(f"http://localhost:{server.port}/") + with urlopen(f"http://localhost:{server.port}/") as response: + assert response.status == 200 _test() -_test_urlopener_file_request_scoped_metrics = [("External/unknown/urllib/", None)] +_test_urlopener_file_request_scoped_metrics = [("External/unknown/urllib2/", None)] _test_urlopener_file_request_rollup_metrics = [ ("External/all", None), ("External/allOther", None), - ("External/unknown/urllib/", None), + ("External/unknown/urllib2/", None), ] @@ -119,18 +114,18 @@ def _test(): @background_task() def test_urlopener_file_request(): file_uri = f"file://{__file__}" - opener = urllib.URLopener() - opener.open(file_uri) + with urlopen(file_uri) as response: + assert response @background_task() @cache_outgoing_headers @validate_cross_process_headers def test_urlopener_cross_process_request(server): - opener = urllib.URLopener() - opener.open(f"http://localhost:{server.port}/") - + with urlopen(f"http://localhost:{server.port}/") as response: + assert response.status == 200 +@pytest.mark.skip("Skipping CAT test") @cat_enabled def test_urlopener_cross_process_response(server): _test_urlopener_cross_process_response_scoped_metrics = [ @@ -161,8 +156,8 @@ def test_urlopener_cross_process_response(server): @validate_external_node_params(params=_test_urlopener_cross_process_response_external_node_params) @background_task(name="test_urllib:test_urlopener_cross_process_response") def _test(): - opener = urllib.URLopener() - opener.open(f"http://localhost:{server.port}/") + with urlopen(f"http://localhost:{server.port}/") as response: + assert response.status == 200 _test() @@ -176,7 +171,7 @@ def test_urlretrieve_http_request(server, metrics): ) @background_task(name="test_urllib:test_urlretrieve_http_request") def _test(): - urllib.urlretrieve(f"http://localhost:{server.port}/") + urlretrieve(f"http://localhost:{server.port}/") _test() @@ -191,7 +186,7 @@ def test_urlretrieve_https_request(server, metrics): @background_task(name="test_urllib:test_urlretrieve_https_request") def _test(): try: - urllib.urlretrieve(f"https://localhost:{server.port}/") + urlretrieve(f"https://localhost:{server.port}/") except Exception: pass @@ -202,7 +197,7 @@ def _test(): @cache_outgoing_headers @validate_cross_process_headers def test_urlretrieve_cross_process_request(server): - urllib.urlretrieve(f"http://localhost:{server.port}/") + urlretrieve(f"http://localhost:{server.port}/") @cat_enabled @@ -235,6 +230,6 @@ def test_urlretrieve_cross_process_response(server): @validate_external_node_params(params=_test_urlretrieve_cross_process_response_external_node_params) @background_task(name="test_urllib:test_urlretrieve_cross_process_response") def _test(): - urllib.urlretrieve(f"http://localhost:{server.port}/") + urlretrieve(f"http://localhost:{server.port}/") _test() diff --git a/tests/external_httplib/test_urllib2.py b/tests/external_httplib/test_urllib2.py index 54aeed721..c744614be 100644 --- a/tests/external_httplib/test_urllib2.py +++ b/tests/external_httplib/test_urllib2.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import urllib.request as urllib2 import pytest From e610cf6a9a09eac4fc03a0d129b57c073f985f6e Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Tue, 16 Sep 2025 17:47:12 -0700 Subject: [PATCH 2/5] Megalinter fixes --- tests/external_httplib/test_urllib.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/external_httplib/test_urllib.py b/tests/external_httplib/test_urllib.py index f6b0aa4fa..3597e2f48 100644 --- a/tests/external_httplib/test_urllib.py +++ b/tests/external_httplib/test_urllib.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest - -from urllib.request import urlopen as urlopen, urlretrieve +from urllib.request import urlopen, urlretrieve +import pytest from testing_support.external_fixtures import cache_outgoing_headers, insert_incoming_headers from testing_support.fixtures import cat_enabled from testing_support.validators.validate_cross_process_headers import validate_cross_process_headers @@ -24,10 +23,11 @@ from newrelic.api.background_task import background_task -# Since Python 3.3, `urllib.URLopener()` has been deprecated in favor of +# Since Python 3.3, `urllib.URLopener()` has been deprecated in favor of # `urllib.request.urlopen`. In Python 3.14, `urllib.URLopener()` will be # removed. `urllib.request.urlopen` corresponds to the old `urllib2.urlopen` + @pytest.fixture(scope="session") def metrics(server): scoped = [(f"External/localhost:{server.port}/urllib2/", 1)] @@ -125,6 +125,7 @@ def test_urlopener_cross_process_request(server): with urlopen(f"http://localhost:{server.port}/") as response: assert response.status == 200 + @pytest.mark.skip("Skipping CAT test") @cat_enabled def test_urlopener_cross_process_response(server): From 5043c12433cd7fe00d3fc1a002fb29a0243ebc8a Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Thu, 18 Sep 2025 19:17:15 -0700 Subject: [PATCH 3/5] Megalinter fixes --- newrelic/config.py | 8 ++++++-- newrelic/hooks/external_pyzeebe.py | 22 ++++++++++++++++------ tests/external_pyzeebe/_mocks.py | 18 +++++++++++------- tests/external_pyzeebe/test_client.py | 14 +++++++++++--- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/newrelic/config.py b/newrelic/config.py index fcbd49bb4..fb63adf80 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -4126,8 +4126,12 @@ def _process_module_builtin_defaults(): "newrelic.hooks.framework_azurefunctions", "instrument_azure_functions_worker_dispatcher", ) - _process_module_definition("pyzeebe.client.client", "newrelic.hooks.external_pyzeebe", "instrument_pyzeebe_client_client") - _process_module_definition("pyzeebe.worker.job_executor", "newrelic.hooks.external_pyzeebe", "instrument_pyzeebe_worker_job_executor") + _process_module_definition( + "pyzeebe.client.client", "newrelic.hooks.external_pyzeebe", "instrument_pyzeebe_client_client" + ) + _process_module_definition( + "pyzeebe.worker.job_executor", "newrelic.hooks.external_pyzeebe", "instrument_pyzeebe_worker_job_executor" + ) def _process_module_entry_points(): diff --git a/newrelic/hooks/external_pyzeebe.py b/newrelic/hooks/external_pyzeebe.py index 83fc4f0cc..6d976670a 100644 --- a/newrelic/hooks/external_pyzeebe.py +++ b/newrelic/hooks/external_pyzeebe.py @@ -16,9 +16,9 @@ import logging from newrelic.api.application import application_instance -from newrelic.api.web_transaction import WebTransaction from newrelic.api.function_trace import FunctionTrace from newrelic.api.transaction import current_transaction +from newrelic.api.web_transaction import WebTransaction from newrelic.common.object_wrapper import wrap_function_wrapper _logger = logging.getLogger(__name__) @@ -28,7 +28,9 @@ # Adds client method params as txn or span attributes def _add_client_input_attributes(method_name, trace, args, kwargs): - bpmn_id = extract_agent_attribute_from_methods(args, kwargs, method_name, ("run_process", "run_process_with_result"), "bpmn_process_id", 0) + bpmn_id = extract_agent_attribute_from_methods( + args, kwargs, method_name, ("run_process", "run_process_with_result"), "bpmn_process_id", 0 + ) if bpmn_id: trace._add_agent_attribute("zeebe.client.bpmnProcessId", bpmn_id) @@ -36,14 +38,16 @@ def _add_client_input_attributes(method_name, trace, args, kwargs): if msg_name: trace._add_agent_attribute("zeebe.client.messageName", msg_name) - correlation_key = extract_agent_attribute_from_methods(args, kwargs, method_name, ("publish_message"), "correlation_key", 1) + correlation_key = extract_agent_attribute_from_methods( + args, kwargs, method_name, ("publish_message"), "correlation_key", 1 + ) if correlation_key: trace._add_agent_attribute("zeebe.client.correlationKey", correlation_key) - + message_id = extract_agent_attribute_from_methods(args, kwargs, method_name, ("publish_message"), "message_id", 4) if message_id: trace._add_agent_attribute("zeebe.client.messageId", message_id) - + resource = extract_agent_attribute_from_methods(args, {}, method_name, ("deploy_resource"), None, 0) if resource: try: @@ -61,7 +65,13 @@ def extract_agent_attribute_from_methods(args, kwargs, method_name, methods, par value = args[index] return value except Exception: - _logger.warning("Exception occurred in PyZeebe instrumentation: failed to extract %s from %s. Report this issue to New Relic support.", param, method_name, exc_info=True) + _logger.warning( + "Exception occurred in PyZeebe instrumentation: failed to extract %s from %s. Report this issue to New Relic support.", + param, + method_name, + exc_info=True, + ) + # Async wrapper that instruments router/worker annotations` async def _nr_wrapper_execute_one_job(wrapped, instance, args, kwargs): diff --git a/tests/external_pyzeebe/_mocks.py b/tests/external_pyzeebe/_mocks.py index c82a8d94e..91ce47946 100644 --- a/tests/external_pyzeebe/_mocks.py +++ b/tests/external_pyzeebe/_mocks.py @@ -31,7 +31,11 @@ # Dummy RPC stub coroutines async def dummy_create_process_instance( - self, bpmn_process_id: str, variables: dict = None, version: int = -1, tenant_id: str = None # noqa: RUF013 + self, + bpmn_process_id: str, + variables: dict = None, # noqa: RUF013 + version: int = -1, + tenant_id: str = None, # noqa: RUF013 ): """Simulate ZeebeAdapter.create_process_instance""" return DummyCreateProcessInstanceResponse @@ -40,17 +44,17 @@ async def dummy_create_process_instance( async def dummy_create_process_instance_with_result( self, bpmn_process_id: str, - variables: dict = None, # noqa: RUF013 + variables: dict = None, # noqa: RUF013 version: int = -1, timeout: int = 0, variables_to_fetch=None, - tenant_id: str = None, # noqa: RUF013 + tenant_id: str = None, # noqa: RUF013 ): """Simulate ZeebeAdapter.create_process_instance_with_result""" return DummyCreateProcessInstanceWithResultResponse -async def dummy_deploy_resource(*resource_file_path: str, tenant_id: str = None): # noqa: RUF013 +async def dummy_deploy_resource(*resource_file_path: str, tenant_id: str = None): # noqa: RUF013 """Simulate ZeebeAdapter.deploy_resource""" # Create dummy deployment metadata for each provided resource path deployments = [ @@ -73,10 +77,10 @@ async def dummy_publish_message( self, name: str, correlation_key: str, - variables: dict = None, # noqa: RUF013 + variables: dict = None, # noqa: RUF013 time_to_live_in_milliseconds: int = 60000, - message_id: str = None, # noqa: RUF013 - tenant_id: str = None, # noqa: RUF013 + message_id: str = None, # noqa: RUF013 + tenant_id: str = None, # noqa: RUF013 ): """Simulate ZeebeAdapter.publish_message""" # Return the dummy response (contains message key) diff --git a/tests/external_pyzeebe/test_client.py b/tests/external_pyzeebe/test_client.py index 658da90fd..a832f20f4 100644 --- a/tests/external_pyzeebe/test_client.py +++ b/tests/external_pyzeebe/test_client.py @@ -20,8 +20,8 @@ ) from pyzeebe import ZeebeClient, create_insecure_channel from pyzeebe.grpc_internals.zeebe_adapter import ZeebeAdapter -from testing_support.validators.validate_span_events import validate_span_events from testing_support.validators.validate_custom_event import validate_custom_event_count +from testing_support.validators.validate_span_events import validate_span_events from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics from newrelic.api.background_task import background_task @@ -114,7 +114,14 @@ async def _test(): @validate_transaction_metrics( "test_zeebe_client:publish_message", rollup_metrics=[("ZeebeClient/publish_message", 1)], background_task=True ) -@validate_span_events(exact_agents={"zeebe.client.messageName": "test_message", "zeebe.client.correlationKey": "999999", "zeebe.client.messageId": "abc123"}, count=1) +@validate_span_events( + exact_agents={ + "zeebe.client.messageName": "test_message", + "zeebe.client.correlationKey": "999999", + "zeebe.client.messageId": "abc123", + }, + count=1, +) def test_publish_message(monkeypatch, loop): monkeypatch.setattr(ZeebeAdapter, "publish_message", dummy_publish_message) @@ -125,6 +132,7 @@ async def _test(): loop.run_until_complete(_test()) + @validate_custom_event_count(count=0) def test_publish_message_outside_txn(monkeypatch, loop): monkeypatch.setattr(ZeebeAdapter, "publish_message", dummy_publish_message) @@ -133,4 +141,4 @@ async def _test(): result = await client.publish_message(name="test_message", correlation_key="999999", message_id="abc123") assert result.key == 999999 - loop.run_until_complete(_test()) \ No newline at end of file + loop.run_until_complete(_test()) From bf9397ae27544cec1788c4c30e94a03df8e31cbe Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Fri, 19 Sep 2025 11:34:53 -0700 Subject: [PATCH 4/5] Add skipif and revert urllib tests --- tests/external_httplib/test_urllib.py | 68 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/tests/external_httplib/test_urllib.py b/tests/external_httplib/test_urllib.py index 3597e2f48..fd51dd7f2 100644 --- a/tests/external_httplib/test_urllib.py +++ b/tests/external_httplib/test_urllib.py @@ -12,9 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from urllib.request import urlopen, urlretrieve - +import sys import pytest + +try: + import urllib.request as urllib +except ImportError: + import urllib + from testing_support.external_fixtures import cache_outgoing_headers, insert_incoming_headers from testing_support.fixtures import cat_enabled from testing_support.validators.validate_cross_process_headers import validate_cross_process_headers @@ -27,21 +32,24 @@ # `urllib.request.urlopen`. In Python 3.14, `urllib.URLopener()` will be # removed. `urllib.request.urlopen` corresponds to the old `urllib2.urlopen` +SKIP_IF_PYTHON_3_14_OR_ABOVE = pytest.mark.skipif( + sys.version_info[0:2] >= (3, 14), reason="urllib.URLopener() is removed in Python 3.14 and above" +) @pytest.fixture(scope="session") def metrics(server): - scoped = [(f"External/localhost:{server.port}/urllib2/", 1)] + scoped = [(f"External/localhost:{server.port}/urllib/", 1)] rollup = [ ("External/all", 1), ("External/allOther", 1), (f"External/localhost:{server.port}/all", 1), - (f"External/localhost:{server.port}/urllib2/", 1), + (f"External/localhost:{server.port}/urllib/", 1), ] return scoped, rollup - +@SKIP_IF_PYTHON_3_14_OR_ABOVE def test_urlopener_http_request(server, metrics): @validate_transaction_metrics( "test_urllib:test_urlopener_http_request", @@ -51,12 +59,13 @@ def test_urlopener_http_request(server, metrics): ) @background_task(name="test_urllib:test_urlopener_http_request") def _test(): - with urlopen(f"http://localhost:{server.port}/") as response: - assert response.status == 200 + opener = urllib.URLopener() + opener.open(f"http://localhost:{server.port}/") _test() +@SKIP_IF_PYTHON_3_14_OR_ABOVE def test_urlopener_https_request(server, metrics): @validate_transaction_metrics( "test_urllib:test_urlopener_https_request", @@ -66,20 +75,23 @@ def test_urlopener_https_request(server, metrics): ) @background_task(name="test_urllib:test_urlopener_https_request") def _test(): - with urlopen(f"http://localhost:{server.port}/") as response: - assert response.status == 200 + opener = urllib.URLopener() + try: + opener.open(f"https://localhost:{server.port}/") + except Exception: + pass _test() - +@SKIP_IF_PYTHON_3_14_OR_ABOVE def test_urlopener_http_request_with_port(server): - scoped = [(f"External/localhost:{server.port}/urllib2/", 1)] + scoped = [(f"External/localhost:{server.port}/urllib/", 1)] rollup = [ ("External/all", 1), ("External/allOther", 1), (f"External/localhost:{server.port}/all", 1), - (f"External/localhost:{server.port}/urllib2/", 1), + (f"External/localhost:{server.port}/urllib/", 1), ] @validate_transaction_metrics( @@ -90,21 +102,22 @@ def test_urlopener_http_request_with_port(server): ) @background_task(name="test_urllib:test_urlopener_http_request_with_port") def _test(): - with urlopen(f"http://localhost:{server.port}/") as response: - assert response.status == 200 + opener = urllib.URLopener() + opener.open(f"http://localhost:{server.port}/") _test() -_test_urlopener_file_request_scoped_metrics = [("External/unknown/urllib2/", None)] +_test_urlopener_file_request_scoped_metrics = [("External/unknown/urllib/", None)] _test_urlopener_file_request_rollup_metrics = [ ("External/all", None), ("External/allOther", None), - ("External/unknown/urllib2/", None), + ("External/unknown/urllib/", None), ] +@SKIP_IF_PYTHON_3_14_OR_ABOVE @validate_transaction_metrics( "test_urllib:test_urlopener_file_request", scoped_metrics=_test_urlopener_file_request_scoped_metrics, @@ -114,19 +127,20 @@ def _test(): @background_task() def test_urlopener_file_request(): file_uri = f"file://{__file__}" - with urlopen(file_uri) as response: - assert response + opener = urllib.URLopener() + opener.open(file_uri) +@SKIP_IF_PYTHON_3_14_OR_ABOVE @background_task() @cache_outgoing_headers @validate_cross_process_headers def test_urlopener_cross_process_request(server): - with urlopen(f"http://localhost:{server.port}/") as response: - assert response.status == 200 + opener = urllib.URLopener() + opener.open(f"http://localhost:{server.port}/") -@pytest.mark.skip("Skipping CAT test") +@SKIP_IF_PYTHON_3_14_OR_ABOVE @cat_enabled def test_urlopener_cross_process_response(server): _test_urlopener_cross_process_response_scoped_metrics = [ @@ -157,8 +171,8 @@ def test_urlopener_cross_process_response(server): @validate_external_node_params(params=_test_urlopener_cross_process_response_external_node_params) @background_task(name="test_urllib:test_urlopener_cross_process_response") def _test(): - with urlopen(f"http://localhost:{server.port}/") as response: - assert response.status == 200 + opener = urllib.URLopener() + opener.open(f"http://localhost:{server.port}/") _test() @@ -172,7 +186,7 @@ def test_urlretrieve_http_request(server, metrics): ) @background_task(name="test_urllib:test_urlretrieve_http_request") def _test(): - urlretrieve(f"http://localhost:{server.port}/") + urllib.urlretrieve(f"http://localhost:{server.port}/") _test() @@ -187,7 +201,7 @@ def test_urlretrieve_https_request(server, metrics): @background_task(name="test_urllib:test_urlretrieve_https_request") def _test(): try: - urlretrieve(f"https://localhost:{server.port}/") + urllib.urlretrieve(f"https://localhost:{server.port}/") except Exception: pass @@ -198,7 +212,7 @@ def _test(): @cache_outgoing_headers @validate_cross_process_headers def test_urlretrieve_cross_process_request(server): - urlretrieve(f"http://localhost:{server.port}/") + urllib.urlretrieve(f"http://localhost:{server.port}/") @cat_enabled @@ -231,6 +245,6 @@ def test_urlretrieve_cross_process_response(server): @validate_external_node_params(params=_test_urlretrieve_cross_process_response_external_node_params) @background_task(name="test_urllib:test_urlretrieve_cross_process_response") def _test(): - urlretrieve(f"http://localhost:{server.port}/") + urllib.urlretrieve(f"http://localhost:{server.port}/") _test() From e9d5515d53391ab0608c2d4c05d5de526a9db46f Mon Sep 17 00:00:00 2001 From: Lalleh Rafeei Date: Fri, 19 Sep 2025 11:38:36 -0700 Subject: [PATCH 5/5] Megalinter fixes --- tests/external_httplib/test_urllib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/external_httplib/test_urllib.py b/tests/external_httplib/test_urllib.py index fd51dd7f2..9b2fde350 100644 --- a/tests/external_httplib/test_urllib.py +++ b/tests/external_httplib/test_urllib.py @@ -13,6 +13,7 @@ # limitations under the License. import sys + import pytest try: @@ -36,6 +37,7 @@ sys.version_info[0:2] >= (3, 14), reason="urllib.URLopener() is removed in Python 3.14 and above" ) + @pytest.fixture(scope="session") def metrics(server): scoped = [(f"External/localhost:{server.port}/urllib/", 1)] @@ -49,6 +51,7 @@ def metrics(server): return scoped, rollup + @SKIP_IF_PYTHON_3_14_OR_ABOVE def test_urlopener_http_request(server, metrics): @validate_transaction_metrics( @@ -83,6 +86,7 @@ def _test(): _test() + @SKIP_IF_PYTHON_3_14_OR_ABOVE def test_urlopener_http_request_with_port(server): scoped = [(f"External/localhost:{server.port}/urllib/", 1)]