Repository: barrust/pyspellchecker

Issue:
Error in load file function 
I stumbled upon an error being raised by the function load_file in utils.py (line 95) that is called from spellchecker.py (line 436).
The error I got: 
"..\spellchecker\utils.py", line 105, in load_file
    if filename[-3:].lower() == ".gz":
       ~~~~~~~~^^^^^
TypeError: 'WindowsPath' object is not subscriptable

Discussion:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus) Report
> Merging [#156](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus) (7bc577b) into [master](https://app.codecov.io/gh/barrust/pyspellchecker/commit/44b62df67cdac928f4bc2c630d419136a4863280?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus) (44b62df) will **increase** coverage by `0.13%`.
> Report is 1 commits behind head on master.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156/graphs/tree.svg?width=650&height=150&src=pr&token=4a1cJEFN28&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus)](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus)

```diff
@@            Coverage Diff             @@
##           master     #156      +/-   ##
==========================================
+ Coverage   92.06%   92.20%   +0.13%     
==========================================
  Files           4        4              
  Lines         290      295       +5     
==========================================
+ Hits          267      272       +5     
  Misses         23       23              
```


| [Files Changed](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus) | Coverage Δ | |
|---|---|---|
| [spellchecker/\_\_init\_\_.py](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus#diff-c3BlbGxjaGVja2VyL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [spellchecker/spellchecker.py](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus#diff-c3BlbGxjaGVja2VyL3NwZWxsY2hlY2tlci5weQ==) | `99.11% <100.00%> (+<0.01%)` | :arrow_up: |
| [spellchecker/utils.py](https://app.codecov.io/gh/barrust/pyspellchecker/pull/156?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tyler+Barrus#diff-c3BlbGxjaGVja2VyL3V0aWxzLnB5) | `63.79% <100.00%> (+2.68%)` | :arrow_up: |


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21fd9df..bf6effd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # pyspellchecker
 
+## Version 0.7.3
+* Remove relative imports in favor of absolute imports
+* Add `Path` support for files
+
+
 ## Version 0.7.2
 * Added `Latvian` language support; see [#145](https://github.com/barrust/pyspellchecker/pull/145)
 * Added `Basque` language support; see [#146](https://github.com/barrust/pyspellchecker/pull/146)
diff --git a/spellchecker/__init__.py b/spellchecker/__init__.py
index 0426be6..52a5b4b 100644
--- a/spellchecker/__init__.py
+++ b/spellchecker/__init__.py
@@ -1,6 +1,6 @@
 """ SpellChecker Module """
-from .spellchecker import SpellChecker, WordFrequency
-from .info import (
+from spellchecker.spellchecker import SpellChecker, WordFrequency
+from spellchecker.info import (
     __author__,
     __maintainer__,
     __email__,
diff --git a/spellchecker/spellchecker.py b/spellchecker/spellchecker.py
index 9558c41..7707d4f 100644
--- a/spellchecker/spellchecker.py
+++ b/spellchecker/spellchecker.py
@@ -7,8 +7,9 @@ import string
 import typing
 from collections import Counter
 from collections.abc import Iterable
+from pathlib import Path
 
-from .utils import KeyT, _parse_into_words, ensure_unicode, load_file, write_file
+from spellchecker.utils import KeyT, PathOrStr, _parse_into_words, ensure_unicode, load_file, write_file
 
 
 class SpellChecker:
@@ -33,7 +34,7 @@ class SpellChecker:
     def __init__(
         self,
         language: typing.Union[str, typing.Iterable[str]] = "en",
-        local_dictionary: typing.Optional[str] = None,
+        local_dictionary: typing.Optional[PathOrStr] = None,
         distance: int = 2,
         tokenizer: typing.Optional[typing.Callable[[str], typing.Iterable[str]]] = None,
         case_sensitive: bool = False,
@@ -122,7 +123,7 @@ class SpellChecker:
         text = ensure_unicode(text)
         return self._tokenizer(text)
 
-    def export(self, filepath: str, encoding: str = "utf-8", gzipped: bool = True) -> None:
+    def export(self, filepath: PathOrStr, encoding: str = "utf-8", gzipped: bool = True) -> None:
         """Export the word frequency list for import in the future
 
         Args:
@@ -330,7 +331,7 @@ class WordFrequency:
 
     @property
     def total_words(self) -> int:
-        """int: The sum of all word occurances in the word frequency dictionary
+        """int: The sum of all word occurrences in the word frequency dictionary
 
         Note:
             Not settable"""
@@ -401,7 +402,7 @@ class WordFrequency:
             This is the same as `dict.items()`"""
         yield from self._dictionary.items()
 
-    def load_dictionary(self, filename: str, encoding: str = "utf-8") -> None:
+    def load_dictionary(self, filename: PathOrStr, encoding: str = "utf-8") -> None:
         """Load in a pre-built word frequency list
 
         Args:
@@ -422,7 +423,7 @@ class WordFrequency:
 
     def load_text_file(
         self,
-        filename: str,
+        filename: PathOrStr,
         encoding: str = "utf-8",
         tokenizer: typing.Optional[typing.Callable[[str], typing.Iterable[str]]] = None,
     ) -> None:
diff --git a/spellchecker/utils.py b/spellchecker/utils.py
index 050415f..fd7db15 100644
--- a/spellchecker/utils.py
+++ b/spellchecker/utils.py
@@ -5,10 +5,12 @@ import gzip
 import re
 import typing
 import warnings
+from pathlib import Path
 
-from .info import __version__
+from spellchecker.info import __version__
 
 KeyT = typing.Union[str, bytes]
+PathOrStr = typing.Union[Path, str]
 
 
 def fail_after(version: str) -> typing.Callable:
@@ -77,7 +79,7 @@ def ensure_unicode(_str: KeyT, encoding: str = "utf-8") -> str:
 
 
 @contextlib.contextmanager
-def __gzip_read(filename: str, mode: str = "rb", encoding: str = "UTF-8") -> typing.Generator[KeyT, None, None]:
+def __gzip_read(filename: PathOrStr, mode: str = "rb", encoding: str = "UTF-8") -> typing.Generator[KeyT, None, None]:
     """Context manager to correctly handle the decoding of the output of the gzip file
 
     Args:
@@ -92,7 +94,7 @@ def __gzip_read(filename: str, mode: str = "rb", encoding: str = "UTF-8") -> typ
 
 
 @contextlib.contextmanager
-def load_file(filename: str, encoding: str) -> typing.Generator[KeyT, None, None]:
+def load_file(filename: PathOrStr, encoding: str) -> typing.Generator[KeyT, None, None]:
     """Context manager to handle opening a gzip or text file correctly and
     reading all the data
 
@@ -102,6 +104,9 @@ def load_file(filename: str, encoding: str) -> typing.Generator[KeyT, None, None
     Yields:
         str: The string data from the file read
     """
+    if isinstance(filename, Path):
+        filename = str(filename)
+
     if filename[-3:].lower() == ".gz":
         with __gzip_read(filename, mode="rt", encoding=encoding) as data:
             yield data
@@ -110,7 +115,7 @@ def load_file(filename: str, encoding: str) -> typing.Generator[KeyT, None, None
             yield fobj.read()
 
 
-def write_file(filepath: str, encoding: str, gzipped: bool, data: str) -> None:
+def write_file(filepath: PathOrStr, encoding: str, gzipped: bool, data: str) -> None:
     """Write the data to file either as a gzip file or text based on the
     gzipped parameter
 
@@ -130,7 +135,7 @@ def write_file(filepath: str, encoding: str, gzipped: bool, data: str) -> None:
 
 def _parse_into_words(text: str) -> typing.Iterable[str]:
     """Parse the text into words; currently removes punctuation except for
-    apostrophies.
+    apostrophizes.
 
     Args:
         text (str): The text to split into words


Test patch:
diff --git a/tests/spellchecker_test.py b/tests/spellchecker_test.py
index a00b054..9a97096 100644
--- a/tests/spellchecker_test.py
+++ b/tests/spellchecker_test.py
@@ -1,7 +1,8 @@
 """ Unittest class """
 
-import unittest
 import os
+import unittest
+from pathlib import Path
 
 from spellchecker import SpellChecker
 
@@ -175,6 +176,14 @@ class TestSpellChecker(unittest.TestCase):
         self.assertEqual(spell["a"], 1)
         self.assertTrue("apple" in spell)
 
+    def test_load_external_dictionary_path(self):
+        """test loading a local dictionary"""
+        here = os.path.dirname(__file__)
+        filepath = Path(f"{here}/resources/small_dictionary.json")
+        spell = SpellChecker(language=None, local_dictionary=filepath)
+        self.assertEqual(spell["a"], 1)
+        self.assertTrue("apple" in spell)
+
     def test_edit_distance_one(self):
         """test a case where edit distance must be one"""
         here = os.path.dirname(__file__)
@@ -217,6 +226,18 @@ class TestSpellChecker(unittest.TestCase):
         self.assertTrue(spell["whale"])
         self.assertTrue("waves" in spell)
 
+    def test_load_text_file_path(self):
+        """test loading a text file"""
+        here = os.path.dirname(__file__)
+        filepath = Path(f"{here}/resources/small_doc.txt")
+        spell = SpellChecker(language=None)  # just from this doc!
+        spell.word_frequency.load_text_file(filepath)
+        self.assertEqual(spell["a"], 3)
+        self.assertEqual(spell["storm"], 2)
+        self.assertFalse("awesome" in spell)
+        self.assertTrue(spell["whale"])
+        self.assertTrue("waves" in spell)
+
     def test_remove_words(self):
         """test is a word is removed"""
         spell = SpellChecker()
@@ -431,6 +452,23 @@ class TestSpellChecker(unittest.TestCase):
         self.assertTrue(spell["whale"])
         self.assertTrue("sea." in spell)
 
+    def test_tokenizer_file_path(self):
+        """def using a custom tokenizer for file loading"""
+
+        def tokens(txt):
+            yield from txt.split()
+
+        here = os.path.dirname(__file__)
+        filepath = Path(f"{here}/resources/small_doc.txt")
+        spell = SpellChecker(language=None)  # just from this doc!
+        spell.word_frequency.load_text_file(filepath, tokenizer=tokens)
+        self.assertEqual(spell["a"], 3)
+        self.assertEqual(spell["storm"], 1)
+        self.assertEqual(spell["storm."], 1)
+        self.assertFalse("awesome" in spell)
+        self.assertTrue(spell["whale"])
+        self.assertTrue("sea." in spell)
+
     def test_tokenizer_provided(self):
         """Test passing in a tokenizer"""