Repository: ppannuto/python-titlecase

Issue:
It is impossible to prevent titlecase() from reading the ‘~/.titlecase.txt’ file
It makes sense to have that behavior as default for the `titlecase`
*command* (especially when it is invoked directly by the user from
the command line), but not for the `titlecase()` *function* that may be
called from some library code, which should not be accessing the file
system at all!

Besides, when writing a script using `titlecase()`, one often needs to
have a way to ensure consistent results across all users’ machines,
regardless of what each particular user has in their `~/.titlecase.txt`
file (and whether they have the file at all). Also, as for libraries,
for some scripts it may not even be appropriate (nor allowed) to
access the user’s file system.

I propose to change the behavior of the `titlecase()` function to, at
the very least, not attempt reading `~/.titlecase.txt` (or any other
files, for that matter) when its `wordlist_file` argument has the
value of `None`. In general, unless an effort is made by the caller,
the call to `titlecase()` should *not* incur any filesystem operations.

I have made the minimal required fix for that in #59.

Comments:

Patch:
diff --git a/titlecase/__init__.py b/titlecase/__init__.py
index 8ee158f..050b6c5 100755
--- a/titlecase/__init__.py
+++ b/titlecase/__init__.py
@@ -62,28 +62,7 @@ def set_small_word_list(small=SMALL):
     SUBPHRASE = regex.compile(r'([:.;?!][ ])(%s)' % small)
 
 
-def create_wordlist_filter(path_to_config=None):
-    """
-    This function checks for a default list of abbreviations which need to 
-    remain as they are (e.g. uppercase only or mixed case).
-    The file is retrieved from ~/.titlecase.txt (platform independent)
-    """
-    if path_to_config is None:
-        path_to_config = os.path.join(os.path.expanduser('~'), ".titlecase.txt")
-    if not os.path.isfile(str(path_to_config)):
-        logger.debug('No config file found at ' + str(path_to_config))
-        return lambda word, **kwargs : None
-    with open(str(path_to_config)) as f:
-        logger.debug('Config file used from ' + str(path_to_config))
-        abbreviations = [abbr.strip() for abbr in f.read().splitlines() if abbr]
-        abbreviations_capitalized = [abbr.upper() for abbr in abbreviations]
-        for abbr in abbreviations:
-            logger.debug("This acronym will be kept as written here: " + abbr)
-        return lambda word, **kwargs : (abbreviations[abbreviations_capitalized.index(word.upper())]
-                                       if word.upper() in abbreviations_capitalized else None)
-
-
-def titlecase(text, callback=None, small_first_last=True, wordlist_file=None):
+def titlecase(text, callback=None, small_first_last=True):
     """
     :param text: Titlecases input text
     :param callback: Callback function that returns the titlecase version of a specific word
@@ -99,8 +78,6 @@ def titlecase(text, callback=None, small_first_last=True, wordlist_file=None):
     the New York Times Manual of Style, plus 'vs' and 'v'.
 
     """
-    wordlist_filter = create_wordlist_filter(wordlist_file)
-
     lines = regex.split('[\r\n]+', text)
     processed = []
     for line in lines:
@@ -116,12 +93,6 @@ def titlecase(text, callback=None, small_first_last=True, wordlist_file=None):
                     tc_line.append(_mark_immutable(new_word))
                     continue
 
-            # If the user has a custom wordlist, defer to that
-            new_word = wordlist_filter(word, all_caps=all_caps)
-            if new_word:
-                tc_line.append(_mark_immutable(new_word))
-                continue
-
             if all_caps:
                 if UC_INITIALS.match(word):
                     tc_line.append(word)
@@ -207,6 +178,30 @@ def titlecase(text, callback=None, small_first_last=True, wordlist_file=None):
     return result
 
 
+def create_wordlist_filter_from_file(file_path):
+    '''
+    Load a list of abbreviations from the file with the provided path,
+    reading one abbreviation from each line, and return a callback to
+    be passed to the `titlecase` function for preserving their given
+    canonical capitalization during title-casing.
+    '''
+    if file_path is None:
+        logger.debug('No abbreviations file path given')
+        return lambda word, **kwargs: None
+    file_path_str = str(file_path)
+    if not os.path.isfile(file_path_str):
+        logger.debug('No abbreviations file found at ' + file_path_str)
+        return lambda word, **kwargs: None
+    with open(file_path_str) as f:
+        logger.debug('Reading abbreviations from file ' + file_path_str)
+        abbrevs_gen = (line.strip() for line in f.read().splitlines() if line)
+        abbrevs = {abbr.upper(): abbr for abbr in abbrevs_gen}
+        if logger.isEnabledFor(logging.DEBUG):
+            for abbr in abbrevs.values():
+                logger.debug('Registered abbreviation: ' + abbr)
+        return lambda word, **kwargs: abbrevs.get(word.upper())
+
+
 def cmd():
     '''Handler for command line invocation'''
 
@@ -249,5 +244,11 @@ def cmd():
         with ifile:
             in_string = ifile.read()
 
+    if args.wordlist is not None:
+        wordlist_file = args.wordlist
+    else:
+        wordlist_file = os.path.join(os.path.expanduser('~'), '.titlecase.txt')
+    wordlist_filter = create_wordlist_filter_from_file(wordlist_file)
+
     with ofile:
-        ofile.write(titlecase(in_string, wordlist_file=args.wordlist))
+        ofile.write(titlecase(in_string, callback=wordlist_filter))


Test patch:
diff --git a/titlecase/tests.py b/titlecase/tests.py
index 0b3aad1..e57e4f8 100644
--- a/titlecase/tests.py
+++ b/titlecase/tests.py
@@ -10,7 +10,7 @@ import sys
 import tempfile
 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
 
-from titlecase import titlecase, set_small_word_list
+from titlecase import titlecase, set_small_word_list, create_wordlist_filter_from_file
 
 
 # (executed by `test_input_output` below)
@@ -363,7 +363,7 @@ def test_custom_abbreviations():
         # Without a wordlist, this will do the "wrong" thing for the context
         assert titlecase('SENDING UDP PACKETS OVER PPPOE WORKS GREAT') == 'Sending Udp Packets Over Pppoe Works Great'
         # A wordlist can provide custom acronyms
-        assert titlecase('sending UDP packets over PPPoE works great', wordlist_file=f.name) == 'Sending UDP Packets Over PPPoE Works Great'
+        assert titlecase('sending UDP packets over PPPoE works great', callback=create_wordlist_filter_from_file(f.name)) == 'Sending UDP Packets Over PPPoE Works Great'
 
 
 if __name__ == "__main__":