Repository: dopefishh/pympi

Issue:
Support pathlib.Path objects in addition to str file paths
I propose to additionally accept `pathlib.Path` objects wherever file paths as `str` are accepted now. This makes tests simpler (when using pytest's `tmp_path`) and is also becoming the standard behaviour of most python stdlib modules.

Discussion:

Patch:
diff --git a/MANIFEST b/MANIFEST
index 5965331..0b43bb0 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,4 +1,5 @@
 # file GENERATED by distutils, do NOT edit
+setup.cfg
 setup.py
 pympi/Elan.py
 pympi/Praat.py
diff --git a/pympi/Elan.py b/pympi/Elan.py
index 4b4553e..0a3975c 100644
--- a/pympi/Elan.py
+++ b/pympi/Elan.py
@@ -1,10 +1,8 @@
-# -*- coding: utf-8 -*-
-
 from xml.etree import cElementTree as etree
-import os
 import re
 import sys
 import time
+import pathlib
 import warnings
 
 VERSION = '1.7'
@@ -663,7 +661,8 @@ class Eaf:
         :returns: Tuple of the form: ``(min_time, max_time)``.
         """
         return (0, 0) if not self.timeslots else\
-            (min(self.timeslots.values()), max(self.timeslots.values()))
+            (min(v for v in self.timeslots.values() if v is not None),
+             max(v for v in self.timeslots.values() if v is not None))
 
     def get_gaps_and_overlaps(self, tier1, tier2, maxlen=-1):
         """Give gaps and overlaps. The return types are shown in the table
@@ -1460,7 +1459,8 @@ def parse_eaf(file_path, eaf_obj, suppress_version_warning=False):
         file_path = sys.stdin
     # Annotation document
     try:
-        tree_root = etree.parse(file_path).getroot()
+        # py3.5 compat: etree.parse does not support pathlib.Path objects in py3.5.
+        tree_root = etree.parse(str(file_path)).getroot()
     except etree.ParseError:
         raise Exception('Unable to parse eaf, can you open it in ELAN?')
 
@@ -1623,15 +1623,7 @@ def to_eaf(file_path, eaf_obj, pretty=True):
     :param bool pretty: Flag to set pretty printing.
     """
     def rm_none(x):
-        try:  # Ugly hack to test if s is a string in py3 and py2
-            basestring
-
-            def isstr(s):
-                return isinstance(s, basestring)
-        except NameError:
-            def isstr(s):
-                return isinstance(s, str)
-        return {k: v if isstr(v) else str(v) for k, v in x.items()
+        return {k: v if isinstance(v, str) else str(v) for k, v in x.items()
                 if v is not None}
     # Annotation Document
     ADOCUMENT = etree.Element('ANNOTATION_DOCUMENT', eaf_obj.adocument)
@@ -1717,7 +1709,8 @@ def to_eaf(file_path, eaf_obj, pretty=True):
         except LookupError:
             sys.stdout.write(etree.tostring(ADOCUMENT, encoding='UTF-8'))
     else:
-        if os.access(file_path, os.F_OK):
-            os.rename(file_path, '{}.bak'.format(file_path))
+        file_path = pathlib.Path(file_path)
+        if file_path.exists():
+            file_path.rename(file_path.with_suffix('.bak'))
         etree.ElementTree(ADOCUMENT).write(
-            file_path, xml_declaration=True, encoding='UTF-8')
+            str(file_path), xml_declaration=True, encoding='UTF-8')
diff --git a/pympi/Praat.py b/pympi/Praat.py
index eba3c5d..9f1ecb6 100644
--- a/pympi/Praat.py
+++ b/pympi/Praat.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
 import codecs
 import re
 import struct
@@ -63,11 +60,9 @@ class TextGrid:
                 elif textlen == -1:
                     textlen = struct.unpack('>h', ifile.read(2))[0]
                     data = ifile.read(textlen*2)
-                    # Hack to go from number to unicode in python3 and python2
-                    fun = unichr if 'unichr' in __builtins__ else chr
                     charlist = (data[i:i+2] for i in range(0, len(data), 2))
-                    return u''.join(
-                        fun(struct.unpack('>h', i)[0]) for i in charlist)
+                    return ''.join(
+                        chr(struct.unpack('>h', i)[0]) for i in charlist)
 
             ifile.read(ord(ifile.read(1)))  # skip oo type
             self.xmin = struct.unpack('>d', ifile.read(8))[0]
@@ -98,9 +93,9 @@ class TextGrid:
                 line = next(ifile).decode(codec)
                 return pat.search(line).group(1)
 
-            regfloat = re.compile('([\d.]+)\s*$', flags=re.UNICODE)
-            regint = re.compile('([\d]+)\s*$', flags=re.UNICODE)
-            regstr = re.compile('"(.*)"\s*$', flags=re.UNICODE)
+            regfloat = re.compile(r'([\d.]+)\s*$', flags=re.UNICODE)
+            regint = re.compile(r'([\d]+)\s*$', flags=re.UNICODE)
+            regstr = re.compile(r'"(.*)"\s*$', flags=re.UNICODE)
             # Skip the Headers and empty line
             next(ifile), next(ifile), next(ifile)
             self.xmin = float(nn(ifile, regfloat))
@@ -159,7 +154,7 @@ class TextGrid:
         elif number < 1 or number > len(self.tiers):
             raise ValueError('Number not in [1..{}]'.format(len(self.tiers)))
         elif tier_type not in Tier.P_TIERS:
-            raise ValueError('tier_type has to be in {}'.format(self.P_TIERS))
+            raise ValueError('tier_type has to be in {}'.format(Tier.P_TIERS))
         self.tiers.insert(number-1,
                           Tier(self.xmin, self.xmax, name, tier_type))
         return self.tiers[number-1]
@@ -254,7 +249,8 @@ class TextGrid:
                         itier and f.write(struct.pack('>d', c[1]))
                         writebstr(c[2 if itier else 1])
         elif mode in ['normal', 'n', 'short', 's']:
-            with codecs.open(filepath, 'w', codec) as f:
+            # py3.5 compat: codecs.open does not support pathlib.Path objects in py3.5.
+            with codecs.open(str(filepath), 'w', codec) as f:
                 short = mode[0] == 's'
 
                 def wrt(indent, prefix, value, ff=''):
diff --git a/pympi/__init__.py b/pympi/__init__.py
index 53f76cc..791f39a 100644
--- a/pympi/__init__.py
+++ b/pympi/__init__.py
@@ -1,5 +1,5 @@
 # Import the packages
 from pympi.Praat import TextGrid
-from pympi.Elan import Eaf
+from pympi.Elan import Eaf, eaf_from_chat
 
 __all__ = ['Praat', 'Elan', 'eaf_from_chat']
diff --git a/tox.ini b/tox.ini
index 142ca55..1ff9207 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py{36,37,38}
+envlist = py{35,36,37,38}
 skip_missing_interpreters = true
 
 [testenv]


Test patch:
diff --git a/test/test_elan.py b/test/test_elan.py
index 0249154..34ee7da 100644
--- a/test/test_elan.py
+++ b/test/test_elan.py
@@ -1153,10 +1153,14 @@ class Elan(unittest.TestCase):
     ]
 )
 def test_to_file_to_eaf(eaf, schema, test_dir, tmp_path):
-    filepath = tmp_path / 'test.eaf'
-    eaf = Eaf(test_dir / eaf)
+    filepath = str(tmp_path / 'test.eaf')
+    eaf = Eaf(str(test_dir / eaf))
     eaf.to_file(filepath)
 
     schema = etree.XMLSchema(etree.XML(test_dir.joinpath(schema).read_text(encoding='utf8')))
     xmlparser = etree.XMLParser(schema=schema)
     etree.parse(str(filepath), xmlparser)
+
+
+def test_to_textgrid(test_dir):
+    _ = Eaf(str(test_dir / 'sample_2.7.eaf')).to_textgrid()
diff --git a/test/test_praat.py b/test/test_praat.py
index f71afc8..1c94c41 100644
--- a/test/test_praat.py
+++ b/test/test_praat.py
@@ -1,9 +1,7 @@
-#!/bin/env python
-# -*- coding: utf-8 -*-
-
+import pathlib
 import unittest
-import tempfile
-import os
+import pytest
+
 from pympi.Praat import TextGrid
 
 
@@ -126,38 +124,6 @@ class PraatTest(unittest.TestCase):
         self.assertEqual([(1, 'tier1'), (2, 'tier3'), (3, 'tier2')],
                          list(self.tg.get_tier_name_num()))
 
-    def test_to_file(self):
-        for codec in ['utf-8', 'latin_1', 'mac_roman']:
-            self.tg = TextGrid(xmax=20)
-            tier1 = self.tg.add_tier('tier')
-            tier1.add_interval(1, 2, 'i1')
-            tier1.add_interval(2, 3, 'i2')
-            tier1.add_interval(4, 5, 'i3')
-
-            tier4 = self.tg.add_tier('tier')
-            tier4.add_interval(1, 2, u'i1ü')
-            tier4.add_interval(2.0, 3, 'i2')
-            tier4.add_interval(4, 5.0, 'i3')
-
-            tier2 = self.tg.add_tier('tier2', tier_type='TextTier')
-            tier2.add_point(1, u'p1ü')
-            tier2.add_point(2, 'p1')
-            tier2.add_point(3, 'p1')
-
-            tempf = tempfile.mkstemp()[1]
-
-# Normal mode
-            self.tg.to_file(tempf, codec=codec)
-            TextGrid(tempf, codec=codec)
-# Short mode
-            self.tg.to_file(tempf, codec=codec, mode='s')
-            TextGrid(tempf, codec=codec)
-# Binary mode
-            self.tg.to_file(tempf, mode='b')
-            TextGrid(tempf)
-
-            os.remove(tempf)
-
     def test_to_eaf(self):
         tier1 = self.tg.add_tier('tier1')
         tier2 = self.tg.add_tier('tier2', tier_type='TextTier')
@@ -267,5 +233,33 @@ class PraatTest(unittest.TestCase):
         self.tier2.clear_intervals()
         self.assertEqual([], self.tier2.intervals)
 
-if __name__ == '__main__':
-    unittest.main()
+
+@pytest.mark.parametrize('codec', ['utf-8', 'latin_1', 'mac_roman'])
+def test_to_file(codec, tmp_path):
+    tg = TextGrid(xmax=20)
+    tier1 = tg.add_tier('tier')
+    tier1.add_interval(1, 2, 'i1')
+    tier1.add_interval(2, 3, 'i2')
+    tier1.add_interval(4, 5, 'i3')
+
+    tier4 = tg.add_tier('tier')
+    tier4.add_interval(1, 2, u'i1ü')
+    tier4.add_interval(2.0, 3, 'i2')
+    tier4.add_interval(4, 5.0, 'i3')
+
+    tier2 = tg.add_tier('tier2', tier_type='TextTier')
+    tier2.add_point(1, u'p1ü')
+    tier2.add_point(2, 'p1')
+    tier2.add_point(3, 'p1')
+
+    tempf = str(tmp_path / 'test')
+
+    # Normal mode
+    tg.to_file(pathlib.Path(tempf), codec=codec)
+    TextGrid(tempf, codec=codec)
+    # Short mode
+    tg.to_file(tempf, codec=codec, mode='s')
+    TextGrid(tempf, codec=codec)
+    # Binary mode
+    tg.to_file(tempf, mode='b')
+    TextGrid(tempf)