From 5eb9cfe1c6f0283b7421f1a8cc681b6995c30e2d Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Sat, 31 Jul 2021 20:36:31 +0300
Subject: [PATCH] Replace the PerfData dataclass with a named tuple.

- Works on Python 3.6.
- The upsides of dataclass don't help in this case.
- Is faster and uses less memory (although in this case it would hard to
  even measure the difference).
---
 .gitlab-ci.yml       |   1 -
 Makefile             |   2 +-
 mnpw/nagios.py       | 125 +++++++++++++++++++++----------------------
 pyproject.toml       |   1 -
 tests/test_nagios.py |   2 +-
 5 files changed, 63 insertions(+), 68 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1249888..b815f5b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,6 @@ build-executable:
   extends: .python3-build
   before_script:
     - poetry install --no-dev
-    - poetry run pip freeze
   script:
     - poetry run make
   artifacts:
diff --git a/Makefile b/Makefile
index f348e25..8f762c1 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
 all: dist/mnpw
 
 dist/mnpw: mnpw/*.py pyproject.toml *.rst LICENSE.txt
-	pyinstaller --noconfirm --onefile "--name=$$(basename $@)" --hidden-import dataclasses  mnpw/__init__.py
+	pyinstaller --noconfirm --onefile "--name=$$(basename $@)" mnpw/__init__.py
 
 .PHONY: clean
 clean:
diff --git a/mnpw/nagios.py b/mnpw/nagios.py
index c1e33c4..30f0edc 100644
--- a/mnpw/nagios.py
+++ b/mnpw/nagios.py
@@ -5,10 +5,10 @@ https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/3/en/pluginapi.ht
 """
 # pylint: disable=logging-fstring-interpolation
 
+import collections
 import enum
 import logging
 import subprocess  # nosec
-from dataclasses import dataclass
 
 DEFAULT_TIMEOUT = 10  # In seconds.
 
@@ -22,66 +22,63 @@ class NagiosCode(enum.IntEnum):
     UNKNOWN = 3
 
 
-@dataclass()
-class PerfData:
-    """Performance data.
-
-    As published by a Nagios plugin.
-    """
-
-    name: str
-    value: float
-    unit: str = ""
-    warning: float = None
-    critical: float = None
-    min: float = None
-    max: float = None
-
-    def __init__(self, line):  # noqa: MC0001
-        """Initialize using a perf data string output from a plugin."""
-        if len(line.splitlines()) > 1:
-            raise RuntimeError("Can only parse a single line at a time.")
-        fields = line.split(";")
-        self.name, self.value = fields[0].split("=")
-
-        # Clean the name, handle a quoted name.
-        self.name = self.name.strip()
-        if self.name[0] in ["'", '"']:
-            self.name = self.name[1:]
-        if self.name[-1] in ["'", '"']:
-            self.name = self.name[:-1]
-        self.name = self.name.strip()
-
-        # Split the unit and value.
-        if not self.value[-1].isdigit():
-            for i in range(len(self.value) - 1, 0, -1):
-                if self.value[i].isdigit():
-                    break
-            self.value, self.unit = (
-                float(self.value[: i + 1]),
-                self.value[i + 1 :],  # noqa: E203
-            )
-
-        # Get the remaining fields, if available.
-        try:
-            self.warning = float(fields[1])
-        except Exception as ex:  # pylint: disable=broad-except
-            logging.info(str(ex))
-
-        try:
-            self.critical = float(fields[2])
-        except Exception as ex:  # pylint: disable=broad-except
-            logging.info(str(ex))
-
-        try:
-            self.min = float(fields[3])
-        except Exception as ex:  # pylint: disable=broad-except
-            logging.info(str(ex))
-
-        try:
-            self.max = float(fields[4])
-        except Exception as ex:  # pylint: disable=broad-except
-            logging.info(str(ex))
+PerfData = collections.namedtuple(
+    "PerfData", ["name", "value", "unit", "warning", "critical", "min", "max"]
+)
+PerfData.__new__.__defaults__ = tuple([None] * 7)
+
+
+def parse_perf_data(line):  # noqa: MC0001
+    """Convert a single-line string to a DataPerf tuple."""
+    if len(line.splitlines()) > 1:
+        raise RuntimeError("Can only parse a single line at a time.")
+
+    fields = line.split(";")
+    name, value = fields[0].split("=")
+
+    # Clean the name, handle a quoted name.
+    name = name.strip()
+    if name[0] in ["'", '"']:
+        name = name[1:]
+    if name[-1] in ["'", '"']:
+        name = name[:-1]
+    name = name.strip()
+    pd = {"name": name}  # pylint: disable=invalid-name
+
+    # Split the unit and value.
+    if not value[-1].isdigit():
+        for i in range(len(value) - 1, 0, -1):
+            if value[i].isdigit():
+                break
+        value, unit = (
+            float(value[: i + 1]),
+            value[i + 1 :],  # noqa: E203
+        )
+    pd["value"] = value
+    pd["unit"] = unit
+
+    # Get the remaining fields, if available.
+    try:
+        pd["warning"] = float(fields[1])
+    except Exception as ex:  # pylint: disable=broad-except
+        logging.info(str(ex))
+
+    try:
+        pd["critical"] = float(fields[2])
+    except Exception as ex:  # pylint: disable=broad-except
+        logging.info(str(ex))
+
+    try:
+        pd["min"] = float(fields[3])
+    except Exception as ex:  # pylint: disable=broad-except
+        logging.info(str(ex))
+
+    try:
+        pd["max"] = float(fields[4])
+    except Exception as ex:  # pylint: disable=broad-except
+        logging.info(str(ex))
+
+    return PerfData(**pd)
 
 
 class Check:
@@ -121,7 +118,7 @@ class Check:
         if "|" in lines[0]:
             p1, p2 = lines[0].split("|")
             self.Output = p1.strip()
-            self.PerfData.append(PerfData(p2))
+            self.PerfData.append(parse_perf_data(p2))
         else:
             self.Output = lines[0].strip()
 
@@ -131,12 +128,12 @@ class Check:
         processing_perfdata = False
         for line in lines[1:]:
             if processing_perfdata:
-                self.PerfData.append(PerfData(line))
+                self.PerfData.append(parse_perf_data(line))
             else:
                 if "|" in line:
                     self.AdditionalOutput.append(line.split("|")[0].strip())
                     processing_perfdata = True
-                    self.PerfData.append(PerfData(line.split("|")[1]))
+                    self.PerfData.append(parse_perf_data(line.split("|")[1]))
                 else:
                     self.AdditionalOutput.append(line)
 
diff --git a/pyproject.toml b/pyproject.toml
index 1118361..4e12108 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,7 +28,6 @@ exclude = [
 [tool.poetry.dependencies]
 python = "^3.6.1"
 requests = "^2.25.1"
-dataclasses = { version = '^0.8', python = '<3.7'}
 
 [tool.poetry.dev-dependencies]
 pre-commit = "^2.13.0"
diff --git a/tests/test_nagios.py b/tests/test_nagios.py
index 49b47ff..e44f2fc 100644
--- a/tests/test_nagios.py
+++ b/tests/test_nagios.py
@@ -114,7 +114,7 @@ def test_output_parsing(output):
 @pytest.mark.parametrize("line", PERF_DATA)
 def test_data_perf(line):
     """Test parsing of perfdata."""
-    pd = nagios.PerfData(line)
+    pd = nagios.parse_perf_data(line)
     assert isinstance(pd.name, str)
     assert len(pd.name) > 0
     assert isinstance(pd.value, float)
-- 
GitLab