Repository: AnonymouX47/term-image (language: python)

Issue:
Separate the library and the image viewer
The reason for this is to foster quicker and less entangled development of both parts.


Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 2e4fe098..e08a4614 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-py_files = *.py src/* docs/source/conf.py tests/
+py_files := *.py src/* docs/source/conf.py tests/
 
 _: check test
 
@@ -61,34 +61,29 @@ uninstall:
 
 # Tests
 
-test-all: test test-url
-test: test-base test-iterator test-others test-graphics test-text test-widget
-test-graphics: test-kitty test-iterm2
-test-text: test-block
-test-widget: test-urwid
+pytest := pytest -v
 
-# Executing using `python -m` adds CWD to `sys.path`.
+## Filepath variables
 
-test-base:
-	python -m pytest -v tests/test_base.py
+test-top := tests/test_top_level.py
+test-base := tests/test_image/test_base.py
+test-block := tests/test_image/test_block.py
+test-kitty := tests/test_image/test_kitty.py
+test-iterm2 := tests/test_image/test_iterm2.py
+test-url := tests/test_image/test_url.py
+test-others := tests/test_image/test_others.py
+test-iterator := tests/test_iterator.py
+test-urwid := tests/test_widget/test_urwid.py
 
-test-iterator:
-	python -m pytest -v tests/test_image_iterator.py
+test-text := $(test-block)
+test-graphics := $(test-kitty) $(test-iterm2)
+test-image := $(test-base) $(test-text) $(test-graphics) $(test-others)
+test-widget := $(test-urwid)
+test := $(test-top) $(test-image) $(test-iterator) $(test-widget)
+test-all := $(test) $(test-url)
 
-test-others:
-	python -m pytest -v tests/test_others.py
+## Targets
 
-test-iterm2:
-	python -m pytest -v tests/test_iterm2.py
-
-test-kitty:
-	python -m pytest -v tests/test_kitty.py
-
-test-block:
-	python -m pytest -v tests/test_block.py
-
-test-url:
-	python -m pytest -v tests/test_url.py
-
-test-urwid:
-	python -m pytest -v tests/test_widget/test_urwid.py
+test-all test test-image test-text test-graphics test-widget \
+test-top test-base test-block test-kitty test-iterm2 test-url test-others test-iterator test-urwid:
+	$(pytest) $($@)
diff --git a/pyproject.toml b/pyproject.toml
index e69a63fa..541d7d27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,3 +5,9 @@ build-backend = "setuptools.build_meta"
 [tool.isort]
 profile = "black"
 combine_as_imports = true
+
+[tool.pytest.ini_options]
+filterwarnings = [
+    "ignore::term_image.exceptions.TermImageWarning:term_image.utils",
+    "error",
+]
diff --git a/requirements.txt b/requirements.txt
index 4c17b9a9..a74e4219 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,5 @@ black==22.12.0
 flake8==5.0.4
 isort[colors]==5.11.4
 pytest==7.2.0
-pytest-order==1.0.1
 requests==2.28.1
 urwid==2.1.2


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index b7585d59..da8c676e 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,3 +1,5 @@
+from contextlib import contextmanager
+
 import term_image
 
 
@@ -12,6 +14,17 @@ def set_cell_size(size):
     term_image.AutoCellRatio.is_supported = None
 
 
+@contextmanager
+def reset_cell_size_ratio():
+    cell_ratio = term_image._cell_ratio
+    cell_size = get_cell_size()
+    try:
+        yield
+    finally:
+        term_image._cell_ratio = cell_ratio
+        set_cell_size(cell_size)
+
+
 def get_terminal_size():
     return (80, 30)
 
diff --git a/tests/test_image/__init__.py b/tests/test_image/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/common.py b/tests/test_image/common.py
similarity index 93%
rename from tests/common.py
rename to tests/test_image/common.py
index db9005e7..fc0107ba 100644
--- a/tests/common.py
+++ b/tests/test_image/common.py
@@ -1,5 +1,6 @@
 """Render-style-dependent (though shared, not specific) tests"""
 
+import atexit
 from operator import gt, lt, mul
 from types import SimpleNamespace
 
@@ -10,7 +11,12 @@
 from term_image.image.common import _ALPHA_THRESHOLD, GraphicsImage, Size, TextImage
 from term_image.utils import BG_FMT, COLOR_RESET, get_fg_bg_colors, get_terminal_size
 
-from . import set_cell_size, set_fg_bg_colors, toggle_is_on_kitty
+from .. import (
+    reset_cell_size_ratio,
+    set_cell_size,
+    set_fg_bg_colors,
+    toggle_is_on_kitty,
+)
 
 columns, lines = get_terminal_size()
 
@@ -24,6 +30,11 @@
 python_img = Image.open("tests/images/python.png")
 
 
+@atexit.register
+def close_imgs():
+    python_img.close()
+
+
 def setup_common(ImageClass):
     globals().update(locals())  # width_height() requires ImageClass
 
@@ -449,50 +460,42 @@ class TestCellRatio_Text:
     def test_setup(self):
         type(self).image = ImageClass(python_img)  # Square
 
+    @reset_cell_size_ratio()
     def test_fixed_width_cell_ratio_adjustment(self):
-        try:
-            for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
-                set_cell_ratio(ratio)
-                self.image.set_size(width=_size)
-                assert self.image.width == _size
-                assert proportional(self.image)
-        finally:
-            set_cell_ratio(0.5)
+        for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
+            set_cell_ratio(ratio)
+            self.image.set_size(width=_size)
+            assert self.image.width == _size
+            assert proportional(self.image)
 
+    @reset_cell_size_ratio()
     def test_fixed_height_cell_ratio_adjustment(self):
-        try:
-            for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
-                set_cell_ratio(ratio)
-                self.image.set_size(height=_size)
-                assert self.image.height == _size
-                assert proportional(self.image)
-        finally:
-            set_cell_ratio(0.5)
+        for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
+            set_cell_ratio(ratio)
+            self.image.set_size(height=_size)
+            assert self.image.height == _size
+            assert proportional(self.image)
 
+    @reset_cell_size_ratio()
     def test_auto_size_cell_ratio_adjustment(self):
-        try:
-            for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
-                set_cell_ratio(ratio)
-                self.image.set_size()
-                assert proportional(self.image)
-        finally:
-            set_cell_ratio(0.5)
+        for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
+            set_cell_ratio(ratio)
+            self.image.set_size()
+            assert proportional(self.image)
 
 
 class TestCellRatio_Graphics:
     def test_setup(self):
         type(self).image = ImageClass(python_img)  # Square
 
+    @reset_cell_size_ratio()
     def test_cell_ratio_adjustment(self):
         self.image.set_size()
         size = self.image.size
-        try:
-            for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
-                set_cell_ratio(ratio)
-                self.image.set_size()
-                assert self.image.size == size
-        finally:
-            set_cell_ratio(0.5)
+        for ratio in (0.01, 0.1, 0.25, 0.4, 0.45, 0.55, 0.6, 0.75, 0.9, 0.99, 1.0):
+            set_cell_ratio(ratio)
+            self.image.set_size()
+            assert self.image.size == size
 
 
 class TestRender_Text:
diff --git a/tests/test_base.py b/tests/test_image/test_base.py
similarity index 99%
rename from tests/test_base.py
rename to tests/test_image/test_base.py
index 1fed9d60..fbe03409 100644
--- a/tests/test_base.py
+++ b/tests/test_image/test_base.py
@@ -1,5 +1,6 @@
 """Render-style-independent tests"""
 
+import atexit
 import io
 import os
 import sys
@@ -26,6 +27,11 @@
 from .common import _height, _width  # noqa:E402
 
 
+@atexit.register
+def close_imgs():
+    anim_img.close()
+
+
 def clear_stdout():
     stdout.seek(0)
     stdout.truncate()
diff --git a/tests/test_block.py b/tests/test_image/test_block.py
similarity index 98%
rename from tests/test_block.py
rename to tests/test_image/test_block.py
index 6679b743..45dda25f 100644
--- a/tests/test_block.py
+++ b/tests/test_image/test_block.py
@@ -2,25 +2,24 @@
 
 from random import random
 
-import pytest
-
 from term_image.image import BlockImage
 from term_image.image.common import _ALPHA_THRESHOLD
 from term_image.utils import BG_FMT, COLOR_RESET, CSI
 
-from . import common, set_fg_bg_colors
+from .. import set_fg_bg_colors
+from . import common
 from .common import _size, setup_common
 
-for name, obj in vars(common).items():
-    if name.endswith(("_All", "_Text")):
-        globals()[name] = obj
-
 
-@pytest.mark.order("first")
 def test_setup_common():
     setup_common(BlockImage)
 
 
+for name, obj in vars(common).items():
+    if name.endswith(("_All", "_Text")):
+        globals()[name] = obj
+
+
 class TestRender:
     # Fully transparent image
     # It's easy to predict it's pixel values
diff --git a/tests/test_iterm2.py b/tests/test_image/test_iterm2.py
similarity index 97%
rename from tests/test_iterm2.py
rename to tests/test_image/test_iterm2.py
index d4d469a3..8cff8637 100644
--- a/tests/test_iterm2.py
+++ b/tests/test_image/test_iterm2.py
@@ -16,22 +16,23 @@
 from term_image.image.iterm2 import LINES, START, WHOLE, ITerm2Image
 from term_image.utils import CSI, ST
 
-from . import common, set_fg_bg_colors
+from .. import set_fg_bg_colors
+from . import common
 from .common import _size, get_actual_render_size, python_img, setup_common
 from .test_base import clear_stdout, stdout
 
 ITerm2Image.READ_FROM_FILE = False
 
-for name, obj in vars(common).items():
-    if name.endswith(("_All", "_Graphics")):
-        globals()[name] = obj
-
 
-@pytest.mark.order("first")
 def test_setup_common():
     setup_common(ITerm2Image)
 
 
+for name, obj in vars(common).items():
+    if name.endswith(("_All", "_Graphics")):
+        globals()[name] = obj
+
+
 def test_set_render_method():
     try:
         assert ITerm2Image._render_method == ITerm2Image._default_render_method == LINES
@@ -765,14 +766,19 @@ def test_jpeg(self):
 
 def test_read_from_file():
     test_image_size = TestRenderWhole._test_image_size
-    png_file = open("tests/images/trans.png", "rb").read()
+
+    with open("tests/images/trans.png", "rb") as f:
+        png_file = f.read()
     png_image = ITerm2Image.from_file("tests/images/trans.png")
-    jpeg_file = open("tests/images/vert.jpg", "rb").read()
-    jpeg_image = ITerm2Image.from_file("tests/images/vert.jpg")
     png_image.set_render_method(WHOLE)
+
+    with open("tests/images/vert.jpg", "rb") as f:
+        jpeg_file = f.read()
+    jpeg_image = ITerm2Image.from_file("tests/images/vert.jpg")
     jpeg_image.set_render_method(WHOLE)
+
+    ITerm2Image.READ_FROM_FILE = True
     try:
-        ITerm2Image.READ_FROM_FILE = True
         ITerm2Image._TERM = ""
 
         # manipulation is required since the mode is RGBA
@@ -824,14 +830,22 @@ def render_native(image):
         return image._renderer(image._render_image, 0.0, native=True)
 
     test_image_size = TestRenderWhole._test_image_size
-    apng_file = open("tests/images/elephant.png", "rb").read()
+
+    with open("tests/images/elephant.png", "rb") as f:
+        apng_file = f.read()
     apng_image = ITerm2Image.from_file("tests/images/elephant.png", height=_size)
-    gif_file = open("tests/images/lion.gif", "rb").read()
+
+    with open("tests/images/lion.gif", "rb") as f:
+        gif_file = f.read()
     gif_image = ITerm2Image.from_file("tests/images/lion.gif", height=_size)
-    webp_file = open("tests/images/anim.webp", "rb").read()
+
+    with open("tests/images/anim.webp", "rb") as f:
+        webp_file = f.read()
     webp_image = ITerm2Image.from_file("tests/images/anim.webp", height=_size)
+
     img = Image.open("tests/images/lion.gif")
     img_image = ITerm2Image(img, height=_size)
+
     no_file_img = Image.open(open("tests/images/lion.gif", "rb"))
     no_file_image = ITerm2Image(no_file_img, height=_size)
 
@@ -888,13 +902,13 @@ def render_native(image):
             webp_file
             != decode_image(render_native(webp_image), term=ITerm2Image._TERM)[3]
         )
-        with pytest.raises(ITerm2ImageError, match="Native WEBP animation"):
-            try:
-                sys.stdout = stdout
+        sys.stdout = stdout
+        try:
+            with pytest.raises(ITerm2ImageError, match="Native WEBP animation"):
                 webp_image.draw(native=True, stall_native=False)
-            finally:
-                clear_stdout()
-                sys.stdout = sys.__stdout__
+        finally:
+            clear_stdout()
+            sys.stdout = sys.__stdout__
 
     # Image data size limit
     ITerm2Image.NATIVE_ANIM_MAXSIZE = 300000
@@ -902,6 +916,9 @@ def render_native(image):
         render_native(apng_image)
     render_native(gif_image)
 
+    img.close()
+    no_file_img.close()
+
 
 class TestClear:
     @contextmanager
diff --git a/tests/test_kitty.py b/tests/test_image/test_kitty.py
similarity index 99%
rename from tests/test_kitty.py
rename to tests/test_image/test_kitty.py
index efcdd6d3..28e668de 100644
--- a/tests/test_kitty.py
+++ b/tests/test_image/test_kitty.py
@@ -13,19 +13,20 @@
 from term_image.image.kitty import LINES, START, WHOLE, KittyImage
 from term_image.utils import CSI, ST
 
-from . import common, set_fg_bg_colors
+from .. import set_fg_bg_colors
+from . import common
 from .common import _size, get_actual_render_size, python_img, setup_common
 
-for name, obj in vars(common).items():
-    if name.endswith(("_All", "_Graphics")):
-        globals()[name] = obj
 
-
-@pytest.mark.order("first")
 def test_setup_common():
     setup_common(KittyImage)
 
 
+for name, obj in vars(common).items():
+    if name.endswith(("_All", "_Graphics")):
+        globals()[name] = obj
+
+
 def test_set_render_method():
     try:
         assert KittyImage._render_method == KittyImage._default_render_method == LINES
diff --git a/tests/test_image/test_others.py b/tests/test_image/test_others.py
new file mode 100644
index 00000000..85d1c868
--- /dev/null
+++ b/tests/test_image/test_others.py
@@ -0,0 +1,48 @@
+import pytest
+
+from term_image.image import AutoImage, BaseImage, ImageSource, from_file
+
+from .test_base import python_image, python_img
+
+
+class TestConvinience:
+    def test_auto_image(self):
+        with pytest.raises(TypeError, match=r"'PIL\.Image\.Image' instance"):
+            AutoImage(python_image)
+
+        # Ensure size arguments get through
+        with pytest.raises(ValueError, match=r"both width and height"):
+            AutoImage(python_img, width=1, height=1)
+
+        # Ensure scale argument gets through
+        with pytest.raises(TypeError, match=r"'scale'"):
+            AutoImage(python_img, scale=0.5)
+
+        assert isinstance(AutoImage(python_img), BaseImage)
+
+    def test_from_file(self):
+        with pytest.raises(TypeError, match=r"a string"):
+            from_file(python_img)
+
+        # Ensure size arguments get through
+        with pytest.raises(ValueError, match=r"both width and height"):
+            from_file(python_image, width=1, height=1)
+
+        # Ensure scale argument gets through
+        with pytest.raises(TypeError, match=r"'scale'"):
+            from_file(python_image, scale=1.0)
+
+        assert isinstance(from_file(python_image), BaseImage)
+
+
+def test_image_source():
+    assert len(ImageSource) == 3
+    assert all(member.name == name for name, member in ImageSource.__members__.items())
+
+
+def test_style():
+    class MyImage(BaseImage):
+        pass
+
+    assert MyImage.style is None
+    assert str(MyImage) == repr(MyImage)
diff --git a/tests/test_url.py b/tests/test_image/test_url.py
similarity index 96%
rename from tests/test_url.py
rename to tests/test_image/test_url.py
index b0fc4357..5946121a 100644
--- a/tests/test_url.py
+++ b/tests/test_image/test_url.py
@@ -1,3 +1,4 @@
+import atexit
 import os
 
 import pytest
@@ -14,6 +15,11 @@
 python_img = Image.open(python_image)
 
 
+@atexit.register
+def close_imgs():
+    python_img.close()
+
+
 def test_from_url():
     with pytest.raises(TypeError, match=r".* a string .*"):
         BlockImage.from_url(python_img)
diff --git a/tests/test_image_iterator.py b/tests/test_iterator.py
similarity index 96%
rename from tests/test_image_iterator.py
rename to tests/test_iterator.py
index ae5e1310..0227b5bf 100644
--- a/tests/test_image_iterator.py
+++ b/tests/test_iterator.py
@@ -1,3 +1,4 @@
+import atexit
 from types import GeneratorType
 
 import pytest
@@ -9,7 +10,8 @@
 
 _size = (30, 15)
 
-png_image = BlockImage(Image.open("tests/images/python.png"))
+png_img = Image.open("tests/images/python.png")
+png_image = BlockImage(png_img)
 gif_img = Image.open("tests/images/lion.gif")
 gif_image = BlockImage(gif_img)
 webp_img = Image.open("tests/images/anim.webp")
@@ -19,6 +21,12 @@
 webp_image._size = _size
 
 
+@atexit.register
+def close_imgs():
+    for img in (png_img, gif_img, webp_img):
+        img.close()
+
+
 def test_args():
     for value in ("tests/images/anim.webp", gif_img, webp_img):
         with pytest.raises(TypeError, match="'image'"):
@@ -139,7 +147,7 @@ def test_iter():
             prev_frame = frame
 
     # Frames are the same as for manual iteration
-    gif_image2 = BlockImage.from_file(gif_image._source.filename)
+    gif_image2 = BlockImage(gif_img)  # Need to change image size
     gif_image2._size = _size
     for n, frame in enumerate(ImageIterator(gif_image, 1, "1.1")):
         gif_image2.seek(n)
@@ -166,7 +174,7 @@ def render(*args, **kwargs):
             raise EOFError
         return ""
 
-    gif_image2 = BlockImage.from_file(gif_image._source.filename)
+    gif_image2 = BlockImage(gif_img)  # Need to change image size
     gif_image2._size = _size
     gif_image2._render_image = render
 
@@ -209,6 +217,8 @@ def render(*args, **kwargs):
     image_it.seek(gif_image2.n_frames - 1)
     assert next(image_it).count("\n") + 1 == 10
 
+    image_it.close()
+
 
 def test_sizing():
     def test(image_it):
@@ -229,7 +239,7 @@ def test(image_it):
         next(image_it)
         assert gif_image2.size is Size.FIT
 
-    gif_image2 = BlockImage.from_file(gif_image._source.filename)
+    gif_image2 = BlockImage(gif_img)  # Need to change image size
 
     # Uncached loop
     image_it = ImageIterator(gif_image2, 1, "1.1")
@@ -285,6 +295,7 @@ def test_loop_no():
 
 
 def test_close():
+    # From PIL image-sourced image
     image_it = ImageIterator(gif_image, 1)
     next(image_it)
     img = image_it._img
@@ -296,6 +307,7 @@ def test_close():
     assert not hasattr(image_it, "_img")
     assert img.load()
 
+    # File-sourced image
     image_it = ImageIterator(BlockImage.from_file(gif_image._source.filename), 1)
     next(image_it)
     img = image_it._img
diff --git a/tests/test_others.py b/tests/test_top_level.py
similarity index 55%
rename from tests/test_others.py
rename to tests/test_top_level.py
index 6dae60af..82589a89 100644
--- a/tests/test_others.py
+++ b/tests/test_top_level.py
@@ -5,43 +5,12 @@
 
 from term_image import AutoCellRatio, get_cell_ratio, set_cell_ratio
 from term_image.exceptions import TermImageError
-from term_image.image import AutoImage, BaseImage, ImageSource, from_file
 
-from . import set_cell_size
-from .test_base import python_image, python_img
-
-
-class TestConvinience:
-    def test_auto_image(self):
-        with pytest.raises(TypeError, match=r"'PIL\.Image\.Image' instance"):
-            AutoImage(python_image)
-
-        # Ensure size arguments get through
-        with pytest.raises(ValueError, match=r"both width and height"):
-            AutoImage(python_img, width=1, height=1)
-
-        # Ensure scale argument gets through
-        with pytest.raises(TypeError, match=r"'scale'"):
-            AutoImage(python_img, scale=0.5)
-
-        assert isinstance(AutoImage(python_img), BaseImage)
-
-    def test_from_file(self):
-        with pytest.raises(TypeError, match=r"a string"):
-            from_file(python_img)
-
-        # Ensure size arguments get through
-        with pytest.raises(ValueError, match=r"both width and height"):
-            from_file(python_image, width=1, height=1)
-
-        # Ensure scale argument gets through
-        with pytest.raises(TypeError, match=r"'scale'"):
-            from_file(python_image, scale=1.0)
-
-        assert isinstance(from_file(python_image), BaseImage)
+from . import reset_cell_size_ratio, set_cell_size
 
 
 class TestCellRatio:
+    @reset_cell_size_ratio()
     def test_args(self):
         for value in (0, "1", ()):
             with pytest.raises(TypeError, match=r"'ratio' must be"):
@@ -55,6 +24,7 @@ def test_args(self):
             with pytest.raises(TermImageError):
                 set_cell_ratio(value)
 
+    @reset_cell_size_ratio()
     def test_fixed(self):
         for value in (0.01, 0.1, 0.9, 0.99, 1.0, 2.0):
             set_cell_ratio(value)
@@ -65,6 +35,7 @@ def test_fixed(self):
             set_cell_ratio(value)
             assert get_cell_ratio() == value
 
+    @reset_cell_size_ratio()
     def test_fixed_auto(self):
         set_cell_size((4, 9))
         set_cell_ratio(AutoCellRatio.FIXED)
@@ -78,6 +49,7 @@ def test_fixed_auto(self):
             set_cell_size((0, 1))
             assert get_cell_ratio() == truediv(*cell_size)
 
+    @reset_cell_size_ratio()
     def test_dynamic_auto(self):
         set_cell_size((4, 9))
         set_cell_ratio(AutoCellRatio.DYNAMIC)
@@ -87,16 +59,3 @@ def test_dynamic_auto(self):
             cell_size = (randint(1, 20), randint(1, 20))
             set_cell_size(cell_size)
             assert get_cell_ratio() == truediv(*cell_size) == get_cell_ratio()
-
-
-def test_image_source():
-    assert len(ImageSource) == 3
-    assert all(member.name == name for name, member in ImageSource.__members__.items())
-
-
-def test_style():
-    class MyImage(BaseImage):
-        pass
-
-    assert MyImage.style is None
-    assert str(MyImage) == repr(MyImage)