summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2016-12-19 13:03:29 -0500
committerEli Collins <elic@assurancetechnologies.com>2016-12-19 13:03:29 -0500
commit632f66738d070a815d2628ab92b5b9ad9fa33942 (patch)
tree0eb6f8c10407231e4bdd122c6e1eb79a9d13e68c
parent2b4b86a81a5b9845a92fab89e804d428a3d6da99 (diff)
parent11c6c6d9caa18ef792ee586afb02ae566f528e61 (diff)
downloadpasslib-632f66738d070a815d2628ab92b5b9ad9fa33942.tar.gz
Merge from stable
-rwxr-xr-xadmin/upload.sh2
-rw-r--r--docs/history/1.7.rst10
-rw-r--r--docs/install.rst2
-rw-r--r--passlib/_setup/docdist.py87
-rw-r--r--passlib/_setup/stamp.py116
-rw-r--r--passlib/pwd.py64
-rw-r--r--passlib/tests/test_pwd.py8
-rw-r--r--passlib/tests/utils.py19
-rw-r--r--setup.py204
9 files changed, 273 insertions, 239 deletions
diff --git a/admin/upload.sh b/admin/upload.sh
index 6d1dfda..5aaa053 100755
--- a/admin/upload.sh
+++ b/admin/upload.sh
@@ -10,7 +10,7 @@ SEP2="-----------------------------------------------------"
# init config
#
-export PASSLIB_SETUP_TAG_RELEASE=no
+export SETUP_TAG_RELEASE=no
if [ -z "$DRY_RUN" ]; then
echo "DRY_RUN not set"
diff --git a/docs/history/1.7.rst b/docs/history/1.7.rst
index 27ab820..449ee56 100644
--- a/docs/history/1.7.rst
+++ b/docs/history/1.7.rst
@@ -11,6 +11,8 @@ Passlib 1.7
keywords. This usage was deprecated in 1.7.0, but warning wasn't properly enabled.
See :ref:`hash-configuring` for the preferred way to pass settings.
+* bugfix: setup.py: prevent erroneous version strings when run from an sdist.
+
.. rst-class:: emphasize-children toc-always-open
**1.7.0** (2016-11-22)
@@ -177,7 +179,8 @@ Deprecations
As part of a long-range plan to restructure and simplify both the API and the internals of Passlib,
a number of methods have been deprecated & replaced. The eventually goal is a large cleanup
and overhaul as part of Passlib 2.0. There will be at least one more 1.x version
-before Passlib 2.0, to provide a final transitional release.
+before Passlib 2.0, to provide a final transitional release
+(see the `Passlib Roadmap <https://bitbucket.org/ecollins/passlib/wiki/Roadmap>`_).
Password Hash API Deprecations
..............................
@@ -203,11 +206,12 @@ Password Hash API Deprecations
To provide settings such as ``rounds`` and ``salt_size``, callers
should use the new :meth:`PasswordHash.using`
method, which generates a new hasher with a customized configuration.
+ For example, instead of::
- >>> # for example, instead of this:
>>> sha256_crypt.encrypt("secret", rounds=12345)
- >>> # callers should now use:
+ ... applications should now use::
+
>>> sha256_crypt.using(rounds=12345).hash("secret")
Support for the old syntax will be removed in Passlib 2.0.
diff --git a/docs/install.rst b/docs/install.rst
index 2212383..2c4ec6a 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -144,5 +144,3 @@ you will need to:
4. From the Passlib source directory, run :samp:`python setup.py build_sphinx`.
5. Once Sphinx completes its run, point a web browser to the file at :samp:`{SOURCE}/build/sphinx/html/index.html`
to access the Passlib documentation in html format.
-6. Alternately, steps 4 & 5 can be replaced by running :samp:`python setup.py docdist`,
- which will build a zip file of the documentation in :samp:`{SOURCE}/dist`.
diff --git a/passlib/_setup/docdist.py b/passlib/_setup/docdist.py
deleted file mode 100644
index 19c4dc1..0000000
--- a/passlib/_setup/docdist.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""custom command to build doc.zip file"""
-#=============================================================================
-# imports
-#=============================================================================
-# core
-import os
-from distutils import dir_util
-from distutils.cmd import Command
-from distutils.errors import *
-from distutils.spawn import spawn
-# local
-__all__ = [
- "docdist"
-]
-#=============================================================================
-# command
-#=============================================================================
-class docdist(Command):
-
- description = "create zip file containing standalone html docs"
-
- user_options = [
- ('build-dir=', None, 'Build directory'),
- ('dist-dir=', 'd',
- "directory to put the source distribution archive(s) in "
- "[default: dist]"),
- ('format=', 'f',
- "archive format to create (tar, ztar, gztar, zip)"),
- ('sign', 's', 'sign files using gpg'),
- ('identity=', 'i', 'GPG identity used to sign files'),
- ]
-
- def initialize_options(self):
- self.build_dir = None
- self.dist_dir = None
- self.format = None
- self.keep_temp = False
- self.sign = False
- self.identity = None
-
- def finalize_options(self):
- if self.identity and not self.sign:
- raise DistutilsOptionError(
- "Must use --sign for --identity to have meaning"
- )
- if self.build_dir is None:
- cmd = self.get_finalized_command('build')
- self.build_dir = os.path.join(cmd.build_base, 'docdist')
- if not self.dist_dir:
- self.dist_dir = "dist"
- if not self.format:
- self.format = "zip"
-
- def run(self):
- # call build sphinx to build docs
- self.run_command("build_sphinx")
- cmd = self.get_finalized_command("build_sphinx")
- source_dir = cmd.builder_target_dir
-
- # copy to directory with appropriate name
- dist = self.distribution
- arc_name = "%s-docs-%s" % (dist.get_name(), dist.get_version())
- tmp_dir = os.path.join(self.build_dir, arc_name)
- if os.path.exists(tmp_dir):
- dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
- self.copy_tree(source_dir, tmp_dir, preserve_symlinks=True)
-
- # make archive from dir
- arc_base = os.path.join(self.dist_dir, arc_name)
- self.arc_filename = self.make_archive(arc_base, self.format,
- self.build_dir)
-
- # Sign if requested
- if self.sign:
- gpg_args = ["gpg", "--detach-sign", "-a", self.arc_filename]
- if self.identity:
- gpg_args[2:2] = ["--local-user", self.identity]
- spawn(gpg_args,
- dry_run=self.dry_run)
-
- # cleanup
- if not self.keep_temp:
- dir_util.remove_tree(tmp_dir, dry_run=self.dry_run)
-
-#=============================================================================
-# eof
-#=============================================================================
diff --git a/passlib/_setup/stamp.py b/passlib/_setup/stamp.py
index d5e559f..2ce3eb3 100644
--- a/passlib/_setup/stamp.py
+++ b/passlib/_setup/stamp.py
@@ -2,16 +2,20 @@
#=============================================================================
# imports
#=============================================================================
-from __future__ import with_statement
+from __future__ import absolute_import, division, print_function
# core
+from distutils.dist import Distribution
import os
import re
-from distutils.dist import Distribution
+import subprocess
+import time
# pkg
# local
__all__ = [
"stamp_source",
"stamp_distutils_output",
+ "append_hg_revision",
+ "as_bool",
]
#=============================================================================
# helpers
@@ -19,19 +23,59 @@ __all__ = [
def get_command_class(opts, name):
return opts['cmdclass'].get(name) or Distribution().get_command_class(name)
+def get_command_options(opts, command):
+ return opts.setdefault("command_options", {}).setdefault(command, {})
+
+def set_command_options(opts, command, _source_="setup.py", **kwds):
+ target = get_command_options(opts, command)
+ target.update(
+ (key, (_source_, value))
+ for key, value in kwds.items()
+ )
+
+def _get_file(path):
+ with open(path, "r") as fh:
+ return fh.read()
+
+
+def _replace_file(path, content, dry_run=False):
+ if dry_run:
+ return
+ if os.path.exists(path):
+ # sdist likes to use hardlinks, have to remove them first,
+ # or we modify *source* file
+ os.unlink(path)
+ with open(path, "w") as fh:
+ fh.write(content)
+
+
def stamp_source(base_dir, version, dry_run=False):
- """update version string in passlib dist"""
+ """
+ update version info in passlib source
+ """
+ #
+ # update version string in toplevel package source
+ #
path = os.path.join(base_dir, "passlib", "__init__.py")
- with open(path) as fh:
- input = fh.read()
- output, count = re.subn('(?m)^__version__\s*=.*$',
+ content = _get_file(path)
+ content, count = re.subn('(?m)^__version__\s*=.*$',
'__version__ = ' + repr(version),
- input)
+ content)
assert count == 1, "failed to replace version string"
- if not dry_run:
- os.unlink(path) # sdist likes to use hardlinks
- with open(path, "w") as fh:
- fh.write(output)
+ _replace_file(path, content, dry_run=dry_run)
+
+ #
+ # update flag in setup.py
+ # (not present when called from bdist_wheel, etc)
+ #
+ path = os.path.join(base_dir, "setup.py")
+ if os.path.exists(path):
+ content = _get_file(path)
+ content, count = re.subn('(?m)^stamp_build\s*=.*$',
+ 'stamp_build = False', content)
+ assert count == 1, "failed to update 'stamp_build' flag"
+ _replace_file(path, content, dry_run=dry_run)
+
def stamp_distutils_output(opts, version):
@@ -51,6 +95,56 @@ def stamp_distutils_output(opts, version):
stamp_source(base_dir, version, self.dry_run)
opts['cmdclass']['sdist'] = sdist
+
+def as_bool(value):
+ return (value or "").lower() in "yes y true t 1".split()
+
+
+def append_hg_revision(version):
+
+ # call HG via subprocess
+ # NOTE: for py26 compat, using Popen() instead of check_output()
+ try:
+ proc = subprocess.Popen(["hg", "tip", "--template", "{date(date, '%Y%m%d%H%M%S')}+hg.{node|short}"],
+ stdout=subprocess.PIPE)
+ stamp, _ = proc.communicate()
+ if proc.returncode:
+ raise subprocess.CalledProcessError(1, [])
+ stamp = stamp.decode("ascii")
+ except (OSError, subprocess.CalledProcessError):
+ # fallback - just use build date
+ stamp = time.strftime("%Y%m%d%H%M%S")
+
+ # modify version
+ if version.endswith((".dev0", ".post0")):
+ version = version[:-1] + stamp
+ else:
+ version += ".post" + stamp
+
+ return version
+
+def install_build_py_exclude(opts):
+
+ _build_py = get_command_class(opts, "build_py")
+
+ class build_py(_build_py):
+
+ user_options = _build_py.user_options + [
+ ("exclude-packages=", None,
+ "exclude packages from builds"),
+ ]
+
+ exclude_packages = None
+
+ def finalize_options(self):
+ _build_py.finalize_options(self)
+ target = self.packages
+ for package in self.exclude_packages or []:
+ if package in target:
+ target.remove(package)
+
+ opts['cmdclass']['build_py'] = build_py
+
#=============================================================================
# eof
#=============================================================================
diff --git a/passlib/pwd.py b/passlib/pwd.py
index 274666a..52e1e64 100644
--- a/passlib/pwd.py
+++ b/passlib/pwd.py
@@ -26,6 +26,7 @@ __all__ = [
# constants
#=============================================================================
+# XXX: rename / publically document this map?
entropy_aliases = dict(
# barest protection from throttled online attack
unsafe=12,
@@ -435,21 +436,24 @@ def genword(entropy=None, length=None, returns=None, **kwds):
'310f1a7ac793f'
:param entropy:
- Strength of resulting password, measured in bits of Shannon entropy
- (defaults to 48). An appropriate **length** value will be calculated
+ Strength of resulting password, measured in 'guessing entropy' bits.
+ An appropriate **length** value will be calculated
based on the requested entropy amount, and the size of the character set.
- If both ``entropy`` and ``length`` are specified,
- the stronger value will be used.
-
- This can also be one of a handful of aliases to predefined
- entropy amounts: ``"weak"`` (24), ``"fair"`` (36),
+ This can be a positive integer, or one of the following preset
+ strings: ``"weak"`` (24), ``"fair"`` (36),
``"strong"`` (48), and ``"secure"`` (56).
+ If neither this or **length** is specified, **entropy** will default
+ to ``"strong"`` (48).
+
:param length:
Size of resulting password, measured in characters.
If omitted, the size is auto-calculated based on the **entropy** parameter.
+ If both **entropy** and **length** are specified,
+ the stronger value will be used.
+
:param returns:
Controls what this function returns:
@@ -457,9 +461,15 @@ def genword(entropy=None, length=None, returns=None, **kwds):
* If an integer, this function will return a list containing that many passwords.
* If the ``iter`` constant, will return an iterator that yields passwords.
+ :param chars:
+
+ Optionally specify custom string of characters to use when randomly
+ generating a password. This option cannot be combined with **charset**.
+
:param charset:
- The character set to draw from, if not specified explicitly by **chars**.
- Can be any of:
+
+ The predefined character set to draw from (if not specified by **chars**).
+ There are currently four presets available:
* ``"ascii_62"`` (the default) -- all digits and ascii upper & lowercase letters.
Provides ~5.95 entropy per character.
@@ -472,11 +482,6 @@ def genword(entropy=None, length=None, returns=None, **kwds):
* ``"hex"`` -- Lower case hexadecimal. Providers 4 bits of entropy per character.
- :param chars:
-
- Optionally specify custom charset as a string of characters.
- This option cannot be combined with **charset**.
-
:returns:
:class:`!unicode` string containing randomly generated password;
or list of 1+ passwords if :samp:`returns={int}` is specified.
@@ -698,21 +703,24 @@ def genphrase(entropy=None, length=None, returns=None, **kwds):
'wheat dilemma reward rescue diary'
:param entropy:
- Strength of resulting password, measured in bits of Shannon entropy
- (defaults to 48). An appropriate **length** value will be calculated
- based on the requested entropy amount, and the size of the character set.
+ Strength of resulting password, measured in 'guessing entropy' bits.
+ An appropriate **length** value will be calculated
+ based on the requested entropy amount, and the size of the word set.
- If both ``entropy`` and ``length`` are specified,
- the stronger value will be used.
-
- This can also be one of a handful of aliases to predefined
- entropy amounts: ``"weak"`` (24), ``"fair"`` (36),
+ This can be a positive integer, or one of the following preset
+ strings: ``"weak"`` (24), ``"fair"`` (36),
``"strong"`` (48), and ``"secure"`` (56).
+ If neither this or **length** is specified, **entropy** will default
+ to ``"strong"`` (48).
+
:param length:
Length of resulting password, measured in words.
If omitted, the size is auto-calculated based on the **entropy** parameter.
+ If both **entropy** and **length** are specified,
+ the stronger value will be used.
+
:param returns:
Controls what this function returns:
@@ -720,8 +728,14 @@ def genphrase(entropy=None, length=None, returns=None, **kwds):
* If an integer, this function will return a list containing that many passwords.
* If the ``iter`` builtin, will return an iterator that yields passwords.
+ :param words:
+
+ Optionally specifies a list/set of words to use when randomly generating a passphrase.
+ This option cannot be combined with **wordset**.
+
:param wordset:
- Optionally use a pre-defined word-set when generating a passphrase.
+
+ The predefined word set to draw from (if not specified by **words**).
There are currently four presets available:
``"eff_long"`` (the default)
@@ -756,10 +770,6 @@ def genphrase(entropy=None, length=None, returns=None, **kwds):
(at the cost of slightly less entropy); and much shorter than
``"eff_prefixed"`` (at the cost of a longer unique prefix).
- :param words:
- Optionally specifies a list/set of words to use when randomly generating a passphrase.
- This option cannot be combined with **wordset**.
-
:param sep:
Optional separator to use when joining words.
Defaults to ``" "`` (a space), but can be an empty string, a hyphen, etc.
diff --git a/passlib/tests/test_pwd.py b/passlib/tests/test_pwd.py
index 6f6a9a5..2c983cd 100644
--- a/passlib/tests/test_pwd.py
+++ b/passlib/tests/test_pwd.py
@@ -65,6 +65,14 @@ class WordGeneratorTest(TestCase):
"""test generation routines"""
descriptionPrefix = "passlib.pwd.genword()"
+ def setUp(self):
+ super(WordGeneratorTest, self).setUp()
+
+ # patch some RNG references so they're reproducible.
+ from passlib.pwd import SequenceGenerator
+ self.patchAttr(SequenceGenerator, "rng",
+ self.getRandom("pwd generator"))
+
def assertResultContents(self, results, count, chars, unique=True):
"""check result list matches expected count & charset"""
self.assertEqual(len(results), count)
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index 235fd76..62f3ab3 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -965,6 +965,10 @@ class HandlerCase(TestCase):
self.addCleanup(handler.set_backend, handler.get_backend())
handler.set_backend(backend)
+ # patch some RNG references so they're reproducible.
+ from passlib.utils import handlers
+ self.patchAttr(handlers, "rng", self.getRandom("salt generator"))
+
#===================================================================
# basic tests
#===================================================================
@@ -1254,21 +1258,22 @@ class HandlerCase(TestCase):
"""test hash() / genconfig() creates new salt each time"""
self.require_salt()
# odds of picking 'n' identical salts at random is '(.5**salt_bits)**n'.
- # we want to pick the smallest N needed s.t. odds are <1/1000, just
- # to eliminate false-positives. which works out to n>7-salt_bits.
- # n=1 is sufficient for most hashes, but a few border cases (e.g.
- # cisco_type7) have < 7 bits of salt, requiring more.
- samples = max(1,7-self.salt_bits)
+ # we want to pick the smallest N needed s.t. odds are <1/10**d, just
+ # to eliminate false-positives. which works out to n>3.33+d-salt_bits.
+ # for 1/1e12 odds, n=1 is sufficient for most hashes, but a few border cases (e.g.
+ # cisco_type7) have < 16 bits of salt, requiring more.
+ samples = max(1, 4 + 12 - self.salt_bits)
+
def sampler(func):
value1 = func()
- for i in irange(samples):
+ for _ in irange(samples):
value2 = func()
if value1 != value2:
return
raise self.failureException("failed to find different salt after "
"%d samples" % (samples,))
sampler(self.do_genconfig)
- sampler(lambda : self.do_encrypt("stub"))
+ sampler(lambda: self.do_encrypt("stub"))
def test_12_min_salt_size(self):
"""test hash() / genconfig() honors min_salt_size"""
diff --git a/setup.py b/setup.py
index 0a1be48..2dc32c2 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
passlib setup script
This script honors one environmental variable:
-PASSLIB_SETUP_TAG_RELEASE
+SETUP_TAG_RELEASE
if "yes" (the default), revision tag is appended to version.
for release, this is explicitly set to "no".
"""
@@ -16,67 +16,48 @@ os.chdir(root_dir)
#=============================================================================
# imports
#=============================================================================
-import re
-from setuptools import setup, find_packages
-import subprocess
+import setuptools
import sys
-import time
-PY3 = (sys.version_info[0] >= 3)
#=============================================================================
# init setup options
#=============================================================================
-opts = {"cmdclass": {}}
-args = sys.argv[1:]
-
-#=============================================================================
-# register docdist command (not required)
-#=============================================================================
-try:
- from passlib._setup.docdist import docdist
- opts['cmdclass']['docdist'] = docdist
-except ImportError:
- pass
+opts = dict(
+ #==================================================================
+ # sources
+ #==================================================================
+ packages=setuptools.find_packages(root_dir),
+ package_data={
+ "passlib.tests": ["*.cfg"],
+ "passlib": ["_data/wordsets/*.txt"],
+ },
+ zip_safe=True,
-#=============================================================================
-# version string / datestamps
-#=============================================================================
+ #==================================================================
+ # metadata
+ #==================================================================
+ name="passlib",
+ # NOTE: 'version' set below
+ author="Eli Collins",
+ author_email="elic@assurancetechnologies.com",
+ license="BSD",
-# pull version string from passlib
-from passlib import __version__ as version
+ url="https://bitbucket.org/ecollins/passlib",
+ # NOTE: 'download_url' set below
-# by default, stamp HG revision to end of version
-if os.environ.get("PASSLIB_SETUP_TAG_RELEASE", "y").lower() in "yes y true t 1".split():
- # call HG via subprocess
- # NOTE: for py26 compat, using Popen() instead of check_output()
- try:
- proc = subprocess.Popen(["hg", "tip", "--template", "{date(date, '%Y%m%d%H%M%S')}+hg.{node|short}"],
- stdout=subprocess.PIPE)
- stamp, _ = proc.communicate()
- if proc.returncode:
- raise subprocess.CalledProcessError(1, [])
- stamp = stamp.decode("ascii")
- except (OSError, subprocess.CalledProcessError):
- # fallback - just use build date
- stamp = time.strftime("%Y%m%d%H%M%S")
-
- # modify version
- if version.endswith((".dev0", ".post0")):
- version = version[:-1] + stamp
- else:
- version += ".post" + stamp
-
- # subclass build_py & sdist so they rewrite passlib/__init__.py
- # to have the correct version string
- from passlib._setup.stamp import stamp_distutils_output
- stamp_distutils_output(opts, version)
+ extras_require={
+ "argon2": "argon2_cffi>=16.2",
+ "bcrypt": "bcrypt>=3.1.0",
+ "totp": "cryptography",
+ },
-#=============================================================================
-# static text
-#=============================================================================
-SUMMARY = "comprehensive password hashing framework supporting over 30 schemes"
+ #==================================================================
+ # details
+ #==================================================================
+ description=
+ "comprehensive password hashing framework supporting over 30 schemes",
-DESCRIPTION = """\
+ long_description="""\
Passlib is a password hashing library for Python 2 & 3, which provides
cross-platform implementations of over 30 password hashing algorithms, as well
as a framework for managing existing password hashes. It's designed to be useful
@@ -94,85 +75,106 @@ providing full-strength password hashing for multi-user applications.
All releases are signed with the gpg key
`4D8592DF4CE1ED31 <http://pgp.mit.edu:11371/pks/lookup?op=get&search=0x4D8592DF4CE1ED31>`_.
-"""
+""",
-KEYWORDS = """\
+ keywords="""\
password secret hash security
crypt md5-crypt
sha256-crypt sha512-crypt pbkdf2 argon2 scrypt bcrypt
apache htpasswd htdigest
totp 2fa
-"""
+""",
-CLASSIFIERS = """\
+ classifiers="""\
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Natural Language :: English
Operating System :: OS Independent
+Programming Language :: Python :: 2
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
+Programming Language :: Python :: 3.3
+Programming Language :: Python :: 3.4
+Programming Language :: Python :: 3.5
+Programming Language :: Python :: 3.6
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: Jython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Security :: Cryptography
Topic :: Software Development :: Libraries
-""".splitlines()
+""".splitlines(),
-# TODO: "Programming Language :: Python :: Implementation :: IronPython" -- issue 34
+ # TODO: add "Programming Language :: Python :: Implementation :: IronPython"
+ # (blocked by issue 34)
-is_release = False
-if '.dev' in version:
- CLASSIFIERS.append("Development Status :: 3 - Alpha")
-elif '.post' in version:
- CLASSIFIERS.append("Development Status :: 4 - Beta")
-else:
- is_release = True
- CLASSIFIERS.append("Development Status :: 5 - Production/Stable")
+ #==================================================================
+ # testing
+ #==================================================================
+ tests_require='nose >= 1.1',
+ test_suite='nose.collector',
+
+ #==================================================================
+ # custom setup
+ #==================================================================
+ script_args=sys.argv[1:],
+ cmdclass={},
+)
#=============================================================================
-# run setup
+# set version string
#=============================================================================
-# XXX: could omit 'passlib._setup' from eggs, but not sdist
-setup(
- # package info
- packages=find_packages(root_dir),
- package_data={
- "passlib.tests": ["*.cfg"],
- "passlib": ["_data/wordsets/*.txt"],
- },
- zip_safe=True,
- # metadata
- name="passlib",
- version=version,
- author="Eli Collins",
- author_email="elic@assurancetechnologies.com",
- license="BSD",
+# pull version string from passlib
+from passlib import __version__ as version
- url="https://bitbucket.org/ecollins/passlib",
- download_url=
- ("https://pypi.python.org/packages/source/p/passlib/passlib-" + version + ".tar.gz")
- if is_release else None,
+# append hg revision to builds
+stamp_build = True # NOTE: modified by stamp_distutils_output()
+if stamp_build:
+ from passlib._setup.stamp import (
+ as_bool, append_hg_revision, stamp_distutils_output,
+ install_build_py_exclude, set_command_options
+ )
- description=SUMMARY,
- long_description=DESCRIPTION,
- keywords=KEYWORDS,
- classifiers=CLASSIFIERS,
+ # add HG revision to end of version
+ if as_bool(os.environ.get("SETUP_TAG_RELEASE", "yes")):
+ version = append_hg_revision(version)
- tests_require='nose >= 1.1',
- test_suite='nose.collector',
+ # subclass build_py & sdist to rewrite source version string,
+ # and clears stamp_build flag so this doesn't run again.
+ stamp_distutils_output(opts, version)
- extras_require={
- "argon2": "argon2_cffi>=16.2",
- "bcrypt": "bcrypt>=3.1.0",
- "totp": "cryptography",
- },
+ # exclude 'passlib._setup' from builds, only needed for sdist
+ install_build_py_exclude(opts)
+ set_command_options(opts, "build_py",
+ exclude_packages=["passlib._setup"],
+ )
- # extra opts
- script_args=args,
- **opts
-)
+opts['version'] = version
+
+#=============================================================================
+# set release status
+#=============================================================================
+
+if '.dev' in version:
+ status = "Development Status :: 3 - Alpha"
+elif '.post' in version:
+ status = "Development Status :: 4 - Beta"
+else:
+ status = "Development Status :: 5 - Production/Stable"
+
+ # only list download url for final release
+ opts.update(
+ download_url=("https://pypi.python.org/packages/source/p/passlib/"
+ "passlib-" + version + ".tar.gz")
+ )
+
+opts['classifiers'].append(status)
+
+#=============================================================================
+# run setup
+#=============================================================================
+setuptools.setup(**opts)
#=============================================================================
# eof