Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,32 @@ self.assertEqual(r, "ueber-ueber-german-umlaut")
For more examples, have a look at the [test.py](test.py) file.


Command Line Options
====================

With the package, a command line tool called `slugify` is also installed.

It allows convenient command line access to all the features the `slugify` function supports. Call it with `-h` for help.

The command can take its input directly on the command line or from STDIN (when the `--stdin` flag is passed):

```
$ echo "Taking input from STDIN" | slugify --stdin
taking-input-from-stdin
```
```
$ slugify taking input from the command line
taking-input-from-the-command-line
```

Please note that when a multi-valued option such as `--stopwords` or `--replacements` is passed, you need to use `--` as separator before you start with the input:

```
$ slugify --stopwords the in a hurry -- the quick brown fox jumps over the lazy dog in a hurry
quick-brown-fox-jumps-over-lazy-dog
```


Running the tests
====================

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ def get_version(package):
extras_require=extras_require,
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
classifiers=classifiers,
entry_points={'console_scripts': ['slugify=slugify.slugify:main']},
entry_points={'console_scripts': ['slugify=slugify.__main__:main']},
)
93 changes: 93 additions & 0 deletions slugify/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import print_function, absolute_import
import argparse
import sys

from .slugify import slugify, DEFAULT_SEPARATOR


def parse_args(argv):
parser = argparse.ArgumentParser(description="Sluggify string")

input_group = parser.add_argument_group(description="Input")
input_group.add_argument("input_string", nargs='*',
help='Text to slugify')
input_group.add_argument("--stdin", action='store_true',
help="Take the text from STDIN")

parser.add_argument("--no-entities", action='store_false', dest='entities', default=True,
help="Do not convert HTML entities to unicode")
parser.add_argument("--no-decimal", action='store_false', dest='decimal', default=True,
help="Do not convert HTML decimal to unicode")
parser.add_argument("--no-hexadecimal", action='store_false', dest='hexadecimal', default=True,
help="Do not convert HTML hexadecimal to unicode")
parser.add_argument("--max-length", type=int, default=0,
help="Output string length, 0 for no limit")
parser.add_argument("--word-boundary", action='store_true', default=False,
help="Truncate to complete word even if length ends up shorter than --max_length")
parser.add_argument("--save-order", action='store_true', default=False,
help="When set and --max_length > 0 return whole words in the initial order")
parser.add_argument("--separator", type=str, default=DEFAULT_SEPARATOR,
help="Separator between words. By default " + DEFAULT_SEPARATOR)
parser.add_argument("--stopwords", nargs='+',
help="Words to discount")
parser.add_argument("--regex-pattern",
help="Python regex pattern for allowed characters")
parser.add_argument("--no-lowercase", action='store_false', dest='lowercase', default=True,
help="Activate case sensitivity")
parser.add_argument("--replacements", nargs='+',
help="""Additional replacement rules e.g. "|->or", "%%->percent".""")

args = parser.parse_args(argv[1:])

if args.input_string and args.stdin:
parser.error("Input strings and --stdin cannot work together")

if args.replacements:
def split_check(repl):
SEP = '->'
if SEP not in repl:
parser.error("Replacements must be of the form: ORIGINAL{SEP}REPLACED".format(SEP=SEP))
return repl.split(SEP, 1)
args.replacements = [split_check(repl) for repl in args.replacements]

if args.input_string:
args.input_string = " ".join(args.input_string)
elif args.stdin:
args.input_string = sys.stdin.read()

if not args.input_string:
args.input_string = ''

return args


def slugify_params(args):
return dict(
text=args.input_string,
entities=args.entities,
decimal=args.decimal,
hexadecimal=args.hexadecimal,
max_length=args.max_length,
word_boundary=args.word_boundary,
save_order=args.save_order,
separator=args.separator,
stopwords=args.stopwords,
lowercase=args.lowercase,
replacements=args.replacements
)


def main(argv=None): # pragma: no cover
""" Run this program """
if argv is None:
argv = sys.argv
args = parse_args(argv)
params = slugify_params(args)
try:
print(slugify(**params))
except KeyboardInterrupt:
sys.exit(-1)


