Repository: yt-project/yt

Issue:
BUG: `urllib.*` modules are never imported
### Bug report

**Bug summary**

The `urllib` submodules (`request`, `parse`, etc.) are used in a few code paths, most notably the command-line interface. Importing `urllib` on its own does nothing, as its `__init__.py` is empty (see https://github.com/python/cpython/tree/3.12/Lib/urllib/). This leads to a crash if the code tries to use the modules.

**Code for reproduction**

```console
$ yt download --list
```

**Actual outcome**

<!--The output produced by the above code, which may be a screenshot, console
output, etc.-->

```
Traceback (most recent call last):
  File "/home/eric/mambaforge/envs/yt-devel/bin/yt", line 8, in <module>
    sys.exit(run_main())
  File "/home/eric/build/yt/yt/utilities/command_line.py", line 1613, in run_main
    args.func(args)
  File "/home/eric/build/yt/yt/utilities/command_line.py", line 223, in run
    self(args)
  File "/home/eric/build/yt/yt/utilities/command_line.py", line 1555, in __call__
    self.get_list()
  File "/home/eric/build/yt/yt/utilities/command_line.py", line 1589, in get_list
    urllib.request.urlopen("http://yt-project.org/data/datafiles.json")
AttributeError: module 'urllib' has no attribute 'request'
```

**Expected outcome**

It should list all the data files available on https://yt-project.org/data.

**Version Information**
  * Operating System: Linux
  * Python Version: 3.10.12
  * yt version: git main (14c49c4)


Discussion:
Hi, and welcome to yt!  Thanks for opening your first issue.  We have an issue template that helps us to gather relevant information to help diagnosing and fixing the issue.

Thanks for spotting this ! I think this is probably a regression in 4.2.0, and my main suspect is https://github.com/yt-project/yt/pull/4265

Resolving patch:
diff --git a/yt/funcs.py b/yt/funcs.py
--- a/yt/funcs.py
+++ b/yt/funcs.py
@@ -13,7 +13,6 @@
 import sys
 import time
 import traceback
-import urllib
 from collections import UserDict
 from collections.abc import Callable
 from copy import deepcopy
@@ -619,6 +618,8 @@ def fancy_download_file(url, filename, requests=None):
 
 
 def simple_download_file(url, filename):
+    import urllib.request
+
     class MyURLopener(urllib.request.FancyURLopener):
         def http_error_default(self, url, fp, errcode, errmsg, headers):
             raise RuntimeError(
@@ -632,6 +633,8 @@ def http_error_default(self, url, fp, errcode, errmsg, headers):
 # This code snippet is modified from Georg Brandl
 def bb_apicall(endpoint, data, use_pass=True):
     import getpass
+    import urllib.parse
+    import urllib.request
 
     uri = f"https://api.bitbucket.org/1.0/{endpoint}/"
     # since bitbucket doesn't return the required WWW-Authenticate header when
diff --git a/yt/utilities/command_line.py b/yt/utilities/command_line.py
--- a/yt/utilities/command_line.py
+++ b/yt/utilities/command_line.py
@@ -5,7 +5,6 @@
 import pprint
 import sys
 import textwrap
-import urllib
 from typing import Any, Optional, Union
 
 import numpy as np
@@ -1202,6 +1201,9 @@ class YTDeleteImageCmd(YTCommand):
     name = "delete_image"
 
     def __call__(self, args):
+        import urllib.error
+        import urllib.request
+
         headers = {"Authorization": f"Client-ID {ytcfg.get('yt', 'imagebin_api_key')}"}
 
         delete_url = ytcfg.get("yt", "imagebin_delete_url")
@@ -1235,6 +1237,10 @@ class YTUploadImageCmd(YTCommand):
     name = "upload_image"
 
     def __call__(self, args):
+        import urllib.error
+        import urllib.parse
+        import urllib.request
+
         filename = args.file
         if not filename.endswith(".png"):
             print("File must be a PNG file!")
@@ -1585,6 +1591,8 @@ def __call__(self, args):
         print(f"File: {args.filename} downloaded successfully to {data_file}")
 
     def get_list(self):
+        import urllib.request
+
         data = (
             urllib.request.urlopen("http://yt-project.org/data/datafiles.json")
             .read()


Test patch:
diff --git a/yt/utilities/answer_testing/framework.py b/yt/utilities/answer_testing/framework.py
--- a/yt/utilities/answer_testing/framework.py
+++ b/yt/utilities/answer_testing/framework.py
@@ -11,7 +11,6 @@
 import sys
 import tempfile
 import time
-import urllib
 import warnings
 import zlib
 from collections import defaultdict
@@ -207,6 +206,9 @@ def get(self, ds_name, default=None):
 
 class AnswerTestCloudStorage(AnswerTestStorage):
     def get(self, ds_name, default=None):
+        import urllib.error
+        import urllib.request
+
         if self.reference_name is None:
             return default
         if ds_name in self.cache: