diff --git a/.gitignore b/.gitignore index 96504ea..68bc17f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,160 @@ -MANIFEST +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ dist/ -hashids.egg-info/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.travis.yml b/.travis.yml index cebf520..7d11add 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python python: - - "2.7" - - "3.5" - - "3.6" - "3.7" - "3.8" + - "3.9" + - "3.10" + - "3.11" - "pypy" - "pypy3" script: python -m pytest diff --git a/hashids.py b/hashids.py index 35d20d0..6348401 100644 --- a/hashids.py +++ b/hashids.py @@ -1,26 +1,25 @@ """Implements the hashids algorithm in python. For more information, visit http://hashids.org/""" -import warnings -from functools import wraps -from math import ceil +from __future__ import annotations -__version__ = '1.3.1' +import functools +import math +import warnings +from collections.abc import Callable, Iterable, Iterator +from typing import TypeVar -RATIO_SEPARATORS = 3.5 -RATIO_GUARDS = 12 +from typing_extensions import ParamSpec -try: - StrType = basestring -except NameError: - StrType = str +__version__ = "1.3.1" +_P = ParamSpec("_P") +_T = TypeVar("_T") -def _is_str(candidate): - """Returns whether a value is a string.""" - return isinstance(candidate, StrType) +RATIO_SEPARATORS = 3.5 +RATIO_GUARDS = 12 -def _is_uint(number): +def _is_uint(number: int) -> bool: """Returns whether a value is an unsigned integer.""" try: return number == int(number) and number >= 0 @@ -28,21 +27,21 @@ def _is_uint(number): return False -def _split(string, splitters): +def _split(string: str, splitters: str) -> Iterator[str]: """Splits a string into parts at multiple characters""" - part = '' + part = "" for character in string: if character in splitters: yield part - part = '' + part = "" else: part += character yield part -def _hash(number, alphabet): +def _hash(number: int, alphabet: str) -> str: """Hashes `number` using the given `alphabet` sequence.""" - hashed = '' + hashed = "" len_alphabet = len(alphabet) while True: hashed = alphabet[number % len_alphabet] + hashed @@ -51,7 +50,7 @@ def _hash(number, alphabet): return hashed -def _unhash(hashed, alphabet): +def _unhash(hashed: str, alphabet: str) -> int: """Restores a number tuple from hashed using the given `alphabet` index.""" number = 0 len_alphabet = len(alphabet) @@ -62,30 +61,32 @@ def _unhash(hashed, alphabet): return number -def _reorder(string, salt): +def _reorder(string: str, salt: str) -> str: """Reorders `string` according to `salt`.""" len_salt = len(salt) if len_salt != 0: - string = list(string) + chars = list(string) index, integer_sum = 0, 0 - for i in range(len(string) - 1, 0, -1): + for i in range(len(chars) - 1, 0, -1): integer = ord(salt[index]) integer_sum += integer j = (integer + index + integer_sum) % i - string[i], string[j] = string[j], string[i] + chars[i], chars[j] = chars[j], chars[i] index = (index + 1) % len_salt - string = ''.join(string) + string = "".join(chars) return string -def _index_from_ratio(dividend, divisor): +def _index_from_ratio(dividend: int, divisor: float) -> int: """Returns the ceiled ratio of two numbers as int.""" - return int(ceil(float(dividend) / divisor)) + return int(math.ceil(float(dividend) / divisor)) -def _ensure_length(encoded, min_length, alphabet, guards, values_hash): +def _ensure_length( + encoded: str, min_length: int, alphabet: str, guards: str, values_hash: int +) -> str: """Ensures the minimal hash length""" len_guards = len(guards) guard_index = (values_hash + ord(encoded[0])) % len_guards @@ -102,14 +103,15 @@ def _ensure_length(encoded, min_length, alphabet, guards, values_hash): excess = len(encoded) - min_length if excess > 0: from_index = excess // 2 - encoded = encoded[from_index:from_index+min_length] + encoded = encoded[from_index : from_index + min_length] return encoded -def _encode(values, salt, min_length, alphabet, separators, guards): +def _encode( + values: Iterable[int], salt: str, min_length: int, alphabet: str, separators: str, guards: str +) -> str: """Helper function that does the hash building without argument checks.""" - len_alphabet = len(alphabet) len_separators = len(separators) values_hash = sum(x % (i + 100) for i, x in enumerate(values)) @@ -125,13 +127,15 @@ def _encode(values, salt, min_length, alphabet, separators, guards): encoded = encoded[:-1] # cut off last separator - return (encoded if len(encoded) >= min_length else - _ensure_length(encoded, min_length, alphabet, guards, values_hash)) + return ( + encoded + if len(encoded) >= min_length + else _ensure_length(encoded, min_length, alphabet, guards, values_hash) + ) -def _decode(hashid, salt, alphabet, separators, guards): - """Helper method that restores the values encoded in a hashid without - argument checks.""" +def _decode(hashid: str, salt: str, alphabet: str, separators: str, guards: str) -> Iterator[int]: + """Helper method that restores the values encoded in a hashid without argument checks.""" parts = tuple(_split(hashid, guards)) hashid = parts[1] if 2 <= len(parts) <= 3 else parts[0] @@ -143,30 +147,29 @@ def _decode(hashid, salt, alphabet, separators, guards): hash_parts = _split(hashid, separators) for part in hash_parts: - alphabet_salt = (lottery_char + salt + alphabet)[:len(alphabet)] + alphabet_salt = (lottery_char + salt + alphabet)[: len(alphabet)] alphabet = _reorder(alphabet, alphabet_salt) yield _unhash(part, alphabet) -def _deprecated(func, name): - """A decorator that warns about deprecation when the passed-in function is - invoked.""" - @wraps(func) - def with_warning(*args, **kwargs): - warnings.warn( - ('The %s method is deprecated and will be removed in v2.*.*' % - name), - DeprecationWarning - ) +def _deprecated(func: Callable[_P, _T], name: str) -> Callable[_P, _T]: + """A decorator that warns about deprecation when the passed-in function is invoked.""" + + @functools.wraps(func) + def with_warning(*args: _P.args, **kwargs: _P.kwargs) -> _T: + msg = f"The {name} method is deprecated and will be removed in v2.*.*" + warnings.warn(msg, DeprecationWarning) return func(*args, **kwargs) + return with_warning -class Hashids(object): +class Hashids: """Hashes and restores values using the "hashids" algorithm.""" - ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' - def __init__(self, salt='', min_length=0, alphabet=ALPHABET): + ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + + def __init__(self, salt: str = "", min_length: int = 0, alphabet: str = ALPHABET) -> None: """ Initializes a Hashids object with salt, minimum length, and alphabet. @@ -177,14 +180,14 @@ def __init__(self, salt='', min_length=0, alphabet=ALPHABET): self._min_length = max(int(min_length), 0) self._salt = salt - separators = ''.join(x for x in 'cfhistuCFHISTU' if x in alphabet) - alphabet = ''.join(x for i, x in enumerate(alphabet) - if alphabet.index(x) == i and x not in separators) + separators = "".join(x for x in "cfhistuCFHISTU" if x in alphabet) + alphabet = "".join( + x for i, x in enumerate(alphabet) if alphabet.index(x) == i and x not in separators + ) len_alphabet, len_separators = len(alphabet), len(separators) if len_alphabet + len_separators < 16: - raise ValueError('Alphabet must contain at least 16 ' - 'unique characters.') + raise ValueError("Alphabet must contain at least 16 " "unique characters.") separators = _reorder(separators, salt) @@ -213,61 +216,61 @@ def __init__(self, salt='', min_length=0, alphabet=ALPHABET): self.decrypt = _deprecated(self.decode, "decrypt") self.encrypt = _deprecated(self.encode, "encrypt") - def encode(self, *values): + def encode(self, *values: int) -> str: """Builds a hash from the passed `values`. - :param values The values to transform into a hashid + :param values: The values to transform into a hashid >>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456') >>> hashids.encode(1, 23, 456) '1d6216i30h53elk3' """ if not (values and all(_is_uint(x) for x in values)): - return '' + return "" - return _encode(values, self._salt, self._min_length, self._alphabet, - self._separators, self._guards) + return _encode( + values, self._salt, self._min_length, self._alphabet, self._separators, self._guards + ) - def decode(self, hashid): + def decode(self, hashid: str) -> tuple[int, ...]: """Restore a tuple of numbers from the passed `hashid`. - :param hashid The hashid to decode + :param hashid: The hashid to decode >>> hashids = Hashids('arbitrary salt', 16, 'abcdefghijkl0123456') >>> hashids.decode('1d6216i30h53elk3') (1, 23, 456) """ - if not hashid or not _is_str(hashid): + if not hashid or not isinstance(hashid, str): return () try: - numbers = tuple(_decode(hashid, self._salt, self._alphabet, - self._separators, self._guards)) - + numbers = tuple( + _decode(hashid, self._salt, self._alphabet, self._separators, self._guards) + ) return numbers if hashid == self.encode(*numbers) else () except ValueError: return () - def encode_hex(self, hex_str): + def encode_hex(self, hex_str: str) -> str: """Converts a hexadecimal string (e.g. a MongoDB id) to a hashid. - :param hex_str The hexadecimal string to encodes + :param hex_str: The hexadecimal string to encodes >>> Hashids.encode_hex('507f1f77bcf86cd799439011') 'y42LW46J9luq3Xq9XMly' """ - numbers = (int('1' + hex_str[i:i+12], 16) - for i in range(0, len(hex_str), 12)) + numbers = (int("1" + hex_str[i : i + 12], 16) for i in range(0, len(hex_str), 12)) try: return self.encode(*numbers) except ValueError: - return '' + return "" - def decode_hex(self, hashid): + def decode_hex(self, hashid: str) -> str: """Restores a hexadecimal string (e.g. a MongoDB id) from a hashid. - :param hashid The hashid to decode + :param hashid: The hashid to decode >>> Hashids.decode_hex('y42LW46J9luq3Xq9XMly') '507f1f77bcf86cd799439011' """ - return ''.join(('%x' % x)[1:] for x in self.decode(hashid)) + return "".join(("%x" % x)[1:] for x in self.decode(hashid)) diff --git a/pyproject.toml b/pyproject.toml index d6c7f5d..7a0dd8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,28 +2,28 @@ requires = ["flit_core >=2,<4"] build-backend = "flit_core.buildapi" +[tool.black] +line-length = 100 + [tool.flit.metadata] module = "hashids" author = "David Aurelio" author-email = "dev@david-aurelio.com" home-page = "https://hashids.org/python/" -description-file="README.rst" +description-file = "README.rst" classifiers = [ "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3 :: Only", "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", ] -requires-python = ">=2.7" +requires-python = ">=3.7" [tool.flit.metadata.requires-extra] -test = [ - "pytest >=2.1.0", -] +test = ["pytest >=2.1.0"] [tool.flit.sdist] include = [ @@ -34,4 +34,4 @@ include = [ "README.rst", "test/*.py", ] -exclude = [ ".*" ] +exclude = [".*"] diff --git a/test/test_hashids.py b/test/test_hashids.py index d4ee5ed..108e349 100644 --- a/test/test_hashids.py +++ b/test/test_hashids.py @@ -2,186 +2,202 @@ import pytest -class TestConstructor(object): +class TestConstructor: def test_small_alphabet_with_no_repeating_characters(self): - pytest.raises(ValueError, Hashids, alphabet='abcdefghijklmno') + pytest.raises(ValueError, Hashids, alphabet="abcdefghijklmno") def test_small_alphabet_with_repeating_characters(self): - pytest.raises(ValueError, Hashids, alphabet='abcdecfghijklbmnoa') + pytest.raises(ValueError, Hashids, alphabet="abcdecfghijklbmnoa") -class TestEncoding(object): +class TestEncoding: def test_empty_call(self): - assert Hashids().encode() == '' + assert Hashids().encode() == "" def test_default_salt(self): - assert Hashids().encode(1, 2, 3) == 'o2fXhV' + assert Hashids().encode(1, 2, 3) == "o2fXhV" def test_single_number(self): h = Hashids() - assert h.encode(12345) == 'j0gW' - assert h.encode(1) == 'jR' - assert h.encode(22) == 'Lw' - assert h.encode(333) == 'Z0E' - assert h.encode(9999) == 'w0rR' + assert h.encode(12345) == "j0gW" + assert h.encode(1) == "jR" + assert h.encode(22) == "Lw" + assert h.encode(333) == "Z0E" + assert h.encode(9999) == "w0rR" def test_multiple_numbers(self): h = Hashids() - assert h.encode(683, 94108, 123, 5) == 'vJvi7On9cXGtD' - assert h.encode(1, 2, 3) == 'o2fXhV' - assert h.encode(2, 4, 6) == 'xGhmsW' - assert h.encode(99, 25) == '3lKfD' + assert h.encode(683, 94108, 123, 5) == "vJvi7On9cXGtD" + assert h.encode(1, 2, 3) == "o2fXhV" + assert h.encode(2, 4, 6) == "xGhmsW" + assert h.encode(99, 25) == "3lKfD" def test_salt(self): - h = Hashids(salt='Arbitrary string') - assert h.encode(683, 94108, 123, 5) == 'QWyf8yboH7KT2' - assert h.encode(1, 2, 3) == 'neHrCa' - assert h.encode(2, 4, 6) == 'LRCgf2' - assert h.encode(99, 25) == 'JOMh1' + h = Hashids(salt="Arbitrary string") + assert h.encode(683, 94108, 123, 5) == "QWyf8yboH7KT2" + assert h.encode(1, 2, 3) == "neHrCa" + assert h.encode(2, 4, 6) == "LRCgf2" + assert h.encode(99, 25) == "JOMh1" def test_alphabet(self): - h = Hashids(alphabet='!"#%&\',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~') - assert h.encode(2839, 12, 32, 5) == '_nJUNTVU3' - assert h.encode(1, 2, 3) == '7xfYh2' - assert h.encode(23832) == 'Z6R>' - assert h.encode(99, 25) == 'AYyIB' + h = Hashids( + alphabet="!\"#%&',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~" + ) + assert h.encode(2839, 12, 32, 5) == "_nJUNTVU3" + assert h.encode(1, 2, 3) == "7xfYh2" + assert h.encode(23832) == "Z6R>" + assert h.encode(99, 25) == "AYyIB" def test_short_alphabet(self): - h = Hashids(alphabet='ABcfhistuCFHISTU') - assert h.encode(2839, 12, 32, 5) == 'AABAABBBABAAAuBBAAUABBBBBCBAB' - assert h.encode(1, 2, 3) == 'AAhBAiAA' - assert h.encode(23832) == 'AABAAABABBBAABBB' - assert h.encode(99, 25) == 'AAABBBAAHBBAAB' + h = Hashids(alphabet="ABcfhistuCFHISTU") + assert h.encode(2839, 12, 32, 5) == "AABAABBBABAAAuBBAAUABBBBBCBAB" + assert h.encode(1, 2, 3) == "AAhBAiAA" + assert h.encode(23832) == "AABAAABABBBAABBB" + assert h.encode(99, 25) == "AAABBBAAHBBAAB" def test_min_length(self): h = Hashids(min_length=25) - assert h.encode(7452, 2967, 21401) == 'pO3K69b86jzc6krI416enr2B5' - assert h.encode(1, 2, 3) == 'gyOwl4B97bo2fXhVaDR0Znjrq' - assert h.encode(6097) == 'Nz7x3VXyMYerRmWeOBQn6LlRG' - assert h.encode(99, 25) == 'k91nqP3RBe3lKfDaLJrvy8XjV' + assert h.encode(7452, 2967, 21401) == "pO3K69b86jzc6krI416enr2B5" + assert h.encode(1, 2, 3) == "gyOwl4B97bo2fXhVaDR0Znjrq" + assert h.encode(6097) == "Nz7x3VXyMYerRmWeOBQn6LlRG" + assert h.encode(99, 25) == "k91nqP3RBe3lKfDaLJrvy8XjV" def test_all_parameters(self): - h = Hashids('arbitrary salt', 16, 'abcdefghijklmnopqrstuvwxyz') - assert h.encode(7452, 2967, 21401) == 'wygqxeunkatjgkrw' - assert h.encode(1, 2, 3) == 'pnovxlaxuriowydb' - assert h.encode(60125) == 'jkbgxljrjxmlaonp' - assert h.encode(99, 25) == 'erdjpwrgouoxlvbx' + h = Hashids("arbitrary salt", 16, "abcdefghijklmnopqrstuvwxyz") + assert h.encode(7452, 2967, 21401) == "wygqxeunkatjgkrw" + assert h.encode(1, 2, 3) == "pnovxlaxuriowydb" + assert h.encode(60125) == "jkbgxljrjxmlaonp" + assert h.encode(99, 25) == "erdjpwrgouoxlvbx" def test_alphabet_without_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890') - assert h.encode(7452, 2967, 21401) == 'X50Yg6VPoAO4' - assert h.encode(1, 2, 3) == 'GAbDdR' - assert h.encode(60125) == '5NMPD' - assert h.encode(99, 25) == 'yGya5' + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890") + assert h.encode(7452, 2967, 21401) == "X50Yg6VPoAO4" + assert h.encode(1, 2, 3) == "GAbDdR" + assert h.encode(60125) == "5NMPD" + assert h.encode(99, 25) == "yGya5" def test_alphabet_with_two_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC') - assert h.encode(7452, 2967, 21401) == 'GJNNmKYzbPBw' - assert h.encode(1, 2, 3) == 'DQCXa4' - assert h.encode(60125) == '38V1D' - assert h.encode(99, 25) == '373az' + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC") + assert h.encode(7452, 2967, 21401) == "GJNNmKYzbPBw" + assert h.encode(1, 2, 3) == "DQCXa4" + assert h.encode(60125) == "38V1D" + assert h.encode(99, 25) == "373az" def test_negative_call(self): - assert Hashids().encode(1, -2, 3) == '' + assert Hashids().encode(1, -2, 3) == "" def test_float_call(self): - assert Hashids().encode(1, 2.5, 3) == '' + assert Hashids().encode(1, 2.5, 3) == "" # type: ignore def test_encode_hex(self): - assert Hashids().encode_hex('507f1f77bcf86cd799439011') == 'y42LW46J9luq3Xq9XMly' - assert len(Hashids(min_length=1000).encode_hex('507f1f77bcf86cd799439011')) >= 1000 - assert Hashids().encode_hex('f000000000000000000000000000000000000000000000000000000000000000000000000000000000000f') == \ - 'WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVzn4zQlqt1WK8jKq7OsEpy2qyw1Vi2p' + assert Hashids().encode_hex("507f1f77bcf86cd799439011") == "y42LW46J9luq3Xq9XMly" + assert len(Hashids(min_length=1000).encode_hex("507f1f77bcf86cd799439011")) >= 1000 + assert ( + Hashids().encode_hex( + "f000000000000000000000000000000000000000000000000000000000000000000000000000000000000f" + ) + == "WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVzn4zQlqt1WK8jKq7OsEpy2qyw1Vi2p" + ) def test_illegal_hex(self): - assert Hashids().encode_hex('') == '' - assert Hashids().encode_hex('1234SGT8') == '' + assert Hashids().encode_hex("") == "" + assert Hashids().encode_hex("1234SGT8") == "" -class TestDecoding(object): + +class TestDecoding: def test_empty_string(self): - assert Hashids().decode('') == () + assert Hashids().decode("") == () def test_non_string(self): - assert Hashids().decode(object()) == () + assert Hashids().decode(object()) == () # type: ignore def test_default_salt(self): - assert Hashids().decode('o2fXhV') == (1, 2, 3) + assert Hashids().decode("o2fXhV") == (1, 2, 3) def test_empty_call(self): - assert Hashids().decode('') == () + assert Hashids().decode("") == () def test_single_number(self): h = Hashids() - assert h.decode('j0gW') == (12345,) - assert h.decode('jR') == (1,) - assert h.decode('Lw') == (22,) - assert h.decode('Z0E') == (333,) - assert h.decode('w0rR') == (9999,) + assert h.decode("j0gW") == (12345,) + assert h.decode("jR") == (1,) + assert h.decode("Lw") == (22,) + assert h.decode("Z0E") == (333,) + assert h.decode("w0rR") == (9999,) def test_multiple_numbers(self): h = Hashids() - assert h.decode('vJvi7On9cXGtD') == (683, 94108, 123, 5) - assert h.decode('o2fXhV') == (1, 2, 3) - assert h.decode('xGhmsW') == (2, 4, 6) - assert h.decode('3lKfD') == (99, 25) + assert h.decode("vJvi7On9cXGtD") == (683, 94108, 123, 5) + assert h.decode("o2fXhV") == (1, 2, 3) + assert h.decode("xGhmsW") == (2, 4, 6) + assert h.decode("3lKfD") == (99, 25) def test_salt(self): - h = Hashids(salt='Arbitrary string') - assert h.decode('QWyf8yboH7KT2') == (683, 94108, 123, 5) - assert h.decode('neHrCa') == (1, 2, 3) - assert h.decode('LRCgf2') == (2, 4, 6) - assert h.decode('JOMh1') == (99, 25) + h = Hashids(salt="Arbitrary string") + assert h.decode("QWyf8yboH7KT2") == (683, 94108, 123, 5) + assert h.decode("neHrCa") == (1, 2, 3) + assert h.decode("LRCgf2") == (2, 4, 6) + assert h.decode("JOMh1") == (99, 25) def test_alphabet(self): - h = Hashids(alphabet='!"#%&\',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~') - assert h.decode('_nJUNTVU3') == (2839, 12, 32, 5) - assert h.decode('7xfYh2') == (1, 2, 3) - assert h.decode('Z6R>') == (23832,) - assert h.decode('AYyIB') == (99, 25) + h = Hashids( + alphabet="!\"#%&',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~" + ) + assert h.decode("_nJUNTVU3") == (2839, 12, 32, 5) + assert h.decode("7xfYh2") == (1, 2, 3) + assert h.decode("Z6R>") == (23832,) + assert h.decode("AYyIB") == (99, 25) def test_min_length(self): h = Hashids(min_length=25) - assert h.decode('pO3K69b86jzc6krI416enr2B5') == (7452, 2967, 21401) - assert h.decode('gyOwl4B97bo2fXhVaDR0Znjrq') == (1, 2, 3) - assert h.decode('Nz7x3VXyMYerRmWeOBQn6LlRG') == (6097,) - assert h.decode('k91nqP3RBe3lKfDaLJrvy8XjV') == (99, 25) + assert h.decode("pO3K69b86jzc6krI416enr2B5") == (7452, 2967, 21401) + assert h.decode("gyOwl4B97bo2fXhVaDR0Znjrq") == (1, 2, 3) + assert h.decode("Nz7x3VXyMYerRmWeOBQn6LlRG") == (6097,) + assert h.decode("k91nqP3RBe3lKfDaLJrvy8XjV") == (99, 25) def test_all_parameters(self): - h = Hashids('arbitrary salt', 16, 'abcdefghijklmnopqrstuvwxyz') - assert h.decode('wygqxeunkatjgkrw') == (7452, 2967, 21401) - assert h.decode('pnovxlaxuriowydb') == (1, 2, 3) - assert h.decode('jkbgxljrjxmlaonp') == (60125,) - assert h.decode('erdjpwrgouoxlvbx') == (99, 25) + h = Hashids("arbitrary salt", 16, "abcdefghijklmnopqrstuvwxyz") + assert h.decode("wygqxeunkatjgkrw") == (7452, 2967, 21401) + assert h.decode("pnovxlaxuriowydb") == (1, 2, 3) + assert h.decode("jkbgxljrjxmlaonp") == (60125,) + assert h.decode("erdjpwrgouoxlvbx") == (99, 25) def test_invalid_hash(self): - assert Hashids(alphabet='abcdefghijklmnop').decode('qrstuvwxyz') == () + assert Hashids(alphabet="abcdefghijklmnop").decode("qrstuvwxyz") == () def test_alphabet_without_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890') - assert h.decode('X50Yg6VPoAO4') == (7452, 2967, 21401) - assert h.decode('GAbDdR') == (1, 2, 3) - assert h.decode('5NMPD') == (60125,) - assert h.decode('yGya5') == (99, 25) + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890") + assert h.decode("X50Yg6VPoAO4") == (7452, 2967, 21401) + assert h.decode("GAbDdR") == (1, 2, 3) + assert h.decode("5NMPD") == (60125,) + assert h.decode("yGya5") == (99, 25) def test_alphabet_with_two_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC') - assert h.decode('GJNNmKYzbPBw') == (7452, 2967, 21401) - assert h.decode('DQCXa4') == (1, 2, 3) - assert h.decode('38V1D') == (60125,) - assert h.decode('373az') == (99, 25) + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC") + assert h.decode("GJNNmKYzbPBw") == (7452, 2967, 21401) + assert h.decode("DQCXa4") == (1, 2, 3) + assert h.decode("38V1D") == (60125,) + assert h.decode("373az") == (99, 25) def test_only_one_valid(self): h = Hashids(min_length=6) - assert h.decode(h.encode(1)[:-1] + '0') == () + assert h.decode(h.encode(1)[:-1] + "0") == () def test_decode_hex(self): - hex_str = '507f1f77bcf86cd799439011' - assert Hashids().decode_hex('y42LW46J9luq3Xq9XMly') == hex_str + hex_str = "507f1f77bcf86cd799439011" + assert Hashids().decode_hex("y42LW46J9luq3Xq9XMly") == hex_str h = Hashids(min_length=1000) assert h.decode_hex(h.encode_hex(hex_str)) == hex_str - assert Hashids().decode_hex('WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVzn4zQlqt1WK8jKq7OsEpy2qyw1Vi2p') == \ - 'f000000000000000000000000000000000000000000000000000000000000000000000000000000000000f' + assert ( + Hashids().decode_hex( + "WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVzn4zQlqt1WK8jKq7OsEpy2qyw1Vi2p" + ) + == "f000000000000000000000000000000000000000000000000000000000000000000000000000000000000f" + ) def test_illegal_decode_hex(self): - assert Hashids().decode_hex('') == '' - assert Hashids().decode_hex('WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVlqt1WK8jKq7OsEp1Vi2p') == '' + assert Hashids().decode_hex("") == "" + assert ( + Hashids().decode_hex("WxMLpERDrmh25Lp4L3xEfM6WovWYO3IjkRMKR2ogCMVlqt1WK8jKq7OsEp1Vi2p") + == "" + ) diff --git a/test/test_legacy.py b/test/test_legacy.py index 532c42c..39b6187 100644 --- a/test/test_legacy.py +++ b/test/test_legacy.py @@ -1,154 +1,160 @@ from hashids import Hashids import pytest -class TestConstructor(object): + +class TestConstructor: def test_small_alphabet(self): - pytest.raises(ValueError, Hashids, alphabet='abcabc') + pytest.raises(ValueError, Hashids, alphabet="abcabc") -class TestEncryption(object): +class TestEncryption: def test_empty_call(self): - assert Hashids().encrypt() == '' + assert Hashids().encrypt() == "" def test_default_salt(self): - assert Hashids().encrypt(1, 2, 3) == 'o2fXhV' + assert Hashids().encrypt(1, 2, 3) == "o2fXhV" def test_single_number(self): h = Hashids() - assert h.encrypt(12345) == 'j0gW' - assert h.encrypt(1) == 'jR' - assert h.encrypt(22) == 'Lw' - assert h.encrypt(333) == 'Z0E' - assert h.encrypt(9999) == 'w0rR' + assert h.encrypt(12345) == "j0gW" + assert h.encrypt(1) == "jR" + assert h.encrypt(22) == "Lw" + assert h.encrypt(333) == "Z0E" + assert h.encrypt(9999) == "w0rR" def test_multiple_numbers(self): h = Hashids() - assert h.encrypt(683, 94108, 123, 5) == 'vJvi7On9cXGtD' - assert h.encrypt(1, 2, 3) == 'o2fXhV' - assert h.encrypt(2, 4, 6) == 'xGhmsW' - assert h.encrypt(99, 25) == '3lKfD' + assert h.encrypt(683, 94108, 123, 5) == "vJvi7On9cXGtD" + assert h.encrypt(1, 2, 3) == "o2fXhV" + assert h.encrypt(2, 4, 6) == "xGhmsW" + assert h.encrypt(99, 25) == "3lKfD" def test_salt(self): - h = Hashids(salt='Arbitrary string') - assert h.encrypt(683, 94108, 123, 5) == 'QWyf8yboH7KT2' - assert h.encrypt(1, 2, 3) == 'neHrCa' - assert h.encrypt(2, 4, 6) == 'LRCgf2' - assert h.encrypt(99, 25) == 'JOMh1' + h = Hashids(salt="Arbitrary string") + assert h.encrypt(683, 94108, 123, 5) == "QWyf8yboH7KT2" + assert h.encrypt(1, 2, 3) == "neHrCa" + assert h.encrypt(2, 4, 6) == "LRCgf2" + assert h.encrypt(99, 25) == "JOMh1" def test_alphabet(self): - h = Hashids(alphabet='!"#%&\',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~') - assert h.encrypt(2839, 12, 32, 5) == '_nJUNTVU3' - assert h.encrypt(1, 2, 3) == '7xfYh2' - assert h.encrypt(23832) == 'Z6R>' - assert h.encrypt(99, 25) == 'AYyIB' + h = Hashids( + alphabet="!\"#%&',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~" + ) + assert h.encrypt(2839, 12, 32, 5) == "_nJUNTVU3" + assert h.encrypt(1, 2, 3) == "7xfYh2" + assert h.encrypt(23832) == "Z6R>" + assert h.encrypt(99, 25) == "AYyIB" def test_min_length(self): h = Hashids(min_length=25) - assert h.encrypt(7452, 2967, 21401) == 'pO3K69b86jzc6krI416enr2B5' - assert h.encrypt(1, 2, 3) == 'gyOwl4B97bo2fXhVaDR0Znjrq' - assert h.encrypt(6097) == 'Nz7x3VXyMYerRmWeOBQn6LlRG' - assert h.encrypt(99, 25) == 'k91nqP3RBe3lKfDaLJrvy8XjV' + assert h.encrypt(7452, 2967, 21401) == "pO3K69b86jzc6krI416enr2B5" + assert h.encrypt(1, 2, 3) == "gyOwl4B97bo2fXhVaDR0Znjrq" + assert h.encrypt(6097) == "Nz7x3VXyMYerRmWeOBQn6LlRG" + assert h.encrypt(99, 25) == "k91nqP3RBe3lKfDaLJrvy8XjV" def test_all_parameters(self): - h = Hashids('arbitrary salt', 16, 'abcdefghijklmnopqrstuvwxyz') - assert h.encrypt(7452, 2967, 21401) == 'wygqxeunkatjgkrw' - assert h.encrypt(1, 2, 3) == 'pnovxlaxuriowydb' - assert h.encrypt(60125) == 'jkbgxljrjxmlaonp' - assert h.encrypt(99, 25) == 'erdjpwrgouoxlvbx' + h = Hashids("arbitrary salt", 16, "abcdefghijklmnopqrstuvwxyz") + assert h.encrypt(7452, 2967, 21401) == "wygqxeunkatjgkrw" + assert h.encrypt(1, 2, 3) == "pnovxlaxuriowydb" + assert h.encrypt(60125) == "jkbgxljrjxmlaonp" + assert h.encrypt(99, 25) == "erdjpwrgouoxlvbx" def test_alphabet_without_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890') - assert h.encrypt(7452, 2967, 21401) == 'X50Yg6VPoAO4' - assert h.encrypt(1, 2, 3) == 'GAbDdR' - assert h.encrypt(60125) == '5NMPD' - assert h.encrypt(99, 25) == 'yGya5' + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890") + assert h.encrypt(7452, 2967, 21401) == "X50Yg6VPoAO4" + assert h.encrypt(1, 2, 3) == "GAbDdR" + assert h.encrypt(60125) == "5NMPD" + assert h.encrypt(99, 25) == "yGya5" def test_alphabet_with_two_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC') - assert h.encrypt(7452, 2967, 21401) == 'GJNNmKYzbPBw' - assert h.encrypt(1, 2, 3) == 'DQCXa4' - assert h.encrypt(60125) == '38V1D' - assert h.encrypt(99, 25) == '373az' + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC") + assert h.encrypt(7452, 2967, 21401) == "GJNNmKYzbPBw" + assert h.encrypt(1, 2, 3) == "DQCXa4" + assert h.encrypt(60125) == "38V1D" + assert h.encrypt(99, 25) == "373az" def test_negative_call(self): - assert Hashids().encrypt(1, -2, 3) == '' + assert Hashids().encrypt(1, -2, 3) == "" def test_float_call(self): - assert Hashids().encrypt(1, 2.5, 3) == '' + assert Hashids().encrypt(1, 2.5, 3) == "" # type: ignore + -class TestDecryption(object): +class TestDecryption: def test_empty_string(self): - assert Hashids().decrypt('') == () + assert Hashids().decrypt("") == () def test_non_string(self): - assert Hashids().decrypt(object()) == () + assert Hashids().decrypt(object()) == () # type: ignore def test_default_salt(self): - assert Hashids().decrypt('o2fXhV') == (1, 2, 3) + assert Hashids().decrypt("o2fXhV") == (1, 2, 3) def test_empty_call(self): - assert Hashids().decrypt('') == () + assert Hashids().decrypt("") == () def test_single_number(self): h = Hashids() - assert h.decrypt('j0gW') == (12345,) - assert h.decrypt('jR') == (1,) - assert h.decrypt('Lw') == (22,) - assert h.decrypt('Z0E') == (333,) - assert h.decrypt('w0rR') == (9999,) + assert h.decrypt("j0gW") == (12345,) + assert h.decrypt("jR") == (1,) + assert h.decrypt("Lw") == (22,) + assert h.decrypt("Z0E") == (333,) + assert h.decrypt("w0rR") == (9999,) def test_multiple_numbers(self): h = Hashids() - assert h.decrypt('vJvi7On9cXGtD') == (683, 94108, 123, 5,) - assert h.decrypt('o2fXhV') == (1, 2, 3,) - assert h.decrypt('xGhmsW') == (2, 4, 6,) - assert h.decrypt('3lKfD') == (99, 25,) + assert h.decrypt("vJvi7On9cXGtD") == (683, 94108, 123, 5) + assert h.decrypt("o2fXhV") == (1, 2, 3) + assert h.decrypt("xGhmsW") == (2, 4, 6) + assert h.decrypt("3lKfD") == (99, 25) def test_salt(self): - h = Hashids(salt='Arbitrary string') - assert h.decrypt('QWyf8yboH7KT2') == (683, 94108, 123, 5,) - assert h.decrypt('neHrCa') == (1, 2, 3,) - assert h.decrypt('LRCgf2') == (2, 4, 6,) - assert h.decrypt('JOMh1') == (99, 25,) + h = Hashids(salt="Arbitrary string") + assert h.decrypt("QWyf8yboH7KT2") == (683, 94108, 123, 5) + assert h.decrypt("neHrCa") == (1, 2, 3) + assert h.decrypt("LRCgf2") == (2, 4, 6) + assert h.decrypt("JOMh1") == (99, 25) def test_alphabet(self): - h = Hashids(alphabet='!"#%&\',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~') - assert h.decrypt('_nJUNTVU3') == (2839, 12, 32, 5,) - assert h.decrypt('7xfYh2') == (1, 2, 3,) - assert h.decrypt('Z6R>') == (23832,) - assert h.decrypt('AYyIB') == (99, 25,) + h = Hashids( + alphabet="!\"#%&',-/0123456789:;<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz~" + ) + assert h.decrypt("_nJUNTVU3") == (2839, 12, 32, 5) + assert h.decrypt("7xfYh2") == (1, 2, 3) + assert h.decrypt("Z6R>") == (23832,) + assert h.decrypt("AYyIB") == (99, 25) def test_min_length(self): h = Hashids(min_length=25) - assert h.decrypt('pO3K69b86jzc6krI416enr2B5') == (7452, 2967, 21401,) - assert h.decrypt('gyOwl4B97bo2fXhVaDR0Znjrq') == (1, 2, 3,) - assert h.decrypt('Nz7x3VXyMYerRmWeOBQn6LlRG') == (6097,) - assert h.decrypt('k91nqP3RBe3lKfDaLJrvy8XjV') == (99, 25,) + assert h.decrypt("pO3K69b86jzc6krI416enr2B5") == (7452, 2967, 21401) + assert h.decrypt("gyOwl4B97bo2fXhVaDR0Znjrq") == (1, 2, 3) + assert h.decrypt("Nz7x3VXyMYerRmWeOBQn6LlRG") == (6097,) + assert h.decrypt("k91nqP3RBe3lKfDaLJrvy8XjV") == (99, 25) def test_all_parameters(self): - h = Hashids('arbitrary salt', 16, 'abcdefghijklmnopqrstuvwxyz') - assert h.decrypt('wygqxeunkatjgkrw') == (7452, 2967, 21401,) - assert h.decrypt('pnovxlaxuriowydb') == (1, 2, 3,) - assert h.decrypt('jkbgxljrjxmlaonp') == (60125,) - assert h.decrypt('erdjpwrgouoxlvbx') == (99, 25,) + h = Hashids("arbitrary salt", 16, "abcdefghijklmnopqrstuvwxyz") + assert h.decrypt("wygqxeunkatjgkrw") == (7452, 2967, 21401) + assert h.decrypt("pnovxlaxuriowydb") == (1, 2, 3) + assert h.decrypt("jkbgxljrjxmlaonp") == (60125,) + assert h.decrypt("erdjpwrgouoxlvbx") == (99, 25) def test_invalid_hash(self): - assert Hashids(alphabet='abcdefghijklmnop').decrypt('qrstuvwxyz') == () + assert Hashids(alphabet="abcdefghijklmnop").decrypt("qrstuvwxyz") == () def test_alphabet_without_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890') - assert h.decrypt('X50Yg6VPoAO4') == (7452, 2967, 21401) - assert h.decrypt('GAbDdR') == (1, 2, 3) - assert h.decrypt('5NMPD') == (60125,) - assert h.decrypt('yGya5') == (99, 25) + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890") + assert h.decrypt("X50Yg6VPoAO4") == (7452, 2967, 21401) + assert h.decrypt("GAbDdR") == (1, 2, 3) + assert h.decrypt("5NMPD") == (60125,) + assert h.decrypt("yGya5") == (99, 25) def test_alphabet_with_two_standard_separators(self): - h = Hashids(alphabet='abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC') - assert h.decrypt('GJNNmKYzbPBw') == (7452, 2967, 21401) - assert h.decrypt('DQCXa4') == (1, 2, 3) - assert h.decrypt('38V1D') == (60125,) - assert h.decrypt('373az') == (99, 25) + h = Hashids(alphabet="abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890uC") + assert h.decrypt("GJNNmKYzbPBw") == (7452, 2967, 21401) + assert h.decrypt("DQCXa4") == (1, 2, 3) + assert h.decrypt("38V1D") == (60125,) + assert h.decrypt("373az") == (99, 25) def test_only_one_valid(self): h = Hashids(min_length=6) - assert h.decrypt(h.encrypt(1)[:-1] + '0') == () + assert h.decrypt(h.encrypt(1)[:-1] + "0") == ()