Commit 8cca69b8 authored by Jason R. Coombs's avatar Jason R. Coombs

Merge branch 'master' into feature/2093-docs-revamp

parents f852d159 662816b6
[bumpversion]
current_version = 46.4.0
current_version = 50.3.0
commit = True
tag = True
......
[run]
source=
pkg_resources
setuptools
omit=
*/_vendor/*
[report]
---
name: Setuptools warns about Python 2 incompatibility
about: Report the issue where setuptools 45 or later stops working on Python 2
title: Incompatible install in (summarize your environment)
labels: Python 2
assignees: ''
---
<!--
Please DO NOT SUBMIT this template without first investigating the issue and answering the questions below. This template is intended mainly for developers of systems and not for end users. If you are an end user experiencing the warning, please work with your system maintainers (starting with the project you're trying to use) to report the issue.
If you did not intend to use this template, but only meant to file a blank issue, just hit the back button and click "Open a blank issue".
It's by design that Setuptools 45 and later will stop working on Python 2. To ease the transition, Setuptools 45 was released to continue to have Python 2 compatibility, but emit a strenuous warning that it will stop working.
In most cases, using pip 9 or later to install Setuptools from PyPI or any index supporting the Requires-Python metadata will do the right thing and install Setuptools 44.x on Python 2.
If you've come to file an issue, it's probably because some process managed to bypass these protections.
Your first course of action should be to reason about how you managed to get an unsupported version of Setuptools on Python 2. Please complete the sections below and provide any other detail about your environment that will help us help you.
-->
## Prerequisites
<!-- These are the recommended workarounds for the issue. Please
try them first. -->
- [ ] Python 2 is required for this application.
- [ ] I maintain the software that installs Setuptools (if not, please contact that project).
- [ ] Setuptools installed with pip 9 or later.
- [ ] Pinning Setuptools to `setuptools<45` in the environment was unsuccessful.
## Environment Details
- Operating System and version:
- Python version:
- Python installed how:
- Virtualenv version (if using virtualenv): n/a
Command(s) used to install setuptools (and output):
```
```
Output of `pip --version` when installing setuptools:
```
```
## Other notes
......@@ -25,23 +25,54 @@ jobs:
- 3.6
- 3.5
os:
- ubuntu-latest
- ubuntu-18.04
- ubuntu-16.04
- macOS-latest
# - windows-2019
# - windows-2016
include:
# Pre-release versions (GH-shipped)
- os: ubuntu-20.04
python-version: 3.9.0-beta.4 - 3.9.0
# Pre-release versions (deadsnakes)
- os: ubuntu-20.04
python-version: 3.9-beta
# Dev versions (deadsnakes)
- os: ubuntu-20.04
python-version: 3.9-dev
- os: ubuntu-20.04
python-version: 3.8-dev
env:
NETWORK_REQUIRED: 1
PYTHON_VERSION: ${{ matrix.python-version }}
TOX_PARALLEL_NO_SPINNER: 1
TOXENV: python
USE_DEADSNAKES: false
steps:
- uses: actions/checkout@master
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1.1.1
- name: Set flag to use deadsnakes
if: >-
endsWith(env.PYTHON_VERSION, '-beta') ||
endsWith(env.PYTHON_VERSION, '-dev')
run: |
from __future__ import print_function
python_version = '${{ env.PYTHON_VERSION }}'.replace('-beta', '')
print('::set-env name=PYTHON_VERSION::{ver}'.format(ver=python_version))
print('::set-env name=USE_DEADSNAKES::true')
shell: python
- name: Set up Python ${{ env.PYTHON_VERSION }} (deadsnakes)
uses: deadsnakes/action@v1.0.0
if: fromJSON(env.USE_DEADSNAKES) && true || false
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v2.1.1
if: >-
!fromJSON(env.USE_DEADSNAKES) && true || false
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ env.PYTHON_VERSION }}
- name: Log Python version
run: >-
python --version
......@@ -73,9 +104,9 @@ jobs:
run: >-
python -m pip freeze --all
- name: Adjust TOXENV for PyPy
if: startsWith(matrix.python-version, 'pypy')
if: startsWith(env.PYTHON_VERSION, 'pypy')
run: >-
echo "::set-env name=TOXENV::${{ matrix.python-version }}"
echo "::set-env name=TOXENV::${{ env.PYTHON_VERSION }}"
- name: Log env vars
run: >-
env
......@@ -91,6 +122,7 @@ jobs:
python -m
tox
--parallel auto
--parallel-live
--notest
--skip-missing-interpreters false
- name: Test with tox
......@@ -98,5 +130,6 @@ jobs:
python -m
tox
--parallel auto
--parallel-live
--
--cov
-vvvvv
......@@ -4,13 +4,7 @@ language: python
jobs:
fast_finish: true
include:
- &latest_py2
python: 2.7
env: TOXENV=py27
- <<: *latest_py2
env: LANG=C TOXENV=py27
- python: pypy3
env: DISABLE_COVERAGE=1 # Don't run coverage on pypy (too slow).
- python: 3.5
- python: 3.6
- python: 3.7
......@@ -19,11 +13,14 @@ jobs:
- <<: *latest_py3
env: LANG=C
- python: 3.8-dev
- python: 3.9-dev
- <<: *latest_py3
env: TOXENV=docs DISABLE_COVERAGE=1
env: TOXENV=docs
allow_failures:
# suppress failures due to pypa/setuptools#2000
- python: pypy3
- <<: *latest_py3
env: TOXENV=docs
cache: pip
......@@ -42,22 +39,8 @@ install:
script:
- export NETWORK_REQUIRED=1
- |
( # Run testsuite.
if [ -z "$DISABLE_COVERAGE" ]
then
tox -- --cov
else
tox
fi
)
- tox
after_success:
- |
( # Upload coverage data.
if [ -z "$DISABLE_COVERAGE" ]
then
export TRAVIS_JOB_NAME="${TRAVIS_PYTHON_VERSION} (LANG=$LANG)" CODECOV_ENV=TRAVIS_JOB_NAME
tox -e coverage,codecov
fi
)
- export TRAVIS_JOB_NAME="${TRAVIS_PYTHON_VERSION} (LANG=$LANG)" CODECOV_ENV=TRAVIS_JOB_NAME
- tox -e coverage,codecov
v50.3.0
-------
* #2368: In distutils, restore support for monkeypatched CCompiler.spawn per pypa/distutils#15.
v50.2.0
-------
* #2355: When pip is imported as part of a build, leave distutils patched.
* #2380: There are some setuptools specific changes in the
`setuptools.command.bdist_rpm` module that are no longer needed, because
they are part of the `bdist_rpm` module in distutils in Python
3.5.0. Therefore, code was removed from `setuptools.command.bdist_rpm`.
v50.1.0
-------
* #2350: Setuptools reverts using the included distutils by default. Platform maintainers and system integrators and others are *strongly* encouraged to set ``SETUPTOOLS_USE_DISTUTILS=local`` to help identify and work through the reported issues with distutils adoption, mainly to file issues and pull requests with pypa/distutils such that distutils performs as needed across every supported environment.
v50.0.3
-------
* #2363: Restore link_libpython support on Python 3.7 and earlier (see pypa/distutils#9).
v50.0.2
-------
* #2352: In distutils hack, use absolute import rather than relative to avoid bpo-30876.
v50.0.1
-------
* #2357: Restored Python 3.5 support in distutils.util for missing `subprocess._optim_args_from_interpreter_flags`.
* #2358: Restored AIX support on Python 3.8 and earlier.
* #2361: Add Python 3.10 support to _distutils_hack. Get the 'Loader' abstract class
from importlib.abc rather than importlib.util.abc (alias removed in Python
3.10).
v50.0.0
-------
* #2232: Once again, Setuptools overrides the stdlib distutils on import. For environments or invocations where this behavior is undesirable, users are provided with a temporary escape hatch. If the environment variable ``SETUPTOOLS_USE_DISTUTILS`` is set to ``stdlib``, Setuptools will fall back to the legacy behavior. Use of this escape hatch is discouraged, but it is provided to ease the transition while proper fixes for edge cases can be addressed.
* #2334: In MSVC module, refine text in error message.
v49.6.0
-------
* #2129: In pkg_resources, no longer detect any pathname ending in .egg as a Python egg. Now the path must be an unpacked egg or a zip file.
v49.5.0
-------
* #2306: When running as a PEP 517 backend, setuptools does not try to install
``setup_requires`` itself. They are reported as build requirements for the
frontend to install.
v49.4.0
-------
* #2310: Updated vendored packaging version to 20.4.
v49.3.2
-------
* #2300: Improve the ``safe_version`` function documentation
* #2297: Once again, in stubs prefer exec_module to the deprecated load_module.
v49.3.1
-------
* #2316: Removed warning when ``distutils`` is imported before ``setuptools`` when ``distutils`` replacement is not enabled.
v49.3.0
-------
* #2259: Setuptools now provides a .pth file (except for editable installs of setuptools) to the target environment to ensure that when enabled, the setuptools-provided distutils is preferred before setuptools has been imported (and even if setuptools is never imported). Honors the SETUPTOOLS_USE_DISTUTILS environment variable.
v49.2.1
-------
* #2257: Fixed two flaws in distutils._msvccompiler.MSVCCompiler.spawn.
v49.2.0
-------
* #2230: Now warn the user when setuptools is imported after distutils modules have been loaded (exempting PyPy for 3.6), directing the users of packages to import setuptools first.
v49.1.3
-------
* #2212: (Distutils) Allow spawn to accept environment. Avoid monkey-patching global state.
* #2249: Fix extension loading technique in stubs.
v49.1.2
-------
* #2232: In preparation for re-enabling a local copy of distutils, Setuptools now honors an environment variable, SETUPTOOLS_USE_DISTUTILS. If set to 'stdlib' (current default), distutils will be used from the standard library. If set to 'local' (default in a imminent backward-incompatible release), the local copy of distutils will be used.
v49.1.1
-------
* #2094: Removed pkg_resources.py2_warn module, which is no longer reachable.
v49.0.1
-------
* #2228: Applied fix for pypa/distutils#3, restoring expectation that spawn will raise a DistutilsExecError when attempting to execute a missing file.
v49.1.0
-------
* #2228: Disabled distutils adoption for now while emergent issues are addressed.
v49.0.0
-------
* #2165: Setuptools no longer installs a site.py file during easy_install or develop installs. As a result, .eggs on PYTHONPATH will no longer take precedence over other packages on sys.path. If this issue affects your production environment, please reach out to the maintainers at #2165.
* #2137: Removed (private) pkg_resources.RequirementParseError, now replaced by packaging.requirements.InvalidRequirement. Kept the name for compatibility, but users should catch InvalidRequirement instead.
* #2180: Update vendored packaging in pkg_resources to 19.2.
* #2199: Fix exception causes all over the codebase by using ``raise new_exception from old_exception``
v48.0.0
-------
* #2143: Setuptools adopts distutils from the Python 3.9 standard library and no longer depends on distutils in the standard library. When importing ``setuptools`` or ``setuptools.distutils_patch``, Setuptools will expose its bundled version as a top-level ``distutils`` package (and unload any previously-imported top-level distutils package), retaining the expectation that ``distutils``' objects are actually Setuptools objects.
To avoid getting any legacy behavior from the standard library, projects are advised to always "import setuptools" prior to importing anything from distutils. This behavior happens by default when using ``pip install`` or ``pep517.build``. Workflows that rely on ``setup.py (anything)`` will need to first ensure setuptools is imported. One way to achieve this behavior without modifying code is to invoke Python thus: ``python -c "import setuptools; exec(open('setup.py').read())" (anything)``.
v47.3.2
-------
* #2071: Replaced references to the deprecated imp package with references to importlib
v47.3.1
-------
* #1973: Removed ``pkg_resources.py31compat.makedirs`` in favor of the stdlib. Use ``os.makedirs()`` instead.
* #2198: Restore ``__requires__`` directive in easy-install wrapper scripts.
v47.3.0
-------
* #2197: Console script wrapper for editable installs now has a unified template and honors importlib_metadata if present for faster script execution on older Pythons.
* #2195: Fix broken entry points generated by easy-install (pip editable installs).
v47.2.0
-------
* #2194: Editable-installed entry points now load significantly faster on Python versions 3.8+.
v47.1.1
-------
* #2156: Update mailing list pointer in developer docs
Incorporate changes from v44.1.1:
* #2158: Avoid loading working set during ``Distribution.finalize_options`` prior to invoking ``_install_setup_requires``, broken since v42.0.0.
v44.1.1
-------
* #2158: Avoid loading working set during ``Distribution.finalize_options`` prior to invoking ``_install_setup_requires``, broken since v42.0.0.
v47.1.0
-------
* #2070: In wheel-to-egg conversion, use simple pkg_resources-style namespace declaration for packages that declare namespace_packages.
v47.0.0
-------
* #2094: Setuptools now actively crashes under Python 2. Python 3.5 or later is required. Users of Python 2 should use ``setuptools<45``.
* #1700: Document all supported keywords by migrating the ones from distutils.
v46.4.0
-------
......
......@@ -54,4 +54,4 @@ Code of Conduct
Everyone interacting in the setuptools project's codebases, issue trackers,
chat rooms, and mailing lists is expected to follow the
`PyPA Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`_.
`PSF Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>`_.
import sys
import os
import re
import importlib
import warnings
is_pypy = '__pypy__' in sys.builtin_module_names
def warn_distutils_present():
if 'distutils' not in sys.modules:
return
if is_pypy and sys.version_info < (3, 7):
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
return
warnings.warn(
"Distutils was imported before Setuptools, but importing Setuptools "
"also replaces the `distutils` module in `sys.modules`. This may lead "
"to undesirable behaviors or errors. To avoid these issues, avoid "
"using distutils directly, ensure that setuptools is installed in the "
"traditional way (e.g. not an editable install), and/or make sure "
"that setuptools is always imported before distutils.")
def clear_distutils():
if 'distutils' not in sys.modules:
return
warnings.warn("Setuptools is replacing distutils.")
mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
for name in mods:
del sys.modules[name]
def enabled():
"""
Allow selection of distutils by environment variable.
"""
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
return which == 'local'
def ensure_local_distutils():
clear_distutils()
distutils = importlib.import_module('setuptools._distutils')
distutils.__name__ = 'distutils'
sys.modules['distutils'] = distutils
# sanity check that submodules load as expected
core = importlib.import_module('distutils.core')
assert '_distutils' in core.__file__, core.__file__
def do_override():
"""
Ensure that the local copy of distutils is preferred over stdlib.
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
for more motivation.
"""
if enabled():
warn_distutils_present()
ensure_local_distutils()
class DistutilsMetaFinder:
def find_spec(self, fullname, path, target=None):
if path is not None:
return
method_name = 'spec_for_{fullname}'.format(**locals())
method = getattr(self, method_name, lambda: None)
return method()
def spec_for_distutils(self):
import importlib.abc
import importlib.util
class DistutilsLoader(importlib.abc.Loader):
def create_module(self, spec):
return importlib.import_module('setuptools._distutils')
def exec_module(self, module):
pass
return importlib.util.spec_from_loader('distutils', DistutilsLoader())
def spec_for_pip(self):
"""
Ensure stdlib distutils when running under pip.
See pypa/pip#8761 for rationale.
"""
if self.pip_imported_during_build():
return
clear_distutils()
self.spec_for_distutils = lambda: None
@staticmethod
def pip_imported_during_build():
"""
Detect if pip is being imported in a build script. Ref #2355.
"""
import traceback
return any(
frame.f_globals['__file__'].endswith('setup.py')
for frame, line in traceback.walk_stack(None)
)
DISTUTILS_FINDER = DistutilsMetaFinder()
def add_shim():
sys.meta_path.insert(0, DISTUTILS_FINDER)
def remove_shim():
try:
sys.meta_path.remove(DISTUTILS_FINDER)
except ValueError:
pass
__import__('_distutils_hack').do_override()
......@@ -8,18 +8,22 @@ environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
APPVEYOR_JOB_NAME: "python35-x64-vs2015"
PYTHON: "C:\\Python35-x64"
APPVEYOR_JOB_NAME: "Python38-x64-vs2015"
PYTHON: "C:\\Python38-x64"
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
APPVEYOR_JOB_NAME: "python35-x64-vs2017"
PYTHON: "C:\\Python35-x64"
APPVEYOR_JOB_NAME: "Python38-x64-vs2017"
PYTHON: "C:\\Python38-x64"
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
APPVEYOR_JOB_NAME: "python35-x64-vs2019"
PYTHON: "C:\\Python35-x64"
- APPVEYOR_JOB_NAME: "python36-x64"
PYTHON: "C:\\Python36-x64"
APPVEYOR_JOB_NAME: "Python38-x64-vs2019"
PYTHON: "C:\\Python38-x64"
- APPVEYOR_JOB_NAME: "python37-x64"
PYTHON: "C:\\Python37-x64"
- APPVEYOR_JOB_NAME: "python36-x64"
PYTHON: "C:\\Python36-x64"
- APPVEYOR_JOB_NAME: "python35-x64"
PYTHON: "C:\\Python35-x64"
PYTEST_ADDOPTS: "--cov"
TOX_TESTENV_PASSENV: "PYTEST_ADDOPTS"
install:
# symlink python from a directory with a space
......@@ -37,7 +41,7 @@ test_script:
- python -m pip install --disable-pip-version-check --upgrade pip setuptools wheel
- pip install --upgrade tox tox-venv virtualenv
- pip freeze --all
- tox -- --cov
- tox
after_test:
- tox -e coverage,codecov
......
......@@ -76,6 +76,7 @@ stages:
env:
TWINE_PASSWORD: $(PyPI-token)
TIDELIFT_TOKEN: $(Tidelift-token)
GITHUB_TOKEN: $(Github-token)
displayName: 'publish to PyPI'
condition: contains(variables['Build.SourceBranch'], 'tags')
......@@ -5,8 +5,6 @@ environment by creating a minimal egg-info directory and then invoking the
egg-info command to flesh out the egg-info directory.
"""
from __future__ import unicode_literals
import os
import sys
import textwrap
......
Document all supported keywords by migrating the ones from distutils.
......@@ -14,13 +14,10 @@ def pytest_addoption(parser):
collect_ignore = [
'tests/manual_test.py',
'setuptools/tests/mod_with_constant.py',
'setuptools/_distutils',
'_distutils_hack',
]
if sys.version_info < (3,):
collect_ignore.append('setuptools/lib2to3_ex.py')
collect_ignore.append('setuptools/_imp.py')
if sys.version_info < (3, 6):
collect_ignore.append('pavement.py')
......@@ -101,7 +101,7 @@ link_files = {
url='http://bugs.jython.org/issue{jython}',
),
dict(
pattern=r'Python #(?P<python>\d+)',
pattern=r'(Python #|bpo-)(?P<python>\d+)',
url='http://bugs.python.org/issue{python}',
),
dict(
......@@ -128,6 +128,10 @@ link_files = {
pattern=r'setuptools_svn #(?P<setuptools_svn>\d+)',
url='{GH}/jaraco/setuptools_svn/issues/{setuptools_svn}',
),
dict(
pattern=r'pypa/distutils#(?P<distutils>\d+)',
url='{GH}/pypa/distutils/issues/{distutils}',
),
dict(
pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n',
with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n',
......
Porting from Distutils
======================
Setuptools and the PyPA have a `stated goal <https://github.com/pypa/packaging-problems/issues/127>`_ to make Setuptools the reference API for distutils.
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
This behavior is planned to become the default.
Prefer Setuptools
-----------------
As Distutils is deprecated, any usage of functions or objects from distutils is similarly discouraged, and Setuptools aims to replace or deprecate all such uses. This section describes the recommended replacements.
``distutils.core.setup`` → ``setuptools.setup``
``distutils.cmd.Command`` → ``setuptools.Command``
``distutils.log`` → (no replacement yet)
``distutils.version.*`` → ``packaging.version.*``
If a project relies on uses of ``distutils`` that do not have a suitable replacement above, please search the `Setuptools issue tracker <https://github.com/pypa/setuptools/issues/>`_ and file a request, describing the use-case so that Setuptools' maintainers can investigate. Please provide enough detail to help the maintainers understand how distutils is used, what value it provides, and why that behavior should be supported.
......@@ -16,4 +16,4 @@ objectives.
python3
python_eggs
easy_install
distutils-legacy
......@@ -23,16 +23,16 @@ contribution.
Project Management
------------------
Setuptools is maintained primarily in Github at `this home
Setuptools is maintained primarily in GitHub at `this home
<https://github.com/pypa/setuptools>`_. Setuptools is maintained under the
Python Packaging Authority (PyPA) with several core contributors. All bugs
for Setuptools are filed and the canonical source is maintained in Github.
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, or on IRC (Freenode) at
issues, through the `distutils-sig mailing list <https://mail.python.org/mailman3/lists/distutils-sig.python.org/>`_, or on IRC (Freenode) at
#pypa.
Discussions about development happen on the pypa-dev mailing list or on
Discussions about development happen on the distutils-sig mailing list or on
`Gitter <https://gitter.im/pypa/setuptools>`_.
-----------------
......@@ -44,7 +44,7 @@ describing the motivation behind making changes. First search to see if a
ticket already exists for your issue. If not, create one. Try to think from
the perspective of the reader. Explain what behavior you expected, what you
got instead, and what factors might have contributed to the unexpected
behavior. In Github, surround a block of code or traceback with the triple
behavior. In GitHub, surround a block of code or traceback with the triple
backtick "\`\`\`" so that it is formatted nicely.
Filing a ticket provides a forum for justification, discussion, and
......@@ -145,5 +145,5 @@ setuptools from source. Eventually, this limitation may be lifted as
PEP 517/518 reach ubiquitous adoption, but for now, Setuptools
cannot declare dependencies other than through
``setuptools/_vendor/vendored.txt`` and
``pkg_reosurces/_vendor/vendored.txt`` and refreshed by way of
``pkg_resources/_vendor/vendored.txt`` and refreshed by way of
``paver update_vendored`` (pavement.py).
Welcome to Setuptools' documentation!
=====================================
Documentation
=============
Setuptools is a fully-featured, actively-maintained, and stable library
designed to facilitate packaging Python projects, where packaging includes:
- Python package and module definitions
- Distribution package metadata
- Test hooks
- Project installation
- Platform-specific details
- Python 3 support
designed to facilitate packaging Python projects.
Documentation content:
......
......@@ -594,7 +594,7 @@ Requirements Parsing
FooProject >= 1.2
Fizzy [foo, bar]
PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1
PickyThing>1.6,<=1.9,!=1.8.6
SomethingWhoseVersionIDontCareAbout
SomethingWithMarker[foo]>1.0;python_version<"2.7"
......@@ -1596,12 +1596,12 @@ Parsing Utilities
See ``to_filename()``.
``safe_version(version)``
This will return the normalized form of any PEP 440 version, if the version
string is not PEP 440 compatible than it is similar to ``safe_name()``
except that spaces in the input become dots, and dots are allowed to exist
in the output. As with ``safe_name()``, if you are generating a filename
from this you should replace any "-" characters in the output with
underscores.
This will return the normalized form of any PEP 440 version. If the version
string is not PEP 440 compatible, this function behaves similar to
``safe_name()`` except that spaces in the input become dots, and dots are
allowed to exist in the output. As with ``safe_name()``, if you are
generating a filename from this you should replace any "-" characters in
the output with underscores.
``safe_extra(extra)``
Return a "safe" form of an extra's name, suitable for use in a requirement
......
:orphan:
Python 2 Sunset
===============
Since January 2020 and the release of Setuptools 45, Python 2 is no longer
supported by the most current release (`discussion
<https://github.com/pypa/setuptools/issues/1458>`_). Setuptools as a project
continues to support Python 2 with bugfixes and important features on
Setuptools 44.x.
By design, most users will be unaffected by this change. That's because
Setuptools 45 declares its supported Python versions to exclude Python 2.7,
and installers such as pip 9 or later will honor this declaration and prevent
installation of Setuptools 45 or later in Python 2 environments.
Users that do import any portion of Setuptools 45 or later on Python 2 are
directed to this documentation to provide guidance on how to work around the
issues.
Workarounds
-----------
The best recommendation is to avoid Python 2 and move to Python 3 where
possible. This project acknowledges that not all environments can drop Python
2 support, so provides other options.
In less common scenarios, later versions of Setuptools can be installed on
unsupported Python versions. In these environments, the installer is advised
to first install ``setuptools<45`` to "pin Setuptools" to a compatible
version.
- When using older versions of pip (before 9.0), the ``Requires-Python``
directive is not honored and invalid versions can be installed. Users are
advised first to upgrade pip and retry or to pin Setuptools. Use ``pip
--version`` to determine the version of pip.
- When using ``easy_install``, ``Requires-Python`` is not honored and later
versions can be installed. In this case, users are advised to pin
Setuptools. This applies to ``setup.py install`` invocations as well, as
they use Setuptools under the hood.
It's still not working
----------------------
If after trying the above steps, the Python environment still has incompatible
versions of Setuptools installed, here are some things to try.
1. Uninstall and reinstall Setuptools. Run ``pip uninstall -y setuptools`` for
the relevant environment. Repeat until there is no Setuptools installed.
Then ``pip install setuptools``.
2. If possible, attempt to replicate the problem in a second environment
(virtual machine, friend's computer, etc). If the issue is isolated to just
one unique enviornment, first determine what is different about those
environments (or reinstall/reset the failing one to defaults).
3. End users who are not themselves the maintainers for the package they are
trying to install should contact the support channels for the relevant
application. Please be considerate of those projects by searching for
existing issues and following the latest guidance before reaching out for
support. When filing an issue, be sure to give as much detail as possible
to help the maintainers understand what factors led to the issue after
following their recommended guidance.
4. Reach out to your local support groups. There's a good chance someone
nearby has the expertise and willingness to help.
5. If all else fails, `file this template
<https://github.com/pypa/setuptools/issues/new?assignees=&labels=Python+2&template=setuptools-warns-about-python-2-incompatibility.md&title=Incompatible+install+in+(summarize+your+environment)>`_
with Setuptools. Please complete the whole template, providing as much
detail about what factors led to the issue. Setuptools maintainers will
summarily close tickets filed without any meaningful detail or engagement
with the issue.
......@@ -22,7 +22,11 @@ def rewrite_packaging(pkg_files, new_root):
"""
for file in pkg_files.glob('*.py'):
text = file.text()
text = re.sub(r' (pyparsing|six)', rf' {new_root}.\1', text)
text = re.sub(r' (pyparsing)', rf' {new_root}.\1', text)
text = text.replace(
'from six.moves.urllib import parse',
'from urllib import parse',
)
file.write_text(text)
......@@ -50,6 +54,7 @@ def install(vendor):
subprocess.check_call(install_args)
remove_all(vendor.glob('*.dist-info'))
remove_all(vendor.glob('*.egg-info'))
remove_all(vendor.glob('six.py'))
(vendor / '__init__.py').write_text('')
......
# coding: utf-8
"""
Package resource API
--------------------
......@@ -15,8 +14,6 @@ The package resource API is designed to work with normal filesystem packages,
method.
"""
from __future__ import absolute_import
import sys
import os
import io
......@@ -54,9 +51,6 @@ try:
except NameError:
FileExistsError = OSError
from pkg_resources.extern import six
from pkg_resources.extern.six.moves import map, filter
# capture these to bypass sandboxing
from os import utime
try:
......@@ -76,27 +70,16 @@ try:
except ImportError:
importlib_machinery = None
from . import py31compat
from pkg_resources.extern import appdirs
from pkg_resources.extern import packaging
__import__('pkg_resources.extern.packaging.version')
__import__('pkg_resources.extern.packaging.specifiers')
__import__('pkg_resources.extern.packaging.requirements')
__import__('pkg_resources.extern.packaging.markers')
__import__('pkg_resources.py2_warn')
__metaclass__ = type
if (3, 0) < sys.version_info < (3, 5):
if sys.version_info < (3, 5):
raise RuntimeError("Python 3.5 or later is required")
if six.PY2:
# Those builtin exceptions are only defined in Python 3
PermissionError = None
NotADirectoryError = None
# declare some globals that will be defined later to
# satisfy the linters.
require = None
......@@ -476,7 +459,7 @@ run_main = run_script
def get_distribution(dist):
"""Return a current distribution object for a Requirement or string"""
if isinstance(dist, six.string_types):
if isinstance(dist, str):
dist = Requirement.parse(dist)
if isinstance(dist, Requirement):
dist = get_provider(dist)
......@@ -1379,7 +1362,7 @@ def evaluate_marker(text, extra=None):
marker = packaging.markers.Marker(text)
return marker.evaluate()
except packaging.markers.InvalidMarker as e:
raise SyntaxError(e)
raise SyntaxError(e) from e
class NullProvider:
......@@ -1420,8 +1403,6 @@ class NullProvider:
return ""
path = self._get_metadata_path(name)
value = self._get(path)
if six.PY2:
return value
try:
return value.decode('utf-8')
except UnicodeDecodeError as exc:
......@@ -1459,7 +1440,8 @@ class NullProvider:
script_filename = self._fn(self.egg_info, script)
namespace['__file__'] = script_filename
if os.path.exists(script_filename):
source = open(script_filename).read()
with open(script_filename) as fid:
source = fid.read()
code = compile(source, script_filename, 'exec')
exec(code, namespace, namespace)
else:
......@@ -1577,6 +1559,17 @@ is not allowed.
register_loader_type(object, NullProvider)
def _parents(path):
"""
yield all parents of path including path
"""
last = None
while path != last:
yield path
last = path
path, _ = os.path.split(path)
class EggProvider(NullProvider):
"""Provider based on a virtual filesystem"""
......@@ -1585,18 +1578,16 @@ class EggProvider(NullProvider):
self._setup_prefix()
def _setup_prefix(self):
# we assume here that our metadata may be nested inside a "basket"
# of multiple eggs; that's why we use module_path instead of .archive
path = self.module_path
old = None
while path != old:
if _is_egg_path(path):
# Assume that metadata may be nested inside a "basket"
# of multiple eggs and use module_path instead of .archive.
eggs = filter(_is_egg_path, _parents(self.module_path))
egg = next(eggs, None)
egg and self._set_egg(egg)
def _set_egg(self, path):
self.egg_name = os.path.basename(path)
self.egg_info = os.path.join(path, 'EGG-INFO')
self.egg_root = path
break
old = path
path, base = os.path.split(path)
class DefaultProvider(EggProvider):
......@@ -1902,8 +1893,7 @@ class FileMetadata(EmptyProvider):
return metadata
def _warn_on_replacement(self, metadata):
# Python 2.7 compat for: replacement_char = '�'
replacement_char = b'\xef\xbf\xbd'.decode('utf-8')
replacement_char = '�'
if replacement_char in metadata:
tmpl = "{self.path} could not be properly decoded in UTF-8"
msg = tmpl.format(**locals())
......@@ -2048,7 +2038,10 @@ def find_on_path(importer, path_item, only=False):
)
return
entries = safe_listdir(path_item)
entries = (
os.path.join(path_item, child)
for child in safe_listdir(path_item)
)
# for performance, before sorting by version,
# screen entries for only those that will yield
......@@ -2098,8 +2091,6 @@ class NoDists:
"""
def __bool__(self):
return False
if six.PY2:
__nonzero__ = __bool__
def __call__(self, fullpath):
return iter(())
......@@ -2116,12 +2107,7 @@ def safe_listdir(path):
except OSError as e:
# Ignore the directory if does not exist, not a directory or
# permission denied
ignorable = (
e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT)
# Python 2 on Windows needs to be handled this way :(
or getattr(e, "winerror", None) == 267
)
if not ignorable:
if e.errno not in (errno.ENOTDIR, errno.EACCES, errno.ENOENT):
raise
return ()
......@@ -2279,8 +2265,8 @@ def declare_namespace(packageName):
__import__(parent)
try:
path = sys.modules[parent].__path__
except AttributeError:
raise TypeError("Not a package:", parent)
except AttributeError as e:
raise TypeError("Not a package:", parent) from e
# Track what packages are namespaces, so when new path items are added,
# they can be updated
......@@ -2364,7 +2350,15 @@ def _is_egg_path(path):
"""
Determine if given path appears to be an egg.
"""
return path.lower().endswith('.egg')
return _is_zip_egg(path) or _is_unpacked_egg(path)
def _is_zip_egg(path):
return (
path.lower().endswith('.egg') and
os.path.isfile(path) and
zipfile.is_zipfile(path)
)
def _is_unpacked_egg(path):
......@@ -2372,7 +2366,7 @@ def _is_unpacked_egg(path):
Determine if given path appears to be an unpacked egg.
"""
return (
_is_egg_path(path) and
path.lower().endswith('.egg') and
os.path.isfile(os.path.join(path, 'EGG-INFO', 'PKG-INFO'))
)
......@@ -2387,7 +2381,7 @@ def _set_parent_ns(packageName):
def yield_lines(strs):
"""Yield non-empty/non-comment lines of a string or sequence"""
if isinstance(strs, six.string_types):
if isinstance(strs, str):
for s in strs.splitlines():
s = s.strip()
# skip blank lines/comments
......@@ -2460,7 +2454,7 @@ class EntryPoint:
try:
return functools.reduce(getattr, self.attrs, module)
except AttributeError as exc:
raise ImportError(str(exc))
raise ImportError(str(exc)) from exc
def require(self, env=None, installer=None):
if self.extras and not self.dist:
......@@ -2680,14 +2674,14 @@ class Distribution:
def version(self):
try:
return self._version
except AttributeError:
except AttributeError as e:
version = self._get_version()
if version is None:
path = self._get_metadata_path_for_display(self.PKG_INFO)
msg = (
"Missing 'Version:' header and/or {} file at path: {}"
).format(self.PKG_INFO, path)
raise ValueError(msg, self)
raise ValueError(msg, self) from e
return version
......@@ -2740,10 +2734,10 @@ class Distribution:
for ext in extras:
try:
deps.extend(dm[safe_extra(ext)])
except KeyError:
except KeyError as e:
raise UnknownExtra(
"%s has no such extra feature %r" % (self, ext)
)
) from e
return deps
def _get_metadata_path_for_display(self, name):
......@@ -2825,10 +2819,6 @@ class Distribution:
)
)
if not hasattr(object, '__dir__'):
# python 2.7 not supported
del __dir__
@classmethod
def from_filename(cls, filename, metadata=None, **kw):
return cls.from_location(
......@@ -3068,11 +3058,6 @@ def issue_warning(*args, **kw):
warnings.warn(stacklevel=level + 1, *args, **kw)
class RequirementParseError(ValueError):
def __str__(self):
return ' '.join(self.args)
def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs`
......@@ -3095,13 +3080,14 @@ def parse_requirements(strs):
yield Requirement(line)
class RequirementParseError(packaging.requirements.InvalidRequirement):
"Compatibility wrapper for InvalidRequirement"
class Requirement(packaging.requirements.Requirement):
def __init__(self, requirement_string):
"""DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!"""
try:
super(Requirement, self).__init__(requirement_string)
except packaging.requirements.InvalidRequirement as e:
raise RequirementParseError(str(e))
self.unsafe_name = self.name
project_name = safe_name(self.name)
self.project_name, self.key = project_name, project_name.lower()
......@@ -3171,7 +3157,7 @@ def _find_adapter(registry, ob):
def ensure_directory(path):
"""Ensure that the parent directory of `path` exists"""
dirname = os.path.dirname(path)
py31compat.makedirs(dirname, exist_ok=True)
os.makedirs(dirname, exist_ok=True)
def _bypass_ensure_directory(path):
......
......@@ -4,18 +4,24 @@
from __future__ import absolute_import, division, print_function
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
"__title__",
"__summary__",
"__uri__",
"__version__",
"__author__",
"__email__",
"__license__",
"__copyright__",
]
__title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "16.8"
__version__ = "20.4"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
__license__ = "BSD or Apache License, Version 2.0"
__copyright__ = "Copyright 2014-2016 %s" % __author__
__license__ = "BSD-2-Clause or Apache-2.0"
__copyright__ = "Copyright 2014-2019 %s" % __author__
......@@ -4,11 +4,23 @@
from __future__ import absolute_import, division, print_function
from .__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
__uri__, __version__
__author__,
__copyright__,
__email__,
__license__,
__summary__,
__title__,
__uri__,
__version__,
)
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
"__title__",
"__summary__",
"__uri__",
"__version__",
"__author__",
"__email__",
"__license__",
"__copyright__",
]
......@@ -5,6 +5,11 @@ from __future__ import absolute_import, division, print_function
import sys
from ._typing import TYPE_CHECKING
if TYPE_CHECKING: # pragma: no cover
from typing import Any, Dict, Tuple, Type
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
......@@ -12,19 +17,22 @@ PY3 = sys.version_info[0] == 3
# flake8: noqa
if PY3:
string_types = str,
string_types = (str,)
else:
string_types = basestring,
string_types = (basestring,)
def with_metaclass(meta, *bases):
# type: (Type[Any], Tuple[Type[Any], ...]) -> Any
"""
Create a base class with a metaclass.
"""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(meta):
class metaclass(meta): # type: ignore
def __new__(cls, name, this_bases, d):
# type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
return type.__new__(metaclass, "temporary_class", (), {})
......@@ -4,65 +4,83 @@
from __future__ import absolute_import, division, print_function
class Infinity(object):
class InfinityType(object):
def __repr__(self):
# type: () -> str
return "Infinity"
def __hash__(self):
# type: () -> int
return hash(repr(self))
def __lt__(self, other):
# type: (object) -> bool
return False
def __le__(self, other):
# type: (object) -> bool
return False
def __eq__(self, other):
# type: (object) -> bool
return isinstance(other, self.__class__)
def __ne__(self, other):
# type: (object) -> bool
return not isinstance(other, self.__class__)
def __gt__(self, other):
# type: (object) -> bool
return True
def __ge__(self, other):
# type: (object) -> bool
return True
def __neg__(self):
# type: (object) -> NegativeInfinityType
return NegativeInfinity
Infinity = Infinity()
Infinity = InfinityType()
class NegativeInfinity(object):
class NegativeInfinityType(object):
def __repr__(self):
# type: () -> str
return "-Infinity"
def __hash__(self):
# type: () -> int
return hash(repr(self))
def __lt__(self, other):
# type: (object) -> bool
return True
def __le__(self, other):
# type: (object) -> bool
return True
def __eq__(self, other):
# type: (object) -> bool
return isinstance(other, self.__class__)
def __ne__(self, other):
# type: (object) -> bool
return not isinstance(other, self.__class__)
def __gt__(self, other):
# type: (object) -> bool
return False
def __ge__(self, other):
# type: (object) -> bool
return False
def __neg__(self):
# type: (object) -> InfinityType
return Infinity
NegativeInfinity = NegativeInfinity()
NegativeInfinity = NegativeInfinityType()
"""For neatly implementing static typing in packaging.
`mypy` - the static type analysis tool we use - uses the `typing` module, which
provides core functionality fundamental to mypy's functioning.
Generally, `typing` would be imported at runtime and used in that fashion -
it acts as a no-op at runtime and does not have any run-time overhead by
design.
As it turns out, `typing` is not vendorable - it uses separate sources for
Python 2/Python 3. Thus, this codebase can not expect it to be present.
To work around this, mypy allows the typing import to be behind a False-y
optional to prevent it from running at runtime and type-comments can be used
to remove the need for the types to be accessible directly during runtime.
This module provides the False-y guard in a nicely named fashion so that a
curious maintainer can reach here to read this.
In packaging, all static-typing related imports should be guarded as follows:
from packaging._typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import ...
Ref: https://github.com/python/mypy/issues/3216
"""
__all__ = ["TYPE_CHECKING", "cast"]
# The TYPE_CHECKING constant defined by the typing module is False at runtime
# but True while type checking.
if False: # pragma: no cover
from typing import TYPE_CHECKING
else:
TYPE_CHECKING = False
# typing's cast syntax requires calling typing.cast at runtime, but we don't
# want to import typing at runtime. Here, we inform the type checkers that
# we're importing `typing.cast` as `cast` and re-implement typing.cast's
# runtime behavior in a block that is ignored by type checkers.
if TYPE_CHECKING: # pragma: no cover
# not executed at runtime
from typing import cast
else:
# executed at runtime
def cast(type_, value): # noqa
return value
......@@ -13,12 +13,21 @@ from pkg_resources.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedStr
from pkg_resources.extern.pyparsing import Literal as L # noqa
from ._compat import string_types
from ._typing import TYPE_CHECKING
from .specifiers import Specifier, InvalidSpecifier
if TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
Operator = Callable[[str, str], bool]
__all__ = [
"InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName",
"Marker", "default_environment",
"InvalidMarker",
"UndefinedComparison",
"UndefinedEnvironmentName",
"Marker",
"default_environment",
]
......@@ -42,77 +51,73 @@ class UndefinedEnvironmentName(ValueError):
class Node(object):
def __init__(self, value):
# type: (Any) -> None
self.value = value
def __str__(self):
# type: () -> str
return str(self.value)
def __repr__(self):
# type: () -> str
return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
def serialize(self):
# type: () -> str
raise NotImplementedError
class Variable(Node):
def serialize(self):
# type: () -> str
return str(self)
class Value(Node):
def serialize(self):
# type: () -> str
return '"{0}"'.format(self)
class Op(Node):
def serialize(self):
# type: () -> str
return str(self)
VARIABLE = (
L("implementation_version") |
L("platform_python_implementation") |
L("implementation_name") |
L("python_full_version") |
L("platform_release") |
L("platform_version") |
L("platform_machine") |
L("platform_system") |
L("python_version") |
L("sys_platform") |
L("os_name") |
L("os.name") | # PEP-345
L("sys.platform") | # PEP-345
L("platform.version") | # PEP-345
L("platform.machine") | # PEP-345
L("platform.python_implementation") | # PEP-345
L("python_implementation") | # undocumented setuptools legacy
L("extra")
L("implementation_version")
| L("platform_python_implementation")
| L("implementation_name")
| L("python_full_version")
| L("platform_release")
| L("platform_version")
| L("platform_machine")
| L("platform_system")
| L("python_version")
| L("sys_platform")
| L("os_name")
| L("os.name") # PEP-345
| L("sys.platform") # PEP-345
| L("platform.version") # PEP-345
| L("platform.machine") # PEP-345
| L("platform.python_implementation") # PEP-345
| L("python_implementation") # undocumented setuptools legacy
| L("extra") # PEP-508
)
ALIASES = {
'os.name': 'os_name',
'sys.platform': 'sys_platform',
'platform.version': 'platform_version',
'platform.machine': 'platform_machine',
'platform.python_implementation': 'platform_python_implementation',
'python_implementation': 'platform_python_implementation'
"os.name": "os_name",
"sys.platform": "sys_platform",
"platform.version": "platform_version",
"platform.machine": "platform_machine",
"platform.python_implementation": "platform_python_implementation",
"python_implementation": "platform_python_implementation",
}
VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
VERSION_CMP = (
L("===") |
L("==") |
L(">=") |
L("<=") |
L("!=") |
L("~=") |
L(">") |
L("<")
L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<")
)
MARKER_OP = VERSION_CMP | L("not in") | L("in")
......@@ -139,6 +144,7 @@ MARKER = stringStart + MARKER_EXPR + stringEnd
def _coerce_parse_result(results):
# type: (Union[ParseResults, List[Any]]) -> List[Any]
if isinstance(results, ParseResults):
return [_coerce_parse_result(i) for i in results]
else:
......@@ -146,14 +152,19 @@ def _coerce_parse_result(results):
def _format_marker(marker, first=True):
# type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str
assert isinstance(marker, (list, tuple, string_types))
# Sometimes we have a structure like [[...]] which is a single item list
# where the single item is itself it's own list. In that case we want skip
# the rest of this function so that we don't get extraneous () on the
# outside.
if (isinstance(marker, list) and len(marker) == 1 and
isinstance(marker[0], (list, tuple))):
if (
isinstance(marker, list)
and len(marker) == 1
and isinstance(marker[0], (list, tuple))
):
return _format_marker(marker[0])
if isinstance(marker, list):
......@@ -177,10 +188,11 @@ _operators = {
"!=": operator.ne,
">=": operator.ge,
">": operator.gt,
}
} # type: Dict[str, Operator]
def _eval_op(lhs, op, rhs):
# type: (str, Op, str) -> bool
try:
spec = Specifier("".join([op.serialize(), rhs]))
except InvalidSpecifier:
......@@ -188,7 +200,7 @@ def _eval_op(lhs, op, rhs):
else:
return spec.contains(lhs)
oper = _operators.get(op.serialize())
oper = _operators.get(op.serialize()) # type: Optional[Operator]
if oper is None:
raise UndefinedComparison(
"Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
......@@ -197,13 +209,18 @@ def _eval_op(lhs, op, rhs):
return oper(lhs, rhs)
_undefined = object()
class Undefined(object):
pass
_undefined = Undefined()
def _get_env(environment, name):
value = environment.get(name, _undefined)
# type: (Dict[str, str], str) -> str
value = environment.get(name, _undefined) # type: Union[str, Undefined]
if value is _undefined:
if isinstance(value, Undefined):
raise UndefinedEnvironmentName(
"{0!r} does not exist in evaluation environment.".format(name)
)
......@@ -212,7 +229,8 @@ def _get_env(environment, name):
def _evaluate_markers(markers, environment):
groups = [[]]
# type: (List[Any], Dict[str, str]) -> bool
groups = [[]] # type: List[List[bool]]
for marker in markers:
assert isinstance(marker, (list, tuple, string_types))
......@@ -239,20 +257,25 @@ def _evaluate_markers(markers, environment):
def format_full_version(info):
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
# type: (sys._version_info) -> str
version = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel
if kind != 'final':
if kind != "final":
version += kind[0] + str(info.serial)
return version
def default_environment():
if hasattr(sys, 'implementation'):
iver = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
# type: () -> Dict[str, str]
if hasattr(sys, "implementation"):
# Ignoring the `sys.implementation` reference for type checking due to
# mypy not liking that the attribute doesn't exist in Python 2.7 when
# run with the `--py27` flag.
iver = format_full_version(sys.implementation.version) # type: ignore
implementation_name = sys.implementation.name # type: ignore
else:
iver = '0'
implementation_name = ''
iver = "0"
implementation_name = ""
return {
"implementation_name": implementation_name,
......@@ -264,28 +287,32 @@ def default_environment():
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"platform_python_implementation": platform.python_implementation(),
"python_version": platform.python_version()[:3],
"python_version": ".".join(platform.python_version_tuple()[:2]),
"sys_platform": sys.platform,
}
class Marker(object):
def __init__(self, marker):
# type: (str) -> None
try:
self._markers = _coerce_parse_result(MARKER.parseString(marker))
except ParseException as e:
err_str = "Invalid marker: {0!r}, parse error at {1!r}".format(
marker, marker[e.loc:e.loc + 8])
marker, marker[e.loc : e.loc + 8]
)
raise InvalidMarker(err_str)
def __str__(self):
# type: () -> str
return _format_marker(self._markers)
def __repr__(self):
# type: () -> str
return "<Marker({0!r})>".format(str(self))
def evaluate(self, environment=None):
# type: (Optional[Dict[str, str]]) -> bool
"""Evaluate a marker.
Return the boolean from evaluating the given marker against the
......
......@@ -9,11 +9,15 @@ import re
from pkg_resources.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException
from pkg_resources.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pkg_resources.extern.pyparsing import Literal as L # noqa
from pkg_resources.extern.six.moves.urllib import parse as urlparse
from urllib import parse as urlparse
from ._typing import TYPE_CHECKING
from .markers import MARKER_EXPR, Marker
from .specifiers import LegacySpecifier, Specifier, SpecifierSet
if TYPE_CHECKING: # pragma: no cover
from typing import List
class InvalidRequirement(ValueError):
"""
......@@ -38,8 +42,8 @@ IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
NAME = IDENTIFIER("name")
EXTRA = IDENTIFIER
URI = Regex(r'[^ ]+')("url")
URL = (AT + URI)
URI = Regex(r"[^ ]+")("url")
URL = AT + URI
EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
......@@ -48,28 +52,31 @@ VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE),
joinString=",", adjacent=False)("_raw_spec")
VERSION_MANY = Combine(
VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False
)("_raw_spec")
_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY))
_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '')
_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "")
VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
MARKER_EXPR.setParseAction(
lambda s, l, t: Marker(s[t._original_start:t._original_end])
lambda s, l, t: Marker(s[t._original_start : t._original_end])
)
MARKER_SEPERATOR = SEMICOLON
MARKER = MARKER_SEPERATOR + MARKER_EXPR
MARKER_SEPARATOR = SEMICOLON
MARKER = MARKER_SEPARATOR + MARKER_EXPR
VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
URL_AND_MARKER = URL + Optional(MARKER)
NAMED_REQUIREMENT = \
NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
# pkg_resources.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see
# issue #104
REQUIREMENT.parseString("x[]")
class Requirement(object):
......@@ -86,19 +93,26 @@ class Requirement(object):
# TODO: Can we normalize the name and extra name?
def __init__(self, requirement_string):
# type: (str) -> None
try:
req = REQUIREMENT.parseString(requirement_string)
except ParseException as e:
raise InvalidRequirement(
"Invalid requirement, parse error at \"{0!r}\"".format(
requirement_string[e.loc:e.loc + 8]))
'Parse error at "{0!r}": {1}'.format(
requirement_string[e.loc : e.loc + 8], e.msg
)
)
self.name = req.name
if req.url:
parsed_url = urlparse.urlparse(req.url)
if not (parsed_url.scheme and parsed_url.netloc) or (
not parsed_url.scheme and not parsed_url.netloc):
if parsed_url.scheme == "file":
if urlparse.urlunparse(parsed_url) != req.url:
raise InvalidRequirement("Invalid URL given")
elif not (parsed_url.scheme and parsed_url.netloc) or (
not parsed_url.scheme and not parsed_url.netloc
):
raise InvalidRequirement("Invalid URL: {0}".format(req.url))
self.url = req.url
else:
self.url = None
......@@ -107,7 +121,8 @@ class Requirement(object):
self.marker = req.marker if req.marker else None
def __str__(self):
parts = [self.name]
# type: () -> str
parts = [self.name] # type: List[str]
if self.extras:
parts.append("[{0}]".format(",".join(sorted(self.extras))))
......@@ -117,6 +132,8 @@ class Requirement(object):
if self.url:
parts.append("@ {0}".format(self.url))
if self.marker:
parts.append(" ")
if self.marker:
parts.append("; {0}".format(self.marker))
......@@ -124,4 +141,5 @@ class Requirement(object):
return "".join(parts)
def __repr__(self):
# type: () -> str
return "<Requirement({0!r})>".format(str(self))
This diff is collapsed.
......@@ -5,10 +5,61 @@ from __future__ import absolute_import, division, print_function
import re
from ._typing import TYPE_CHECKING, cast
from .version import InvalidVersion, Version
if TYPE_CHECKING: # pragma: no cover
from typing import NewType, Union
NormalizedName = NewType("NormalizedName", str)
_canonicalize_regex = re.compile(r"[-_.]+")
def canonicalize_name(name):
# type: (str) -> NormalizedName
# This is taken from PEP 503.
return _canonicalize_regex.sub("-", name).lower()
value = _canonicalize_regex.sub("-", name).lower()
return cast("NormalizedName", value)
def canonicalize_version(_version):
# type: (str) -> Union[Version, str]
"""
This is very similar to Version.__str__, but has one subtle difference
with the way it handles the release segment.
"""
try:
version = Version(_version)
except InvalidVersion:
# Legacy versions cannot be normalized
return _version
parts = []
# Epoch
if version.epoch != 0:
parts.append("{0}!".format(version.epoch))
# Release segment
# NB: This strips trailing '.0's to normalize
parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release)))
# Pre-release
if version.pre is not None:
parts.append("".join(str(x) for x in version.pre))
# Post-release
if version.post is not None:
parts.append(".post{0}".format(version.post))
# Development release
if version.dev is not None:
parts.append(".dev{0}".format(version.dev))
# Local version segment
if version.local is not None:
parts.append("+{0}".format(version.local))
return "".join(parts)
This diff is collapsed.
This diff is collapsed.
packaging==16.8
packaging==20.4
pyparsing==2.2.1
six==1.10.0
appdirs==1.4.3
......@@ -62,5 +62,5 @@ class VendorImporter:
sys.meta_path.append(self)
names = 'packaging', 'pyparsing', 'six', 'appdirs'
names = 'packaging', 'pyparsing', 'appdirs'
VendorImporter(__name__, names).install()
import sys
import warnings
import textwrap
msg = textwrap.dedent("""
You are running Setuptools on Python 2, which is no longer
supported and
>>> SETUPTOOLS WILL STOP WORKING <<<
in a subsequent release (no sooner than 2020-04-20).
Please ensure you are installing
Setuptools using pip 9.x or later or pin to `setuptools<45`
in your environment.
If you have done those things and are still encountering
this message, please follow up at
https://bit.ly/setuptools-py2-warning.
""")
pre = "Setuptools will stop working on Python 2\n"
sys.version_info < (3,) and warnings.warn(pre + "*" * 60 + msg + "*" * 60)
import os
import errno
import sys
from .extern import six
def _makedirs_31(path, exist_ok=False):
try:
os.makedirs(path)
except OSError as exc:
if not exist_ok or exc.errno != errno.EEXIST:
raise
# rely on compatibility behavior until mode considerations
# and exists_ok considerations are disentangled.
# See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663
needs_makedirs = (
six.PY2 or
(3, 4) <= sys.version_info < (3, 4, 1)
)
makedirs = _makedirs_31 if needs_makedirs else os.makedirs
# coding: utf-8
from __future__ import unicode_literals
import sys
import tempfile
import os
......@@ -20,16 +17,11 @@ except ImportError:
from pkg_resources import (
DistInfoDistribution, Distribution, EggInfoDistribution,
)
from setuptools.extern import six
from pkg_resources.extern.six.moves import map
from pkg_resources.extern.six import text_type, string_types
import pytest
import pkg_resources
__metaclass__ = type
def timestamp(dt):
"""
......@@ -42,7 +34,7 @@ def timestamp(dt):
return time.mktime(dt.timetuple())
class EggRemover(text_type):
class EggRemover(str):
def __call__(self):
if self in sys.path:
sys.path.remove(self)
......@@ -143,7 +135,7 @@ class TestResourceManager:
path = mgr.get_cache_path('foo')
type_ = str(type(path))
message = "Unexpected type from get_cache_path: " + type_
assert isinstance(path, string_types), message
assert isinstance(path, str), message
def test_get_cache_path_race(self, tmpdir):
# Patch to os.path.isdir to create a race condition
......@@ -225,13 +217,6 @@ def test_get_metadata__bad_utf8(tmpdir):
metadata = 'née'.encode('iso-8859-1')
dist = make_test_distribution(metadata_path, metadata=metadata)
if six.PY2:
# In Python 2, get_metadata() doesn't do any decoding.
actual = dist.get_metadata(filename)
assert actual == metadata
return
# Otherwise, we are in the Python 3 case.
with pytest.raises(UnicodeDecodeError) as excinfo:
dist.get_metadata(filename)
......@@ -247,25 +232,18 @@ def test_get_metadata__bad_utf8(tmpdir):
assert actual.endswith(metadata_path), 'actual: {}'.format(actual)
# TODO: remove this in favor of Path.touch() when Python 2 is dropped.
def touch_file(path):
"""
Create an empty file.
"""
with open(path, 'w'):
pass
def make_distribution_no_version(tmpdir, basename):
"""
Create a distribution directory with no file containing the version.
"""
# Convert the LocalPath object to a string before joining.
dist_dir = os.path.join(str(tmpdir), basename)
os.mkdir(dist_dir)
dist_dir = tmpdir / basename
dist_dir.ensure_dir()
# Make the directory non-empty so distributions_from_metadata()
# will detect it and yield it.
touch_file(os.path.join(dist_dir, 'temp.txt'))
dist_dir.join('temp.txt').ensure()
if sys.version_info < (3, 6):
dist_dir = str(dist_dir)
dists = list(pkg_resources.distributions_from_metadata(dist_dir))
assert len(dists) == 1
......
from __future__ import unicode_literals
import os
import sys
import string
import platform
import itertools
from pkg_resources.extern.six.moves import map
import pytest
from pkg_resources.extern import packaging
......
......@@ -9,8 +9,6 @@ import pkg_resources
from .test_resources import Metadata
__metaclass__ = type
def strip_comments(s):
return '\n'.join(
......
......@@ -41,3 +41,9 @@ backend-path = ["."]
directory = "misc"
name = "Misc"
showcontent = true
[tool.jaraco.pytest.plugins.flake8]
addopts = "--flake8"
[tool.jaraco.pytest.plugins.cov]
addopts = "--cov"
[pytest]
addopts=--doctest-modules --flake8 --doctest-glob=pkg_resources/api_tests.txt -r sxX
addopts=--doctest-modules --doctest-glob=pkg_resources/api_tests.txt -r sxX
norecursedirs=dist build *.egg setuptools/extern pkg_resources/extern pkg_resources/tests/data tools .* setuptools/_vendor pkg_resources/_vendor
doctest_optionflags=ELLIPSIS ALLOW_UNICODE
filterwarnings =
......@@ -9,17 +9,10 @@ filterwarnings =
ignore:bdist_wininst command is deprecated
# Suppress this error; unimportant for CI tests
ignore:Extraction path is writable by group/others:UserWarning
# Suppress Python 2 deprecation warning
ignore:Setuptools will stop working on Python 2:UserWarning
# Suppress weird RuntimeWarning.
ignore:Parent module 'setuptools' not found while handling absolute import:RuntimeWarning
# Suppress use of bytes for filenames on Windows until fixed #2016
ignore:The Windows bytes API has been deprecated:DeprecationWarning
# Suppress other Python 2 UnicodeWarnings
ignore:Unicode equal comparison failed to convert:UnicodeWarning
ignore:Unicode unequal comparison failed to convert:UnicodeWarning
# https://github.com/pypa/setuptools/issues/2025
ignore:direct construction of .*Item has been deprecated:DeprecationWarning
# https://github.com/pypa/setuptools/issues/2081
ignore:lib2to3 package is deprecated:PendingDeprecationWarning
ignore:lib2to3 package is deprecated:DeprecationWarning
......@@ -16,7 +16,7 @@ formats = zip
[metadata]
name = setuptools
version = 46.4.0
version = 50.3.0
description = Easily download, build, install, upgrade, and uninstall Python packages
author = Python Packaging Authority
author_email = distutils-sig@python.org
......@@ -68,10 +68,12 @@ tests =
pytest>=3.7
wheel
coverage>=4.5.1
pytest-cov>=2.5.1
# Coverage is unbearably slow on PyPy
pytest-cov>=2.5.1; python_implementation != "PyPy"
paver; python_version>="3.6"
futures; python_version=="2.7"
pip>=19.1 # For proper file:// URLs support.
jaraco.envs
jaraco.test >= 3.1.1; python_version >= "3.6"
docs =
# Keep these in sync with docs/requirements.txt
......
......@@ -5,8 +5,10 @@ Distutils setup file, used to install or test 'setuptools'
import os
import sys
import textwrap
import setuptools
from setuptools.command.install import install
here = os.path.dirname(__file__)
......@@ -81,8 +83,48 @@ def pypi_link(pkg_filename):
return '/'.join(parts)
class install_with_pth(install):
"""
Custom install command to install a .pth file for distutils patching.
This hack is necessary because there's no standard way to install behavior
on startup (and it's debatable if there should be one). This hack (ab)uses
the `extra_path` behavior in Setuptools to install a `.pth` file with
implicit behavior on startup to give higher precedence to the local version
of `distutils` over the version from the standard library.
Please do not replicate this behavior.
"""
_pth_name = 'distutils-precedence'
_pth_contents = textwrap.dedent("""
import os
var = 'SETUPTOOLS_USE_DISTUTILS'
enabled = os.environ.get(var, 'stdlib') == 'local'
enabled and __import__('_distutils_hack').add_shim()
""").lstrip().replace('\n', '; ')
def initialize_options(self):
install.initialize_options(self)
self.extra_path = self._pth_name, self._pth_contents
def finalize_options(self):
install.finalize_options(self)
self._restore_install_lib()
def _restore_install_lib(self):
"""
Undo secondary effect of `extra_path` adding to `install_lib`
"""
suffix = os.path.relpath(self.install_lib, self.install_libbase)
if suffix.strip() == self._pth_contents.strip():
self.install_lib = self.install_libbase
setup_params = dict(
src_root=None,
cmdclass={'install': install_with_pth},
package_data=package_data,
entry_points={
"distutils.commands": [
......
"""Extensions to the 'distutils' for large or complex distributions"""
import os
from fnmatch import fnmatchcase
import functools
import distutils.core
import distutils.filelist
import os
import re
import _distutils_hack.override # noqa: F401
import distutils.core
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path
from fnmatch import fnmatchcase
from ._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.extern.six import PY3, string_types
from setuptools.extern.six.moves import filter, map
import setuptools.version
from setuptools.extension import Extension
from setuptools.dist import Distribution
from setuptools.depends import Require
from . import monkey
__metaclass__ = type
__all__ = [
'setup', 'Distribution', 'Command', 'Extension', 'Require',
'SetuptoolsDeprecationWarning',
'find_packages'
'find_packages', 'find_namespace_packages',
]
if PY3:
__all__.append('find_namespace_packages')
__version__ = setuptools.version.__version__
bootstrap_install_from = None
......@@ -120,18 +114,33 @@ class PEP420PackageFinder(PackageFinder):
find_packages = PackageFinder.find
if PY3:
find_namespace_packages = PEP420PackageFinder.find
find_namespace_packages = PEP420PackageFinder.find
def _install_setup_requires(attrs):
# Note: do not use `setuptools.Distribution` directly, as
# our PEP 517 backend patch `distutils.core.Distribution`.
dist = distutils.core.Distribution(dict(
(k, v) for k, v in attrs.items()
if k in ('dependency_links', 'setup_requires')
))
class MinimalDistribution(distutils.core.Distribution):
"""
A minimal version of a distribution for supporting the
fetch_build_eggs interface.
"""
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)
def finalize_options(self):
"""
Disable finalize_options to avoid building the working set.
Ref #2158.
"""
dist = MinimalDistribution(attrs)
# Honor setup.cfg's options.
dist.parse_config_files(ignore_option_errors=True)
if dist.setup_requires:
......@@ -168,7 +177,7 @@ class Command(_Command):
if val is None:
setattr(self, option, default)
return default
elif not isinstance(val, string_types):
elif not isinstance(val, str):
raise DistutilsOptionError("'%s' must be a %s (got `%s`)"
% (option, what, val))
return val
......@@ -182,11 +191,11 @@ class Command(_Command):
val = getattr(self, option)
if val is None:
return
elif isinstance(val, string_types):
elif isinstance(val, str):
setattr(self, option, re.split(r',\s*|\s+', val))
else:
if isinstance(val, list):
ok = all(isinstance(v, string_types) for v in val)
ok = all(isinstance(v, str) for v in val)
else:
ok = False
if not ok:
......
This directory contains the Distutils package.
There's a full documentation available at:
http://docs.python.org/distutils/
The Distutils-SIG web page is also a good starting point:
http://www.python.org/sigs/distutils-sig/
$Id$
"""distutils
The main package for the Python Module Distribution Utilities. Normally
used from a setup script as
from distutils.core import setup
setup (...)
"""
import sys
__version__ = sys.version[:sys.version.index(' ')]
local = True
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import os
# If DISTUTILS_DEBUG is anything other than the empty string, we run in
# debug mode.
DEBUG = os.environ.get('DISTUTILS_DEBUG')
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment