File python-slugify-8.0.1.obscpio of Package python-python-slugify

07070100000000000041ED00000000000000000000000263F8DECF00000000000000000000000000000000000000000000001D00000000python-slugify-8.0.1/.github07070100000001000041ED00000000000000000000000263F8DECF00000000000000000000000000000000000000000000002700000000python-slugify-8.0.1/.github/workflows07070100000002000081A400000000000000000000000163F8DECF000004B7000000000000000000000000000000000000002E00000000python-slugify-8.0.1/.github/workflows/ci.ymlname: CI

# Run on push only for dev/sandbox
# Otherwise it may trigger concurrently `push & pull_request` on PRs.
on:
  push:
    branches:
      - ci
      - staging

jobs:
  build:
    name: Python ${{ matrix.python }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: [3.7, 3.8, 3.9, "3.10", 3.11, pypy3.8]

    steps:
      - uses: actions/checkout@v3
      - name: setup python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .
          pip install coveralls --upgrade
      - name: Run flake8
        run: |
          pip install flake8 --upgrade
          flake8 --exclude=build --ignore=E501,F403,F401,E241,E225,E128 .
      - name: Run pycodestyle
        run: |
          pip install pycodestyle --upgrade
          pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py
      - name: Run test
        run: |
          coverage run --source=slugify test.py
      - name: Coveralls
        run: coveralls --service=github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
07070100000003000081A400000000000000000000000163F8DECF000004B9000000000000000000000000000000000000002F00000000python-slugify-8.0.1/.github/workflows/dev.ymlname: DEV

# Run on push only for dev/sandbox
# Otherwise it may trigger concurrently `push & pull_request` on PRs.
on:
  push:
    branches:
      - sandbox
      - dev

jobs:
  build:
    name: Python ${{ matrix.python }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: [3.7, 3.8, 3.9, "3.10", 3.11, pypy3.8]

    steps:
      - uses: actions/checkout@v3
      - name: setup python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .
          pip install coveralls --upgrade
      - name: Run flake8
        run: |
          pip install flake8 --upgrade
          flake8 --exclude=build --ignore=E501,F403,F401,E241,E225,E128 .
      - name: Run pycodestyle
        run: |
          pip install pycodestyle --upgrade
          pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py
      - name: Run test
        run: |
          coverage run --source=slugify test.py
      - name: Coveralls
        run: coveralls --service=github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
07070100000004000081A400000000000000000000000163F8DECF000004AD000000000000000000000000000000000000003000000000python-slugify-8.0.1/.github/workflows/main.ymlname: Main

# Run on push only for dev/sandbox
# Otherwise it may trigger concurrently `push & pull_request` on PRs.
on:
  push:
    branches:
      - master

jobs:
  build:
    name: Python ${{ matrix.python }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: [3.7, 3.8, 3.9, "3.10", 3.11, pypy3.8]

    steps:
      - uses: actions/checkout@v3
      - name: setup python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .
          pip install coveralls --upgrade
      - name: Run flake8
        run: |
          pip install flake8 --upgrade
          flake8 --exclude=build --ignore=E501,F403,F401,E241,E225,E128 .
      - name: Run pycodestyle
        run: |
          pip install pycodestyle --upgrade
          pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py
      - name: Run test
        run: |
          coverage run --source=slugify test.py
      - name: Coveralls
        run: coveralls --service=github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
07070100000005000081A400000000000000000000000163F8DECF000002E3000000000000000000000000000000000000002000000000python-slugify-8.0.1/.gitignore# JebBrains IDE
.idea/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

*.*DS_Store
07070100000006000041ED00000000000000000000000263F8DECF00000000000000000000000000000000000000000000001D00000000python-slugify-8.0.1/.vscode07070100000007000081A400000000000000000000000163F8DECF00000087000000000000000000000000000000000000002B00000000python-slugify-8.0.1/.vscode/settings.json{
  "python.linting.pylintEnabled": false,
  "python.pythonPath": "/usr/bin/python3",
  "cSpell.words": ["Neekman", "shch", "xlate"]
}
07070100000008000081A400000000000000000000000163F8DECF00000E9C000000000000000000000000000000000000002200000000python-slugify-8.0.1/CHANGELOG.md## 8.0.1

- Added license notice to readme (@C-nit - thx)

## 8.0.0

- By default, prefer unidecode if installed (@enkidulan - thx)

## 7.0.0

- Drop python 3.6, add python 3.11 (@hugovk - thx)

## 6.1.2

- Reintroduce the cli options

## 6.1.1

- Remove type hinting (temporarily)

## 6.1.0

- Add `allow_unicode` flag to allow unicode characters in the slug

## 6.0.1

- Rework regex_pattern to mean the opposite (disallowed chars instead of allowed)
- Thanks to @yyyyyyyan for the initial PR followed by the final PR by @mrezzamoradi

## 6.0.0

- Enable github action
- Remove tox, as we run the test on github action, the end users can refer to those test

## 5.0.2

- Enable twine publish

## 5.0.1

- Drop support for python 2.7, 3.5 & tox, clean up

## 5.0.0

- Add support for Py 3.9 - added tox (@jon-betts - Thx)
- Drop support for python 2.7, 3.5 & friends

## 4.0.1

- Add support for Py 3.8
- Last version with `official` python 2.7 and <= 3.5 support

## 4.0.0

- Drop support from 2.6, & < 3.4.5

## 3.0.6

- Fixed encoding in special.py

## 3.0.5

- Add test for pre-translation (e.g German Umlaut)
- Add special char supports (optional Use)

## 3.0.4

- Now supporting text-unidecode>=1.3
- Now supporting Unidecode>=1.1.1

## 3.0.3

- Remove unicode chars from file

## 3.0.2

- Add official support of Py 3.7

## 3.0.1

- Add test.py to manifest

## 3.0.0

- Upgrade Unidecode
- Promote text-unidecode as the primary decoding package
- Add Unidecode as an optional extra. "pip install python-slugify[unidecode]"

## 2.0.1

- Add replacements option e.g. [['|', 'or'], ['%', 'percent'], ['-', '_']] (@andriyor)

## 2.0.0

- Fix alternative dependency installation

## 1.2.6

- Add support for case sensitive slugs (@s-m-e)

## 1.2.5

- Add support for using text-unidecode (@bolkedebruin)
- Switch to pycodestyle instead of pep8

## 1.2.4

- Remove build artifacts during packaging
- Simplify the setup.py file (@reece)

## 1.2.3

- Republish - possible corrupt 1.2.2 build

## 1.2.2

- Add `regex_pattern` option. (@vrbaskiz)
- Add Python 3.6 support

## 1.2.1

- Including certain files (e.g. license.md) in sdists via MANIFEST.in (@proinsias)
- Relax licensing by moving from BSD to MIT
- Add Python 3.5 support
- Add more tests

## 1.2.0

Backward incompatible change: (@fabiocaccamo)

- In version < 1.2.0 all single quotes ( ' ) were removed, and
  moving forward, >= 1.2.0, they will be replaced with ( - ).
  Example:
  < 1.2.0 -- ('C\'est déjà l\'été.' -> "cest-deja-lete")
  > = 1.2.0 -- ('C\'est déjà l\'été.' -> "c-est-deja-l-ete")

## 1.1.4

Bugfix:

- Add more test cases, dropped `official` support for python 3.2

## 1.1.3

Bugfix:

- Handle unichar in python 3.x

## 1.1.2

Enhancement:

- Ability to remove `stopwords` from string

## 1.0.2

Enhancement:

- A new PyPI release

## 1.0.1

Enhancement:

- Promoting to production grade

## 0.1.1

Enhancement:

- Added option to save word order
- Removed 2to3 dependency
- Added more tests

## 0.1.0

Enhancement:

- Added more test
- Added test for python 3.4

## 0.0.9

Enhancement:

- Enable console_scripts

## 0.0.8

Enhancement:

- Move logic out of **init**.py
- Added console_scripts (@ekamil)
- Updated pep8.sh
- Added pypy support

## 0.0.7

Enhancement:

- Handle encoding in setup file
- Update ReadME, ChangeLog, License files

## 0.0.6

Enhancement:

- Update for smart_truncate

## 0.0.5

Features:

- Added Python 3.2 and 3.3 support (work by: arthurdarcet@github)

## 0.0.4

Features:

- Added option to choose non-dash separators (request by: danilodimoia@github)

## 0.0.3

Features:

- Added the ability to truncate slugs (request by: juanriaza@github)

## 0.0.2

Enhancement:

- Incremental update

## 0.0.1

- Initial version
07070100000009000081A400000000000000000000000163F8DECF0000044F000000000000000000000000000000000000001D00000000python-slugify-8.0.1/LICENSEThe MIT License

Copyright (c) Val Neekman @ Neekware Inc. http://neekware.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
0707010000000A000081A400000000000000000000000163F8DECF00000037000000000000000000000000000000000000002100000000python-slugify-8.0.1/MANIFEST.ininclude LICENSE
include README.md
include CHANGELOG.md
0707010000000B000081A400000000000000000000000163F8DECF00001C1E000000000000000000000000000000000000001F00000000python-slugify-8.0.1/README.md# Python Slugify

**A Python slugify application that handles unicode**.

[![status-image]][status-link]
[![version-image]][version-link]
[![coverage-image]][coverage-link]

# Overview

**Best attempt** to create slugs from unicode strings while keeping it **DRY**.

# Notice

This module, by default installs and uses [text-unidecode](https://github.com/kmike/text-unidecode) _(GPL & Perl Artistic)_ for its decoding needs.

However, there is an alternative decoding package called [Unidecode](https://github.com/avian2/unidecode) _(GPL)_. It can be installed as `python-slugify[unidecode]` for those who prefer it. `Unidecode` is believed to be more advanced.

### `Official` Support Matrix

| Python         | Slugify            |
| -------------- | ------------------ |
| `>= 2.7 < 3.6` | `< 5.0.0`          |
| `>= 3.6 < 3.7` | `>= 5.0.0 < 7.0.0` |
| `>= 3.7`       | `>= 7.0.0`         |

# How to install

    easy_install python-slugify |OR| easy_install python-slugify[unidecode]
    -- OR --
    pip install python-slugify |OR| pip install python-slugify[unidecode]

# Options

```python
def slugify(
    text,
    entities=True,
    decimal=True,
    hexadecimal=True,
    max_length=0,
    word_boundary=False,
    separator='-',
    save_order=False,
    stopwords=(),
    regex_pattern=None,
    lowercase=True,
    replacements=(),
    allow_unicode=False
  ):
  """
  Make a slug from the given text.
  :param text (str): initial text
  :param entities (bool): converts html entities to unicode (foo &amp; bar -> foo-bar)
  :param decimal (bool): converts html decimal to unicode (&#381; -> Ž -> z)
  :param hexadecimal (bool): converts html hexadecimal to unicode (&#x17D; -> Ž -> z)
  :param max_length (int): output string length
  :param word_boundary (bool): truncates to end of full words (length may be shorter than max_length)
  :param save_order (bool): if parameter is True and max_length > 0 return whole words in the initial order
  :param separator (str): separator between words
  :param stopwords (iterable): words to discount
  :param regex_pattern (str): regex pattern for disallowed characters
  :param lowercase (bool): activate case sensitivity by setting it to False
  :param replacements (iterable): list of replacement rules e.g. [['|', 'or'], ['%', 'percent']]
  :param allow_unicode (bool): allow unicode characters
  :return (str): slugify text
  """
```

# How to use

```python
from slugify import slugify

txt = "This is a test ---"
r = slugify(txt)
self.assertEqual(r, "this-is-a-test")

txt = '影師嗎'
r = slugify(txt)
self.assertEqual(r, "ying-shi-ma")

txt = '影師嗎'
r = slugify(txt, allow_unicode=True)
self.assertEqual(r, "影師嗎")

txt = 'C\'est déjà l\'été.'
r = slugify(txt)
self.assertEqual(r, "c-est-deja-l-ete")

txt = 'Nín hǎo. Wǒ shì zhōng guó rén'
r = slugify(txt)
self.assertEqual(r, "nin-hao-wo-shi-zhong-guo-ren")

txt = 'Компьютер'
r = slugify(txt)
self.assertEqual(r, "kompiuter")

txt = 'jaja---lol-méméméoo--a'
r = slugify(txt, max_length=9)
self.assertEqual(r, "jaja-lol")

txt = 'jaja---lol-méméméoo--a'
r = slugify(txt, max_length=15, word_boundary=True)
self.assertEqual(r, "jaja-lol-a")

txt = 'jaja---lol-méméméoo--a'
r = slugify(txt, max_length=20, word_boundary=True, separator=".")
self.assertEqual(r, "jaja.lol.mememeoo.a")

txt = 'one two three four five'
r = slugify(txt, max_length=13, word_boundary=True, save_order=True)
self.assertEqual(r, "one-two-three")

txt = 'the quick brown fox jumps over the lazy dog'
r = slugify(txt, stopwords=['the'])
self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

txt = 'the quick brown fox jumps over the lazy dog in a hurry'
r = slugify(txt, stopwords=['the', 'in', 'a', 'hurry'])
self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

txt = 'thIs Has a stopword Stopword'
r = slugify(txt, stopwords=['Stopword'], lowercase=False)
self.assertEqual(r, 'thIs-Has-a-stopword')

txt = "___This is a test___"
regex_pattern = r'[^-a-z0-9_]+'
r = slugify(txt, regex_pattern=regex_pattern)
self.assertEqual(r, "___this-is-a-test___")

txt = "___This is a test___"
regex_pattern = r'[^-a-z0-9_]+'
r = slugify(txt, separator='_', regex_pattern=regex_pattern)
self.assertNotEqual(r, "_this_is_a_test_")

txt = '10 | 20 %'
r = slugify(txt, replacements=[['|', 'or'], ['%', 'percent']])
self.assertEqual(r, "10-or-20-percent")

txt = 'ÜBER Über German Umlaut'
r = slugify(txt, replacements=[['Ü', 'UE'], ['ü', 'ue']])
self.assertEqual(r, "ueber-ueber-german-umlaut")

txt = 'i love 🦄'
r = slugify(txt, allow_unicode=True)
self.assertEqual(r, "i-love")

txt = 'i love 🦄'
r = slugify(txt, allow_unicode=True, regex_pattern=r'[^🦄]+')
self.assertEqual(r, "🦄")

```

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

To run the tests against the current environment:

    python test.py

# Contribution

Please read the ([wiki](https://github.com/un33k/python-slugify/wiki/Python-Slugify-Wiki)) page prior to raising any PRs.

# License

Released under a ([MIT](LICENSE)) license.

### Notes on GPL dependencies
Though the dependencies may be GPL licensed, `python-slugify` itself is not considered a derivative work and will remain under the MIT license.  
If you wish to avoid installation of any GPL licensed packages, please note that the default dependency `text-unidecode` explicitly lets you choose to use the [Artistic License](https://opensource.org/license/artistic-perl-1-0-2/) instead. Use without concern.

# Version

X.Y.Z Version

    `MAJOR` version -- when you make incompatible API changes,
    `MINOR` version -- when you add functionality in a backwards-compatible manner, and
    `PATCH` version -- when you make backwards-compatible bug fixes.

[status-image]: https://github.com/un33k/python-slugify/actions/workflows/ci.yml/badge.svg
[status-link]: https://github.com/un33k/python-slugify/actions/workflows/ci.yml
[version-image]: https://img.shields.io/pypi/v/python-slugify.svg
[version-link]: https://pypi.python.org/pypi/python-slugify
[coverage-image]: https://coveralls.io/repos/un33k/python-slugify/badge.svg
[coverage-link]: https://coveralls.io/r/un33k/python-slugify
[download-image]: https://img.shields.io/pypi/dm/python-slugify.svg
[download-link]: https://pypi.python.org/pypi/python-slugify

# Sponsors

[Neekware Inc.](http://neekware.com)
0707010000000C000081A400000000000000000000000163F8DECF0000002D000000000000000000000000000000000000002A00000000python-slugify-8.0.1/dev.requirements.txtpycodestyle==2.8.0
twine==3.4.1
flake8==4.0.10707010000000D000081ED00000000000000000000000163F8DECF000001A2000000000000000000000000000000000000001F00000000python-slugify-8.0.1/format.sh#!/bin/bash

# Ignoring autogenerated files
#  -- Migration directories
# Ignoring error codes
#  -- E128 continuation line under-indented for visual indent
#  -- E261 at least two spaces before inline comment
#  -- E225 missing whitespace around operator
#  -- E501 line too long
# Ignoring warning codes
#  -- W605 invalid escape sequence '\d'

pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py
0707010000000E000081ED00000000000000000000000163F8DECF000009BE000000000000000000000000000000000000001E00000000python-slugify-8.0.1/setup.py#!/usr/bin/env python
# Learn more: https://github.com/un33k/setup.py
import os
import sys

from codecs import open
from shutil import rmtree
from setuptools import setup


package = 'slugify'
python_requires = ">=3.7"
here = os.path.abspath(os.path.dirname(__file__))

install_requires = ['text-unidecode>=1.3']
extras_requires = {'unidecode': ['Unidecode>=1.1.1']}
test_requires = []

about = {}
with open(os.path.join(here, package, '__version__.py'), 'r', 'utf-8') as f:
    exec(f.read(), about)

with open('README.md', 'r', 'utf-8') as f:
    readme = f.read()


def status(s):
    print('\033[1m{0}\033[0m'.format(s))


# 'setup.py publish' shortcut.
if sys.argv[-1] == 'publish':
    try:
        status('Removing previous builds…')
        rmtree(os.path.join(here, 'dist'))
    except OSError:
        pass

    status('Building Source and Wheel (universal) distribution…')
    os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))

    status('Uploading the package to PyPI via Twine…')
    os.system('twine upload dist/*')

    status('Pushing git tags…')
    os.system('git tag v{0}'.format(about['__version__']))
    os.system('git push --tags')
    sys.exit()

setup(
    name=about['__title__'],
    version=about['__version__'],
    description=about['__description__'],
    long_description=readme,
    long_description_content_type='text/markdown',
    author=about['__author__'],
    author_email=about['__author_email__'],
    url=about['__url__'],
    license=about['__license__'],
    packages=[package],
    package_data={'': ['LICENSE']},
    package_dir={'slugify': 'slugify'},
    include_package_data=True,
    python_requires=python_requires,
    install_requires=install_requires,
    tests_require=test_requires,
    extras_require=extras_requires,
    zip_safe=False,
    cmdclass={},
    project_urls={},
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Developers',
        'Natural Language :: English',
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: 3.10',
        'Programming Language :: Python :: 3.11',
    ],
    entry_points={'console_scripts': ['slugify=slugify.__main__:main']},
)
0707010000000F000041ED00000000000000000000000263F8DECF00000000000000000000000000000000000000000000001D00000000python-slugify-8.0.1/slugify07070100000010000081A400000000000000000000000163F8DECF0000015A000000000000000000000000000000000000002900000000python-slugify-8.0.1/slugify/__init__.pyfrom .special import *
from .slugify import *
from .__version__ import __title__
from .__version__ import __author__
from .__version__ import __author_email__
from .__version__ import __description__
from .__version__ import __url__
from .__version__ import __license__
from .__version__ import __copyright__
from .__version__ import __version__
07070100000011000081A400000000000000000000000163F8DECF00000F1A000000000000000000000000000000000000002900000000python-slugify-8.0.1/slugify/__main__.pyfrom __future__ import print_function, absolute_import
import argparse
import sys

from .slugify import slugify, DEFAULT_SEPARATOR


def parse_args(argv):
    parser = argparse.ArgumentParser(description="Slug 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 disallowed 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".""")
    parser.add_argument("--allow-unicode", action='store_true', default=False,
                        help="Allow unicode characters")

    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,
        allow_unicode=args.allow_unicode
    )


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()
07070100000012000081A400000000000000000000000163F8DECF00000145000000000000000000000000000000000000002C00000000python-slugify-8.0.1/slugify/__version__.py__title__ = 'python-slugify'
__author__ = 'Val Neekman'
__author_email__ = 'info@neekware.com'
__description__ = 'A Python slugify application that also handles Unicode'
__url__ = 'https://github.com/un33k/python-slugify'
__license__ = 'MIT'
__copyright__ = 'Copyright 2022 Val Neekman @ Neekware Inc.'
__version__ = '8.0.1'
07070100000013000081A400000000000000000000000163F8DECF000016A3000000000000000000000000000000000000002800000000python-slugify-8.0.1/slugify/slugify.pyimport re
import sys
import unicodedata
from html.entities import name2codepoint

try:
    import unidecode
except ImportError:
    import text_unidecode as unidecode

__all__ = ['slugify', 'smart_truncate']


CHAR_ENTITY_PATTERN = re.compile(r'&(%s);' % '|'.join(name2codepoint))
DECIMAL_PATTERN = re.compile(r'&#(\d+);')
HEX_PATTERN = re.compile(r'&#x([\da-fA-F]+);')
QUOTE_PATTERN = re.compile(r'[\']+')
DISALLOWED_CHARS_PATTERN = re.compile(r'[^-a-zA-Z0-9]+')
DISALLOWED_UNICODE_CHARS_PATTERN = re.compile(r'[\W_]+')
DUPLICATE_DASH_PATTERN = re.compile(r'-{2,}')
NUMBERS_PATTERN = re.compile(r'(?<=\d),(?=\d)')
DEFAULT_SEPARATOR = '-'


def smart_truncate(string, max_length=0, word_boundary=False, separator=' ', save_order=False):
    """
    Truncate a string.
    :param string (str): string for modification
    :param max_length (int): output string length
    :param word_boundary (bool):
    :param save_order (bool): if True then word order of output string is like input string
    :param separator (str): separator between words
    :return:
    """

    string = string.strip(separator)

    if not max_length:
        return string

    if len(string) < max_length:
        return string

    if not word_boundary:
        return string[:max_length].strip(separator)

    if separator not in string:
        return string[:max_length]

    truncated = ''
    for word in string.split(separator):
        if word:
            next_len = len(truncated) + len(word)
            if next_len < max_length:
                truncated += '{}{}'.format(word, separator)
            elif next_len == max_length:
                truncated += '{}'.format(word)
                break
            else:
                if save_order:
                    break
    if not truncated:  # pragma: no cover
        truncated = string[:max_length]
    return truncated.strip(separator)


def slugify(text, entities=True, decimal=True, hexadecimal=True, max_length=0, word_boundary=False,
            separator=DEFAULT_SEPARATOR, save_order=False, stopwords=(), regex_pattern=None, lowercase=True,
            replacements=(), allow_unicode=False):
    """
    Make a slug from the given text.
    :param text (str): initial text
    :param entities (bool): converts html entities to unicode
    :param decimal (bool): converts html decimal to unicode
    :param hexadecimal (bool): converts html hexadecimal to unicode
    :param max_length (int): output string length
    :param word_boundary (bool): truncates to complete word even if length ends up shorter than max_length
    :param save_order (bool): if parameter is True and max_length > 0 return whole words in the initial order
    :param separator (str): separator between words
    :param stopwords (iterable): words to discount
    :param regex_pattern (str): regex pattern for disallowed characters
    :param lowercase (bool): activate case sensitivity by setting it to False
    :param replacements (iterable): list of replacement rules e.g. [['|', 'or'], ['%', 'percent']]
    :param allow_unicode (bool): allow unicode characters
    :return (str):
    """

    # user-specific replacements
    if replacements:
        for old, new in replacements:
            text = text.replace(old, new)

    # ensure text is unicode
    if not isinstance(text, str):
        text = str(text, 'utf-8', 'ignore')

    # replace quotes with dashes - pre-process
    text = QUOTE_PATTERN.sub(DEFAULT_SEPARATOR, text)

    # decode unicode
    if not allow_unicode:
        text = unidecode.unidecode(text)

    # ensure text is still in unicode
    if not isinstance(text, str):
        text = str(text, 'utf-8', 'ignore')

    # character entity reference
    if entities:
        text = CHAR_ENTITY_PATTERN.sub(lambda m: chr(name2codepoint[m.group(1)]), text)

    # decimal character reference
    if decimal:
        try:
            text = DECIMAL_PATTERN.sub(lambda m: chr(int(m.group(1))), text)
        except Exception:
            pass

    # hexadecimal character reference
    if hexadecimal:
        try:
            text = HEX_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), text)
        except Exception:
            pass

    # translate
    if allow_unicode:
        text = unicodedata.normalize('NFKC', text)
    else:
        text = unicodedata.normalize('NFKD', text)

    if sys.version_info < (3,):
        text = text.encode('ascii', 'ignore')

    # make the text lowercase (optional)
    if lowercase:
        text = text.lower()

    # remove generated quotes -- post-process
    text = QUOTE_PATTERN.sub('', text)

    # cleanup numbers
    text = NUMBERS_PATTERN.sub('', text)

    # replace all other unwanted characters
    if allow_unicode:
        pattern = regex_pattern or DISALLOWED_UNICODE_CHARS_PATTERN
    else:
        pattern = regex_pattern or DISALLOWED_CHARS_PATTERN

    text = re.sub(pattern, DEFAULT_SEPARATOR, text)

    # remove redundant
    text = DUPLICATE_DASH_PATTERN.sub(DEFAULT_SEPARATOR, text).strip(DEFAULT_SEPARATOR)

    # remove stopwords
    if stopwords:
        if lowercase:
            stopwords_lower = [s.lower() for s in stopwords]
            words = [w for w in text.split(DEFAULT_SEPARATOR) if w not in stopwords_lower]
        else:
            words = [w for w in text.split(DEFAULT_SEPARATOR) if w not in stopwords]
        text = DEFAULT_SEPARATOR.join(words)

    # finalize user-specific replacements
    if replacements:
        for old, new in replacements:
            text = text.replace(old, new)

    # smart truncate if requested
    if max_length > 0:
        text = smart_truncate(text, max_length, word_boundary, DEFAULT_SEPARATOR, save_order)

    if separator != DEFAULT_SEPARATOR:
        text = text.replace(DEFAULT_SEPARATOR, separator)

    return text
07070100000014000081A400000000000000000000000163F8DECF0000048F000000000000000000000000000000000000002800000000python-slugify-8.0.1/slugify/special.py# -*- coding: utf-8 -*-


def add_uppercase_char(char_list):
    """ Given a replacement char list, this adds uppercase chars to the list """

    for item in char_list:
        char, xlate = item
        upper_dict = char.upper(), xlate.capitalize()
        if upper_dict not in char_list and char != upper_dict[0]:
            char_list.insert(0, upper_dict)
        return char_list


# Language specific pre translations
# Source awesome-slugify

_CYRILLIC = [      # package defaults:
    (u'ё', u'e'),    # io / yo
    (u'я', u'ya'),   # ia
    (u'х', u'h'),    # kh
    (u'у', u'y'),    # u
    (u'щ', u'sch'),  # sch
    (u'ю', u'u'),    # iu / yu
]
CYRILLIC = add_uppercase_char(_CYRILLIC)

_GERMAN = [        # package defaults:
    (u'ä', u'ae'),   # a
    (u'ö', u'oe'),   # o
    (u'ü', u'ue'),   # u
]
GERMAN = add_uppercase_char(_GERMAN)

_GREEK = [         # package defaults:
    (u'χ', u'ch'),   # kh
    (u'Ξ', u'X'),    # Ks
    (u'ϒ', u'Y'),    # U
    (u'υ', u'y'),    # u
    (u'ύ', u'y'),
    (u'ϋ', u'y'),
    (u'ΰ', u'y'),
]
GREEK = add_uppercase_char(_GREEK)

# Pre translations
PRE_TRANSLATIONS = CYRILLIC + GERMAN + GREEK
07070100000015000081A400000000000000000000000163F8DECF00005D35000000000000000000000000000000000000001D00000000python-slugify-8.0.1/test.py# -*- coding: utf-8 -*-
import io
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 TestSlugify(unittest.TestCase):

    def test_extraneous_seperators(self):

        txt = "This is a test ---"
        r = slugify(txt)
        self.assertEqual(r, "this-is-a-test")

        txt = "___This is a test ---"
        r = slugify(txt)
        self.assertEqual(r, "this-is-a-test")

        txt = "___This is a test___"
        r = slugify(txt)
        self.assertEqual(r, "this-is-a-test")

    def test_non_word_characters(self):
        txt = "This -- is a ## test ---"
        r = slugify(txt)
        self.assertEqual(r, "this-is-a-test")

    def test_phonetic_conversion_of_eastern_scripts(self):
        txt = '影師嗎'
        r = slugify(txt)
        self.assertEqual(r, "ying-shi-ma")

    def test_accented_text(self):
        txt = 'C\'est déjà l\'été.'
        r = slugify(txt)
        self.assertEqual(r, "c-est-deja-l-ete")

        txt = 'Nín hǎo. Wǒ shì zhōng guó rén'
        r = slugify(txt)
        self.assertEqual(r, "nin-hao-wo-shi-zhong-guo-ren")

    def test_accented_text_with_non_word_characters(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt)
        self.assertEqual(r, "jaja-lol-mememeoo-a")

    def test_cyrillic_text(self):
        txt = 'Компьютер'
        r = slugify(txt)
        self.assertEqual(r, "kompiuter")

    def test_max_length(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=9)
        self.assertEqual(r, "jaja-lol")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=15)
        self.assertEqual(r, "jaja-lol-mememe")

    def test_max_length_cutoff_not_required(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=50)
        self.assertEqual(r, "jaja-lol-mememeoo-a")

    def test_word_boundary(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=15, word_boundary=True)
        self.assertEqual(r, "jaja-lol-a")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=17, word_boundary=True)
        self.assertEqual(r, "jaja-lol-mememeoo")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=18, word_boundary=True)
        self.assertEqual(r, "jaja-lol-mememeoo")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=19, word_boundary=True)
        self.assertEqual(r, "jaja-lol-mememeoo-a")

    def test_custom_separator(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=20, word_boundary=True, separator=".")
        self.assertEqual(r, "jaja.lol.mememeoo.a")

    def test_multi_character_separator(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, max_length=20, word_boundary=True, separator="ZZZZZZ")
        self.assertEqual(r, "jajaZZZZZZlolZZZZZZmememeooZZZZZZa")

    def test_save_order(self):
        txt = 'one two three four five'
        r = slugify(txt, max_length=13, word_boundary=True, save_order=True)
        self.assertEqual(r, "one-two-three")

        txt = 'one two three four five'
        r = slugify(txt, max_length=13, word_boundary=True, save_order=False)
        self.assertEqual(r, "one-two-three")

        txt = 'one two three four five'
        r = slugify(txt, max_length=12, word_boundary=True, save_order=False)
        self.assertEqual(r, "one-two-four")

        txt = 'one two three four five'
        r = slugify(txt, max_length=12, word_boundary=True, save_order=True)
        self.assertEqual(r, "one-two")

    def test_stopword_removal(self):
        txt = 'this has a stopword'
        r = slugify(txt, stopwords=['stopword'])
        self.assertEqual(r, 'this-has-a')

    def test_stopword_removal_casesensitive(self):
        txt = 'thIs Has a stopword Stopword'
        r = slugify(txt, stopwords=['Stopword'], lowercase=False)
        self.assertEqual(r, 'thIs-Has-a-stopword')

    def test_multiple_stopword_occurances(self):
        txt = 'the quick brown fox jumps over the lazy dog'
        r = slugify(txt, stopwords=['the'])
        self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

    def test_differently_cased_stopword_match(self):
        txt = 'Foo A FOO B foo C'
        r = slugify(txt, stopwords=['foo'])
        self.assertEqual(r, 'a-b-c')

        txt = 'Foo A FOO B foo C'
        r = slugify(txt, stopwords=['FOO'])
        self.assertEqual(r, 'a-b-c')

    def test_multiple_stopwords(self):
        txt = 'the quick brown fox jumps over the lazy dog in a hurry'
        r = slugify(txt, stopwords=['the', 'in', 'a', 'hurry'])
        self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

    def test_stopwords_with_different_separator(self):
        txt = 'the quick brown fox jumps over the lazy dog'
        r = slugify(txt, stopwords=['the'], separator=' ')
        self.assertEqual(r, 'quick brown fox jumps over lazy dog')

    def test_html_entities_on(self):
        txt = 'foo &amp; bar'
        r = slugify(txt)
        self.assertEqual(r, 'foo-bar')

    def test_html_entities_off(self):
        txt = 'foo &amp; bar'
        r = slugify(txt, entities=False)
        self.assertEqual(r, 'foo-amp-bar')

    def test_html_decimal_on(self):
        txt = '&#381;'
        r = slugify(txt, decimal=True)
        self.assertEqual(r, 'z')

    def test_html_decimal_off(self):
        txt = '&#381;'
        r = slugify(txt, entities=False, decimal=False)
        self.assertEqual(r, '381')

    def test_html_hexadecimal_on(self):
        txt = '&#x17D;'
        r = slugify(txt, hexadecimal=True)
        self.assertEqual(r, 'z')

    def test_html_hexadecimal_off(self):
        txt = '&#x17D;'
        r = slugify(txt, hexadecimal=False)
        self.assertEqual(r, 'x17d')

    def test_starts_with_number(self):
        txt = '10 amazing secrets'
        r = slugify(txt)
        self.assertEqual(r, '10-amazing-secrets')

    def test_contains_numbers(self):
        txt = 'buildings with 1000 windows'
        r = slugify(txt)
        self.assertEqual(r, 'buildings-with-1000-windows')

    def test_ends_with_number(self):
        txt = 'recipe number 3'
        r = slugify(txt)
        self.assertEqual(r, 'recipe-number-3')

    def test_numbers_only(self):
        txt = '404'
        r = slugify(txt)
        self.assertEqual(r, '404')

    def test_numbers_and_symbols(self):
        txt = '1,000 reasons you are #1'
        r = slugify(txt)
        self.assertEqual(r, '1000-reasons-you-are-1')

    def test_regex_pattern_keep_underscore(self):
        txt = "___This is a test___"
        regex_pattern = r'[^-a-z0-9_]+'
        r = slugify(txt, regex_pattern=regex_pattern)
        self.assertEqual(r, "___this-is-a-test___")

    def test_regex_pattern_keep_underscore_with_underscore_as_separator(self):
        """
        The regex_pattern turns the power to the caller.
        Hence the caller must ensure that a custom separator doesn't clash
        with the regex_pattern.
        """
        txt = "___This is a test___"
        regex_pattern = r'[^-a-z0-9_]+'
        r = slugify(txt, separator='_', regex_pattern=regex_pattern)
        self.assertNotEqual(r, "_this_is_a_test_")

    def test_replacements(self):
        txt = '10 | 20 %'
        r = slugify(txt, replacements=[['|', 'or'], ['%', 'percent']])
        self.assertEqual(r, "10-or-20-percent")

        txt = 'I ♥ 🦄'
        r = slugify(txt, replacements=[['♥', 'amour'], ['🦄', 'licorne']])
        self.assertEqual(r, "i-amour-licorne")

    def test_replacements_german_umlaut_custom(self):
        txt = 'ÜBER Über German Umlaut'
        r = slugify(txt, replacements=[['Ü', 'UE'], ['ü', 'ue']])
        self.assertEqual(r, "ueber-ueber-german-umlaut")


class TestSlugifyUnicode(unittest.TestCase):

    def test_extraneous_seperators(self):

        txt = "This is a test ---"
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "this-is-a-test")

        txt = "___This is a test ---"
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "this-is-a-test")

        txt = "___This is a test___"
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "this-is-a-test")

    def test_non_word_characters(self):
        txt = "This -- is a ## test ---"
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "this-is-a-test")

    def test_phonetic_conversion_of_eastern_scripts(self):
        txt = '影師嗎'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, txt)

    def test_accented_text(self):
        txt = 'C\'est déjà l\'été.'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "c-est-déjà-l-été")

        txt = 'Nín hǎo. Wǒ shì zhōng guó rén'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "nín-hǎo-wǒ-shì-zhōng-guó-rén")

    def test_accented_text_with_non_word_characters(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "jaja-lol-méméméoo-a")

    def test_cyrillic_text(self):
        txt = 'Компьютер'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "компьютер")

    def test_max_length(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=9)
        self.assertEqual(r, "jaja-lol")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=15)
        self.assertEqual(r, "jaja-lol-mémémé")

    def test_max_length_cutoff_not_required(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=50)
        self.assertEqual(r, "jaja-lol-méméméoo-a")

    def test_word_boundary(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=15, word_boundary=True)
        self.assertEqual(r, "jaja-lol-a")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=17, word_boundary=True)
        self.assertEqual(r, "jaja-lol-méméméoo")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=18, word_boundary=True)
        self.assertEqual(r, "jaja-lol-méméméoo")

        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=19, word_boundary=True)
        self.assertEqual(r, "jaja-lol-méméméoo-a")

    def test_custom_separator(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=20, word_boundary=True, separator=".")
        self.assertEqual(r, "jaja.lol.méméméoo.a")

    def test_multi_character_separator(self):
        txt = 'jaja---lol-méméméoo--a'
        r = slugify(txt, allow_unicode=True, max_length=20, word_boundary=True, separator="ZZZZZZ")
        self.assertEqual(r, "jajaZZZZZZlolZZZZZZméméméooZZZZZZa")

    def test_save_order(self):
        txt = 'one two three four five'
        r = slugify(txt, allow_unicode=True, max_length=13, word_boundary=True, save_order=True)
        self.assertEqual(r, "one-two-three")

        txt = 'one two three four five'
        r = slugify(txt, allow_unicode=True, max_length=13, word_boundary=True, save_order=False)
        self.assertEqual(r, "one-two-three")

        txt = 'one two three four five'
        r = slugify(txt, allow_unicode=True, max_length=12, word_boundary=True, save_order=False)
        self.assertEqual(r, "one-two-four")

        txt = 'one two three four five'
        r = slugify(txt, allow_unicode=True, max_length=12, word_boundary=True, save_order=True)
        self.assertEqual(r, "one-two")

    def test_save_order_rtl(self):
        """For right-to-left unicode languages"""
        txt = 'دو سه چهار پنج'
        r = slugify(txt, allow_unicode=True, max_length=10, word_boundary=True, save_order=True)
        self.assertEqual(r, "دو-سه-چهار")

        txt = 'دو سه چهار پنج'
        r = slugify(txt, allow_unicode=True, max_length=10, word_boundary=True, save_order=False)
        self.assertEqual(r, "دو-سه-چهار")

        txt = 'دو سه چهار پنج'
        r = slugify(txt, allow_unicode=True, max_length=9, word_boundary=True, save_order=False)
        self.assertEqual(r, "دو-سه-پنج")

        txt = 'دو سه چهار پنج'
        r = slugify(txt, allow_unicode=True, max_length=9, word_boundary=True, save_order=True)
        self.assertEqual(r, "دو-سه")

    def test_stopword_removal(self):
        txt = 'this has a stopword'
        r = slugify(txt, allow_unicode=True, stopwords=['stopword'])
        self.assertEqual(r, 'this-has-a')

        txt = 'this has a Öländ'
        r = slugify(txt, allow_unicode=True, stopwords=['Öländ'])
        self.assertEqual(r, 'this-has-a')

    def test_stopword_removal_casesensitive(self):
        txt = 'thIs Has a stopword Stopword'
        r = slugify(txt, allow_unicode=True, stopwords=['Stopword'], lowercase=False)
        self.assertEqual(r, 'thIs-Has-a-stopword')

        txt = 'thIs Has a öländ Öländ'
        r = slugify(txt, allow_unicode=True, stopwords=['Öländ'], lowercase=False)
        self.assertEqual(r, 'thIs-Has-a-öländ')

    def test_multiple_stopword_occurances(self):
        txt = 'the quick brown fox jumps over the lazy dog'
        r = slugify(txt, allow_unicode=True, stopwords=['the'])
        self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

    def test_differently_cased_stopword_match(self):
        txt = 'Foo A FOO B foo C'
        r = slugify(txt, allow_unicode=True, stopwords=['foo'])
        self.assertEqual(r, 'a-b-c')

        txt = 'Foo A FOO B foo C'
        r = slugify(txt, allow_unicode=True, stopwords=['FOO'])
        self.assertEqual(r, 'a-b-c')

    def test_multiple_stopwords(self):
        txt = 'the quick brown fox jumps over the lazy dog in a hurry'
        r = slugify(txt, allow_unicode=True, stopwords=['the', 'in', 'a', 'hurry'])
        self.assertEqual(r, 'quick-brown-fox-jumps-over-lazy-dog')

    def test_stopwords_with_different_separator(self):
        txt = 'the quick brown fox jumps over the lazy dog'
        r = slugify(txt, allow_unicode=True, stopwords=['the'], separator=' ')
        self.assertEqual(r, 'quick brown fox jumps over lazy dog')

    def test_html_entities_on(self):
        txt = 'foo &amp; bar'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, 'foo-bar')

    def test_html_entities_off(self):
        txt = 'foo &amp; bår'
        r = slugify(txt, allow_unicode=True, entities=False)
        self.assertEqual(r, 'foo-amp-bår')

    def test_html_decimal_on(self):
        txt = '&#381;'
        r = slugify(txt, allow_unicode=True, decimal=True)
        self.assertEqual(r, 'ž')

    def test_html_decimal_off(self):
        txt = '&#381;'
        r = slugify(txt, allow_unicode=True, entities=False, decimal=False)
        self.assertEqual(r, '381')

    def test_html_hexadecimal_on(self):
        txt = '&#x17D;'
        r = slugify(txt, allow_unicode=True, hexadecimal=True)
        self.assertEqual(r, 'ž')

    def test_html_hexadecimal_off(self):
        txt = '&#x17D;'
        r = slugify(txt, allow_unicode=True, hexadecimal=False)
        self.assertEqual(r, 'x17d')

    def test_starts_with_number(self):
        txt = '10 amazing secrets'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, '10-amazing-secrets')

    def test_contains_numbers(self):
        txt = 'buildings with 1000 windows'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, 'buildings-with-1000-windows')

    def test_ends_with_number(self):
        txt = 'recipe number 3'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, 'recipe-number-3')

    def test_numbers_only(self):
        txt = '404'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, '404')

    def test_numbers_and_symbols(self):
        txt = '1,000 reasons you are #1'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, '1000-reasons-you-are-1')

        txt = '۱,۰۰۰ reasons you are #۱'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, '۱۰۰۰-reasons-you-are-۱')

    def test_regex_pattern_keep_underscore(self):
        """allowing unicode should not overrule the passed regex_pattern"""
        txt = "___This is a test___"
        regex_pattern = r'[^-a-z0-9_]+'
        r = slugify(txt, allow_unicode=True, regex_pattern=regex_pattern)
        self.assertEqual(r, "___this-is-a-test___")

    def test_regex_pattern_keep_underscore_with_underscore_as_separator(self):
        """
        The regex_pattern turns the power to the caller.
        Hence, the caller must ensure that a custom separator doesn't clash
        with the regex_pattern.
        """
        txt = "___This is a test___"
        regex_pattern = r'[^-a-z0-9_]+'
        r = slugify(txt, allow_unicode=True, separator='_', regex_pattern=regex_pattern)
        self.assertNotEqual(r, "_this_is_a_test_")

    def test_replacements(self):
        txt = '10 | 20 %'
        r = slugify(txt, allow_unicode=True, replacements=[['|', 'or'], ['%', 'percent']])
        self.assertEqual(r, "10-or-20-percent")

        txt = 'I ♥ 🦄'
        r = slugify(txt, allow_unicode=True, replacements=[['♥', 'amour'], ['🦄', 'licorne']])
        self.assertEqual(r, "i-amour-licorne")

        txt = 'I ♥ 🦄'
        r = slugify(txt, allow_unicode=True, replacements=[['♥', 'სიყვარული'], ['🦄', 'licorne']])
        self.assertEqual(r, "i-სიყვარული-licorne")

    def test_replacements_german_umlaut_custom(self):
        txt = 'ÜBER Über German Umlaut'
        r = slugify(txt, allow_unicode=True, replacements=[['Ü', 'UE'], ['ü', 'ue']])
        self.assertEqual(r, "ueber-ueber-german-umlaut")

    def test_emojis(self):
        """
        allowing unicode shouldn't allow emojis, even in replacements.
        the only exception is when it is allowed by the regex_pattern. regex_pattern overrules all
        """
        txt = 'i love 🦄'
        r = slugify(txt, allow_unicode=True)
        self.assertEqual(r, "i-love")

        txt = 'i love 🦄'
        r = slugify(txt, allow_unicode=True, decimal=True)
        self.assertEqual(r, "i-love")

        txt = 'i love 🦄'
        r = slugify(txt, allow_unicode=True, hexadecimal=True)
        self.assertEqual(r, "i-love")

        txt = 'i love 🦄'
        r = slugify(txt, allow_unicode=True, entities=True)
        self.assertEqual(r, "i-love")

        txt = 'i love you'
        r = slugify(txt, allow_unicode=True, replacements=[['you', '🦄']])
        self.assertEqual(r, "i-love")

        txt = 'i love 🦄'
        r = slugify(txt, allow_unicode=True, regex_pattern=r'[^🦄]+')
        self.assertEqual(r, "🦄")


class TestUtils(unittest.TestCase):

    def test_smart_truncate_no_max_length(self):
        txt = '1,000 reasons you are #1'
        r = smart_truncate(txt)
        self.assertEqual(r, txt)

    def test_smart_truncate_no_seperator(self):
        txt = '1,000 reasons you are #1'
        r = smart_truncate(txt, max_length=100, separator='_')
        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()
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!114 blocks
openSUSE Build Service is sponsored by