Pārlūkot izejas kodu

Merge pull request #4 from Miksus/dev/drop_pandas

Seems CI still executes Pandas blocks (only one missed new block in code cov, the import error).
Mikael Koli 4 gadi atpakaļ
vecāks
revīzija
e7ac7ffd47

+ 9 - 5
redmail/email/attachment.py

@@ -8,8 +8,7 @@ import io
 from pathlib import Path, PurePath
 from typing import Union
 
-from .utils import PIL, plt
-import pandas as pd
+from .utils import PIL, plt, pd
 
 
 class Attachments:
@@ -67,12 +66,17 @@ class Attachments:
             raise TypeError(f"Unknown attachment {type(item)}")
 
     def _get_bytes_named(self, item, name:str) -> bytes:
+
+        has_pandas = pd is not None
+        has_pillow = PIL is not None
+        has_matplotlib = plt is not None
+
         if isinstance(item, str):
             # Considered as raw document
             return item
         elif isinstance(item, PurePath):
             return item.read_bytes()
-        elif isinstance(item, (pd.DataFrame, pd.Series)):
+        elif has_pandas and isinstance(item, (pd.DataFrame, pd.Series)):
             buff = io.BytesIO()
             if name.endswith(".xlsx"):
                 item.to_excel(buff)
@@ -87,12 +91,12 @@ class Attachments:
                 raise ValueError(f"Unknown dataframe conversion for '{name}'")
         elif isinstance(item, (bytes, bytearray)):
             return item
-        elif PIL is not None and isinstance(item, PIL.Image.Image):
+        elif has_pillow and isinstance(item, PIL.Image.Image):
             buf = io.BytesIO()
             item.save(buf, format='PNG')
             buf.seek(0)
             return buf.read()
-        elif plt is not None and isinstance(item, plt.Figure):
+        elif has_matplotlib and isinstance(item, plt.Figure):
             buf = io.BytesIO()
             item.savefig(buf, format=Path(name).suffix[1:])
             buf.seek(0)

+ 10 - 4
redmail/email/body.py

@@ -2,7 +2,7 @@ from email.message import EmailMessage
 import mimetypes
 from io import BytesIO
 from pathlib import Path
-from typing import Dict, Union, ByteString
+from typing import TYPE_CHECKING, Dict, Union, ByteString
 from pathlib import Path
 
 
@@ -12,12 +12,15 @@ from redmail.utils import import_from_string
 from email.utils import make_msgid
 
 from jinja2.environment import Template, Environment
-import pandas as pd
 
 from markupsafe import Markup
 
 # We try to import matplotlib and PIL but if fails, they will be None
-from .utils import PIL, plt
+from .utils import PIL, plt, pd
+
+if TYPE_CHECKING:
+    # For type hinting
+    from pandas import DataFrame
 
 class BodyImage:
     "Utility class to represent image on HTML"
@@ -54,6 +57,9 @@ class Body:
         # TODO: Nicer tables. 
         #   https://stackoverflow.com/a/55356741/13696660
         #   Email HTML (generally) does not support CSS
+        if pd is None:
+            raise ImportError("Missing package 'pandas'. Prettifying tables requires Pandas.")
+        
         extra = {} if extra is None else extra
         df = pd.DataFrame(tbl)
 
@@ -119,7 +125,7 @@ class HTMLBody(Body):
             
             self.attach_imgs(html_msg, cid_path_mapping)
 