if __name__ == '__main__': # pragma: no cover
main()
8 changes: 0 additions & 8 deletions slugify/slugify.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,3 @@ def slugify(text, entities=True, decimal=True, hexadecimal=True, max_length=0, w
text = text.replace(DEFAULT_SEPARATOR, separator)

return text


def main(): # pragma: no cover
if len(sys.argv) < 2:
print("Usage %s TEXT TO SLUGIFY" % sys.argv[0])
else:
text = ' '.join(sys.argv[1:])
print(slugify(text))
119 changes: 118 additions & 1 deletion test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-

import io
import os
import sys
import unittest
from contextlib import contextmanager

from slugify import slugify
from slugify import smart_truncate
from slugify.__main__ import slugify_params, parse_args


class TestSlugification(unittest.TestCase):
Expand Down Expand Up @@ -242,5 +247,117 @@ def test_smart_truncate_no_seperator(self):
self.assertEqual(r, txt)


PY3 = sys.version_info.major == 3


@contextmanager
def captured_stderr():
backup = sys.stderr
sys.stderr = io.StringIO() if PY3 else io.BytesIO()
try:
yield sys.stderr
finally:
sys.stderr = backup


@contextmanager
def loaded_stdin(contents):
backup = sys.stdin
sys.stdin = io.StringIO(contents) if PY3 else io.BytesIO(contents)
try:
yield sys.stdin
finally:
sys.stdin = backup


class TestCommandParams(unittest.TestCase):
DEFAULTS = {
'entities': True,
'decimal': True,
'hexadecimal': True,
'max_length': 0,
'word_boundary': False,
'save_order': False,
'separator': '-',
'stopwords': None,
'lowercase': True,
'replacements': None
}

def get_params_from_cli(self, *argv):
args = parse_args([None] + list(argv))
return slugify_params(args)

def make_params(self, **values):
return dict(self.DEFAULTS, **values)

def assertParamsMatch(self, expected, checked):
reduced_checked = {}
for key in expected.keys():
reduced_checked[key] = checked[key]
self.assertEqual(expected, reduced_checked)

def test_defaults(self):
params = self.get_params_from_cli()
self.assertParamsMatch(self.DEFAULTS, params)

def test_negative_flags(self):
params = self.get_params_from_cli('--no-entities', '--no-decimal', '--no-hexadecimal', '--no-lowercase')
expected = self.make_params(entities=False, decimal=False, hexadecimal=False, lowercase=False)
self.assertFalse(expected['lowercase'])
self.assertFalse(expected['word_boundary'])
self.assertParamsMatch(expected, params)

def test_affirmative_flags(self):
params = self.get_params_from_cli('--word-boundary', '--save-order')
expected = self.make_params(word_boundary=True, save_order=True)
self.assertParamsMatch(expected, params)

def test_valued_arguments(self):
params = self.get_params_from_cli('--stopwords', 'abba', 'beatles', '--max-length', '98', '--separator', '+')
expected = self.make_params(stopwords=['abba', 'beatles'], max_length=98, separator='+')
self.assertParamsMatch(expected, params)

def test_replacements_right(self):
params = self.get_params_from_cli('--replacements', 'A->B', 'C->D')
expected = self.make_params(replacements=[['A', 'B'], ['C', 'D']])
self.assertParamsMatch(expected, params)

def test_replacements_wrong(self):
with self.assertRaises(SystemExit) as err, captured_stderr() as cse:
self.get_params_from_cli('--replacements', 'A--B')
self.assertEqual(err.exception.code, 2)
self.assertIn("Replacements must be of the form: ORIGINAL->REPLACED", cse.getvalue())

def test_text_in_cli(self):
params = self.get_params_from_cli('Cool Text')
expected = self.make_params(text='Cool Text')
self.assertParamsMatch(expected, params)

def test_text_in_cli_multi(self):
params = self.get_params_from_cli('Cool', 'Text')
expected = self.make_params(text='Cool Text')
self.assertParamsMatch(expected, params)

def test_text_in_stdin(self):
with loaded_stdin("Cool Stdin"):
params = self.get_params_from_cli('--stdin')
expected = self.make_params(text='Cool Stdin')
self.assertParamsMatch(expected, params)

def test_two_text_sources_fails(self):
with self.assertRaises(SystemExit) as err, captured_stderr() as cse:
self.get_params_from_cli('--stdin', 'Text')
self.assertEqual(err.exception.code, 2)
self.assertIn("Input strings and --stdin cannot work together", cse.getvalue())

def test_multivalued_options_with_text(self):
text = "the quick brown fox jumps over the lazy dog in a hurry"
cli_args = "--stopwords the in a hurry -- {}".format(text).split()
params = self.get_params_from_cli(*cli_args)
self.assertEqual(params['text'], text)
self.assertEqual(params['stopwords'], ['the', 'in', 'a', 'hurry'])


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