3
\$\begingroup\$

I wrote this Python script to rename specified files to "titlecase", for example:

  • hello.txt to Hello.txt
  • hello there.txt to Hello There.txt
  • path/to/file.txt to path/to/File.txt

The main script:

#!/usr/bin/env python

import os
import re
import argparse

re_junk = re.compile(r'[._-]')
re_spaces = re.compile(r'\s\s+')


def print_rename(old_filename, new_filename):
    print('{} -> {}'.format(old_filename, new_filename))


def do_rename(old_path, new_path):
    print_rename(old_path, new_path)
    os.rename(old_path, new_path)


def get_new_path(old_path):
    """ Get the new path, titlecased and (a little bit) sanitized.
    - Only operate on the basename:
        + don't touch parent directories
        + don't touch the extension
    - Sanitize:
        + replace junk characters with space
        + replace multiple spaces with single space
        + trim extra spaces at start and end

    :param old_path: the path to rename
    :return: titlecased and a little bit sanitized new path
    """
    dirpart, filepart = os.path.split(old_path)
    if filepart.startswith('.'):
        return old_path

    base, ext = os.path.splitext(filepart)
    base = re_junk.sub(' ', base)
    base = re_spaces.sub(' ', base).strip()
    if not base:
        return old_path

    return os.path.join(dirpart, base.title() + ext)


def titlecase(old_path, fun):
    if not os.path.exists(old_path):
        return

    new_path = get_new_path(old_path)
    if old_path == new_path:
        return

    fun(old_path, new_path)


def main():
    parser = argparse.ArgumentParser(description='Rename files to "titlecased" and "sanitized".')
    parser.add_argument('-n', '--dry-run', action='store_true', help='Print what would happen, don\'t rename')
    parser.add_argument('paths', nargs='+')

    args = parser.parse_args()
    fun = print_rename if args.dry_run else do_rename

    for path in args.paths:
        titlecase(path, fun)


if __name__ == '__main__':
    main()

Unit tests:

#!/usr/bin/env python

import unittest

from titlecase import get_new_path


class TestTitleCase(unittest.TestCase):
    def test_hello(self):
        self.assertEqual('Hello', get_new_path('hello'))

    def test_hello_txt(self):
        self.assertEqual('Hello.txt', get_new_path('hello.txt'))

    def test_hello_there_txt_dotted(self):
        self.assertEqual('Hello There.txt', get_new_path('hello.there.txt'))

    def test_hello_there(self):
        self.assertEqual('Hello There', get_new_path('hello there'))

    def test_hello__there(self):
        self.assertEqual('Hello There', get_new_path('hello  there'))

    def test_dot(self):
        self.assertEqual('.', get_new_path('.'))

    def test_dotdot(self):
        self.assertEqual('..', get_new_path('..'))

    def test_subdir(self):
        self.assertEqual('some/path/File.txt', get_new_path('some/path/file.txt'))

    def test_subdir_nobasename(self):
        self.assertEqual('some/path/.txt', get_new_path('some/path/.txt'))

    def test_subdir_dotdot(self):
        self.assertEqual('../path/File.txt', get_new_path('../path/file.txt'))

    def test_subdir_dotdot_in_middle(self):
        self.assertEqual('some/../path/File.txt', get_new_path('some/../path/file.txt'))


if __name__ == '__main__':
    unittest.main()

Is there a better way I missed? Anything to improve? Better names maybe? Interesting unit test cases I missed?

(This script (along with some other utility scripts) is on GitHub.)

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
  • I strongly oppose testing for file existence. It introduces a race condition: the path may exist at the time of test and disappear by the time of use; yes I am paranoid. Besides, it really impedes dry runs/unit testing - to try an interesting case you need to actually create a file. The pythonic way is to ask forgiveness, not permission:

    try:
        os.rename(old_path, new_path)
    except OSError:
        pass # or an error message
    

    Notice that it would also take care of other failures of os.rename, such as new_path already exists or is too long; the directory is read-only, etc.

  • I am also not sure that "sanitization" is to be done unconditionally. A command line option would be nice.

  • Typically a debugging/diagnostic messages are sent to stderr. A quiet (or verbose) option is also desirable.

  • fun doesn't strike as a particularly good name.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.