Repository: cogent3/cogent3
Pull request #2078 (2024-10-29 21:07:13)

ENH: open_url() return type respects mode
## Summary by Sourcery

Enhance the open_url() function to respect the mode parameter, allowing it to return the appropriate type based on the specified mode. Update the README.md to improve the layout by repositioning the project logo. Add tests to ensure the correct functionality of open_url() with different modes and compressed file handling.

Enhancements:
- Ensure open_url() function respects the mode parameter, allowing for both binary and text reading modes.

Documentation:
- Update README.md to reposition the project logo for better alignment.

Tests:
- Add tests for open_url() to verify correct return type based on mode, including handling of gzip URIs and compressed file URIs.


Patch:
diff --git a/.hgignore b/.hgignore
index fed9764e5..ff32c6866 100644
--- a/.hgignore
+++ b/.hgignore
@@ -38,4 +38,5 @@ working/*
 lcov*.info
 .ruff_cache/*
 .devcontainer/*
-hg_old.zip
\ No newline at end of file
+hg_old*zip
+hg_old/*
\ No newline at end of file
diff --git a/README.md b/README.md
index 3a99654fb..85e26356c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+
+<p align="left">
+  <img src="https://raw.githubusercontent.com/cogent3/cogent3.github.io/e72df8c155c100f502b6a7009347d1821ab3adef/doc/_static/c3-logo.svg" width="300">
+</p>
+
 [![PyPI version](https://badge.fury.io/py/cogent3.svg)](https://badge.fury.io/py/cogent3)
 [![Downloads](https://pepy.tech/badge/cogent3/month)](https://pepy.tech/project/cogent3)
 
@@ -10,11 +15,6 @@
 [![CodeQL](https://github.com/cogent3/cogent3/actions/workflows/codeql.yml/badge.svg)](https://github.com/cogent3/cogent3/actions/workflows/codeql.yml)
 [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e80e3441de59449bb1a4d8ad1fdea4fa)](https://app.codacy.com/gh/cogent3/cogent3/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
 
-<p align="left">
-  <img src="https://raw.githubusercontent.com/cogent3/cogent3.github.io/e72df8c155c100f502b6a7009347d1821ab3adef/doc/_static/c3-logo.svg" width="300">
-</p>
-
-
 `cogent3` is a mature python library for analysis of genomic sequence data. We endeavour to provide a first-class experience within Jupyter notebooks, but the algorithms also support parallel execution on compute systems with 1000's of processors.
 
 ## 📣 Feature Announcements 📣
diff --git a/src/cogent3/util/io.py b/src/cogent3/util/io.py
index 32b6624c0..901b4c364 100644
--- a/src/cogent3/util/io.py
+++ b/src/cogent3/util/io.py
@@ -142,7 +142,7 @@ def open_(filename: PathType, mode="rt", **kwargs) -> IO:
     return op(filename, mode, encoding=encoding, **kwargs)
 
 
-def open_url(url: Union[str, ParseResult], mode="r", **kwargs) -> IO:
+def open_url(url: Union[str, ParseResult], mode="rt", **kwargs) -> IO:
     """open a url
 
     Parameters
@@ -154,33 +154,29 @@ def open_url(url: Union[str, ParseResult], mode="r", **kwargs) -> IO:
 
     Raises
     ------
-    If not http(s).
+    Rasies IOError if mode is write or it's not a url.
 
-    Notes
-    -----
-    If mode='b' or 'rb' (binary read), the function returns file object to read
-    else returns TextIOWrapper to read text with specified encoding in the URL
+    Returns
+    -------
+    file object which reads binary if "b" in mode, else text.
     """
     _, compression = get_format_suffixes(
         getattr(url, "path", url)
     )  # handling possibility of ParseResult
-    mode = "rb" if compression else mode or "r"
+    mode = mode or "r"
+
+    if "r" not in mode:
+        raise IOError("opening a url only allowed in read mode")
 
     if not is_url(url):
-        raise ValueError(
-            f"URL scheme must be http, https or file, not {str(url)[:20]!r}"
-        )
+        raise IOError(f"URL scheme must be http, https or file, not {str(url)[:20]!r}")
 
     url_parsed = url if isinstance(url, ParseResult) else urlparse(url)
 
-    if "r" not in mode:
-        raise ValueError("opening a url only allowed in read mode")
-
     response = urlopen(url_parsed.geturl(), timeout=10)
     encoding = response.headers.get_content_charset()
     if compression:
         response = _get_compression_open(compression=compression)(response)
-        mode = "r"  # wrap it as text
 
     return response if "b" in mode else TextIOWrapper(response, encoding=encoding)
 


Test patch:
diff --git a/tests/test_parse/test_fasta.py b/tests/test_parse/test_fasta.py
index e47b3c083..aaa75269a 100644
--- a/tests/test_parse/test_fasta.py
+++ b/tests/test_parse/test_fasta.py
@@ -460,6 +460,23 @@ def test_iter_fasta_records_path_types(fasta_path):
     assert len(got) == 1
 
 
+@pytest.fixture(params=("gz", "xz"))
+def fa_gz(DATA_DIR, tmp_path, request):
+    from cogent3 import open_
+
+    path = DATA_DIR / "brca1.fasta"
+    data = path.read_bytes()
+    outpath = tmp_path / f"brca1.fasta.{request.param}"
+    with open_(outpath, "wb") as out:
+        out.write(data)
+    return outpath.as_uri()
+
+
+def test_iter_fasta_records_compressed_file_uri(fa_gz):
+    got = dict(iter_fasta_records(str(fa_gz)))
+    assert len(got) == 55
+
+
 def test_iter_fasta_records_invalid():
     with pytest.raises(TypeError):
         list(iter_fasta_records(numpy.array([">abcd", "ACGG"])))
diff --git a/tests/test_util/test_io.py b/tests/test_util/test_io.py
index 896ca2482..b22eec21c 100644
--- a/tests/test_util/test_io.py
+++ b/tests/test_util/test_io.py
@@ -367,7 +367,7 @@ def test_open_url_compressed(DATA_DIR):
     with open_(DATA_DIR / file_name) as infile:
         local_data = infile.read()
 
-    with open_url(remote_root.format(file_name)) as infile:
+    with open_url(remote_root.format(file_name), mode="rt") as infile:
         remote_data = infile.read()
 
     assert remote_data.splitlines() == local_data.splitlines()
@@ -520,3 +520,22 @@ def test_is_url(url):
 )
 def test_not_is_url(url):
     assert not is_url(url)
+
+
+@pytest.fixture
+def gzip_uri(DATA_DIR, tmp_path):
+    inpath = DATA_DIR / "sample.tsv"
+    data = inpath.read_text()
+    outpath = tmp_path / "sample.tsv.gz"
+    with open_(outpath, "wb") as outfile:
+        outfile.write(data.encode("utf8"))
+
+    return outpath.as_uri()
+
+
+@pytest.mark.parametrize("mode", ("r", "rb", "rt"))
+def test_open_url_gzip_mode(gzip_uri, mode):
+    with open_url(gzip_uri, mode=mode) as infile:
+        got = infile.read()
+    expect_type = bytes if "b" in mode else str
+    assert isinstance(got, expect_type)