Repository: codespell-project/codespell

Issue:
There is no ./codespell.py
README says:

> Check usage with `./codespell.py -h`

But there is no `./codespell.py` (unless you cd into `bin`, in which case the command doesn't work either).

`codespell.py` was moved from `.` to `bin` in b400e4250d7342b76e361cfcf738da6f65fee035, but I don't understand why. The commit message is enigmatic.



Discussion:
It follows a more standard Python lib pattern to put it in `bin`. Really most people will / should probably now install via `pip` and thus just be able to do `codespell.py` directly

```
    copying build/scripts-3.4/codespell.py -> /home/scrutinizer/.pyenv/versions/3.4.1/bin
    changing mode of /home/scrutinizer/.pyenv/versions/3.4.1/bin/codespell.py to 775
```

is this normal? That I cant just run `codespell.py` but it seems only to be able able in `/home/scrutinizer/.pyenv/versions/3.4.1/bin/codespell.py` ?

> most people will / should probably now install via `pip` and thus just be able to do `codespell.py` directly

...except now that pypi has been upgraded ( #101 ), `pip` will actually install the executable `codespell` instead of `codespell.py`
Argh, you're right. I guess we should revert to `codespell.py` for backward compatibility reasons
@lucasdemarchi do you agree we should change the name to `bin/codespell.py` instead of `bin/codespell`? I think it's necessary for backward compatibility.
Isn't it 'better' to have installed commands without the extension? (I assumed this change was deliberate)
Yeah it is better, but I'm worried about backward compatibility. For people who used to do `codespell.py` it won't work anymore. So I'm starting to think we should use the old name.
Please keep `codespell` file without file extension. It has been done this way for a long time. The convention has been changed in previous release and that is why we requested reverting it back to `codespell`.
Ahh okay, thanks @anatol 
But the problem is that removing `.py` breaks the tool Windows, which relies on extensions to know how to execute a file. Could we have `codespell.py` for Windows, and just `codespell` elsewhere?
Should we just provide both, then?
> Could we have codespell.py for Windows, and just codespell elsewhere?

I guess that's an option, too. I'll look and see what happens with binaries installed from other repos I work on. I know we have command-line utilities and AFAIK we don't do anything special for Windows. It's possible that they have just never worked there, I guess.
I guess that you can use [setuptools](http://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation) to [take care of it](https://stackoverflow.com/questions/14596057/distributing-python-scripts-without-py-extension). This might be the best/simplest route. Someone want to give it a try?

Resolving patch:
diff --git a/codespell_lib/__init__.py b/codespell_lib/__init__.py
--- a/codespell_lib/__init__.py
+++ b/codespell_lib/__init__.py
@@ -1 +1 @@
-from ._codespell import main, VERSION as __version__  # noqa
+from ._codespell import main, _script_main, VERSION as __version__  # noqa
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 #
 # This program is free software; you can redistribute it and/or modify
@@ -209,7 +208,8 @@ def parse_options(args):
     parser.set_defaults(colors=sys.stdout.isatty())
     parser.add_option('-d', '--disable-colors',
                       action='store_false', dest='colors',
-                      help='disable colors even when printing to terminal')
+                      help='disable colors even when printing to terminal '
+                      '(always on for Windows)')
     parser.add_option('-c', '--enable-colors',
                       action='store_true', dest='colors',
                       help='enable colors even when not printing to terminal')
@@ -552,6 +552,11 @@ def parse_file(filename, colors, summary):
     return bad_count
 
 
+def _script_main():
+    """Wrap to main() for setuptools."""
+    return main(*sys.argv[1:])
+
+
 def main(*args):
     """Contains flow control"""
     global options
@@ -593,7 +598,7 @@ def main(*args):
             return 1
         build_dict(dictionary)
     colors = TermColors()
-    if not options.colors:
+    if not options.colors or sys.platform == 'win32':
         colors.disable()
 
     if options.summary:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -5,11 +5,7 @@
 import os
 from os import path as op
 
-try:
-    import setuptools  # noqa to allow --develop
-except Exception:
-    pass
-from distutils.core import setup
+from setuptools import setup
 
 from codespell_lib import __version__
 
@@ -55,4 +51,9 @@
               op.join('data', 'dictionary.txt'),
               op.join('data', 'linux-kernel.exclude'),
           ]},
