Bläddra i källkod

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 år sedan
förälder
incheckning
e7ac7ffd47

+ 9 - 5
redmail/email/attachment.py

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

+ 10 - 4
redmail/email/body.py

@@ -2,7 +2,7 @@ from email.message import EmailMessage
 import mimetypes
 import mimetypes
 from io import BytesIO
 from io import BytesIO
 from pathlib import Path
 from pathlib import Path
-from typing import Dict, Union, ByteString
+from typing import TYPE_CHECKING, Dict, Union, ByteString
 from pathlib import Path
 from pathlib import Path
 
 
 
 
@@ -12,12 +12,15 @@ from redmail.utils import import_from_string
 from email.utils import make_msgid
 from email.utils import make_msgid
 
 
 from jinja2.environment import Template, Environment
 from jinja2.environment import Template, Environment
-import pandas as pd
 
 
 from markupsafe import Markup
 from markupsafe import Markup
 
 
 # We try to import matplotlib and PIL but if fails, they will be None
 # 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:
 class BodyImage:
     "Utility class to represent image on HTML"
     "Utility class to represent image on HTML"
@@ -54,6 +57,9 @@ class Body:
         # TODO: Nicer tables. 
         # TODO: Nicer tables. 
         #   https://stackoverflow.com/a/55356741/13696660
         #   https://stackoverflow.com/a/55356741/13696660
         #   Email HTML (generally) does not support CSS
         #   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
         extra = {} if extra is None else extra
         df = pd.DataFrame(tbl)
         df = pd.DataFrame(tbl)
 
 
@@ -119,7 +125,7 @@ class HTMLBody(Body):
             
             
             self.attach_imgs(html_msg, cid_path_mapping)
             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)
         """Render Email HTML body (sets cid for image sources and adds data as other parameters)
 
 
         Parameters
         Parameters

+ 2 - 1
redmail/email/utils.py

@@ -2,4 +2,5 @@
 from redmail.utils import import_from_string
 from redmail.utils import import_from_string
 
 
 plt = import_from_string("matplotlib.pyplot", if_missing="ignore")
 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
 from redmail import EmailSender
 
 
-import re
 import base64
 import base64
 
 
 from pathlib import Path
 from pathlib import Path
-from io import BytesIO
 import pytest
 import pytest
-import pandas as pd
-
-import numpy as np
 
 
 from resources import get_mpl_fig, get_pil_image
 from resources import get_mpl_fig, get_pil_image
 from convert import remove_extra_lines
 from convert import remove_extra_lines

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

@@ -1,7 +1,6 @@
 from redmail import EmailSender
 from redmail import EmailSender
 
 
 import pytest
 import pytest
-import pandas as pd
 
 
 from convert import remove_extra_lines
 from convert import remove_extra_lines
 from getpass import getpass, getuser
 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 pathlib import Path
 from io import BytesIO
 from io import BytesIO
 import pytest
 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 resources import get_mpl_fig, get_pil_image
 from convert import remove_extra_lines
 from convert import remove_extra_lines
@@ -108,23 +108,23 @@ def test_with_image_obj(get_image_obj):
 
 
 
 
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
-    "df,", [
+    "get_df,", [
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 columns=pd.Index(["first", "second", "third"]),
                 columns=pd.Index(["first", "second", "third"]),
             ), 
             ), 
             id="Simple dataframe"
             id="Simple dataframe"
         ),
         ),
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1], [2], [3]],
                 [[1], [2], [3]],
                 columns=pd.Index(["first"]),
                 columns=pd.Index(["first"]),
             ), 
             ), 
             id="Single column datafram"
             id="Single column datafram"
         ),
         ),
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 columns=pd.Index(["first", "second", "third"]),
                 columns=pd.Index(["first", "second", "third"]),
                 index=pd.Index(["a", "b", "c"], name="category")
                 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"
             id="Simple dataframe with index"
         ),
         ),
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2, 3, "a"], [4, 5, 6, "b"], [7, 8, 9, "c"], [10, 11, 12, "d"]],
                 [[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"]),
                 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"]),
                 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"
             id="Complex dataframe"
         ),
         ),
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [[1, 2], [4, 5]],
                 [[1, 2], [4, 5]],
                 columns=pd.MultiIndex.from_tuples([("col a", "child b", "subchild a"), ("col a", "child b", "subchild a")]),
                 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")]),
                 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"
             id="Multiindex end with spanned"
         ),
         ),
         pytest.param(
         pytest.param(
-            pd.DataFrame(
+            lambda: pd.DataFrame(
                 [],
                 [],
                 columns=pd.Index(["first", "second", "third"]),
                 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)
     sender = EmailSender(host=None, port=1234)
     msg = sender.get_message(
     msg = sender.get_message(
         sender="me@gmail.com",
         sender="me@gmail.com",

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

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

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

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

+ 6 - 1
requirements.txt

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

+ 5 - 0
requirements/build.txt

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

+ 1 - 1
requirements/coverage.txt

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

+ 0 - 1
setup.py

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