[bumpversion]
-current_version = 60.5.4
+current_version = 60.6.0
commit = True
tag = True
coverage:
status:
project:
- threshold: 0.5%
+ default:
+ threshold: 0.5%
validations:
required: true
-
-- type: checkboxes
- attributes:
- label: Code of Conduct
- description: |
- Read the [PSF Code of Conduct][CoC] first.
-
- [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
- options:
- - label: I agree to follow the PSF Code of Conduct
- required: true
...
-# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
-blank_issues_enabled: false # default: true
contact_links:
- name: 🤔 Have questions or need support?
url: https://github.com/pypa/setuptools/discussions
about: |
Please ask typical Q&A here: general ideas for Python packaging,
questions about structuring projects and so on
-- name: >-
- 💬 IRC: #pypa @ Freenode
- url: https://webchat.freenode.net/#pypa
+- name: 💬 Discord (chat)
+ url: https://discord.com/invite/pypa
about: Chat with devs
${{ matrix.python }}
test_cygwin:
- if: ${{ false }} # failing #3016
strategy:
matrix:
distutils:
SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }}
steps:
- uses: actions/checkout@v2
- - name: Install Cygwin with Python and tox
+ - name: Install Cygwin with Python
+ uses: cygwin/cygwin-install-action@v1
+ with:
+ platform: x86_64
+ packages: >-
+ git,
+ gcc-core,
+ python38,
+ python38-devel,
+ python38-pip
+ - name: Install tox
+ shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0}
run: |
- choco install git gcc-core python38-devel python38-pip --source cygwin
- C:\\tools\\cygwin\\bin\\bash -l -x -c 'python3.8 -m pip install tox'
+ python3.8 -m pip install tox
- name: Run tests
+ shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0}
run: |
- C:\\tools\\cygwin\\bin\\bash -l -x -c 'cd $(cygpath -u "$GITHUB_WORKSPACE") && tox -- --cov-report xml'
+ tox -- --cov-report xml
integration-test:
strategy:
+v60.6.0
+-------
+
+
+Changes
+^^^^^^^
+* #3043: Merge with pypa/distutils@bb018f1ac3 including consolidated behavior in sysconfig.get_platform (pypa/distutils#104).
+* #3057: Don't include optional ``Home-page`` in metadata if no ``url`` is specified. -- by :user:`cdce8p`
+* #3062: Merge with pypa/distutils@b53a824ec3 including improved support for lib directories on non-x64 Windows builds.
+
+Misc
+^^^^
+* #3054: Used Py3 syntax ``super().__init__()`` -- by :user:`imba-tjd`
+
+
v60.5.4
-------
Misc
^^^^
-* #NNN:
* #3009: Remove filtering of distutils warnings.
* #3031: Suppress distutils replacement when building or testing CPython.
User's Guide for instructions on installing, upgrading, and uninstalling
Setuptools.
-Questions and comments should be directed to the `distutils-sig
-mailing list <http://mail.python.org/pipermail/distutils-sig/>`_.
+Questions and comments should be directed to `GitHub Discussions
+<https://github.com/pypa/setuptools/discussions>`_.
Bug reports and especially tested patches may be
submitted directly to the `bug tracker
<https://github.com/pypa/setuptools/issues>`_.
===============
Everyone interacting in the setuptools project's codebases, issue trackers,
-chat rooms, and mailing lists is expected to follow the
+chat rooms, and fora is expected to follow the
`PSF Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>`_.
--- /dev/null
+Added documentation about wrapping ``setuptools.build_meta`` in a in-tree
+custom backend. This is a :pep:`517`-compliant way of dynamically specifying
+build dependencies (e.g. when platform, OS and other markers are not enough)
+-- by :user:`abravalheri`.
--- /dev/null
+Replaced occurrences of the defunct distutils-sig mailing list with pointers
+to GitHub Discussions.
+-- by :user:`ashemedai`
+
--- /dev/null
+The documentation has stopped suggesting to add ``wheel`` to
+:pep:`517` requirements -- by :user:`webknjaz`
The traditional ``setuptools`` way of packaging Python modules
uses a ``setup()`` function within the ``setup.py`` script. Commands such as
-``python setup.py bdist`` or ``python setup.py bdist_wheel`` generate a
-distribution bundle and ``python setup.py install`` installs the distribution.
-This interface makes it difficult to choose other packaging tools without an
+``python setup.py bdist`` or ``python setup.py bdist_wheel`` generate a
+distribution bundle and ``python setup.py install`` installs the distribution.
+This interface makes it difficult to choose other packaging tools without an
overhaul. Because ``setup.py`` scripts allowed for arbitrary execution, it
proved difficult to provide a reliable user experience across environments
and history.
`PEP 517 <https://www.python.org/dev/peps/pep-0517/>`_ therefore came to
-rescue and specified a new standard to
+rescue and specified a new standard to
package and distribute Python modules. Under PEP 517:
a ``pyproject.toml`` file is used to specify what program to use
- for generating distribution.
+ for generating distribution.
- Then, two functions provided by the program, ``build_wheel(directory: str)``
- and ``build_sdist(directory: str)`` create the distribution bundle at the
- specified ``directory``. The program is free to use its own configuration
- script or extend the ``.toml`` file.
+ Then, two functions provided by the program, ``build_wheel(directory: str)``
+ and ``build_sdist(directory: str)`` create the distribution bundle at the
+ specified ``directory``. The program is free to use its own configuration
+ script or extend the ``.toml`` file.
Lastly, ``pip install *.whl`` or ``pip install *.tar.gz`` does the actual
installation. If ``*.whl`` is available, ``pip`` will go ahead and copy
the files into ``site-packages`` directory. If not, ``pip`` will look at
- ``pyproject.toml`` and decide what program to use to 'build from source'
+ ``pyproject.toml`` and decide what program to use to 'build from source'
(the default is ``setuptools``)
With this standard, switching between packaging tools becomes a lot easier. ``build_meta``
setup.cfg
meowpkg/__init__.py
-The pyproject.toml file is required to specify the build system (i.e. what is
-being used to package your scripts and install from source). To use it with
+The pyproject.toml file is required to specify the build system (i.e. what is
+being used to package your scripts and install from source). To use it with
setuptools, the content would be::
[build-system]
- requires = ["setuptools", "wheel"]
+ requires = ["setuptools"]
build-backend = "setuptools.build_meta"
The ``setuptools`` package implements the ``build_sdist``
command and the ``wheel`` package implements the ``build_wheel``
-command; both are required to be compliant with PEP 517.
+command; the latter is a dependency of the former
+exposed via :pep:`517` hooks.
Use ``setuptools``' :ref:`declarative config <declarative config>` to
specify the package information::
name = meowpkg
version = 0.0.1
description = a package that meows
-
+
[options]
packages = find:
$ pip install -q build
$ python -m build
-And now it's done! The ``.whl`` file and ``.tar.gz`` can then be distributed
+And now it's done! The ``.whl`` file and ``.tar.gz`` can then be distributed
and installed::
dist/
or::
$ pip install dist/meowpkg-0.0.1.tar.gz
+
+Dynamic build dependencies and other ``build_meta`` tweaks
+----------------------------------------------------------
+
+With the changes introduced by :pep:`517` and :pep:`518`, the
+``setup_requires`` configuration field was made deprecated in ``setup.cfg`` and
+``setup.py``, in favour of directly listing build dependencies in the
+``requires`` field of the ``build-system`` table of ``pyproject.toml``.
+This approach has a series of advantages and gives package managers and
+installers the ability to inspect in advance the build requirements and
+perform a series of optimisations.
+
+However some package authors might still need to dynamically inspect the final
+users machine before deciding these requirements. One way of doing that, as
+specified by :pep:`517`, is to "tweak" ``setuptools.build_meta`` by using a
+:pep:`in-tree backend <517#in-tree-build-backends>`.
+
+.. tip:: Before implementing a *in-tree* backend, have a look on
+ :pep:`PEP 508 <508#environment-markers>`. Most of the times, dependencies
+ with **environment markers** are enough to differentiate operating systems
+ and platforms.
+
+If you add the following configuration to your ``pyprojec.toml``:
+
+
+.. code-block:: toml
+
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "backend"
+ backend-path = ["_custom_build"]
+
+
+then you should be able to implement a thin wrapper around ``build_meta`` in
+the ``_custom_build/backend.py`` file, as shown in the following example:
+
+.. code-block:: python
+
+ from setuptools import build_meta as _orig
+
+ prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel
+ build_wheel = _orig.build_wheel
+ build_sdist = _orig.build_sdist
+
+
+ def get_requires_for_build_wheel(self, config_settings=None):
+ return _orig.get_requires_for_build_wheel(config_settings) + [...]
+
+
+ def get_requires_for_build_sdist(self, config_settings=None):
+ return _orig.get_requires_for_build_sdist(config_settings) + [...]
+
+
+Note that you can override any of the functions specified in :pep:`PEP 517
+<517#build-backend-interface>`, not only the ones responsible for gathering
+requirements.
+
+.. important:: Make sure your backend script is included in the :doc:`source
+ distribution </userguide/distribution>`, otherwise the build will fail.
+ This can be done by using a SCM_/VCS_ plugin (like :pypi:`setuptools-scm`
+ and :pypi:`setuptools-svn`), or by correctly setting up :ref:`MANIFEST.in
+ <manifest>`.
+
+ If this is the first time you are using a customised backend, please have a
+ look on the generated ``.tar.gz`` and ``.whl``.
+ On POSIX systems that can be done with ``tar -tf dist/*.tar.gz``
+ and ``unzip -l dist/*.whl``.
+ On Windows systems you can rename the ``.whl`` to ``.zip`` to be able to
+ inspect it on the file explorer, and use the same ``tar`` command in a
+ command prompt (alternativelly there are GUI programs like `7-zip`_ that
+ handle ``.tar.gz``).
+
+ In general the backend script should be present in the ``.tar.gz`` (so the
+ project can be build from the source) but not in the ``.whl`` (otherwise the
+ backend script would end up being distributed alongside your package).
+ See ":doc:`/userguide/package_discovery`" for more details about package
+ files.
+
+
+.. _SCM: https://en.wikipedia.org/wiki/Software_configuration_management
+.. _VCS: https://en.wikipedia.org/wiki/Version_control
+.. _7-zip: https://www.7-zip.org
github_sponsors_url = f'{github_url}/sponsors'
extlinks = {
'user': (f'{github_sponsors_url}/%s', '@'), # noqa: WPS323
+ 'pypi': ('https://pypi.org/project/%s', '%s'),
}
extensions += ['sphinx.ext.extlinks']
Since the 49.1.2 release, Setuptools includes a local, vendored copy of distutils (from late copies of CPython) that is disabled by default. To enable the use of this copy of distutils when invoking setuptools, set the enviroment variable:
- SETUPTOOLS_USE_DISTUTILS=local
+ SETUPTOOLS_USE_DISTUTILS=local
This behavior is planned to become the default.
order to ensure that, once running, ``pkg_resources`` will know what project
and version is in use. The header script will check this and exit with an
error if the ``.egg`` file has been renamed or is invoked via a symlink that
-changes its base name.
\ No newline at end of file
+changes its base name.
Python Packaging Authority (PyPA) with several core contributors. All bugs
for Setuptools are filed and the canonical source is maintained in GitHub.
-User support and discussions are done through the issue tracker (for specific)
-issues, through the `distutils-sig mailing list <https://mail.python.org/mailman3/lists/distutils-sig.python.org/>`_, or on IRC (Freenode) at
-#pypa.
+User support and discussions are done through
+`GitHub Discussions <https://github.com/pypa/setuptools/discussions>`_,
+or the issue tracker (for specific issues).
-Discussions about development happen on the distutils-sig mailing list or on
-`Gitter <https://gitter.im/pypa/setuptools>`_.
+Discussions about development happen on GitHub Discussions or
+the ``setuptools`` channel on `PyPA Discord <https://discord.com/invite/pypa>`_.
-----------------
Authoring Tickets
``setuptools/_vendor/vendored.txt`` and
``pkg_resources/_vendor/vendored.txt``.
-All the dependencies specified in these files are "vendorized" using Paver_, a
-simple Python-based project scripting and task running tool.
+All the dependencies specified in these files are "vendorized" using a
+simple Python script ``tools/vendor.py``.
-To refresh the dependencies, you can run the following command (defined in
-``pavement.py``)::
+To refresh the dependencies, run the following command::
- $ paver update_vendored
-
-.. _Paver: https://pythonhosted.org/Paver/
+ $ tox -e vendor
-Mailing List and Bug Tracker
-============================
+Forum and Bug Tracker
+=====================
-Please use the `distutils-sig mailing list`_ for questions and discussion about
+Please use `GitHub Discussions`_ for questions and discussion about
setuptools, and the `setuptools bug tracker`_ ONLY for issues you have
-confirmed via the list are actual bugs, and which you have reduced to a minimal
+confirmed via the forum are actual bugs, and which you have reduced to a minimal
set of steps to reproduce.
-.. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/
+.. _GitHub Discussions: https://github.com/pypa/setuptools/discussions
.. _setuptools bug tracker: https://github.com/pypa/setuptools/
.. [#opt-1] In the ``package_data`` section, a key named with a single asterisk
(``*``) refers to all packages, in lieu of the empty string used in ``setup.py``.
-
+
.. [#opt-2] In the ``extras_require`` section, values are parsed as ``list-semi``.
This implies that in order to include markers, they **must** be *dangling*:
-
+
.. code-block:: ini
[options.extras_require]
.. code-block:: ini
[build-system]
- requires = ["setuptools", "wheel"]
+ requires = ["setuptools"]
#...
.. note::
---------------------------------
Setuptools can work well with most versioning schemes. Over the years,
-setuptools has tried to closely follow the
+setuptools has tried to closely follow the
`PEP 440 <https://www.python.org/dev/peps/pep-0440/>`_ scheme, but it
also supports legacy versions. There are, however, a
few special things to watch out for, in order to ensure that setuptools and
.. code-block:: toml
[build-system]
- requires = ["setuptools", "wheel"]
+ requires = ["setuptools"]
build-backend = "setuptools.build_meta"
Then, you will need a ``setup.cfg`` or ``setup.py`` to specify your package
Automatic package discovery
===========================
For simple projects, it's usually easy enough to manually add packages to
-the ``packages`` keyword in ``setup.cfg``. However, for very large projects,
+the ``packages`` keyword in ``setup.cfg``. However, for very large projects,
it can be a big burden to keep the package list updated. ``setuptools``
therefore provides two convenient tools to ease the burden: :literal:`find:\ ` and
:literal:`find_namespace:\ `. To use it in your project:
.. tip::
- Prior to :ref:`pip v21.1 <pip:v21-1>`, a ``setup.py`` script was
- required to be compatible with development mode. With late
- versions of pip, any project may be installed in this mode.
+ Prior to :ref:`pip v21.1 <pip:v21-1>`, a ``setup.py`` script was
+ required to be compatible with development mode. With late
+ versions of pip, any project may be installed in this mode.
``setuptools`` allows you to install a package without copying any files
to your interpreter directory (e.g. the ``site-packages`` directory).
"""Provider based on a virtual filesystem"""
def __init__(self, module):
- NullProvider.__init__(self, module)
+ super().__init__(module)
self._setup_prefix()
def _setup_prefix(self):
_zip_manifests = MemoizedZipManifests()
def __init__(self, module):
- EggProvider.__init__(self, module)
+ super().__init__(module)
self.zip_pre = self.loader.archive + os.sep
def _zipinfo_name(self, fspath):
@functools.singledispatch
def yield_lines(iterable):
- """Yield valid lines of a string or iterable"""
+ r"""
+ Yield valid lines of a string or iterable.
+
+ >>> list(yield_lines(''))
+ []
+ >>> list(yield_lines(['foo', 'bar']))
+ ['foo', 'bar']
+ >>> list(yield_lines('foo\nbar'))
+ ['foo', 'bar']
+ >>> list(yield_lines('\nfoo\n#bar\nbaz #comment'))
+ ['foo', 'baz #comment']
+ >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n']))
+ ['foo', 'bar', 'baz', 'bing']
+ """
return itertools.chain.from_iterable(map(yield_lines, iterable))
warnings.warn(stacklevel=level + 1, *args, **kw)
-def parse_requirements(strs):
- """Yield ``Requirement`` objects for each specification in `strs`
+def drop_comment(line):
+ """
+ Drop comments.
- `strs` must be a string, or a (possibly-nested) iterable thereof.
+ >>> drop_comment('foo # bar')
+ 'foo'
+
+ A hash without a space may be in a URL.
+
+ >>> drop_comment('http://example.com/foo#bar')
+ 'http://example.com/foo#bar'
"""
- # create a steppable iterator, so we can handle \-continuations
- lines = iter(yield_lines(strs))
+ return line.partition(' #')[0]
+
+
+def join_continuation(lines):
+ r"""
+ Join lines continued by a trailing backslash.
- for line in lines:
- # Drop comments -- a hash without a space may be in a URL.
- if ' #' in line:
- line = line[:line.find(' #')]
- # If there is a line continuation, drop it, and append the next line.
- if line.endswith('\\'):
- line = line[:-2].strip()
+ >>> list(join_continuation(['foo \\', 'bar', 'baz']))
+ ['foobar', 'baz']
+ >>> list(join_continuation(['foo \\', 'bar', 'baz']))
+ ['foobar', 'baz']
+ >>> list(join_continuation(['foo \\', 'bar \\', 'baz']))
+ ['foobarbaz']
+
+ Not sure why, but...
+ The character preceeding the backslash is also elided.
+
+ >>> list(join_continuation(['goo\\', 'dly']))
+ ['godly']
+
+ A terrible idea, but...
+ If no line is available to continue, suppress the lines.
+
+ >>> list(join_continuation(['foo', 'bar\\', 'baz\\']))
+ ['foo']
+ """
+ lines = iter(lines)
+ for item in lines:
+ while item.endswith('\\'):
try:
- line += next(lines)
+ item = item[:-2].strip() + next(lines)
except StopIteration:
return
- yield Requirement(line)
+ yield item
+
+
+def parse_requirements(strs):
+ """
+ Yield ``Requirement`` objects for each specification in `strs`.
+
+ `strs` must be a string, or a (possibly-nested) iterable thereof.
+ """
+ return map(Requirement, join_continuation(map(drop_comment, yield_lines(strs))))
class RequirementParseError(packaging.requirements.InvalidRequirement):
[metadata]
name = setuptools
-version = 60.5.4
+version = 60.6.0
author = Python Packaging Authority
author_email = distutils-sig@python.org
description = Easily download, build, install, upgrade, and uninstall Python packages
jaraco.path>=3.2.0
build[virtualenv]
filelock>=3.4.0
+ pip_run>=8.8
testing-integration =
pytest
def __init__(self, attrs):
_incl = 'dependency_links', 'setup_requires'
filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
- distutils.core.Distribution.__init__(self, filtered)
+ super().__init__(filtered)
def finalize_options(self):
"""
Construct the command for dist, updating
vars(self) with any keyword parameters.
"""
- _Command.__init__(self, dist)
+ super().__init__(dist)
vars(self).update(kw)
def _ensure_stringlike(self, option, what, default=None):
def __init__(self, verbose=0, dry_run=0, force=0):
- CCompiler.__init__ (self, verbose, dry_run, force)
+ super().__init__(verbose, dry_run, force)
# target platform (.plat_name is consistent with 'bdist')
self.plat_name = None
self.initialized = False
dry_run=0,
force=0):
- CCompiler.__init__ (self, verbose, dry_run, force)
+ super().__init__(verbose, dry_run, force)
# These executables are assumed to all be in the path.
# Borland doesn't seem to use any special registry settings to
def __init__(self, *args, **kw):
"""Dialog(database, name, x, y, w, h, attributes, title, first,
default, cancel, bitmap=true)"""
- Dialog.__init__(self, *args)
+ super().__init__(*args)
ruler = self.h - 36
bmwidth = 152*ruler/328
#if kw.get("bitmap", True):
def __init__(self, source, report_level, halt_level, stream=None,
debug=0, encoding='ascii', error_handler='replace'):
self.messages = []
- Reporter.__init__(self, source, report_level, halt_level, stream,
+ super().__init__(source, report_level, halt_level, stream,
debug, encoding, error_handler)
def system_message(self, level, message, *children, **kwargs):
INSTALL_SCHEMES['nt_user'] = {
'purelib': '{usersite}',
'platlib': '{usersite}',
- 'headers': '{userbase}/{implementation}{py_version_nodot}/Include/{dist_name}',
- 'scripts': '{userbase}/{implementation}{py_version_nodot}/Scripts',
+ 'headers': '{userbase}/{implementation}{py_version_nodot_plat}/Include/{dist_name}',
+ 'scripts': '{userbase}/{implementation}{py_version_nodot_plat}/Scripts',
'data' : '{userbase}',
}
'implementation': _get_implementation(),
}
+ # vars for compatibility on older Pythons
+ compat_vars = dict(
+ # Python 3.9 and earlier
+ py_version_nodot_plat=getattr(sys, 'winver', '').replace('.', ''),
+ )
+
if HAS_USER_SITE:
local_vars['userbase'] = self.install_userbase
local_vars['usersite'] = self.install_usersite
self.config_vars = _collections.DictStack(
- [sysconfig.get_config_vars(), local_vars])
+ [compat_vars, sysconfig.get_config_vars(), local_vars])
self.expand_basedirs()
def __init__(self, verbose=0, dry_run=0, force=0):
- UnixCCompiler.__init__(self, verbose, dry_run, force)
+ super().__init__(verbose, dry_run, force)
status, details = check_config_h()
self.debug_print("Python's GCC status: %s (details: %s)" %
def __init__(self, verbose=0, dry_run=0, force=0):
- CygwinCCompiler.__init__ (self, verbose, dry_run, force)
+ super().__init__ (verbose, dry_run, force)
shared_option = "-shared"
exe_extension = '.exe'
def __init__(self, verbose=0, dry_run=0, force=0):
- CCompiler.__init__ (self, verbose, dry_run, force)
+ super().__init__(verbose, dry_run, force)
self.__version = VERSION
self.__root = r"Software\Microsoft\VisualStudio"
# self.__macros = MACROS
exe_extension = '.exe'
def __init__(self, verbose=0, dry_run=0, force=0):
- CCompiler.__init__ (self, verbose, dry_run, force)
+ super().__init__(verbose, dry_run, force)
self.__version = get_build_version()
self.__arch = get_build_architecture()
if self.__arch == "Intel":
import os
import subprocess
-from distutils.errors import DistutilsPlatformError, DistutilsExecError
+from distutils.errors import DistutilsExecError
from distutils.debug import DEBUG
from distutils import log
class command(PyPIRCCommand):
def __init__(self, dist):
- PyPIRCCommand.__init__(self, dist)
+ super().__init__(dist)
def initialize_options(self):
pass
finalize_options = initialize_options
install_module.USER_SITE = self.user_site
def _expanduser(path):
- return self.tmpdir
+ if path.startswith('~'):
+ return os.path.normpath(self.tmpdir + path[1:])
+ return path
self.old_expand = os.path.expanduser
os.path.expanduser = _expanduser
self.assertIn('userbase', cmd.config_vars)
self.assertIn('usersite', cmd.config_vars)
+ actual_headers = os.path.relpath(cmd.install_headers, self.user_base)
+ if os.name == 'nt':
+ site_path = os.path.relpath(
+ os.path.dirname(self.old_user_site), self.old_user_base)
+ include = os.path.join(site_path, 'Include')
+ else:
+ include = sysconfig.get_python_inc(0, '')
+ expect_headers = os.path.join(include, 'xx')
+
+ self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers))
+
def test_handle_extra_path(self):
dist = Distribution({'name': 'xx', 'extra_path': 'path,dirs'})
cmd = install(dist)
import os
import sys
import unittest
+import sysconfig as stdlib_sysconfig
from copy import copy
from test.support import run_unittest
from unittest import mock
from distutils.util import (get_platform, convert_path, change_root,
check_environ, split_quoted, strtobool,
rfc822_escape, byte_compile,
- grok_environment_error)
+ grok_environment_error, get_host_platform)
from distutils import util # used to patch _environ_checked
-from distutils.sysconfig import get_config_vars
from distutils import sysconfig
from distutils.tests import support
-import _osx_support
class UtilTestCase(support.EnvironGuard, unittest.TestCase):
def _get_uname(self):
return self._uname
- def test_get_platform(self):
-
- # windows XP, 32bits
- os.name = 'nt'
- sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) '
- '[MSC v.1310 32 bit (Intel)]')
- sys.platform = 'win32'
- self.assertEqual(get_platform(), 'win32')
-
- # windows XP, amd64
- os.name = 'nt'
- sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) '
- '[MSC v.1310 32 bit (Amd64)]')
- sys.platform = 'win32'
- self.assertEqual(get_platform(), 'win-amd64')
-
- # macbook
- os.name = 'posix'
- sys.version = ('2.5 (r25:51918, Sep 19 2006, 08:49:13) '
- '\n[GCC 4.0.1 (Apple Computer, Inc. build 5341)]')
- sys.platform = 'darwin'
- self._set_uname(('Darwin', 'macziade', '8.11.1',
- ('Darwin Kernel Version 8.11.1: '
- 'Wed Oct 10 18:23:28 PDT 2007; '
- 'root:xnu-792.25.20~1/RELEASE_I386'), 'i386'))
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
-
- get_config_vars()['CFLAGS'] = ('-fno-strict-aliasing -DNDEBUG -g '
- '-fwrapv -O3 -Wall -Wstrict-prototypes')
-
- cursize = sys.maxsize
- sys.maxsize = (2 ** 31)-1
- try:
- self.assertEqual(get_platform(), 'macosx-10.3-i386')
- finally:
- sys.maxsize = cursize
-
- # macbook with fat binaries (fat, universal or fat64)
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['MACOSX_DEPLOYMENT_TARGET'] = '10.4'
- get_config_vars()['CFLAGS'] = ('-arch ppc -arch i386 -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3')
-
- self.assertEqual(get_platform(), 'macosx-10.4-fat')
-
- _osx_support._remove_original_values(get_config_vars())
- os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.1'
- self.assertEqual(get_platform(), 'macosx-10.4-fat')
-
+ def test_get_host_platform(self):
+ with unittest.mock.patch('os.name', 'nt'):
+ with unittest.mock.patch('sys.version', '... [... (ARM64)]'):
+ self.assertEqual(get_host_platform(), 'win-arm64')
+ with unittest.mock.patch('sys.version', '... [... (ARM)]'):
+ self.assertEqual(get_host_platform(), 'win-arm32')
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch i386 -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3')
+ with unittest.mock.patch('sys.version_info', (3, 9, 0, 'final', 0)):
+ self.assertEqual(get_host_platform(), stdlib_sysconfig.get_platform())
- self.assertEqual(get_platform(), 'macosx-10.4-intel')
-
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc -arch i386 -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3')
- self.assertEqual(get_platform(), 'macosx-10.4-fat3')
-
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['CFLAGS'] = ('-arch ppc64 -arch x86_64 -arch ppc -arch i386 -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3')
- self.assertEqual(get_platform(), 'macosx-10.4-universal')
-
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc64 -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3')
-
- self.assertEqual(get_platform(), 'macosx-10.4-fat64')
-
- for arch in ('ppc', 'i386', 'x86_64', 'ppc64'):
- _osx_support._remove_original_values(get_config_vars())
- get_config_vars()['CFLAGS'] = ('-arch %s -isysroot '
- '/Developer/SDKs/MacOSX10.4u.sdk '
- '-fno-strict-aliasing -fno-common '
- '-dynamic -DNDEBUG -g -O3'%(arch,))
-
- self.assertEqual(get_platform(), 'macosx-10.4-%s'%(arch,))
-
-
- # linux debian sarge
- os.name = 'posix'
- sys.version = ('2.3.5 (#1, Jul 4 2007, 17:28:59) '
- '\n[GCC 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)]')
- sys.platform = 'linux2'
- self._set_uname(('Linux', 'aglae', '2.6.21.1dedibox-r7',
- '#1 Mon Apr 30 17:25:38 CEST 2007', 'i686'))
-
- self.assertEqual(get_platform(), 'linux-i686')
-
- # XXX more platforms to tests here
+ def test_get_platform(self):
+ with unittest.mock.patch('os.name', 'nt'):
+ with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x86'}):
+ self.assertEqual(get_platform(), 'win32')
+ with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x64'}):
+ self.assertEqual(get_platform(), 'win-amd64')
+ with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}):
+ self.assertEqual(get_platform(), 'win-arm32')
+ with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'}):
+ self.assertEqual(get_platform(), 'win-arm64')
def test_convert_path(self):
# linux/mac
import importlib.util
import string
import sys
+import sysconfig
from distutils.errors import DistutilsPlatformError
from distutils.dep_util import newer
from distutils.spawn import spawn
def get_host_platform():
"""Return a string that identifies the current platform. This is used mainly to
distinguish platform-specific build directories and platform-specific built
- distributions. Typically includes the OS name and version and the
- architecture (as supplied by 'os.uname()'), although the exact information
- included depends on the OS; eg. on Linux, the kernel version isn't
- particularly important.
-
- Examples of returned values:
- linux-i586
- linux-alpha (?)
- solaris-2.6-sun4u
-
- Windows will return one of:
- win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
- win32 (all others - specifically, sys.platform is returned)
-
- For other non-POSIX platforms, currently just returns 'sys.platform'.
-
+ distributions.
"""
- if os.name == 'nt':
- if 'amd64' in sys.version.lower():
- return 'win-amd64'
- if '(arm)' in sys.version.lower():
- return 'win-arm32'
- if '(arm64)' in sys.version.lower():
- return 'win-arm64'
- return sys.platform
-
- # Set for cross builds explicitly
- if "_PYTHON_HOST_PLATFORM" in os.environ:
- return os.environ["_PYTHON_HOST_PLATFORM"]
-
- if os.name != "posix" or not hasattr(os, 'uname'):
- # XXX what about the architecture? NT is Intel or Alpha,
- # Mac OS is M68k or PPC, etc.
- return sys.platform
-
- # Try to distinguish various flavours of Unix
-
- (osname, host, release, version, machine) = os.uname()
-
- # Convert the OS name to lowercase, remove '/' characters, and translate
- # spaces (for "Power Macintosh")
- osname = osname.lower().replace('/', '')
- machine = machine.replace(' ', '_')
- machine = machine.replace('/', '-')
-
- if osname[:5] == "linux":
- # At least on Linux/Intel, 'machine' is the processor --
- # i386, etc.
- # XXX what about Alpha, SPARC, etc?
- return "%s-%s" % (osname, machine)
- elif osname[:5] == "sunos":
- if release[0] >= "5": # SunOS 5 == Solaris 2
- osname = "solaris"
- release = "%d.%s" % (int(release[0]) - 3, release[2:])
- # We can't use "platform.architecture()[0]" because a
- # bootstrap problem. We use a dict to get an error
- # if some suspicious happens.
- bitness = {2147483647:"32bit", 9223372036854775807:"64bit"}
- machine += ".%s" % bitness[sys.maxsize]
- # fall through to standard osname-release-machine representation
- elif osname[:3] == "aix":
- from .py38compat import aix_platform
- return aix_platform(osname, version, release)
- elif osname[:6] == "cygwin":
- osname = "cygwin"
- rel_re = re.compile (r'[\d.]+', re.ASCII)
- m = rel_re.match(release)
- if m:
- release = m.group()
- elif osname[:6] == "darwin":
- import _osx_support, distutils.sysconfig
- osname, release, machine = _osx_support.get_platform_osx(
- distutils.sysconfig.get_config_vars(),
- osname, release, machine)
-
- return "%s-%s-%s" % (osname, release, machine)
+
+ # We initially exposed platforms as defined in Python 3.9
+ # even with older Python versions when distutils was split out.
+ # Now that we delegate to stdlib sysconfig we need to restore this
+ # in case anyone has started to depend on it.
+
+ if sys.version_info < (3, 8):
+ if os.name == 'nt':
+ if '(arm)' in sys.version.lower():
+ return 'win-arm32'
+ if '(arm64)' in sys.version.lower():
+ return 'win-arm64'
+
+ if sys.version_info < (3, 9):
+ if os.name == "posix" and hasattr(os, 'uname'):
+ osname, host, release, version, machine = os.uname()
+ if osname[:3] == "aix":
+ from .py38compat import aix_platform
+ return aix_platform(osname, version, release)
+
+ return sysconfig.get_platform()
def get_platform():
if os.name == 'nt':
'implementation': install._get_implementation(),
})
+ # pypa/distutils#113 Python 3.9 compat
+ self.config_vars.setdefault(
+ 'py_version_nodot_plat',
+ getattr(sys, 'windir', '').replace('.', ''),
+ )
+
if site.ENABLE_USER_SITE:
self.config_vars['userbase'] = self.install_userbase
self.config_vars['usersite'] = self.install_usersite
self.sitedirs = list(map(normalize_path, sitedirs))
self.basedir = normalize_path(os.path.dirname(self.filename))
self._load()
- Environment.__init__(self, [], None, None)
+ super().__init__([], None, None)
for path in yield_lines(self.paths):
list(map(self.add, find_distributions(path, True)))
self.author_email = _read_field_from_msg(msg, 'author-email')
self.maintainer_email = None
self.url = _read_field_from_msg(msg, 'home-page')
+ self.download_url = _read_field_from_msg(msg, 'download-url')
self.license = _read_field_unescaped_from_msg(msg, 'license')
- if 'download-url' in msg:
- self.download_url = _read_field_from_msg(msg, 'download-url')
- else:
- self.download_url = None
-
self.long_description = _read_field_unescaped_from_msg(msg, 'description')
if (
self.long_description is None and
write_field('Name', self.get_name())
write_field('Version', self.get_version())
write_field('Summary', single_line(self.get_description()))
- write_field('Home-page', self.get_url())
optional_fields = (
+ ('Home-page', 'url'),
+ ('Download-URL', 'download_url'),
('Author', 'author'),
('Author-email', 'author_email'),
('Maintainer', 'maintainer'),
license = rfc822_escape(self.get_license())
write_field('License', license)
- if self.download_url:
- write_field('Download-URL', self.download_url)
for project_url in self.project_urls.items():
write_field('Project-URL', '%s, %s' % project_url)
)
self._finalize_requires()
+ def _validate_metadata(self):
+ required = {"name"}
+ provided = {
+ key
+ for key in vars(self.metadata)
+ if getattr(self.metadata, key, None) is not None
+ }
+ missing = required - provided
+
+ if missing:
+ msg = f"Required package metadata is missing: {missing}"
+ raise DistutilsSetupError(msg)
+
def _set_metadata_defaults(self, attrs):
"""
Fill-in missing metadata fields not supported by distutils.
# The *args is needed for compatibility as calls may use positional
# arguments. py_limited_api may be set only via keyword.
self.py_limited_api = kw.pop("py_limited_api", False)
- _Extension.__init__(self, name, sources, *args, **kw)
+ super().__init__(name, sources, *args, **kw)
def _convert_pyx_sources_to_lang(self):
"""
format="{message}", style='{', handlers=handlers, level=logging.DEBUG)
monkey.patch_func(set_threshold, distutils.log, 'set_threshold')
+ # For some reason `distutils.log` module is getting cached in `distutils.dist`
+ # and then loaded again when patched,
+ # implying: id(distutils.log) != id(distutils.dist.log).
+ # Make sure the same module object is used everywhere:
+ distutils.dist.log = distutils.log
+
def set_threshold(level):
logging.root.setLevel(level*10)
self, index_url="https://pypi.org/simple/", hosts=('*',),
ca_bundle=None, verify_ssl=True, *args, **kw
):
- Environment.__init__(self, *args, **kw)
+ super().__init__(*args, **kw)
self.index_url = index_url + "/" [:not index_url.endswith('/')]
self.scanned_urls = {}
self.fetched_urls = {}
Load from ~/.pypirc
"""
defaults = dict.fromkeys(['username', 'password', 'repository'], '')
- configparser.RawConfigParser.__init__(self, defaults)
+ super().__init__(defaults)
rc = os.path.join(os.path.expanduser('~'), '.pypirc')
if os.path.exists(rc):
+++ /dev/null
-mock
-pytest-flake8
-flake8-2020; python_version>="3.6"
-virtualenv>=13.0.0
-pytest-virtualenv>=1.2.7
-pytest>=3.7
-wheel
-coverage>=4.5.1
-pytest-cov>=2.5.1
-paver; python_version>="3.6"
-futures; python_version=="2.7"
-pip>=19.1 # For proper file:// URLs support.
-jaraco.envs
-sphinx
SETUP_PY = """\
from setuptools import setup
-setup(name='foo', py_modules=['hi'])
+setup(py_modules=['hi'])
"""
dist = Distribution(dict(
script_name='setup.py',
script_args=['bdist_egg', '--exclude-source-files'],
- name='foo',
py_modules=['hi'],
))
with contexts.quiet():
script_name='setup.py',
script_args=['build_py'],
packages=[''],
- name='foo',
package_data={'': ['path/*']},
))
os.makedirs('path/subpath')
script_args=['build_py'],
packages=['pkg'],
package_data={'pkg': ['data.dat']},
- name='pkg',
))
os.makedirs('pkg')
open('pkg/__init__.py', 'w').close()
script_args=['build_py'],
packages=['pkg'],
package_data={'pkg': ['run-me']},
- name='pkg',
))
os.makedirs('pkg')
open('pkg/__init__.py', 'w').close()
import subprocess
import platform
import pathlib
-import textwrap
from setuptools.command import test
import pytest
+import pip_run.launch
from setuptools.command.develop import develop
from setuptools.dist import Distribution
with test.test.paths_on_pythonpath([str(target)]):
subprocess.check_call(pkg_resources_imp)
- @staticmethod
- def install_workaround(site_packages):
- site_packages.mkdir(parents=True)
- sc = site_packages / 'sitecustomize.py'
- sc.write_text(
- textwrap.dedent(
- """
- import site
- import pathlib
- here = pathlib.Path(__file__).parent
- site.addsitedir(str(here))
- """
- ).lstrip()
- )
-
@pytest.mark.xfail(
platform.python_implementation() == 'PyPy',
reason="Workaround fails on PyPy (why?)",
Editable install to a prefix should be discoverable.
"""
prefix = tmp_path / 'prefix'
- prefix.mkdir()
# figure out where pip will likely install the package
site_packages = prefix / next(
for path in sys.path
if 'site-packages' in path and path.startswith(sys.prefix)
)
+ site_packages.mkdir(parents=True)
- # install the workaround
- self.install_workaround(site_packages)
+ # install workaround
+ pip_run.launch.inject_sitecustomize(str(site_packages))
env = dict(os.environ, PYTHONPATH=str(site_packages))
cmd = [
)
def test_rfc822_unescape(content, result):
assert (result or content) == rfc822_unescape(rfc822_escape(content))
+
+
+def test_metadata_name():
+ with pytest.raises(DistutilsSetupError, match='missing.*name'):
+ Distribution()._validate_metadata()
SETUP_PY = DALS("""
from setuptools import setup
- setup(name='foo')
+ setup()
""")
--- /dev/null
+import logging
+
+import pytest
+
+
+setup_py = """\
+from setuptools import setup
+
+setup(
+ name="test_logging",
+ version="0.0"
+)
+"""
+
+
+@pytest.mark.parametrize(
+ "flag, expected_level", [("--dry-run", "INFO"), ("--verbose", "DEBUG")]
+)
+def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level):
+ """Make sure the correct verbosity level is set (issue #3038)"""
+ import setuptools # noqa: Import setuptools to monkeypatch distutils
+ import distutils # <- load distutils after all the patches take place
+
+ logger = logging.Logger(__name__)
+ monkeypatch.setattr(logging, "root", logger)
+ unset_log_level = logger.getEffectiveLevel()
+ assert logging.getLevelName(unset_log_level) == "NOTSET"
+
+ setup_script = tmp_path / "setup.py"
+ setup_script.write_text(setup_py)
+ dist = distutils.core.run_setup(setup_script, stop_after="init")
+ dist.script_args = [flag, "sdist"]
+ dist.parse_command_line() # <- where the log level is set
+ log_level = logger.getEffectiveLevel()
+ log_level_name = logging.getLevelName(log_level)
+ assert log_level_name == expected_level
class TestSphinxUploadDocs:
def test_sphinx_doc(self):
params = dict(
- name='foo',
packages=['test'],
)
dist = Distribution(params)
@pytest.mark.usefixtures('tmpdir_cwd')
def test_tests_are_run_once(capfd):
params = dict(
- name='foo',
packages=['dummy'],
)
files = {
'setup.py': DALS("""
from setuptools import setup
- setup(name='foo')
+ setup()
"""),
'build': {
'index.html': 'Hello world.',