-          scripts=['bin/codespell'])
+          entry_points={
+              'console_scripts': [
+                  'codespell = codespell_lib:_script_main'
+              ],
+          }
+          )


Test patch:
diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py
--- a/codespell_lib/tests/test_basic.py
+++ b/codespell_lib/tests/test_basic.py
@@ -47,19 +47,23 @@ def test_basic():
     """Test some basic functionality"""
     assert_equal(cs.main('_does_not_exist_'), 0)
     with tempfile.NamedTemporaryFile('w') as f:
-        with CaptureStdout() as sio:
-            assert_equal(cs.main('-D', 'foo', f.name), 1)  # missing dictionary
+        pass
+    with CaptureStdout() as sio:
+        assert_equal(cs.main('-D', 'foo', f.name), 1)  # missing dictionary
+    try:
         assert_true('cannot find dictionary' in sio[1])
         assert_equal(cs.main(f.name), 0)  # empty file
-        f.write('this is a test file\n')
-        f.flush()
+        with open(f.name, 'a') as f:
+            f.write('this is a test file\n')
         assert_equal(cs.main(f.name), 0)  # good
-        f.write('abandonned\n')
-        f.flush()
+        with open(f.name, 'a') as f:
+            f.write('abandonned\n')
         assert_equal(cs.main(f.name), 1)  # bad
-        f.write('abandonned\n')
-        f.flush()
+        with open(f.name, 'a') as f:
+            f.write('abandonned\n')
         assert_equal(cs.main(f.name), 2)  # worse
+    finally:
+        os.remove(f.name)
     with TemporaryDirectory() as d:
         with open(op.join(d, 'bad.txt'), 'w') as f:
             f.write('abandonned\nAbandonned\nABANDONNED\nAbAnDoNnEd')
@@ -94,10 +98,14 @@ def test_basic():
 
 def test_interactivity():
     """Test interaction"""
+    # Windows can't read a currently-opened file, so here we use
+    # NamedTemporaryFile just to get a good name
     with tempfile.NamedTemporaryFile('w') as f:
+        pass
+    try:
         assert_equal(cs.main(f.name), 0)  # empty file
-        f.write('abandonned\n')
-        f.flush()
+        with open(f.name, 'w') as f:
+            f.write('abandonned\n')
         assert_equal(cs.main('-i', '-1', f.name), 1)  # bad
         with FakeStdin('y\n'):
             assert_equal(cs.main('-i', '3', f.name), 1)
@@ -109,18 +117,30 @@ def test_interactivity():
             with FakeStdin('x\ny\n'):
                 assert_equal(cs.main('-w', '-i', '3', f.name), 0)
         assert_equal(cs.main(f.name), 0)
+    finally:
+        os.remove(f.name)
+
+    # New example
     with tempfile.NamedTemporaryFile('w') as f:
-        f.write('abandonned\n')
-        f.flush()
+        pass
+    try:
+        with open(f.name, 'w') as f:
+            f.write('abandonned\n')
         assert_equal(cs.main(f.name), 1)
         with CaptureStdout():
             with FakeStdin(' '):  # blank input -> Y
                 assert_equal(cs.main('-w', '-i', '3', f.name), 0)
         assert_equal(cs.main(f.name), 0)
+    finally:
+        os.remove(f.name)
+
     # multiple options
     with tempfile.NamedTemporaryFile('w') as f:
-        f.write('ackward\n')
-        f.flush()
+        pass
+    try:
+        with open(f.name, 'w') as f:
+            f.write('ackward\n')
+
         assert_equal(cs.main(f.name), 1)
         with CaptureStdout():
             with FakeStdin(' \n'):  # blank input -> nothing
@@ -132,9 +152,8 @@ def test_interactivity():
         assert_equal(cs.main(f.name), 0)
         with open(f.name, 'r') as f_read:
             assert_equal(f_read.read(), 'awkward\n')
-        f.seek(0)
-        f.write('ackward\n')
-        f.flush()
+        with open(f.name, 'w') as f:
+            f.write('ackward\n')
         assert_equal(cs.main(f.name), 1)
         with CaptureStdout() as sio:
             with FakeStdin('x\n1\n'):  # blank input -> nothing
@@ -143,11 +162,15 @@ def test_interactivity():
         assert_equal(cs.main(f.name), 0)
         with open(f.name, 'r') as f:
             assert_equal(f.read(), 'backward\n')