-    def render(self, html:str, images:Dict[str, Union[dict, bytes, Path]]=None, tables:Dict[str, pd.DataFrame]=None, jinja_params:dict=None, domain=None):
+    def render(self, html:str, images:Dict[str, Union[dict, bytes, Path]]=None, tables:Dict[str, 'DataFrame']=None, jinja_params:dict=None, domain=None):
         """Render Email HTML body (sets cid for image sources and adds data as other parameters)
 
         Parameters

+ 2 - 1
redmail/email/utils.py

@@ -2,4 +2,5 @@
 from redmail.utils import import_from_string
 
 plt = import_from_string("matplotlib.pyplot", if_missing="ignore")
-PIL = import_from_string("PIL", if_missing="ignore")
+PIL = import_from_string("PIL", if_missing="ignore")
+pd = import_from_string("pandas", if_missing="ignore")

+ 0 - 5
redmail/test/email/test_attachments.py

@@ -1,15 +1,10 @@
 
 from redmail import EmailSender
 
-import re
 import base64
 
 from pathlib import Path
-from io import BytesIO
 import pytest
-import pandas as pd
-
-import numpy as np
 
 from resources import get_mpl_fig, get_pil_image
 from convert import remove_extra_lines

+ 0 - 1
redmail/test/email/test_body.py

@@ -1,7 +1,6 @@
 from redmail import EmailSender
 
 import pytest
-import pandas as pd
 
 from convert import remove_extra_lines
 from getpass import getpass, getuser

+ 12 - 11
redmail/test/email/test_inline_media.py

@@ -6,9 +6,9 @@ import re
 from pathlib import Path
 from io import BytesIO
 import pytest
-import pandas as pd
 
-import numpy as np
+# Importing Pandas from utils (is None if missing package)
+from redmail.email.utils import pd
 
 from resources import get_mpl_fig, get_pil_image
 from convert import remove_extra_lines
@@ -108,23 +108,23 @@ def test_with_image_obj(get_image_obj):
 
 
 @pytest.mark.parametrize(
-    "df,", [
+    "get_df,", [
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 columns=pd.Index(["first", "second", "third"]),
             ), 
             id="Simple dataframe"
         ),
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1], [2], [3]],
                 columns=pd.Index(["first"]),
             ), 
             id="Single column datafram"
         ),
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 columns=pd.Index(["first", "second", "third"]),
                 index=pd.Index(["a", "b", "c"], name="category")
@@ -132,7 +132,7 @@ def test_with_image_obj(get_image_obj):
             id="Simple dataframe with index"
         ),
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3, "a"], [4, 5, 6, "b"], [7, 8, 9, "c"], [10, 11, 12, "d"]],
                 columns=pd.MultiIndex.from_tuples([("parent a", "child a"), ("parent a", "child b"), ("parent b", "child a"), ("parent c", "child a")], names=["lvl 1", "lvl 2"]),
                 index=pd.MultiIndex.from_tuples([("row a", "sub a"), ("row a", "sub b"), ("row b", "sub a"), ("row c", "sub a")], names=["cat 1", "cat 2"]),
@@ -140,7 +140,7 @@ def test_with_image_obj(get_image_obj):
             id="Complex dataframe"
         ),
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2], [4, 5]],
                 columns=pd.MultiIndex.from_tuples([("col a", "child b", "subchild a"), ("col a", "child b", "subchild a")]),
                 index=pd.MultiIndex.from_tuples([("row a", "child b", "subchild a"), ("row a", "child b", "subchild a")]),
@@ -148,7 +148,7 @@ def test_with_image_obj(get_image_obj):
             id="Multiindex end with spanned"
         ),
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [],
                 columns=pd.Index(["first", "second", "third"]),
             ), 
@@ -156,8 +156,9 @@ def test_with_image_obj(get_image_obj):
         ),
     ]
 )
-def test_with_html_table_no_error(df, tmpdir):
-
+def test_with_html_table_no_error(get_df, tmpdir):
+    pytest.importorskip("pandas")
+    df = get_df()
     sender = EmailSender(host=None, port=1234)
     msg = sender.get_message(
         sender="me@gmail.com",

+ 0 - 1
redmail/test/email/test_template.py

@@ -1,7 +1,6 @@
 from redmail import EmailSender
 
 import pytest
-import pandas as pd
 
 from convert import remove_email_extra
 from getpass import getpass, getuser

+ 1 - 4
redmail/test/helpers/resources.py

@@ -1,5 +1,4 @@
 
-import numpy as np
 from io import BytesIO
 
 import pytest
@@ -8,10 +7,8 @@ def get_mpl_fig():
     pytest.importorskip("matplotlib")
     import matplotlib.pyplot as plt
     # Data for plotting
-    t = np.arange(0.0, 2.0, 0.01)
-    s = 1 + np.sin(2 * np.pi * t)
     fig, ax = plt.subplots()
-    ax.plot(t, s)
+    ax.plot([1, 2, 3])
 
     buf = BytesIO()
     fig.savefig(buf, format='png')

+ 6 - 1
requirements.txt

@@ -1,2 +1,7 @@
+# Hard dep
 jinja2
-pandas
+
+# Soft dep
+pandas
+matplotlib
+Pillow

+ 5 - 0
requirements/build.txt

@@ -1,4 +1,9 @@
 pytest
 
+# Package requirements
+jinja2
+
+# Optional
+pandas
 matplotlib
 Pillow

+ 1 - 1
requirements/coverage.txt

@@ -3,9 +3,9 @@ pytest
 pytest-cov
 
 # Package requirements
-pandas
 jinja2
 
 # Optional
+pandas
 matplotlib
 Pillow

+ 0 - 1
setup.py

@@ -28,6 +28,5 @@ setup(
 
     install_requires = [
         'jinja2',
-        'pandas',
     ],
 )