Repository: cookiecutter/cookiecutter

Issue:
Create a `cookiecutter.compat` module to DRY up python and os differences
https://github.com/audreyr/cookiecutter/search?q=PY3&ref=cmdform



Discussion:
:+1:

Nice idea.

Judging from the gist you linked to, it seems this module could contain a single `is_py3` function, at least for now.

Should we use a single module for these issues and the ones related to #205 ?  The basic idea is the same, so it makes sense to me to dump it all there. If it grows too large, we can always split it into a package later.

@raphigaziano six already has  [`six.PY3`](http://pythonhosted.org/six/#six.PY3).

@raphigaziano :+1: to a single compat module for windows and python differences.

@thedrow I like the idea of using a library for this, but at the same time I think we can get many of the benefits of `six` by refactoring our existing code without having to add another dependency.
Having the compat code in a single place also makes switching our code out for `six` much easier.
https://twitter.com/jbrains/status/488015844090589184

cc: @audreyr @pydanny 

`Six` is nice but doesn't deal with OS compat issues. Also, :+1: for keeping dependencies to a minimum.

:+1: on factoring this stuff out to `cookiecutter.compat`.

Awhile back I looked into `six` (and others) which was promising but didn't have everything I needed at the time. I'm open to reconsidering it or other compatibility packages, but after the first step of refactoring like you said @michaeljoseph.

I think, if we're going to use a library, it pretty much _has_ to be [pies](https://github.com/timothycrosley/pies).
It makes thematic sense :cookie: :cake: :birthday: 

@michaeljoseph That's an awesome argument. :cookie: 


Resolving patch:
diff --git a/cookiecutter/compat.py b/cookiecutter/compat.py
new file mode 100644
--- /dev/null
+++ b/cookiecutter/compat.py
@@ -0,0 +1,70 @@
+import sys
+
+PY3 = sys.version_info[0] == 3
+OLD_PY2 = sys.version_info[:2] < (2, 7)
+
+if PY3:
+    input_str = 'builtins.input'
+    iteritems = lambda d: iter(d.items())
+    from unittest.mock import patch
+    from io import StringIO
+    import unittest
+
+    import json
+    from collections import OrderedDict
+
+    def read_response(prompt=''):
+        """
+        Prompt the user for a response.
+
+        Prints the given prompt (which should be a Unicode string),
+        and returns the text entered by the user as a Unicode string.
+
+        :param prompt: A Unicode string that is presented to the user.
+        """
+        # The Python 3 input function does exactly what we want
+        return input(prompt)
+else:
+    from __builtin__ import raw_input
+    input = raw_input
+    input_str = '__builtin__.raw_input'
+    iteritems = lambda d: d.iteritems()
+    from mock import patch
+    from cStringIO import StringIO
+
+    if OLD_PY2:
+        from ordereddict import OrderedDict
+        import simplejson as json
+        import unittest2 as unittest
+    else:
+        import json
+        from collections import OrderedDict
+        import unittest
+
+    def read_response(prompt=''):
+        """
+        Prompt the user for a response.
+
+        Prints the given prompt (which should be a Unicode string),
+        and returns the text entered by the user as a Unicode string.
+
+        :param prompt: A Unicode string that is presented to the user.
+        """
+        # For Python 2, raw_input takes a byte string argument for the prompt.
+        # This must be encoded using the encoding used by sys.stdout.
+        # The result is a byte string encoding using sys.stdin.encoding.
+        # However, if the program is not being run interactively, sys.stdout
+        # and sys.stdin may not have encoding attributes.
+        # In that case we don't print a prompt (stdin/out isn't interactive,
+        # so prompting is pointless), and we assume the returned data is
+        # encoded using sys.getdefaultencoding(). This may not be right,
+        # but it's likely the best we can do.
+        # Isn't Python 2 encoding support wonderful? :-)
+        if sys.stdout.encoding:
+            prompt = prompt.encode(sys.stdout.encoding)
+        else:
+            prompt = ''
+        enc = sys.stdin.encoding or sys.getdefaultencoding()
+        return raw_input(prompt).decode(enc)
+
+_hush_pyflakes = (patch, StringIO, json, OrderedDict, unittest)
diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py
--- a/cookiecutter/generate.py
+++ b/cookiecutter/generate.py
@@ -12,27 +12,19 @@
 import os
 import io
 import shutil
-import sys
 
 from jinja2 import FileSystemLoader, Template
 from jinja2.environment import Environment
 from jinja2.exceptions import TemplateSyntaxError
 from binaryornot.check import is_binary
 
+from .compat import json, OrderedDict
 from .exceptions import NonTemplatedInputDirException
 from .find import find_template
 from .utils import make_sure_path_exists, work_in
 from .hooks import run_hook
 
 
-if sys.version_info[:2] < (2, 7):
-    import simplejson as json
-    from ordereddict import OrderedDict
-else:
-    import json
-    from collections import OrderedDict
-
-
 def generate_context(context_file='cookiecutter.json', default_context=None,
                      extra_context=None):
     """
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -11,48 +11,7 @@
 from __future__ import unicode_literals
 import sys
 
-PY3 = sys.version > '3'
-if PY3:
-    iteritems = lambda d: iter(d.items())
-
-    def read_response(prompt=''):
-        """
-        Prompt the user for a response.
-
-        Prints the given prompt (which should be a Unicode string),
-        and returns the text entered by the user as a Unicode string.
-
-        :param prompt: A Unicode string that is presented to the user.
-        """
-        # The Python 3 input function does exactly what we want
-        return input(prompt)
-else:
-    def read_response(prompt=''):
-        """
-        Prompt the user for a response.
-
-        Prints the given prompt (which should be a Unicode string),
-        and returns the text entered by the user as a Unicode string.
-
-        :param prompt: A Unicode string that is presented to the user.
-        """
-        # For Python 2, raw_input takes a byte string argument for the prompt.
-        # This must be encoded using the encoding used by sys.stdout.
-        # The result is a byte string encoding using sys.stdin.encoding.
-        # However, if the program is not being run interactively, sys.stdout
-        # and sys.stdin may not have encoding attributes.
-        # In that case we don't print a prompt (stdin/out isn't interactive,
-        # so prompting is pointless), and we assume the returned data is
-        # encoded using sys.getdefaultencoding(). This may not be right,
-        # but it's likely the best we can do.
-        # Isn't Python 2 encoding support wonderful? :-)
-        if sys.stdout.encoding:
-            prompt = prompt.encode(sys.stdout.encoding)
-        else:
-            prompt = ''
-        enc = sys.stdin.encoding or sys.getdefaultencoding()
-        return raw_input(prompt).decode(enc)
-    iteritems = lambda d: d.iteritems()
+from .compat import iteritems, read_response
 
 
 def prompt_for_config(context):


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -10,25 +10,19 @@
 
 import os
 import shutil
-import stat
-import sys
+from cookiecutter.compat import unittest
 from cookiecutter import utils
 
-if sys.version_info[:2] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class CookiecutterCleanSystemTestCase(unittest.TestCase):
     """
     Test case that simulates a clean system with no config/cloned cookiecutters.
-    
+
     During setUp:
 
     * Back up the `~/.cookiecutterrc` config file to `~/.cookiecutterrc.backup`
     * Back up the `~/.cookiecutters/` dir to `~/.cookiecutters.backup/`
-    * Starts off a test case with no pre-existing `~/.cookiecutterrc` or 
+    * Starts off a test case with no pre-existing `~/.cookiecutterrc` or
       `~/.cookiecutters/`
 
     During tearDown:
@@ -76,8 +70,8 @@ def tearDown(self):
             os.remove(self.user_config_path_backup)
 
         # Carefully delete the created ~/.cookiecutters dir only in certain
-        # conditions. 
-        if self.cookiecutters_dir_found:        
+        # conditions.
+        if self.cookiecutters_dir_found:
             # Delete the created ~/.cookiecutters dir as long as a backup exists
             if os.path.isdir(self.cookiecutters_dir) and os.path.isdir(self.cookiecutters_dir_backup):
                 utils.rmtree(self.cookiecutters_dir)
@@ -86,7 +80,7 @@ def tearDown(self):
             # There's no backup because it never existed
             if os.path.isdir(self.cookiecutters_dir):
                 utils.rmtree(self.cookiecutters_dir)
-        
+
         # Restore the user's default cookiecutters_dir contents
         if os.path.isdir(self.cookiecutters_dir_backup):
             shutil.copytree(self.cookiecutters_dir_backup, self.cookiecutters_dir)
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -10,19 +10,11 @@
 
 import os
 import shutil
-import sys
-import unittest
-
-import yaml
 
+from cookiecutter.compat import unittest
 from cookiecutter import config
 from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration
 
-if sys.version_info[:2] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestGetConfig(unittest.TestCase):
 
@@ -62,7 +54,7 @@ class TestGetConfigWithDefaults(unittest.TestCase):
 
     def test_get_config_with_defaults(self):
         """ A config file that overrides 1 of 2 defaults """
-        
+
         conf = config.get_config('tests/test-config/valid-partial-config.yaml')
         default_cookiecutters_dir = os.path.expanduser('~/.cookiecutters/')
         expected_conf = {
@@ -95,7 +87,6 @@ def tearDown(self):
             shutil.copy(self.user_config_path_backup, self.user_config_path)
             os.remove(self.user_config_path_backup)
 
-
     def test_get_user_config_valid(self):
         """ Get config from a valid ~/.cookiecutterrc file """
         shutil.copy('tests/test-config/valid-config.yaml', self.user_config_path)
@@ -118,8 +109,6 @@ def test_get_user_config_invalid(self):
     def test_get_user_config_nonexistent(self):
         """ Get config from a nonexistent ~/.cookiecutterrc file """
         self.assertEqual(config.get_user_config(), config.DEFAULT_CONFIG)
-        
-
 
 
 if __name__ == '__main__':
diff --git a/tests/test_examples.py b/tests/test_examples.py
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -9,17 +9,13 @@
 """
 
 from __future__ import unicode_literals
-import errno
 import logging
 import os
-import shutil
 import subprocess
-import sys
 
-if sys.version_info[:3] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
+from cookiecutter.compat import unittest
+from cookiecutter import config, utils
+from tests import CookiecutterCleanSystemTestCase
 
 try:
     travis = os.environ[u'TRAVIS']
@@ -31,10 +27,6 @@
 except KeyError:
     no_network = False
 
-from cookiecutter import config, utils
-from tests import CookiecutterCleanSystemTestCase
-
-
 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
 
 
@@ -42,7 +34,6 @@
 @unittest.skipIf(condition=no_network, reason='Needs a network connection to GitHub.')
 class TestPyPackage(CookiecutterCleanSystemTestCase):
 
-
     def tearDown(self):
         if os.path.isdir('cookiecutter-pypackage'):
             utils.rmtree('cookiecutter-pypackage')
@@ -77,7 +68,6 @@ def test_cookiecutter_pypackage(self):
 @unittest.skipIf(condition=no_network, reason='Needs a network connection to GitHub.')
 class TestJQuery(CookiecutterCleanSystemTestCase):
 
-
     def tearDown(self):
         if os.path.isdir('cookiecutter-jquery'):
             utils.rmtree('cookiecutter-jquery')
@@ -129,9 +119,8 @@ def test_cookiecutter_pypackage_git(self):
 
         # Just skip all the prompts
         proc.communicate(input=b'\n\n\n\n\n\n\n\n\n\n\n\n')
-        
-        self.assertTrue(os.path.isfile('boilerplate/README.rst'))
 
+        self.assertTrue(os.path.isfile('boilerplate/README.rst'))
 
 
 @unittest.skipIf(condition=travis, reason='Works locally with tox but fails on Travis.')
diff --git a/tests/test_generate.py b/tests/test_generate.py
--- a/tests/test_generate.py
+++ b/tests/test_generate.py
@@ -24,11 +24,9 @@
 from cookiecutter import utils
 from tests import CookiecutterCleanSystemTestCase
 
-
-PY3 = sys.version > '3'
-
 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
 
+
 class TestGenerateFile(unittest.TestCase):
 
     def test_generate_file(self):
@@ -323,7 +321,7 @@ def tearDown(self):
     def test_ignore_hooks_dirs(self):
         generate.generate_files(
             context={
-                'cookiecutter' : {'pyhooks': 'pyhooks'}
+                'cookiecutter': {'pyhooks': 'pyhooks'}
             },
             repo_dir='tests/test-pyhooks/',
             output_dir='tests/test-pyhooks/'
@@ -333,7 +331,7 @@ def test_ignore_hooks_dirs(self):
     def test_run_python_hooks(self):
         generate.generate_files(
             context={
-                'cookiecutter' : {'pyhooks': 'pyhooks'}
+                'cookiecutter': {'pyhooks': 'pyhooks'}
             },
             repo_dir='tests/test-pyhooks/'.replace("/", os.sep),
             output_dir='tests/test-pyhooks/'.replace("/", os.sep)
@@ -344,7 +342,7 @@ def test_run_python_hooks(self):
     def test_run_python_hooks_cwd(self):
         generate.generate_files(
             context={
-                'cookiecutter' : {'pyhooks': 'pyhooks'}
+                'cookiecutter': {'pyhooks': 'pyhooks'}
             },
             repo_dir='tests/test-pyhooks/'
         )
@@ -355,7 +353,7 @@ def test_run_shell_hooks(self):
         make_test_repo('tests/test-shellhooks')
         generate.generate_files(
             context={
-                'cookiecutter' : {'shellhooks': 'shellhooks'}
+                'cookiecutter': {'shellhooks': 'shellhooks'}
             },
             repo_dir='tests/test-shellhooks/',
             output_dir='tests/test-shellhooks/'
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -10,28 +10,16 @@
 
 import logging
 import os
-import sys
 
-from cookiecutter import config, main, utils
+from cookiecutter.compat import patch, unittest
+from cookiecutter import main, utils
 from tests import CookiecutterCleanSystemTestCase
 
-if sys.version_info[:2] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
-
-PY3 = sys.version > '3'
-if PY3:
-    from unittest.mock import patch
-else:
-    from mock import patch
-
 try:
     no_network = os.environ[u'DISABLE_NETWORK_TESTS']
 except KeyError:
     no_network = False
 
-
 # Log debug and above to console
 logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG)
 
@@ -57,8 +45,8 @@ def test_cookiecutter_no_slash(self):
     def test_cookiecutter_no_input_extra_context(self):
         """ `Call cookiecutter()` with `no_input=True` and `extra_context` """
         main.cookiecutter(
-            'tests/fake-repo-pre', 
-            no_input=True, 
+            'tests/fake-repo-pre',
+            no_input=True,
             extra_context={'repo_name': 'fake-project-extra'}
         )
         self.assertTrue(os.path.isdir('fake-project-extra'))
@@ -85,8 +73,8 @@ def test_cookiecutter_local_with_input(self):
     def test_cookiecutter_input_extra_context(self):
         """ `Call cookiecutter()` with `no_input=False` and `extra_context` """
         main.cookiecutter(
-            'tests/fake-repo-pre', 
-            no_input=True, 
+            'tests/fake-repo-pre',
+            no_input=True,
             extra_context={'repo_name': 'fake-project-input-extra'}
         )
         self.assertTrue(os.path.isdir('fake-project-input-extra'))
diff --git a/tests/test_prompt.py b/tests/test_prompt.py
--- a/tests/test_prompt.py
+++ b/tests/test_prompt.py
@@ -8,22 +8,20 @@
 Tests for `cookiecutter.prompt` module.
 """
 
-import sys
 import platform
-import unittest
+import sys
 
+from cookiecutter.compat import patch, unittest
 from cookiecutter import prompt
 
-PY3 = sys.version > '3'
-if PY3:
-    from unittest.mock import patch
-else:
-    from mock import patch
+if 'windows' in platform.platform().lower():
 
-if sys.version_info[:2] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
+    old_stdin = sys.stdin
+
+    class X(object):
+        def readline(self):
+            return '\n'
+    sys.stdin = X()
 
 
 class TestPrompt(unittest.TestCase):
@@ -79,11 +77,6 @@ def test_query_n(self):
         answer = prompt.query_yes_no("Blah?")
         self.assertFalse(answer)
 
-    @patch('cookiecutter.prompt.read_response', lambda x=u'': u'no')
-    def test_query_n(self):
-        answer = prompt.query_yes_no("Blah?")
-        self.assertFalse(answer)
-
 
 class TestQueryDefaults(unittest.TestCase):
 
diff --git a/tests/test_vcs.py b/tests/test_vcs.py
--- a/tests/test_vcs.py
+++ b/tests/test_vcs.py
@@ -12,20 +12,8 @@
 import logging
 import os
 import subprocess
-import sys
-import unittest
-
-PY3 = sys.version > '3'
-if PY3:
-    from unittest.mock import patch
-else:
-    from mock import patch
-
-if sys.version_info[:3] < (2, 7):
-    import unittest2 as unittest
-else:
-    import unittest
 
+from cookiecutter.compat import patch, unittest
 from cookiecutter import utils, vcs
 
 try: