+v40.1.0
+-------
+
+* #1410: Deprecated ``upload`` and ``register`` commands.
+* #1312: Introduced find_namespace_packages() to find PEP 420 namespace packages.
+* #1420: Added find_namespace: directive to config parser.
+* #1418: Solved race in when creating egg cache directories.
+* #1450: Upgraded vendored PyParsing from 2.1.10 to 2.2.0.
+* #1451: Upgraded vendored appdirs from 1.4.0 to 1.4.3.
+* #1388: Fixed "Microsoft Visual C++ Build Tools" link in exception when Visual C++ not found.
+* #1389: Added support for scripts which have unicode content.
+* #1416: Moved several Python version checks over to using ``six.PY2`` and ``six.PY3``.
+* #1441: Removed spurious executable permissions from files that don't need them.
+
+
v40.0.0
-------
+
* #1342: Drop support for Python 3.3.
* #1366: In package_index, fixed handling of encoded entities in URLs.
* #1383: In pkg_resources VendorImporter, avoid removing packages imported from the root.
* Command aliases - create project-specific, per-user, or site-wide shortcut
names for commonly used commands and options
-* PyPI upload support - upload your source distributions and eggs to PyPI
-
* Deploy your project in "development mode", such that it's available on
``sys.path``, yet can still be edited directly from its source checkout.
* Create extensible applications and frameworks that automatically discover
extensions, using simple "entry points" declared in a project's setup script.
+* Full support for PEP 420 via ``find_namespace_packages()``, which is also backwards
+ compatible to the existing ``find_packages()`` for Python >= 3.3.
+
.. contents:: **Table of Contents**
.. _ez_setup.py: `bootstrap module`_
Run that script in your project folder, alongside the Python packages
you have developed.
-Invoke that script to produce eggs, upload to
-PyPI, and automatically include all packages in the directory where the
-setup.py lives. See the `Command Reference`_ section below to see what
-commands you can give to this setup script. For example,
-to produce a source distribution, simply invoke::
+Invoke that script to produce distributions and automatically include all
+packages in the directory where the setup.py lives. See the `Command
+Reference`_ section below to see what commands you can give to this setup
+script. For example, to produce a source distribution, simply invoke::
python setup.py sdist
'hello': ['*.msg'],
},
- # metadata for upload to PyPI
+ # metadata to display on PyPI
author="Me",
author_email="me@example.com",
description="This is an Example Package",
remember to modify your setup script whenever your project grows additional
top-level packages or subpackages.
+``find_namespace_packages()``
+-----------------------------
+In Python 3.3+, ``setuptools`` also provides the ``find_namespace_packages`` variant
+of ``find_packages``, which has the same function signature as
+``find_packages``, but works with `PEP 420`_ compliant implicit namespace
+packages. Here is a minimal setup script using ``find_namespace_packages``::
+
+ from setuptools import setup, find_namespace_packages
+ setup(
+ name="HelloWorld",
+ version="0.1",
+ packages=find_namespace_packages(),
+ )
+
+
+Keep in mind that according to PEP 420, you may have to either re-organize your
+codebase a bit or define a few exclusions, as the definition of an implicit
+namespace package is quite lenient, so for a project organized like so::
+
+
+ ├── namespace
+ │ └── mypackage
+ │ ├── __init__.py
+ │ └── mod1.py
+ ├── setup.py
+ └── tests
+ └── test_mod1.py
+
+A naive ``find_namespace_packages()`` would install both ``namespace.mypackage`` and a
+top-level package called ``tests``! One way to avoid this problem is to use the
+``include`` keyword to whitelist the packages to include, like so::
+
+ from setuptools import setup, find_namespace_packages
+
+ setup(
+ name="namespace.mypackage",
+ version="0.1",
+ packages=find_namespace_packages(include=['namespace.*'])
+ )
+
+Another option is to use the "src" layout, where all package code is placed in
+the ``src`` directory, like so::
+
+
+ ├── setup.py
+ ├── src
+ │ └── namespace
+ │ └── mypackage
+ │ ├── __init__.py
+ │ └── mod1.py
+ └── tests
+ └── test_mod1.py
+
+With this layout, the package directory is specified as ``src``, as such::
+
+ setup(name="namespace.mypackage",
+ version="0.1",
+ package_dir={'': 'src'},
+ packages=find_namespace_packages(where='src'))
+
+.. _PEP 420: https://www.python.org/dev/peps/pep-0420/
Automatic Script Creation
=========================
Dependencies that aren't in PyPI
--------------------------------
-If your project depends on packages that aren't registered in PyPI, you may
-still be able to depend on them, as long as they are available for download
-as:
+If your project depends on packages that don't exist on PyPI, you may still be
+able to depend on them, as long as they are available for download as:
- an egg, in the standard distutils ``sdist`` format,
- a single ``.py`` file, or
your projects' ``ez_setup.py`` files, so that a matching version gets installed
on the target machine(s).
-By the way, setuptools supports the new PyPI "upload" command, so you can use
-``setup.py sdist upload`` or ``setup.py bdist_egg upload`` to upload your
-source or egg distributions respectively. Your project's current version must
-be registered with PyPI first, of course; you can use ``setup.py register`` to
-do that. Or you can do it all in one step, e.g. ``setup.py register sdist
-bdist_egg upload`` will register the package, build source and egg
-distributions, and then upload them both to PyPI, where they'll be easily
-found by other projects that depend on them.
-
(By the way, if you need to distribute a specific version of ``setuptools``,
you can specify the exact version and base download URL as parameters to the
``use_setuptools()`` function. See the function's docstring for details.)
Making your package available for EasyInstall
---------------------------------------------
-If you use the ``register`` command (``setup.py register``) to register your
-package with PyPI, that's most of the battle right there. (See the
-`docs for the register command`_ for more details.)
-
-.. _docs for the register command: http://docs.python.org/dist/package-index.html
-
-If you also use the `upload`_ command to upload actual distributions of your
-package, that's even better, because EasyInstall will be able to find and
-download them directly from your project's PyPI page.
-
-However, there may be reasons why you don't want to upload distributions to
+There may be reasons why you don't want to upload distributions to
PyPI, and just want your existing distributions (or perhaps a Subversion
checkout) to be used instead.
-So here's what you need to do before running the ``register`` command. There
-are three ``setup()`` arguments that affect EasyInstall:
+There are three ``setup()`` arguments that affect EasyInstall:
``url`` and ``download_url``
These become links on your project's PyPI page. EasyInstall will examine
Alternately, if you are not branching for releases, you can override the
default version options on the command line, using something like::
- python setup.py egg_info -Db "" sdist bdist_egg register upload
+ python setup.py egg_info -Db "" sdist bdist_egg
The first part of this command (``egg_info -Db ""``) will override the
-configured tag information, before creating source and binary eggs, registering
-the project with PyPI, and uploading the files. Thus, these commands will use
-the plain version from your ``setup.py``, without adding the build designation
-string.
+configured tag information, before creating source and binary eggs. Thus, these
+commands will use the plain version from your ``setup.py``, without adding the
+build designation string.
Of course, if you will be doing this a lot, you may wish to create a personal
alias for this operation, e.g.::
You can then use it like this::
- python setup.py release sdist bdist_egg register upload
+ python setup.py release sdist bdist_egg
Or of course you can create more elaborate aliases that do all of the above.
See the sections below on the `egg_info`_ and `alias`_ commands for more ideas.
metadata directory (used by the ``bdist_egg``, ``develop``, and ``test``
commands), and it allows you to temporarily change a project's version string,
to support "daily builds" or "snapshot" releases. It is run automatically by
-the ``sdist``, ``bdist_egg``, ``develop``, ``register``, and ``test`` commands
-in order to update the project's metadata, but you can also specify it
-explicitly in order to temporarily change the project's version string while
-executing other commands. (It also generates the``.egg-info/SOURCES.txt``
-manifest file, which is used when you are building source distributions.)
+the ``sdist``, ``bdist_egg``, ``develop``, and ``test`` commands in order to
+update the project's metadata, but you can also specify it explicitly in order
+to temporarily change the project's version string while executing other
+commands. (It also generates the``.egg-info/SOURCES.txt`` manifest file, which
+is used when you are building source distributions.)
In addition to writing the core egg metadata defined by ``setuptools`` and
required by ``pkg_resources``, this command can be extended to write other
python setup.py egg_info --tag-date --tag-build=DEV bdist_egg
-Creating and uploading a release with no version tags, even if some default
-tags are specified in ``setup.cfg``::
+Creating a release with no version tags, even if some default tags are
+specified in ``setup.cfg``::
- python setup.py egg_info -RDb "" sdist bdist_egg register upload
+ python setup.py egg_info -RDb "" sdist bdist_egg
(Notice that ``egg_info`` must always appear on the command line *before* any
commands that you want the version changes to apply to.)
``upload`` - Upload source and/or egg distributions to PyPI
===========================================================
+.. warning::
+ **upload** is deprecated in favor of using `twine
+ <https://pypi.org/p/twine>`_
+
The ``upload`` command is implemented and `documented
<https://docs.python.org/3.1/distutils/uploading.html>`_
in distutils.
-Setuptools augments the ``upload`` command with support
-for `keyring <https://pypi.org/project/keyring/>`_,
-allowing the password to be stored in a secure
-location and not in plaintext in the .pypirc file. To use
-keyring, first install keyring and set the password for
-the relevant repository, e.g.::
-
- python -m keyring set <repository> <username>
- Password for '<username>' in '<repository>': ********
-
-Then, in .pypirc, set the repository configuration as normal,
-but omit the password. Thereafter, uploads will use the
-password from the keyring.
-
New in 20.1: Added keyring support.
+New in 40.0: Deprecated the upload command.
-----------------------------------------
* In some cases, complex values can be provided in dedicated subsections for
clarity.
-* Some keys allow ``file:``, ``attr:``, and ``find:`` directives in order to
- cover common usecases.
+* Some keys allow ``file:``, ``attr:``, and ``find:`` and ``find_namespace:`` directives in
+ order to cover common usecases.
* Unknown keys are ignored.
dependency_links list-comma
tests_require list-semi
include_package_data bool
-packages find:, list-comma
+packages find:, find_namespace:, list-comma
package_dir dict
package_data section
exclude_package_data section
.. note::
- **packages** - The ``find:`` directive can be further configured
+ **packages** - The ``find:`` and ``find_namespace:`` directive can be further configured
in a dedicated subsection ``options.packages.find``. This subsection
- accepts the same keys as the `setuptools.find` function:
+ accepts the same keys as the `setuptools.find_packages` and the
+ `setuptools.find_namespace_packages` function:
``where``, ``include``, and ``exclude``.
+
+ **find_namespace directive** - The ``find_namespace:`` directive is supported since Python >=3.3.
Configuration API
# Python 3.2 compatibility
import imp as _imp
+try:
+ FileExistsError
+except NameError:
+ FileExistsError = OSError
+
from pkg_resources.extern import six
from pkg_resources.extern.six.moves import urllib, map, filter
distributions in the working set, otherwise only ones matching
both `group` and `name` are yielded (in distribution order).
"""
- for dist in self:
- entries = dist.get_entry_map(group)
- if name is None:
- for ep in entries.values():
- yield ep
- elif name in entries:
- yield entries[name]
+ return (
+ entry
+ for dist in self
+ for entry in dist.get_entry_map(group).values()
+ if name is None or name == entry.name
+ )
def run_script(self, requires, script_name):
"""Locate distribution for `requires` and run `script_name` script"""
dirname, filename = split(path)
if dirname and filename and not isdir(dirname):
_bypass_ensure_directory(dirname)
- mkdir(dirname, 0o755)
+ try:
+ mkdir(dirname, 0o755)
+ except FileExistsError:
+ pass
def split_sections(s):
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
-__version_info__ = (1, 4, 0)
+__version_info__ = (1, 4, 3)
__version__ = '.'.join(map(str, __version_info__))
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
- """Return full path to the user-shared data dir for this application.
+ r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
- Typical user data directories are:
+ Typical site data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
- Typical user data directories are:
+ Typical user config directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
- That means, by deafult "~/.config/<AppName>".
+ That means, by default "~/.config/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
- """Return full path to the user-shared data dir for this application.
+ r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
- Typical user data directories are:
+ Typical site config directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
return path
+def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
+ r"""Return full path to the user-specific state dir for this application.
+
+ "appname" is the name of application.
+ If None, just the system directory is returned.
+ "appauthor" (only used on Windows) is the name of the
+ appauthor or distributing body for this application. Typically
+ it is the owning company name. This falls back to appname. You may
+ pass False to disable it.
+ "version" is an optional version path element to append to the
+ path. You might want to use this if you want multiple versions
+ of your app to be able to run independently. If used, this
+ would typically be "<major>.<minor>".
+ Only applied when appname is present.
+ "roaming" (boolean, default False) can be set True to use the Windows
+ roaming appdata directory. That means that for users on a Windows
+ network setup for roaming profiles, this user data will be
+ sync'd on login. See
+ <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
+ for a discussion of issues.
+
+ Typical user state directories are:
+ Mac OS X: same as user_data_dir
+ Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
+ Win *: same as user_data_dir
+
+ For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
+ to extend the XDG spec and support $XDG_STATE_HOME.
+
+ That means, by default "~/.local/state/<AppName>".
+ """
+ if system in ["win32", "darwin"]:
+ path = user_data_dir(appname, appauthor, None, roaming)
+ else:
+ path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
+ if appname:
+ path = os.path.join(path, appname)
+ if appname and version:
+ path = os.path.join(path, version)
+ return path
+
+
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific log dir for this application.
"Logs" to the base app data dir for Windows, and "log" to the
base cache dir for Unix. See discussion below.
- Typical user cache directories are:
+ Typical user log directories are:
Mac OS X: ~/Library/Logs/<AppName>
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
class AppDirs(object):
"""Convenience wrapper for getting application dirs."""
- def __init__(self, appname, appauthor=None, version=None, roaming=False,
- multipath=False):
+ def __init__(self, appname=None, appauthor=None, version=None,
+ roaming=False, multipath=False):
self.appname = appname
self.appauthor = appauthor
self.version = version
return user_cache_dir(self.appname, self.appauthor,
version=self.version)
+ @property
+ def user_state_dir(self):
+ return user_state_dir(self.appname, self.appauthor,
+ version=self.version)
+
@property
def user_log_dir(self):
return user_log_dir(self.appname, self.appauthor,
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
- import _winreg
+ if PY3:
+ import winreg as _winreg
+ else:
+ import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
if has_high_char:
buf = array.zeros('c', buf_size)
kernel = win32.Kernel32.INSTANCE
- if kernal.GetShortPathName(dir, buf, buf_size):
+ if kernel.GetShortPathName(dir, buf, buf_size):
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
return dir
appname = "MyApp"
appauthor = "MyCompany"
- props = ("user_data_dir", "site_data_dir",
- "user_config_dir", "site_config_dir",
- "user_cache_dir", "user_log_dir")
+ props = ("user_data_dir",
+ "user_config_dir",
+ "user_cache_dir",
+ "user_state_dir",
+ "user_log_dir",
+ "site_data_dir",
+ "site_config_dir")
+
+ print("-- app dirs %s --" % __version__)
print("-- app dirs (with optional 'version')")
dirs = AppDirs(appname, appauthor, version="1.0")
- embedded comments\r
"""\r
\r
-__version__ = "2.1.10"\r
-__versionTime__ = "07 Oct 2016 01:31 UTC"\r
+__version__ = "2.2.0"\r
+__versionTime__ = "06 Mar 2017 02:06 UTC"\r
__author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"\r
\r
import string\r
except UnicodeEncodeError:\r
# Else encode it\r
ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')\r
- xmlcharref = Regex('&#\d+;')\r
+ xmlcharref = Regex(r'&#\d+;')\r
xmlcharref.setParseAction(lambda t: '\\u' + hex(int(t[0][2:-1]))[2:])\r
return xmlcharref.transformString(ret)\r
\r
return None\r
\r
def getName(self):\r
- """\r
+ r"""\r
Returns the results name for this token expression. Useful when several \r
different expressions might match at a particular location.\r
\r
\r
def setParseAction( self, *fns, **kwargs ):\r
"""\r
- Define action to perform when successfully matching parse element definition.\r
+ Define one or more actions to perform when successfully matching parse element definition.\r
Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},\r
C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:\r
- s = the original string being parsed (see note below)\r
\r
def addParseAction( self, *fns, **kwargs ):\r
"""\r
- Add parse action to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\r
+ Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\r
\r
See examples in L{I{copy}<copy>}.\r
"""\r
\r
def clear(self):\r
cache.clear()\r
+ \r
+ def cache_len(self):\r
+ return len(cache)\r
\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
if _OrderedDict is not None:\r
class _FifoCache(object):\r
\r
def set(self, key, value):\r
cache[key] = value\r
- if len(cache) > size:\r
- cache.popitem(False)\r
+ while len(cache) > size:\r
+ try:\r
+ cache.popitem(False)\r
+ except KeyError:\r
+ pass\r
\r
def clear(self):\r
cache.clear()\r
\r
+ def cache_len(self):\r
+ return len(cache)\r
+\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
else:\r
class _FifoCache(object):\r
\r
def set(self, key, value):\r
cache[key] = value\r
- if len(cache) > size:\r
+ while len(key_fifo) > size:\r
cache.pop(key_fifo.popleft(), None)\r
key_fifo.append(key)\r
\r
cache.clear()\r
key_fifo.clear()\r
\r
+ def cache_len(self):\r
+ return len(cache)\r
+\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
# argument cache for optimizing repeated calls when backtracking through recursive expressions\r
packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail\r
cap_word = Word(alphas.upper(), alphas.lower())\r
\r
print(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity"))\r
+\r
+ # the sum() builtin can be used to merge results into a single ParseResults object\r
+ print(sum(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity")))\r
prints::\r
- ['More', 'Iron', 'Lead', 'Gold', 'I']\r
+ [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]\r
+ ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']\r
"""\r
try:\r
return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])\r
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),\r
SyntaxWarning, stacklevel=2)\r
return None\r
- return And( [ self, And._ErrorStop(), other ] )\r
+ return self + And._ErrorStop() + other\r
\r
def __rsub__(self, other ):\r
"""\r
\r
\r
class Regex(Token):\r
- """\r
+ r"""\r
Token for matching strings that match a given regular expression.\r
Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.\r
If the given regex contains named groups (defined using C{(?P<name>...)}), these will be preserved as \r
\r
# replace escaped characters\r
if self.escChar:\r
- ret = re.sub(self.escCharReplacePattern,"\g<1>",ret)\r
+ ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret)\r
\r
# replace escaped quotes\r
if self.escQuote:\r
constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}.\r
- parseAction is the parse action to be associated with\r
expressions matching this operator expression (the\r
- parse action tuple member may be omitted)\r
+ parse action tuple member may be omitted); if the parse action\r
+ is passed a tuple or list of functions, this is equivalent to\r
+ calling C{setParseAction(*fn)} (L{ParserElement.setParseAction})\r
- lpar - expression for matching left-parentheses (default=C{Suppress('(')})\r
- rpar - expression for matching right-parentheses (default=C{Suppress(')')})\r
\r
else:\r
raise ValueError("operator must indicate right or left associativity")\r
if pa:\r
- matchExpr.setParseAction( pa )\r
+ if isinstance(pa, (tuple, list)):\r
+ matchExpr.setParseAction(*pa)\r
+ else:\r
+ matchExpr.setParseAction(pa)\r
thisExpr <<= ( matchExpr.setName(termName) | lastExpr )\r
lastExpr = thisExpr\r
ret <<= lastExpr\r
packaging==16.8
-pyparsing==2.1.10
+pyparsing==2.2.0
six==1.10.0
-appdirs==1.4.0
+appdirs==1.4.3
import errno
import sys
+from .extern import six
+
def _makedirs_31(path, exist_ok=False):
try:
# and exists_ok considerations are disentangled.
# See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663
needs_makedirs = (
- sys.version_info.major == 2 or
+ six.PY2 or
(3, 4) <= sys.version_info < (3, 4, 1)
)
makedirs = _makedirs_31 if needs_makedirs else os.makedirs
import distutils.dist
import distutils.command.install_egg_info
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
from pkg_resources.extern.six.moves import map
from pkg_resources.extern.six import text_type, string_types
message = "Unexpected type from get_cache_path: " + type_
assert isinstance(path, string_types), message
+ def test_get_cache_path_race(self, tmpdir):
+ # Patch to os.path.isdir to create a race condition
+ def patched_isdir(dirname, unpatched_isdir=pkg_resources.isdir):
+ patched_isdir.dirnames.append(dirname)
+
+ was_dir = unpatched_isdir(dirname)
+ if not was_dir:
+ os.makedirs(dirname)
+ return was_dir
+
+ patched_isdir.dirnames = []
+
+ # Get a cache path with a "race condition"
+ mgr = pkg_resources.ResourceManager()
+ mgr.set_extraction_path(str(tmpdir))
+
+ archive_name = os.sep.join(('foo', 'bar', 'baz'))
+ with mock.patch.object(pkg_resources, 'isdir', new=patched_isdir):
+ mgr.get_cache_path(archive_name)
+
+ # Because this test relies on the implementation details of this
+ # function, these assertions are a sentinel to ensure that the
+ # test suite will not fail silently if the implementation changes.
+ called_dirnames = patched_isdir.dirnames
+ assert len(called_dirnames) == 2
+ assert called_dirnames[0].split(os.sep)[-2:] == ['foo', 'bar']
+ assert called_dirnames[1].split(os.sep)[-1:] == ['foo']
-class TestIndependence:
"""
Tests to ensure that pkg_resources runs independently from setuptools.
"""
[bumpversion]
-current_version = 40.0.0
+current_version = 40.1.0
commit = True
tag = True
setup_params = dict(
name="setuptools",
- version="40.0.0",
+ version="40.1.0",
description=(
"Easily download, build, install, upgrade, and uninstall "
"Python packages"
"""Extensions to the 'distutils' for large or complex distributions"""
import os
+import sys
import functools
import distutils.core
import distutils.filelist
from distutils.util import convert_path
from fnmatch import fnmatchcase
+from setuptools.extern.six import PY3
from setuptools.extern.six.moves import filter, map
import setuptools.version
__metaclass__ = type
+
__all__ = [
'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',
- 'find_packages',
+ 'find_packages'
]
+if PY3:
+ __all__.append('find_namespace_packages')
+
__version__ = setuptools.version.__version__
bootstrap_install_from = None
find_packages = PackageFinder.find
+if PY3:
+ find_namespace_packages = PEP420PackageFinder.find
+
def _install_setup_requires(attrs):
# Note: do not use `setuptools.Distribution` directly, as
- embedded comments\r
"""\r
\r
-__version__ = "2.1.10"\r
-__versionTime__ = "07 Oct 2016 01:31 UTC"\r
+__version__ = "2.2.0"\r
+__versionTime__ = "06 Mar 2017 02:06 UTC"\r
__author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"\r
\r
import string\r
except UnicodeEncodeError:\r
# Else encode it\r
ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')\r
- xmlcharref = Regex('&#\d+;')\r
+ xmlcharref = Regex(r'&#\d+;')\r
xmlcharref.setParseAction(lambda t: '\\u' + hex(int(t[0][2:-1]))[2:])\r
return xmlcharref.transformString(ret)\r
\r
return None\r
\r
def getName(self):\r
- """\r
+ r"""\r
Returns the results name for this token expression. Useful when several \r
different expressions might match at a particular location.\r
\r
\r
def setParseAction( self, *fns, **kwargs ):\r
"""\r
- Define action to perform when successfully matching parse element definition.\r
+ Define one or more actions to perform when successfully matching parse element definition.\r
Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},\r
C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:\r
- s = the original string being parsed (see note below)\r
\r
def addParseAction( self, *fns, **kwargs ):\r
"""\r
- Add parse action to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\r
+ Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}<setParseAction>}.\r
\r
See examples in L{I{copy}<copy>}.\r
"""\r
\r
def clear(self):\r
cache.clear()\r
+ \r
+ def cache_len(self):\r
+ return len(cache)\r
\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
if _OrderedDict is not None:\r
class _FifoCache(object):\r
\r
def set(self, key, value):\r
cache[key] = value\r
- if len(cache) > size:\r
- cache.popitem(False)\r
+ while len(cache) > size:\r
+ try:\r
+ cache.popitem(False)\r
+ except KeyError:\r
+ pass\r
\r
def clear(self):\r
cache.clear()\r
\r
+ def cache_len(self):\r
+ return len(cache)\r
+\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
else:\r
class _FifoCache(object):\r
\r
def set(self, key, value):\r
cache[key] = value\r
- if len(cache) > size:\r
+ while len(key_fifo) > size:\r
cache.pop(key_fifo.popleft(), None)\r
key_fifo.append(key)\r
\r
cache.clear()\r
key_fifo.clear()\r
\r
+ def cache_len(self):\r
+ return len(cache)\r
+\r
self.get = types.MethodType(get, self)\r
self.set = types.MethodType(set, self)\r
self.clear = types.MethodType(clear, self)\r
+ self.__len__ = types.MethodType(cache_len, self)\r
\r
# argument cache for optimizing repeated calls when backtracking through recursive expressions\r
packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail\r
cap_word = Word(alphas.upper(), alphas.lower())\r
\r
print(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity"))\r
+\r
+ # the sum() builtin can be used to merge results into a single ParseResults object\r
+ print(sum(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity")))\r
prints::\r
- ['More', 'Iron', 'Lead', 'Gold', 'I']\r
+ [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]\r
+ ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']\r
"""\r
try:\r
return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])\r
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),\r
SyntaxWarning, stacklevel=2)\r
return None\r
- return And( [ self, And._ErrorStop(), other ] )\r
+ return self + And._ErrorStop() + other\r
\r
def __rsub__(self, other ):\r
"""\r
\r
\r
class Regex(Token):\r
- """\r
+ r"""\r
Token for matching strings that match a given regular expression.\r
Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.\r
If the given regex contains named groups (defined using C{(?P<name>...)}), these will be preserved as \r
\r
# replace escaped characters\r
if self.escChar:\r
- ret = re.sub(self.escCharReplacePattern,"\g<1>",ret)\r
+ ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret)\r
\r
# replace escaped quotes\r
if self.escQuote:\r
constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}.\r
- parseAction is the parse action to be associated with\r
expressions matching this operator expression (the\r
- parse action tuple member may be omitted)\r
+ parse action tuple member may be omitted); if the parse action\r
+ is passed a tuple or list of functions, this is equivalent to\r
+ calling C{setParseAction(*fn)} (L{ParserElement.setParseAction})\r
- lpar - expression for matching left-parentheses (default=C{Suppress('(')})\r
- rpar - expression for matching right-parentheses (default=C{Suppress(')')})\r
\r
else:\r
raise ValueError("operator must indicate right or left associativity")\r
if pa:\r
- matchExpr.setParseAction( pa )\r
+ if isinstance(pa, (tuple, list)):\r
+ matchExpr.setParseAction(*pa)\r
+ else:\r
+ matchExpr.setParseAction(pa)\r
thisExpr <<= ( matchExpr.setName(termName) | lastExpr )\r
lastExpr = thisExpr\r
ret <<= lastExpr\r
packaging==16.8
-pyparsing==2.1.10
+pyparsing==2.2.0
six==1.10.0
return True # Extension module
pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
- if sys.version_info.major == 2:
+ if six.PY2:
skip = 8 # skip magic & date
elif sys.version_info < (3, 7):
skip = 12 # skip magic & date & file size
if six.PY2:
- def _to_ascii(s):
+ def _to_bytes(s):
return s
def isascii(s):
return False
else:
- def _to_ascii(s):
- return s.encode('ascii')
+ def _to_bytes(s):
+ return s.encode('utf8')
def isascii(s):
try:
if is_script:
body = self._load_template(dev_path) % locals()
script_text = ScriptWriter.get_header(script_text) + body
- self.write_script(script_name, _to_ascii(script_text), 'b')
+ self.write_script(script_name, _to_bytes(script_text), 'b')
@staticmethod
def _load_template(dev_path):
return re.compile(pat, flags=re.MULTILINE|re.DOTALL)
-class egg_info(Command):
+class InfoCommon:
+ tag_build = None
+ tag_date = None
+
+ @property
+ def name(self):
+ return safe_name(self.distribution.get_name())
+
+ def tagged_version(self):
+ version = self.distribution.get_version()
+ # egg_info may be called more than once for a distribution,
+ # in which case the version string already contains all tags.
+ if self.vtags and version.endswith(self.vtags):
+ return safe_version(version)
+ return safe_version(version + self.vtags)
+
+ def tags(self):
+ version = ''
+ if self.tag_build:
+ version += self.tag_build
+ if self.tag_date:
+ version += time.strftime("-%Y%m%d")
+ return version
+ vtags = property(tags)
+
+
+class egg_info(InfoCommon, Command):
description = "create a distribution's .egg-info directory"
user_options = [
}
def initialize_options(self):
- self.egg_name = None
- self.egg_version = None
self.egg_base = None
self.egg_info = None
- self.tag_build = None
- self.tag_date = 0
self.broken_egg_info = False
- self.vtags = None
####################################
# allow the 'tag_svn_revision' to be detected and
egg_info['tag_date'] = 0
edit_config(filename, dict(egg_info=egg_info))
- def finalize_options(self):
- self.egg_name = safe_name(self.distribution.get_name())
- self.vtags = self.tags()
- self.egg_version = self.tagged_version()
+ @property
+ def egg_name(self):
+ return self.name
+
+ @property
+ def egg_version(self):
+ return self.tagged_version()
+ def finalize_options(self):
parsed_version = parse_version(self.egg_version)
try:
if not self.dry_run:
os.unlink(filename)
- def tagged_version(self):
- version = self.distribution.get_version()
- # egg_info may be called more than once for a distribution,
- # in which case the version string already contains all tags.
- if self.vtags and version.endswith(self.vtags):
- return safe_version(version)
- return safe_version(version + self.vtags)
-
def run(self):
self.mkpath(self.egg_info)
installer = self.distribution.fetch_build_egg
self.find_sources()
- def tags(self):
- version = ''
- if self.tag_build:
- version += self.tag_build
- if self.tag_date:
- version += time.strftime("-%Y%m%d")
- return version
-
def find_sources(self):
"""Generate SOURCES.txt manifest file"""
manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")
+from distutils import log
import distutils.command.register as orig
__doc__ = orig.register.__doc__
def run(self):
- # Make sure that we are using valid current name/version info
- self.run_command('egg_info')
- orig.register.run(self)
+ try:
+ # Make sure that we are using valid current name/version info
+ self.run_command('egg_info')
+ orig.register.run(self)
+ finally:
+ self.announce(
+ "WARNING: Registering is deprecated, use twine to "
+ "upload instead (https://pypi.org/p/twine/)",
+ log.WARN
+ )
import getpass
+from distutils import log
from distutils.command import upload as orig
in a variety of different ways.
"""
+ def run(self):
+ try:
+ orig.upload.run(self)
+ finally:
+ self.announce(
+ "WARNING: Uploading via this command is deprecated, use twine "
+ "to upload instead (https://pypi.org/p/twine/)",
+ log.WARN
+ )
+
def finalize_options(self):
orig.upload.finalize_options(self)
self.username = (
from distutils.errors import DistutilsOptionError, DistutilsFileError
from setuptools.extern.packaging.version import LegacyVersion, parse
-from setuptools.extern.six import string_types
+from setuptools.extern.six import string_types, PY3
__metaclass__ = type
:param value:
:rtype: list
"""
- find_directive = 'find:'
+ find_directives = ['find:', 'find_namespace:']
+ trimmed_value = value.strip()
- if not value.startswith(find_directive):
+ if not trimmed_value in find_directives:
return self._parse_list(value)
+ findns = trimmed_value == find_directives[1]
+ if findns and not PY3:
+ raise DistutilsOptionError('find_namespace: directive is unsupported on Python < 3.3')
+
# Read function arguments from a dedicated section.
find_kwargs = self.parse_section_packages__find(
self.sections.get('packages.find', {}))
- from setuptools import find_packages
+ if findns:
+ from setuptools import find_namespace_packages as find_packages
+ else:
+ from setuptools import find_packages
return find_packages(**find_kwargs)
# on later Python versions to cause relative imports
# in the vendor package to resolve the same modules
# as those going through this importer.
- if sys.version_info.major >= 3:
+ if sys.version_info >= (3, ):
del sys.modules[extant]
return mod
except ImportError:
elif version >= 14.0:
# For VC++ 14.0 Redirect user to Visual C++ Build Tools
message += (' Get it with "Microsoft Visual C++ Build Tools": '
- r'http://landinghub.visualstudio.com/'
- 'visual-cpp-build-tools')
+ r'https://visualstudio.microsoft.com/downloads/')
exc.args = (message, )
import warnings
from collections import OrderedDict
+from .extern import six
+
from . import glibc
_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)')
lambda: sys.maxunicode == 0x10ffff,
expected=4,
warn=(impl == 'cp' and
- sys.version_info.major == 2)) \
- and sys.version_info.major == 2:
+ six.PY2)) \
+ and six.PY2:
u = 'u'
abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u)
elif soabi and soabi.startswith('cpython-'):
import pytest
+from setuptools.extern.six import PY2, PY3
+
+
+__all__ = [
+ 'fail_on_ascii', 'py2_only', 'py3_only'
+]
+
+
is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968'
fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")
+
+
+py2_only = pytest.mark.skipif(not PY2, reason="Test runs on Python 2 only")
+py3_only = pytest.mark.skipif(not PY3, reason="Test runs on Python 3 only")
from mock import patch
from setuptools.dist import Distribution, _Distribution
from setuptools.config import ConfigHandler, read_configuration
-
+from . import py2_only, py3_only
class ErrConfigHandler(ConfigHandler):
"""Erroneous handler. Fails to implement required methods."""
-def make_package_dir(name, base_dir):
+def make_package_dir(name, base_dir, ns=False):
dir_package = base_dir
for dir_name in name.split('/'):
dir_package = dir_package.mkdir(dir_name)
- init_file = dir_package.join('__init__.py')
- init_file.write('')
+ init_file = None
+ if not ns:
+ init_file = dir_package.join('__init__.py')
+ init_file.write('')
return dir_package, init_file
assert set(dist.packages) == set(
['fake_package', 'fake_package.sub_two'])
+ @py2_only
+ def test_find_namespace_directive_fails_on_py2(self, tmpdir):
+ dir_package, config = fake_env(
+ tmpdir,
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ )
+
+ with pytest.raises(DistutilsOptionError):
+ with get_dist(tmpdir) as dist:
+ dist.parse_config_files()
+
+ @py3_only
+ def test_find_namespace_directive(self, tmpdir):
+ dir_package, config = fake_env(
+ tmpdir,
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ )
+
+ dir_sub_one, _ = make_package_dir('sub_one', dir_package)
+ dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True)
+
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == {
+ 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one'
+ }
+
+ config.write(
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'where = .\n'
+ 'include =\n'
+ ' fake_package.sub_one\n'
+ ' two\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert dist.packages == ['fake_package.sub_one']
+
+ config.write(
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'exclude =\n'
+ ' fake_package.sub_one\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == {
+ 'fake_package', 'fake_package.sub_two'
+ }
+
def test_extras_require(self, tmpdir):
fake_env(
tmpdir,
cmd.ensure_finalized()
cmd.easy_install(sdist_unicode)
+ @pytest.fixture
+ def sdist_unicode_in_script(self, tmpdir):
+ files = [
+ (
+ "setup.py",
+ DALS("""
+ import setuptools
+ setuptools.setup(
+ name="setuptools-test-unicode",
+ version="1.0",
+ packages=["mypkg"],
+ include_package_data=True,
+ scripts=['mypkg/unicode_in_script'],
+ )
+ """),
+ ),
+ ("mypkg/__init__.py", ""),
+ (
+ "mypkg/unicode_in_script",
+ DALS(
+ """
+ #!/bin/sh
+ # \xc3\xa1
+
+ non_python_fn() {
+ }
+ """),
+ ),
+ ]
+ sdist_name = "setuptools-test-unicode-script-1.0.zip"
+ sdist = tmpdir / sdist_name
+ # can't use make_sdist, because the issue only occurs
+ # with zip sdists.
+ sdist_zip = zipfile.ZipFile(str(sdist), "w")
+ for filename, content in files:
+ sdist_zip.writestr(filename, content)
+ sdist_zip.close()
+ return str(sdist)
+
+ @fail_on_ascii
+ def test_unicode_content_in_sdist(self, sdist_unicode_in_script, tmpdir, monkeypatch):
+ """
+ The install command should execute correctly even if
+ the package has unicode in scripts.
+ """
+ dist = Distribution({"script_args": ["easy_install"]})
+ target = (tmpdir / "target").ensure_dir()
+ cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
+ monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
+ cmd.ensure_finalized()
+ cmd.easy_install(sdist_unicode_in_script)
+
@pytest.fixture
def sdist_script(self, tmpdir):
files = [
import pytest
-import setuptools
-from setuptools import find_packages
+from . import py3_only
-find_420_packages = setuptools.PEP420PackageFinder.find
+from setuptools.extern.six import PY3
+from setuptools import find_packages
+if PY3:
+ from setuptools import find_namespace_packages
# modeled after CPython's test.support.can_symlink
-
def can_symlink():
TESTFN = tempfile.mktemp()
symlink_path = TESTFN + "can_symlink"
def _assert_packages(self, actual, expected):
assert set(actual) == set(expected)
+ @py3_only
def test_pep420_ns_package(self):
- packages = find_420_packages(
+ packages = find_namespace_packages(
self.dist_dir, include=['pkg*'], exclude=['pkg.subpkg.assets'])
self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+ @py3_only
def test_pep420_ns_package_no_includes(self):
- packages = find_420_packages(
+ packages = find_namespace_packages(
self.dist_dir, exclude=['pkg.subpkg.assets'])
self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg'])
+ @py3_only
def test_pep420_ns_package_no_includes_or_excludes(self):
- packages = find_420_packages(self.dist_dir)
- expected = [
- 'docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
+ packages = find_namespace_packages(self.dist_dir)
+ expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
self._assert_packages(packages, expected)
+ @py3_only
def test_regular_package_with_nested_pep420_ns_packages(self):
self._touch('__init__.py', self.pkg_dir)
- packages = find_420_packages(
+ packages = find_namespace_packages(
self.dist_dir, exclude=['docs', 'pkg.subpkg.assets'])
self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+ @py3_only
def test_pep420_ns_package_no_non_package_dirs(self):
shutil.rmtree(self.docs_dir)
shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
- packages = find_420_packages(self.dist_dir)
+ packages = find_namespace_packages(self.dist_dir)
self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+
--- /dev/null
+import mock
+from distutils import log
+
+import pytest
+
+from setuptools.command.register import register
+from setuptools.dist import Distribution
+
+
+class TestRegisterTest:
+ def test_warns_deprecation(self):
+ dist = Distribution()
+
+ cmd = register(dist)
+ cmd.run_command = mock.Mock()
+ cmd.send_metadata = mock.Mock()
+ cmd.announce = mock.Mock()
+
+ cmd.run()
+
+ cmd.announce.assert_called_with(
+ "WARNING: Registering is deprecated, use twine to upload instead "
+ "(https://pypi.org/p/twine/)",
+ log.WARN
+ )
+
+ def test_warns_deprecation_when_raising(self):
+ dist = Distribution()
+
+ cmd = register(dist)
+ cmd.run_command = mock.Mock()
+ cmd.send_metadata = mock.Mock()
+ cmd.send_metadata.side_effect = Exception
+ cmd.announce = mock.Mock()
+
+ with pytest.raises(Exception):
+ cmd.run()
+
+ cmd.announce.assert_called_with(
+ "WARNING: Registering is deprecated, use twine to upload instead "
+ "(https://pypi.org/p/twine/)",
+ log.WARN
+ )
--- /dev/null
+import mock
+from distutils import log
+
+import pytest
+
+from setuptools.command.upload import upload
+from setuptools.dist import Distribution
+
+
+class TestUploadTest:
+ def test_warns_deprecation(self):
+ dist = Distribution()
+ dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())]
+
+ cmd = upload(dist)
+ cmd.upload_file = mock.Mock()
+ cmd.announce = mock.Mock()
+
+ cmd.run()
+
+ cmd.announce.assert_called_once_with(
+ "WARNING: Uploading via this command is deprecated, use twine to "
+ "upload instead (https://pypi.org/p/twine/)",
+ log.WARN
+ )
+
+ def test_warns_deprecation_when_raising(self):
+ dist = Distribution()
+ dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())]
+
+ cmd = upload(dist)
+ cmd.upload_file = mock.Mock()
+ cmd.upload_file.side_effect = Exception
+ cmd.announce = mock.Mock()
+
+ with pytest.raises(Exception):
+ cmd.run()
+
+ cmd.announce.assert_called_once_with(
+ "WARNING: Uploading via this command is deprecated, use twine to "
+ "upload instead (https://pypi.org/p/twine/)",
+ log.WARN
+ )
{% set underline = underlines[0] %}{% if section %}{{section}}
{{ underline * section|length }}
{% endif %}
+
{% if sections[section] %}
{% for category, val in definitions.items() if category in sections[section]%}
{% if definitions[category]['showcontent'] %}
[testenv]
deps=-rtests/requirements.txt
+# Changed from default (`python -m pip ...`)
+# to prevent the current working directory
+# from being added to `sys.path`.
+install_command={envbindir}/pip install {opts} {packages}
+# Same as above.
+list_dependencies_command={envbindir}/pip freeze
setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname}
# TODO: The passed environment variables came from copying other tox.ini files
# These should probably be individually annotated to explain what needs them.