+    finally:
+        os.remove(f.name)
 
 
 def test_summary():
     """Test summary functionality"""
     with tempfile.NamedTemporaryFile('w') as f:
+        pass
+    try:
         with CaptureStdout() as sio:
             cs.main(f.name)
         for ii in range(2):
@@ -157,14 +180,16 @@ def test_summary():
         assert_equal(sio[1], '')  # stderr
         assert_true('SUMMARY' in sio[0])
         assert_equal(len(sio[0].split('\n')), 5)  # no output
-        f.write('abandonned\nabandonned')
-        f.flush()
+        with open(f.name, 'w') as f:
+            f.write('abandonned\nabandonned')
         with CaptureStdout() as sio:
             cs.main(f.name, '--summary')
         assert_equal(sio[1], '')  # stderr
         assert_true('SUMMARY' in sio[0])
         assert_equal(len(sio[0].split('\n')), 7)
         assert_true('abandonned' in sio[0].split()[-2])
+    finally:
+        os.remove(f.name)
 
 
 @with_setup(reload_codespell_lib, reload_codespell_lib)
@@ -174,9 +199,10 @@ def test_ignore_dictionary():
         with open(op.join(d, 'bad.txt'), 'w') as f:
             f.write('abandonned\nabondon\n')
         with tempfile.NamedTemporaryFile('w') as f:
+            pass
+        with open(f.name, 'w') as f:
             f.write('abandonned\n')
-            f.flush()
-            assert_equal(cs.main('-I', f.name, d), 1)
+        assert_equal(cs.main('-I', f.name, d), 1)
 
 
 @with_setup(reload_codespell_lib, reload_codespell_lib)
@@ -192,39 +218,43 @@ def test_custom_regex():
 def test_exclude_file():
     """Test exclude file functionality"""
     with TemporaryDirectory() as d:
-        with open(op.join(d, 'bad.txt'), 'w') as f:
-            f.write('abandonned 1\nabandonned 2\n')
+        with open(op.join(d, 'bad.txt'), 'wb') as f:
+            f.write('abandonned 1\nabandonned 2\n'.encode('utf-8'))
+        assert_equal(cs.main(d), 2)
         with tempfile.NamedTemporaryFile('w') as f:
-            f.write('abandonned 1\n')
-            f.flush()
-            assert_equal(cs.main(d), 2)
-            assert_equal(cs.main('-x', f.name, d), 1)
+            pass
+        with open(f.name, 'wb') as f:
+            f.write('abandonned 1\n'.encode('utf-8'))
+        assert_equal(cs.main(d), 2)
+        assert_equal(cs.main('-x', f.name, d), 1)
 
 
 def test_encoding():
     """Test encoding handling"""
     # Some simple Unicode things
     with tempfile.NamedTemporaryFile('wb') as f:
-        # with CaptureStdout() as sio:
-        assert_equal(cs.main(f.name), 0)
-        f.write(u'naïve\n'.encode('utf-8'))
-        f.flush()
+        pass
+    # with CaptureStdout() as sio:
+    assert_equal(cs.main(f.name), 0)
+    try:
+        with open(f.name, 'wb') as f:
+            f.write(u'naïve\n'.encode('utf-8'))
         assert_equal(cs.main(f.name), 0)
         assert_equal(cs.main('-e', f.name), 0)
-        f.write(u'naieve\n'.encode('utf-8'))
-        f.flush()
+        with open(f.name, 'ab') as f:
+            f.write(u'naieve\n'.encode('utf-8'))
         assert_equal(cs.main(f.name), 1)
-    # Binary file warning
-    with tempfile.NamedTemporaryFile('wb') as f:
-        assert_equal(cs.main(f.name), 0)
-        f.write(b'\x00\x00naiive\x00\x00')
-        f.flush()
+        # Binary file warning
+        with open(f.name, 'wb') as f:
+            f.write(b'\x00\x00naiive\x00\x00')
         with CaptureStdout() as sio:
             assert_equal(cs.main(f.name), 0)
         assert_true('WARNING: Binary file' in sio[1])
         with CaptureStdout() as sio:
             assert_equal(cs.main('-q', '2', f.name), 0)
         assert_equal(sio[1], '')
+    finally:
+        os.remove(f.name)
 
 
 def test_ignore():