diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 97840e4275..dd76f43d91 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 50.3.2 +current_version = 56.0.0 commit = True tag = True diff --git a/.coveragerc b/.coveragerc index 2f0e871437..6a34e662d3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,7 @@ [run] -source= - pkg_resources - setuptools -omit= - */_vendor/* +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* [report] +show_missing = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..6385b57343 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.flake8 b/.flake8 index c65806160d..dd3cc20661 100644 --- a/.flake8 +++ b/.flake8 @@ -1,12 +1,15 @@ [flake8] -exclude= - .tox - setuptools/_vendor, - pkg_resources/_vendor -ignore = - # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 - W503 - # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 - W504 - setuptools/site-patch.py F821 - setuptools/py*compat.py F811 +max-line-length = 88 + +# jaraco/skeleton#34 +max-complexity = 10 + +extend-exclude = + build + setuptools/_vendor + setuptools/_distutils + pkg_resources/_vendor + +extend-ignore = + # Black creates whitespace before colon + E203 diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000000..73911ec8a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,129 @@ +--- +name: 🐛 Bug report +description: >- + Create a report to help us improve when + something is not working correctly +title: '[BUG] ' +labels: +- bug +- Needs Triage + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to report a bug in setuptools!** + + + ⚠ + Verify first that your issue is not + [already reported on GitHub][issue search] and keep in mind and + keep in mind that we may have to keep the current behavior because + [every change breaks someone's workflow][XKCD 1172]. + We try to be mindful about this. + + Also test if the latest release and main branch are affected too. + + + If you are seeking community support, please consider + [starting a discussion][Discussions]. + + + Thank you for your collaboration! + + + [Discussions]: https://github.com/pypa/setuptools/discussions + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + + [XKCD 1172]: https://xkcd.com/1172/ + +- type: markdown + attributes: + value: >- + **Environment** +- type: input + attributes: + label: setuptools version + placeholder: For example, setuptools===60.4.2 + validations: + required: true +- type: input + attributes: + label: Python version + placeholder: For example, Python 3.10 + validations: + required: true +- type: input + attributes: + label: OS + placeholder: For example, Gentoo Linux, RHEL 8, Arch Linux, macOS etc. + validations: + required: true +- type: textarea + attributes: + label: Additional environment information + description: >- + Feel free to add more information about your environment here. + placeholder: >- + This is only happening when I run setuptools on my fridge's patched firmware 🤯 + +- type: textarea + attributes: + label: Description + description: >- + A clear and concise description of what the bug is. + placeholder: >- + I tried doing X and I expected it to result in Y because the docs + mentioned Z but what happened next what totally unexpected! + And here's why... + validations: + required: true + +- type: textarea + attributes: + label: Expected behavior + description: >- + A clear and concise description of what you expected to happen. + placeholder: >- + I tried doing X and I expected it to result in Y. I'm confused... + validations: + required: true + +- type: textarea + attributes: + label: How to Reproduce + description: >- + Describe the steps to reproduce this bug. + placeholder: | + 1. Integrate setuptools via '...' + 2. Then run '...' + 3. An error occurs. + validations: + required: true + +- type: textarea + attributes: + label: Output + description: >- + Paste the output of the steps above, including the commands + themselves and setuptools' output/traceback etc. + value: | + ```console + + ``` + 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 +... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..dde102ca11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,15 @@ +# 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: This is a place for the community to exchange ideas and recipes +- name: 💬 Discourse + url: https://discuss.python.org/c/packaging + 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 + about: Chat with devs diff --git a/.github/ISSUE_TEMPLATE/documentation-report.yml b/.github/ISSUE_TEMPLATE/documentation-report.yml new file mode 100644 index 0000000000..238ce89650 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-report.yml @@ -0,0 +1,91 @@ +--- +name: 📝 Documentation Report +title: '[Docs] ' +description: Ask us about docs +labels: +- documentation +- Needs Triage + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to report a problem with setuptools + documentation!** + + + Please fill out your suggestions below. If the problem seems + straightforward, feel free to go ahead and + submit a pull request instead! + + + ⚠ + Verify first that your issue is not [already reported on + GitHub][issue search]. + + + If you are seeking community support, please consider + [starting a discussion][Discussions]. + + + Thank you for your collaboration! + + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + + [Discussions]: https://github.com/pypa/setuptools/discussions + +- type: textarea + attributes: + label: Summary + description: > + Explain the problem briefly below, add suggestions to wording + or structure. + + + **HINT:** Did you know the documentation has a `View on GitHub` + link on every page? Feel free to use it to start a pull request + right from the GitHub UI! + placeholder: >- + I was reading the setuptools documentation of version X and I'm + having problems understanding Y. It would be very helpful if that + got rephrased as Z. + validations: + required: true + +- type: textarea + attributes: + label: OS / Environment + description: >- + Provide all relevant information below, e.g. OS version, + browser, etc. + placeholder: Fedora 33, Firefox etc. + + +- type: textarea + attributes: + label: Additional Information + description: > + Describe how this improves the documentation, e.g. before/after + situation or screenshots. + + + **HINT:** You can paste https://gist.github.com links for larger files. + placeholder: >- + When the improvement is applied, it makes it more straightforward + to understand X. + 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 +... diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000000..88ae6741ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,104 @@ +--- +name: ✨ Feature request +description: Suggest an idea for setuptools +title: '[FR] ' +labels: +- enhancement +- Needs Triage + +body: +- type: markdown + attributes: + value: > + **Thank you for wanting to suggest a feature for setuptools!** + + + 💡 + Before you go ahead with your request, please first consider if it + would be useful for majority of the setuptools users. As a general + rule of thumb, any feature that is only of interest to a small sub + group should be implemented in a third-party plugin or maybe even + just your project alone. Be mindful of the fact that the core + setuptools features have a broad impact. + + +
+ + ❗ Every change breaks someone's workflow. + + + + [![❗ Every change breaks someone's workflow.](https://imgs.xkcd.com/comics/workflow.png) + ](https://xkcd.com/1172/) +
+ + + ⚠ + Verify first that your idea is not [already requested on GitHub][issue search]. + + + + [issue search]: https://github.com/pypa/setuptools/search?q=is%3Aissue&type=issues + +- type: textarea + attributes: + label: What's the problem this feature will solve? + description: >- + What are you trying to do, that you are unable to achieve + with setuptools as it currently stands? + placeholder: >- + I'm trying to do X and I'm missing feature Y for this to be + easily achievable. + validations: + required: true + +- type: textarea + attributes: + label: Describe the solution you'd like + description: > + Clear and concise description of what you want to happen. + + + Provide examples of real world use cases that this would enable + and how it solves the problem described above. + placeholder: >- + When I do X, I want to achieve Y in a situation when Z. + validations: + required: true + +- type: textarea + attributes: + label: Alternative Solutions + description: >- + Have you tried to workaround the problem using other tools? Or a + different approach to solving this issue? Please elaborate here. + placeholder: >- + I tried doing X, Y and Z. But they are subobpimal because of P. + +- type: textarea + attributes: + label: Additional context + description: > + Add any other context, links, etc. about the feature here. + Describe how the feature would be used, why it is needed and what + it would solve. + + + **HINT:** You can paste https://gist.github.com links for + larger files. + placeholder: >- + I asked on https://stackoverflow.com/.... and the community + advised me to do X, Y and Z. + + +- 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 +... diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000000..4f70acfbcb --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,27 @@ +name: automerge +on: + pull_request: + types: + - labeled + - unlabeled + - synchronize + - opened + - edited + - ready_for_review + - reopened + - unlocked + pull_request_review: + types: + - submitted + check_suite: + types: + - completed + status: {} +jobs: + automerge: + runs-on: ubuntu-latest + steps: + - name: automerge + uses: "pascalgn/automerge-action@v0.12.0" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..37d65f33d0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,42 @@ +name: tests + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: [3.6, 3.8, 3.9, pypy3] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + release: + needs: test + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml deleted file mode 100644 index 2ee7c0fbf5..0000000000 --- a/.github/workflows/python-tests.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: >- - 👷 - Test suite - -on: - push: - pull_request: - schedule: - - cron: 1 0 * * * # Run daily at 0:01 UTC - -jobs: - tests: - name: >- - ${{ matrix.python-version }} - / - ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - # max-parallel: 5 - matrix: - python-version: - - 3.9 - - 3.8 - - pypy3 - - 3.7 - - 3.6 - - 3.5 - os: - - ubuntu-18.04 - - ubuntu-16.04 - - macOS-latest - # - windows-2019 - # - windows-2016 - include: - # 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 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: ${{ env.PYTHON_VERSION }} - - name: Log Python version - run: >- - python --version - - name: Log Python location - run: >- - which python - - name: Log Python env - run: >- - python -m sysconfig - - name: Pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - name: Upgrade pip/setuptools/wheel - run: >- - python - -m pip install - --disable-pip-version-check - --upgrade - pip setuptools wheel - - name: Install tox - run: >- - python -m pip install --upgrade tox tox-venv - - name: Log installed dists - run: >- - python -m pip freeze --all - - name: Adjust TOXENV for PyPy - if: startsWith(env.PYTHON_VERSION, 'pypy') - run: >- - echo "::set-env name=TOXENV::${{ env.PYTHON_VERSION }}" - - name: Log env vars - run: >- - env - - - name: Verify that there's no cached Python modules in sources - if: >- - ! startsWith(matrix.os, 'windows-') - run: >- - ! grep pyc setuptools.egg-info/SOURCES.txt - - - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' - run: >- - python -m - tox - --parallel auto - --parallel-live - --notest - --skip-missing-interpreters false - - name: Test with tox - run: >- - python -m - tox - --parallel auto - --parallel-live - -- - -vvvvv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..c15ab0c9e6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + +- repo: https://github.com/asottile/blacken-docs + rev: v1.9.1 + hooks: + - id: blacken-docs diff --git a/.readthedocs.yml b/.readthedocs.yml index cb10a7f991..cc698548db 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ +version: 2 python: - version: 3 - extra_requirements: - - docs - pip_install: false - requirements: docs/requirements.txt + install: + - path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7d8c102629..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -dist: xenial -language: python - -jobs: - fast_finish: true - include: - - python: pypy3 - - python: 3.5 - - python: 3.6 - - python: 3.7 - - &latest_py3 - python: 3.8 - - <<: *latest_py3 - env: LANG=C - - python: 3.8-dev - - python: 3.9-dev - - <<: *latest_py3 - env: TOXENV=docs - - arch: ppc64le - python: pypy3 - - arch: ppc64le - python: 3.5 - - &latest_py3_ppc - arch: ppc64le - python: 3.8 - - <<: *latest_py3_ppc - env: LANG=C - - arch: ppc64le - python: 3.9-dev - allow_failures: - # suppress failures due to pypa/setuptools#2000 - - python: pypy3 - - <<: *latest_py3 - env: TOXENV=docs - - -cache: pip - -before_install: -- python tools/ppc64le-patch.py - -install: - -# ensure we have recent pip/setuptools/wheel -- pip install --disable-pip-version-check --upgrade pip setuptools wheel -# need tox to get started -- pip install --upgrade tox tox-venv - -# Output the env, to verify behavior -- pip freeze --all -- env - -- "! grep pyc setuptools.egg-info/SOURCES.txt" - -script: - - export NETWORK_REQUIRED=1 - - tox - -after_success: - - export TRAVIS_JOB_NAME="${TRAVIS_PYTHON_VERSION} (LANG=$LANG)" CODECOV_ENV=TRAVIS_JOB_NAME - - tox -e coverage,codecov diff --git a/CHANGES.rst b/CHANGES.rst index 30750c0aa0..ef1d926b15 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,262 @@ +v56.0.0 +------- + + +Deprecations +^^^^^^^^^^^^ +* #2620: The ``license_file`` option is now marked as deprecated. + Use ``license_files`` instead. -- by :user:`cdce8p` + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2620: If neither ``license_file`` nor ``license_files`` is specified, the ``sdist`` + option will now auto-include files that match the following patterns: + ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS*``. + This matches the behavior of ``bdist_wheel``. -- by :user:`cdce8p` + +Changes +^^^^^^^ +* #2620: The ``license_file`` and ``license_files`` options now support glob patterns. -- by :user:`cdce8p` +* #2632: Implemented ``VendorImporter.find_spec()`` method to get rid + of ``ImportWarning`` that Python 3.10 emits when only the old-style + importer hooks are present -- by :user:`webknjaz` + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2620: Added documentation for the ``license_files`` option. -- by :user:`cdce8p` + + +v55.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2566: Remove the deprecated ``bdist_wininst`` command. Binary packages should be built as wheels instead. -- by :user:`hroncok` + + +v54.2.0 +------- + + +Changes +^^^^^^^ +* #2608: Added informative error message to PEP 517 build failures owing to + an empty ``setup.py`` -- by :user:`layday` + + +v54.1.3 +------- + +No significant changes. + + +v54.1.2 +------- + + +Misc +^^^^ +* #2595: Reduced scope of dash deprecation warning to Setuptools/distutils only -- by :user:`melissa-kun-li` + + +v54.1.1 +------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2584: Added ``sphinx-inline-tabs`` extension to allow for comparison of ``setup.py`` and its equivalent ``setup.cfg`` -- by :user:`amy-lei` + +Misc +^^^^ +* #2592: Made option keys in the ``[metadata]`` section of ``setup.cfg`` case-sensitive. Users having + uppercase option spellings will get a warning suggesting to make them to lowercase + -- by :user:`melissa-kun-li` + + +v54.1.0 +------- + + +Changes +^^^^^^^ +* #1608: Removed the conversion of dashes to underscores in the :code:`extras_require` and :code:`data_files` of :code:`setup.cfg` to support the usage of dashes. Method will warn users when they use a dash-separated key which in the future will only allow an underscore. Note: the method performs the dash to underscore conversion to preserve compatibility, but future versions will no longer support it -- by :user:`melissa-kun-li` + + +v54.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2582: Simplified build-from-source story by providing bootstrapping metadata in a separate egg-info directory. Build requirements no longer include setuptools itself. Sdist once again includes the pyproject.toml. Project can no longer be installed from source on pip 19.x, but install from source is still supported on pip < 19 and pip >= 20 and install from wheel is still supported with pip >= 9. + +Changes +^^^^^^^ +* #1932: Handled :code:`AttributeError` by raising :code:`DistutilsSetupError` in :code:`dist.check_specifier()` when specifier is not a string -- by :user:`melissa-kun-li` +* #2570: Correctly parse cmdclass in setup.cfg. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2553: Added userguide example for markers in extras_require -- by :user:`pwoolvett` + + +v53.1.0 +------- + + +Changes +^^^^^^^ +* #1937: Preserved case-sensitivity of keys in setup.cfg so that entry point names are case-sensitive. Changed sensitivity of configparser. NOTE: Any projects relying on case-insensitivity will need to adapt to accept the original case as published. -- by :user:`melissa-kun-li` +* #2573: Fixed error in uploading a Sphinx doc with the :code:`upload_docs` command. An html builder will be used. + Note: :code:`upload_docs` is deprecated for PyPi, but is supported for other sites -- by :user:`melissa-kun-li` + + +v53.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #1527: Removed bootstrap script. Now Setuptools requires pip or another pep517-compliant builder such as 'build' to build. Now Setuptools can be installed from Github main branch. + + +v52.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2537: Remove fallback support for fetch_build_eggs using easy_install. Now pip is required for setup_requires to succeed. +* #2544: Removed 'easy_install' top-level model (runpy entry point) and 'easy_install' console script. +* #2545: Removed support for eggsecutables. + +Changes +^^^^^^^ +* #2459: Tests now run in parallel via pytest-xdist, completing in about half the time. Special thanks to :user:`webknjaz` for hard work implementing test isolation. To run without parallelization, disable the plugin with ``tox -- -p no:xdist``. + + +v51.3.3 +------- + + +Misc +^^^^ +* #2539: Fix AttributeError in Description validation. + + +v51.3.2 +------- + + +Misc +^^^^ +* #1390: Validation of Description field now is more lenient, emitting a warning and mangling the value to be valid (replacing newlines with spaces). + + +v51.3.1 +------- + + +Misc +^^^^ +* #2536: Reverted tag deduplication handling. + + +v51.3.0 +------- + + +Changes +^^^^^^^ +* #1390: Newlines in metadata description/Summary now trigger a ValueError. +* #2481: Define ``create_module()`` and ``exec_module()`` methods in ``VendorImporter`` + to get rid of ``ImportWarning`` -- by :user:`hroncok` +* #2489: ``pkg_resources`` behavior for zipimport now matches the regular behavior, and finds + ``.egg-info`` (previoulsy would only find ``.dist-info``) -- by :user:`thatch` +* #2529: Fixed an issue where version tags may be added multiple times + + +v51.2.0 +------- + + +Changes +^^^^^^^ +* #2493: Use importlib.import_module() rather than the deprectated loader.load_module() + in pkg_resources namespace delaration -- by :user:`encukou` + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2525: Fix typo in the document page about entry point. -- by :user:`jtr109` + +Misc +^^^^ +* #2534: Avoid hitting network during test_easy_install. + + +v51.1.2 +------- + + +Misc +^^^^ +* #2505: Disable inclusion of package data as it causes 'tests' to be included as data. + + +v51.1.1 +------- + + +Misc +^^^^ +* #2534: Avoid hitting network during test_virtualenv.test_test_command. + + +v51.1.0 +------- + + +Changes +^^^^^^^ +* #2486: Project adopts jaraco/skeleton for shared package maintenance. + +Misc +^^^^ +* #2477: Restore inclusion of rst files in sdist. +* #2484: Setuptools has replaced the master branch with the main branch. +* #2485: Fixed failing test when pip 20.3+ is present. + -- by :user:`yan12125` +* #2487: Fix tests with pytest 6.2 + -- by :user:`yan12125` + + +v51.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2435: Require Python 3.6 or later. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2430: Fixed inconsistent RST title nesting levels caused by #2399 + -- by :user:`webknjaz` +* #2430: Fixed a typo in Sphinx docs that made docs dev section disappear + as a result of PR #2426 -- by :user:`webknjaz` + +Misc +^^^^ +* #2471: Removed the tests that guarantee that the vendored dependencies can be built by distutils. + + v50.3.2 ------- + Documentation changes ^^^^^^^^^^^^^^^^^^^^^ * #2394: Extended towncrier news template to include change note categories. @@ -23,12 +278,19 @@ Misc v50.3.1 ------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #2093: Finalized doc revamp. * #2097: doc: simplify index and group deprecated files * #2102: doc overhaul step 2: break main doc into multiple sections * #2111: doc overhaul step 3: update userguide * #2395: Added a ``:user:`` role to Sphinx config -- by :user:`webknjaz` * #2395: Added an illustrative explanation about the change notes to fragments dir -- by :user:`webknjaz` + +Misc +^^^^ * #2379: Travis CI test suite now tests against PPC64. * #2413: Suppress EOF errors (and other exceptions) when importing lib2to3. @@ -36,12 +298,20 @@ v50.3.1 v50.3.0 ------- + + +Changes +^^^^^^^ * #2368: In distutils, restore support for monkeypatched CCompiler.spawn per pypa/distutils#15. v50.2.0 ------- + + +Changes +^^^^^^^ * #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 @@ -52,24 +322,40 @@ v50.2.0 v50.1.0 ------- + + +Changes +^^^^^^^ * #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 ------- + + +Misc +^^^^ * #2363: Restore link_libpython support on Python 3.7 and earlier (see pypa/distutils#9). v50.0.2 ------- + + +Misc +^^^^ * #2352: In distutils hack, use absolute import rather than relative to avoid bpo-30876. v50.0.1 ------- + + +Misc +^^^^ * #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 @@ -80,19 +366,34 @@ v50.0.1 v50.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #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. + +Changes +^^^^^^^ * #2334: In MSVC module, refine text in error message. v49.6.0 ------- + + +Changes +^^^^^^^ * #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 ------- + + +Changes +^^^^^^^ * #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. @@ -101,43 +402,74 @@ v49.5.0 v49.4.0 ------- + + +Changes +^^^^^^^ * #2310: Updated vendored packaging version to 20.4. v49.3.2 ------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #2300: Improve the ``safe_version`` function documentation + +Misc +^^^^ * #2297: Once again, in stubs prefer exec_module to the deprecated load_module. v49.3.1 ------- + + +Changes +^^^^^^^ * #2316: Removed warning when ``distutils`` is imported before ``setuptools`` when ``distutils`` replacement is not enabled. v49.3.0 ------- + + +Changes +^^^^^^^ * #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 ------- + + +Misc +^^^^ * #2257: Fixed two flaws in distutils._msvccompiler.MSVCCompiler.spawn. v49.2.0 ------- + + +Changes +^^^^^^^ * #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 ------- + + +Misc +^^^^ * #2212: (Distutils) Allow spawn to accept environment. Avoid monkey-patching global state. * #2249: Fix extension loading technique in stubs. @@ -145,39 +477,69 @@ v49.1.3 v49.1.2 ------- + + +Changes +^^^^^^^ * #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 ------- + + +Misc +^^^^ * #2094: Removed pkg_resources.py2_warn module, which is no longer reachable. v49.0.1 ------- + + +Misc +^^^^ * #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 ------- + + +Changes +^^^^^^^ * #2228: Disabled distutils adoption for now while emergent issues are addressed. v49.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #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. + +Changes +^^^^^^^ * #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. + +Misc +^^^^ * #2199: Fix exception causes all over the codebase by using ``raise new_exception from old_exception`` v48.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #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)``. @@ -185,12 +547,20 @@ v48.0.0 v47.3.2 ------- + + +Misc +^^^^ * #2071: Replaced references to the deprecated imp package with references to importlib v47.3.1 ------- + + +Misc +^^^^ * #1973: Removed ``pkg_resources.py31compat.makedirs`` in favor of the stdlib. Use ``os.makedirs()`` instead. * #2198: Restore ``__requires__`` directive in easy-install wrapper scripts. @@ -198,22 +568,39 @@ v47.3.1 v47.3.0 ------- + + +Changes +^^^^^^^ * #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. + +Misc +^^^^ * #2195: Fix broken entry points generated by easy-install (pip editable installs). v47.2.0 ------- + + +Changes +^^^^^^^ * #2194: Editable-installed entry points now load significantly faster on Python versions 3.8+. +* #1471: Incidentally fixed by #2194 on Python 3.8 or when importlib_metadata is present. v47.1.1 ------- + + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #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. @@ -221,25 +608,44 @@ Incorporate changes from v44.1.1: v44.1.1 ------- + + +Misc +^^^^ * #2158: Avoid loading working set during ``Distribution.finalize_options`` prior to invoking ``_install_setup_requires``, broken since v42.0.0. v47.1.0 ------- + + +Changes +^^^^^^^ * #2070: In wheel-to-egg conversion, use simple pkg_resources-style namespace declaration for packages that declare namespace_packages. v47.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #2094: Setuptools now actively crashes under Python 2. Python 3.5 or later is required. Users of Python 2 should use ``setuptools<45``. + +Changes +^^^^^^^ * #1700: Document all supported keywords by migrating the ones from distutils. v46.4.0 ------- + + +Changes +^^^^^^^ * #1753: ``attr:`` now extracts variables through rudimentary examination of the AST, thereby supporting modules with third-party imports. If examining the AST fails to find the variable, ``attr:`` falls back to the old behavior of @@ -255,7 +661,14 @@ No significant changes. v46.3.0 ------- + + +Changes +^^^^^^^ * #2089: Package index functionality no longer attempts to remove an md5 fragment from the index URL. This functionality, added for distribute #163 is no longer relevant. + +Misc +^^^^ * #2041: Preserve file modes during pkg files copying, but clear read only flag for target afterwards. * #2105: Filter ``2to3`` deprecation warnings from ``TestDevelop.test_2to3_user_mode``. @@ -263,11 +676,21 @@ v46.3.0 v46.2.0 ------- + + +Changes +^^^^^^^ * #2040: Deprecated the ``bdist_wininst`` command. Binary packages should be built as wheels instead. * #2062: Change 'Mac OS X' to 'macOS' in code. * #2075: Stop recognizing files ending with ``.dist-info`` as distribution metadata. * #2086: Deprecate 'use_2to3' functionality. Packagers are encouraged to use single-source solutions or build tool chains to manage conversions outside of setuptools. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1698: Added documentation for ``build_meta`` (a bare minimum, not completed). + +Misc +^^^^ * #2082: Filter ``lib2to3`` ``PendingDeprecationWarning`` and ``DeprecationWarning`` in tests, because ``lib2to3`` is `deprecated in Python 3.9 `_. @@ -281,6 +704,10 @@ No significant changes. v46.1.2 ------- + + +Misc +^^^^ * #1458: Added template for reporting Python 2 incompatibilities. @@ -293,12 +720,17 @@ No significant changes. v46.1.0 ------- + + +Changes +^^^^^^^ * #308: Allow version number normalization to be bypassed by wrapping in a 'setuptools.sic()' call. * #1424: Prevent keeping files mode for package_data build. It may break a build if user's package data has read only flag. * #1431: In ``easy_install.check_site_dir``, ensure the installation directory exists. * #1563: In ``pkg_resources`` prefer ``find_spec`` (PEP 451) to ``find_module``. Incorporate changes from v44.1.0: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * #1704: Set sys.argv[0] in setup script run by build_meta.__legacy__ * #1959: Fix for Python 4: replace unsafe six.PY3 with six.PY2 @@ -308,6 +740,10 @@ Incorporate changes from v44.1.0: v44.1.0 ------- + + +Changes +^^^^^^^ * #1704: Set sys.argv[0] in setup script run by build_meta.__legacy__ * #1959: Fix for Python 4: replace unsafe six.PY3 with six.PY2 * #1994: Fixed a bug in the "setuptools.finalize_distribution_options" hook that lead to ignoring the order attribute of entry points managed by this hook. @@ -316,16 +752,33 @@ v44.1.0 v46.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #65: Once again as in 3.0, removed the Features feature. + +Changes +^^^^^^^ * #1890: Fix vendored dependencies so importing ``setuptools.extern.some_module`` gives the same object as ``setuptools._vendor.some_module``. This makes Metadata picklable again. * #1899: Test suite now fails on warnings. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #2011: Fix broken link to distutils docs on package_data + +Misc +^^^^ * #1991: Include pkg_resources test data in sdist, so tests can be executed from it. v45.3.0 ------- + + +Changes +^^^^^^^ * #1557: Deprecated eggsecutable scripts and updated docs. * #1904: Update msvc.py to use CPython 3.8.0 mechanism to find msvc 14+ @@ -333,6 +786,10 @@ v45.3.0 v45.2.0 ------- + + +Changes +^^^^^^^ * #1905: Fixed defect in _imp, introduced in 41.6.0 when the 'tests' directory is not present. * #1941: Improve editable installs with PEP 518 build isolation: @@ -340,12 +797,19 @@ v45.2.0 * The error shown when the install directory is not in ``PYTHONPATH`` has been turned into a warning. * #1981: Setuptools now declares its ``tests`` and ``docs`` dependencies in metadata (extras). * #1985: Add support for installing scripts in environments where bdist_wininst is missing (i.e. Python 3.9). + +Misc +^^^^ * #1968: Add flake8-2020 to check for misuse of sys.version or sys.version_info. v45.1.0 ------- + + +Changes +^^^^^^^ * #1458: Add minimum sunset date and preamble to Python 2 warning. * #1704: Set sys.argv[0] in setup script run by build_meta.__legacy__ * #1974: Add Python 3 Only Trove Classifier and remove universal wheel declaration for more complete transition from Python 2. @@ -354,26 +818,47 @@ v45.1.0 v45.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1458: Drop support for Python 2. Setuptools now requires Python 3.5 or later. Install setuptools using pip >=9 or pin to Setuptools <45 to maintain 2.7 support. + +Changes +^^^^^^^ * #1959: Fix for Python 4: replace unsafe six.PY3 with six.PY2 v44.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1908: Drop support for Python 3.4. v43.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1634: Include ``pyproject.toml`` in source distribution by default. Projects relying on the previous behavior where ``pyproject.toml`` was excluded by default should stop relying on that behavior or add ``exclude pyproject.toml`` to their MANIFEST.in file. + +Changes +^^^^^^^ * #1927: Setuptools once again declares 'setuptools' in the ``build-system.requires`` and adds PEP 517 build support by declaring itself as the ``build-backend``. It additionally specifies ``build-system.backend-path`` to rely on itself for those builders that support it. v42.0.2 ------- +Changes +^^^^^^^ + * #1921: Fix support for easy_install's ``find-links`` option in ``setup.cfg``. * #1922: Build dependencies (setup_requires and tests_require) now install transitive dependencies indicated by extras. @@ -381,12 +866,20 @@ v42.0.2 v42.0.1 ------- + + +Changes +^^^^^^^ * #1918: Fix regression in handling wheels compatibility tags. v42.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1830, #1909: Mark the easy_install script and setuptools command as deprecated, and use `pip `_ when available to fetch/build wheels for missing ``setup_requires``/``tests_require`` requirements, with the following differences in behavior: * support for ``python_requires`` * better support for wheels (proper handling of priority with respect to PEP 425 tags) @@ -395,6 +888,9 @@ v42.0.0 * no support for the ``allow_hosts`` easy_install option (``index_url``/``find_links`` are still honored) * pip environment variables are honored (and take precedence over easy_install options) * #1898: Removed the "upload" and "register" commands in favor of `twine `_. + +Changes +^^^^^^^ * #1767: Add support for the ``license_files`` option in ``setup.cfg`` to automatically include multiple license files in a source distribution. * #1829: Update handling of wheels compatibility tags: @@ -407,46 +903,82 @@ v42.0.0 v41.6.0 ------- + + +Changes +^^^^^^^ * #479: Replace usage of deprecated ``imp`` module with local re-implementation in ``setuptools._imp``. v41.5.1 ------- + + +Changes +^^^^^^^ * #1891: Fix code for detecting Visual Studio's version on Windows under Python 2. v41.5.0 ------- + + +Changes +^^^^^^^ * #1811: Improve Visual C++ 14.X support, mainly for Visual Studio 2017 and 2019. * #1814: Fix ``pkg_resources.Requirement`` hash/equality implementation: take PEP 508 direct URL into account. * #1824: Fix tests when running under ``python3.10``. * #1878: Formally deprecated the ``test`` command, with the recommendation that users migrate to ``tox``. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1860: Update documentation to mention the egg format is not supported by pip and dependency links support was dropped starting with pip 19.0. * #1862: Drop ez_setup documentation: deprecated for some time (last updated in 2016), and still relying on easy_install (deprecated too). * #1868: Drop most documentation references to (deprecated) EasyInstall. * #1884: Added a trove classifier to document support for Python 3.8. + +Misc +^^^^ * #1886: Added Python 3.8 release to the Travis test matrix. v41.4.0 ------- + + +Changes +^^^^^^^ * #1847: In declarative config, now traps errors when invalid ``python_requires`` values are supplied. v41.3.0 ------- + + +Changes +^^^^^^^ * #1690: When storing extras, rely on OrderedSet to retain order of extras as indicated by the packager, which will also be deterministic on Python 2.7 (with PYTHONHASHSEED unset) and Python 3.6+. + +Misc +^^^^ * #1858: Fixed failing integration test triggered by 'long_description_content_type' in packaging. v41.2.0 ------- + + +Changes +^^^^^^^ * #479: Remove some usage of the deprecated ``imp`` module. + +Misc +^^^^ * #1565: Changed html_sidebars from string to list of string as per https://www.sphinx-doc.org/en/master/changes.html#id58 @@ -454,6 +986,10 @@ v41.2.0 v41.1.0 ------- + + +Misc +^^^^ * #1697: Moved most of the constants from setup.py to setup.cfg * #1749: Fixed issue with the PEP 517 backend where building a source distribution would fail if any tarball existed in the destination directory. * #1750: Fixed an issue with PEP 517 backend where wheel builds would fail if the destination directory did not already exist. @@ -461,12 +997,19 @@ v41.1.0 * #1769: Improve ``package_data`` check: ensure the dictionary values are lists/tuples of strings. * #1788: Changed compatibility fallback logic for ``html.unescape`` to avoid accessing ``HTMLParser.unescape`` when not necessary. ``HTMLParser.unescape`` is deprecated and will be removed in Python 3.9. * #1790: Added the file path to the error message when a ``UnicodeDecodeError`` occurs while reading a metadata file. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1776: Use license classifiers rather than the license field. v41.0.1 ------- + + +Changes +^^^^^^^ * #1671: Fixed issue with the PEP 517 backend that prevented building a wheel when the ``dist/`` directory contained existing ``.whl`` files. * #1709: In test.paths_on_python_path, avoid adding unnecessary duplicates to the PYTHONPATH. * #1741: In package_index, now honor "current directory" during a checkout of git and hg repositories under Windows @@ -475,22 +1018,37 @@ v41.0.1 v41.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1735: When parsing setup.cfg files, setuptools now requires the files to be encoded as UTF-8. Any other encoding will lead to a UnicodeDecodeError. This change removes support for specifying an encoding using a 'coding: ' directive in the header of the file, a feature that was introduces in 40.7. Given the recent release of the aforementioned feature, it is assumed that few if any projects are utilizing the feature to specify an encoding other than UTF-8. v40.9.0 ------- + + +Changes +^^^^^^^ * #1675: Added support for ``setup.cfg``-only projects when using the ``setuptools.build_meta`` backend. Projects that have enabled PEP 517 no longer need to have a ``setup.py`` and can use the purely declarative ``setup.cfg`` configuration file instead. * #1720: Added support for ``pkg_resources.parse_requirements``-style requirements in ``setup_requires`` when ``setup.py`` is invoked from the ``setuptools.build_meta`` build backend. * #1664: Added the path to the ``PKG-INFO`` or ``METADATA`` file in the exception text when the ``Version:`` header can't be found. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1705: Removed some placeholder documentation sections referring to deprecated features. v40.8.0 ------- + + +Changes +^^^^^^^ * #1652: Added the ``build_meta:__legacy__`` backend, a "compatibility mode" PEP 517 backend that can be used as the default when ``build-backend`` is left unspecified in ``pyproject.toml``. * #1635: Resource paths are passed to ``pkg_resources.resource_string`` and similar no longer accept paths that traverse parents, that begin with a leading ``/``. Violations of this expectation raise DeprecationWarnings and will become errors. Additionally, any paths that are absolute on Windows are strictly disallowed and will raise ValueErrors. * #1536: ``setuptools`` will now automatically include licenses if ``setup.cfg`` contains a ``license_file`` attribute, unless this file is manually excluded inside ``MANIFEST.in``. @@ -499,25 +1057,44 @@ v40.8.0 v40.7.3 ------- + + +Changes +^^^^^^^ * #1670: In package_index, revert to using a copy of splituser from Python 3.8. Attempts to use ``urllib.parse.urlparse`` led to problems as reported in #1663 and #1668. This change serves as an alternative to #1499 and fixes #1668. v40.7.2 ------- + + +Changes +^^^^^^^ * #1666: Restore port in URL handling in package_index. v40.7.1 ------- + + +Changes +^^^^^^^ * #1660: On Python 2, when reading config files, downcast options from text to bytes to satisfy distutils expectations. v40.7.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1551: File inputs for the ``license`` field in ``setup.cfg`` files now explicitly raise an error. + +Changes +^^^^^^^ * #1180: Add support for non-ASCII in setup.cfg (#1062). Add support for native strings on some parameters (#1136). * #1499: ``setuptools.package_index`` no longer relies on the deprecated ``urllib.parse.splituser`` per Python #27485. * #1544: Added tests for PackageIndex.download (for git URLs). @@ -527,30 +1104,51 @@ v40.7.0 v40.6.3 ------- + + +Changes +^^^^^^^ * #1594: PEP 517 backend no longer declares setuptools as a dependency as it can be assumed. v40.6.2 ------- + + +Changes +^^^^^^^ * #1592: Fix invalid dependency on external six module (instead of vendored version). v40.6.1 ------- + + +Changes +^^^^^^^ * #1590: Fixed regression where packages without ``author`` or ``author_email`` fields generated malformed package metadata. v40.6.0 ------- + + +Deprecations +^^^^^^^^^^^^ * #1541: Officially deprecated the ``requires`` parameter in ``setup()``. + +Changes +^^^^^^^ * #1519: In ``pkg_resources.normalize_path``, additional path normalization is now performed to ensure path values to a directory is always the same, preventing false positives when checking scripts have a consistent prefix to set up on Windows. * #1545: Changed the warning class of all deprecation warnings; deprecation warning classes are no longer derived from ``DeprecationWarning`` and are thus visible by default. * #1554: ``build_meta.build_sdist`` now includes ``setup.py`` in source distributions by default. * #1576: Started monkey-patching ``get_metadata_version`` and ``read_pkg_file`` onto ``distutils.DistributionMetadata`` to retain the correct version on the ``PKG-INFO`` file in the (deprecated) ``upload`` command. -* #1533: Restricted the ``recursive-include setuptools/_vendor`` to contain only .py and .txt files. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1395: Changed Pyrex references to Cython in the documentation. * #1456: Documented that the ``rpmbuild`` packages is required for the ``bdist_rpm`` command. * #1537: Documented how to use ``setup.cfg`` for ``src/ layouts`` @@ -559,69 +1157,115 @@ v40.6.0 * #1553: Updated installation instructions to point to ``pip install`` instead of ``ez_setup.py``. * #1560: Updated ``setuptools`` distribution documentation to remove some outdated information. * #1564: Documented ``setup.cfg`` minimum version for version and project_urls. + +Misc +^^^^ +* #1533: Restricted the ``recursive-include setuptools/_vendor`` to contain only .py and .txt files. * #1572: Added the ``concurrent.futures`` backport ``futures`` to the Python 2.7 test suite requirements. v40.5.0 ------- + + +Changes +^^^^^^^ * #1335: In ``pkg_resources.normalize_path``, fix issue on Cygwin when cwd contains symlinks. * #1502: Deprecated support for downloads from Subversion in package_index/easy_install. * #1517: Dropped use of six.u in favor of ``u""`` literals. * #1520: Added support for ``data_files`` in ``setup.cfg``. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1525: Fixed rendering of the deprecation warning in easy_install doc. v40.4.3 ------- + + +Changes +^^^^^^^ * #1480: Bump vendored pyparsing in pkg_resources to 2.2.1. v40.4.2 ------- + + +Misc +^^^^ * #1497: Updated gitignore in repo. v40.4.1 ------- + + +Changes +^^^^^^^ * #1480: Bump vendored pyparsing to 2.2.1. v40.4.0 ------- + + +Changes +^^^^^^^ * #1481: Join the sdist ``--dist-dir`` and the ``build_meta`` sdist directory argument to point to the same target (meaning the build frontend no longer needs to clean manually the dist dir to avoid multiple sdist presence, and setuptools no longer needs to handle conflicts between the two). v40.3.0 ------- + + +Changes +^^^^^^^ * #1402: Fixed a bug with namespace packages under Python 3.6 when one package in current directory hides another which is installed. * #1427: Set timestamp of ``.egg-info`` directory whenever ``egg_info`` command is run. * #1474: ``build_meta.get_requires_for_build_sdist`` now does not include the ``wheel`` package anymore. * #1486: Suppress warnings in pkg_resources.handle_ns. + +Misc +^^^^ * #1479: Remove internal use of six.binary_type. v40.2.0 ------- + + +Changes +^^^^^^^ * #1466: Fix handling of Unicode arguments in PEP 517 backend v40.1.1 -------- + + +Changes +^^^^^^^ * #1465: Fix regression with ``egg_info`` command when tagging is used. v40.1.0 ------- + + +Changes +^^^^^^^ * #1410: Deprecated ``upload`` and ``register`` commands. * #1312: Introduced find_namespace_packages() to find PEP 420 namespace packages. * #1420: Added find_namespace: directive to config parser. @@ -631,24 +1275,44 @@ v40.1.0 * #1388: Fixed "Microsoft Visual C++ Build Tools" link in exception when Visual C++ not found. * #1389: Added support for scripts which have unicode content. * #1416: Moved several Python version checks over to using ``six.PY2`` and ``six.PY3``. + +Misc +^^^^ * #1441: Removed spurious executable permissions from files that don't need them. v40.0.0 ------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ * #1342: Drop support for Python 3.3. + +Changes +^^^^^^^ * #1366: In package_index, fixed handling of encoded entities in URLs. * #1383: In pkg_resources VendorImporter, avoid removing packages imported from the root. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1379: Minor doc fixes after actually using the new release process. * #1385: Removed section on non-package data files. * #1403: Fix developer's guide. + +Misc +^^^^ * #1404: Fix PEP 518 configuration: set build requirements in ``pyproject.toml`` to ``["wheel"]``. v39.2.0 ------- + + +Changes +^^^^^^^ * #1359: Support using "file:" to load a PEP 440-compliant package version from a text file. * #1360: Fixed issue with a mismatch between the name of the package and the @@ -657,11 +1321,17 @@ v39.2.0 includes the attributes in the ``_provider`` instance variable. * #1365: Take the package_dir option into account when loading the version from a module attribute. + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ * #1353: Added coverage badge to README. * #1356: Made small fixes to the developer guide documentation. * #1357: Fixed warnings in documentation builds and started enforcing that the docs build without warnings in tox. * #1376: Updated release process docs. + +Misc +^^^^ * #1343: The ``setuptools`` specific ``long_description_content_type``, ``project_urls`` and ``provides_extras`` fields are now set consistently after any ``distutils`` ``setup_keywords`` calls, allowing them to override diff --git a/LICENSE b/LICENSE index 6e0693b4b0..353924be0e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,19 @@ -Copyright (C) 2016 Jason R Coombs +Copyright Jason R. Coombs -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 128ae280ec..3e8f09de37 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,12 @@ -recursive-include setuptools *.py *.exe *.xml +recursive-include setuptools *.py *.exe *.xml *.tmpl recursive-include tests *.py recursive-include setuptools/tests *.html -recursive-include docs *.py *.txt *.conf *.css *.css_t Makefile indexsidebar.html +recursive-include docs *.py *.txt *.rst *.conf *.css *.css_t Makefile indexsidebar.html recursive-include setuptools/_vendor *.py *.txt recursive-include pkg_resources *.py *.txt recursive-include pkg_resources/tests/data * +recursive-include tools * +recursive-include changelog.d * include *.py include *.rst include MANIFEST.in @@ -13,4 +15,3 @@ include launcher.c include msvc-build-launcher.cmd include pytest.ini include tox.ini -exclude pyproject.toml # Temporary workaround for #1644. diff --git a/README.rst b/README.rst index 824a033fb8..9bd03cf949 100644 --- a/README.rst +++ b/README.rst @@ -6,14 +6,13 @@ .. _PyPI link: https://pypi.org/project/setuptools -.. image:: https://dev.azure.com/jaraco/setuptools/_apis/build/status/pypa.setuptools?branchName=master - :target: https://dev.azure.com/jaraco/setuptools/_build/latest?definitionId=1&branchName=master +.. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests -.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20CI&logo=travis&logoColor=white - :target: https://travis-ci.org/pypa/setuptools - -.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20CI&logo=appveyor&logoColor=white - :target: https://ci.appveyor.com/project/pypa/setuptools/branch/master +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black .. image:: https://img.shields.io/readthedocs/setuptools/latest.svg :target: https://setuptools.readthedocs.io @@ -35,9 +34,13 @@ Bug reports and especially tested patches may be submitted directly to the `bug tracker `_. -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. + +Code of Conduct +=============== + +Everyone interacting in the setuptools project's codebases, issue trackers, +chat rooms, and mailing lists is expected to follow the +`PSF Code of Conduct `_. For Enterprise @@ -49,9 +52,10 @@ Setuptools and the maintainers of thousands of other packages are working with T `Learn more `_. -Code of Conduct -=============== -Everyone interacting in the setuptools project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the -`PSF Code of Conduct `_. +Security Contact +================ + +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index c31edfed17..47ce24944b 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -8,6 +8,11 @@ is_pypy = '__pypy__' in sys.builtin_module_names +warnings.filterwarnings('ignore', + '.+ distutils .+ deprecated', + DeprecationWarning) + + def warn_distutils_present(): if 'distutils' not in sys.modules: return diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4d1ae55f4f..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,54 +0,0 @@ -clone_depth: 50 - -environment: - - APPVEYOR: True - NETWORK_REQUIRED: True - CODECOV_ENV: APPVEYOR_JOB_NAME - - matrix: - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - APPVEYOR_JOB_NAME: "Python38-x64-vs2015" - PYTHON: "C:\\Python38-x64" - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - APPVEYOR_JOB_NAME: "Python38-x64-vs2017" - PYTHON: "C:\\Python38-x64" - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - 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 - - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" - - "SET PYTHON=\"C:\\Program Files\\Python\"" - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - -build: off - -cache: - - '%LOCALAPPDATA%\pip\Cache' - -test_script: - - python --version - - python -m pip install --disable-pip-version-check --upgrade pip setuptools wheel - - pip install --upgrade tox tox-venv virtualenv - - pip freeze --all - - tox -- --junit-xml=test-results.xml - -after_test: - - tox -e coverage,codecov - -on_finish: - - ps: | - $wc = New-Object 'System.Net.WebClient' - $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test-results.xml)) - -version: '{build}' diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 4567b9b043..0000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,82 +0,0 @@ -# Create the project in Azure with: -# az devops project create --name $name --organization https://dev.azure.com/$org/ --visibility public -# then configure the pipelines (through web UI) - -trigger: - branches: - include: - - '*' - tags: - include: - - '*' - -pool: - vmImage: $(pool_vm_image) - -variables: -- group: Azure secrets -- name: pool_vm_image - value: Ubuntu-18.04 - -stages: -- stage: Test - jobs: - - - job: 'Test' - strategy: - matrix: - Bionic Python 3.6: - python.version: '3.6' - Bionic Python 3.8: - python.version: '3.8' - Windows: - python.version: '3.8' - pool_vm_image: vs2017-win2016 - MacOS: - python.version: '3.8' - pool_vm_image: macos-10.15 - - maxParallel: 4 - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - - - script: python -m pip install tox - displayName: 'Install tox' - - - script: | - tox -- --junit-xml=test-results.xml - displayName: 'run tests' - - - task: PublishTestResults@2 - inputs: - testResultsFiles: '**/test-results.xml' - testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() - -- stage: Publish - dependsOn: Test - jobs: - - job: 'Publish' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.8' - architecture: 'x64' - - - script: python -m pip install tox - displayName: 'Install tox' - - - script: | - tox -e release - env: - TWINE_PASSWORD: $(PyPI-token) - TIDELIFT_TOKEN: $(Tidelift-token) - GITHUB_TOKEN: $(Github-token) - displayName: 'publish to PyPI' - - condition: contains(variables['Build.SourceBranch'], 'tags') diff --git a/bootstrap.egg-info/PKG-INFO b/bootstrap.egg-info/PKG-INFO new file mode 100644 index 0000000000..6e11ceeb9b --- /dev/null +++ b/bootstrap.egg-info/PKG-INFO @@ -0,0 +1,2 @@ +Name: setuptools-bootstrap +Version: 1.0 diff --git a/bootstrap.egg-info/entry_points.txt b/bootstrap.egg-info/entry_points.txt new file mode 100644 index 0000000000..834d674e57 --- /dev/null +++ b/bootstrap.egg-info/entry_points.txt @@ -0,0 +1,14 @@ +[distutils.commands] +egg_info = setuptools.command.egg_info:egg_info + +[distutils.setup_keywords] +include_package_data = setuptools.dist:assert_bool +install_requires = setuptools.dist:check_requirements +extras_require = setuptools.dist:check_extras +entry_points = setuptools.dist:check_entry_points + +[egg_info.writers] +PKG-INFO = setuptools.command.egg_info:write_pkg_info +dependency_links.txt = setuptools.command.egg_info:overwrite_arg +entry_points.txt = setuptools.command.egg_info:write_entries +requires.txt = setuptools.command.egg_info:write_requirements diff --git a/bootstrap.py b/bootstrap.py index 118671f62c..229b996503 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -1,57 +1,7 @@ -""" -If setuptools is not already installed in the environment, it's not possible -to invoke setuptools' own commands. This routine will bootstrap this local -environment by creating a minimal egg-info directory and then invoking the -egg-info command to flesh out the egg-info directory. -""" +import warnings -import os -import sys -import textwrap -import subprocess -import io +msg = "bootstrap.py is no longer needed. Use a PEP-517-compatible builder instead." -minimal_egg_info = textwrap.dedent(""" - [distutils.commands] - egg_info = setuptools.command.egg_info:egg_info - [distutils.setup_keywords] - include_package_data = setuptools.dist:assert_bool - install_requires = setuptools.dist:check_requirements - extras_require = setuptools.dist:check_extras - entry_points = setuptools.dist:check_entry_points - - [egg_info.writers] - PKG-INFO = setuptools.command.egg_info:write_pkg_info - dependency_links.txt = setuptools.command.egg_info:overwrite_arg - entry_points.txt = setuptools.command.egg_info:write_entries - requires.txt = setuptools.command.egg_info:write_requirements - """) - - -def ensure_egg_info(): - if os.path.exists('setuptools.egg-info'): - return - print("adding minimal entry_points") - add_minimal_info() - run_egg_info() - - -def add_minimal_info(): - """ - Build a minimal egg-info, enough to invoke egg_info - """ - - os.mkdir('setuptools.egg-info') - with io.open('setuptools.egg-info/entry_points.txt', 'w') as ep: - ep.write(minimal_egg_info) - - -def run_egg_info(): - cmd = [sys.executable, 'setup.py', 'egg_info'] - print("Regenerating egg_info") - subprocess.check_call(cmd) - - -__name__ == '__main__' and ensure_egg_info() +__name__ == '__main__' and warnings.warn(msg) diff --git a/changelog.d/2430.doc.1.rst b/changelog.d/2430.doc.1.rst deleted file mode 100644 index d09e0b9e7b..0000000000 --- a/changelog.d/2430.doc.1.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed a typo in Sphinx docs that made docs dev section disappear -as a result of PR #2426 -- by :user:`webknjaz` diff --git a/changelog.d/2430.doc.2.rst b/changelog.d/2430.doc.2.rst deleted file mode 100644 index 0ac8782323..0000000000 --- a/changelog.d/2430.doc.2.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed inconsistent RST title nesting levels caused by #2399 --- by :user:`webknjaz` diff --git a/changelog.d/2644.misc.rst b/changelog.d/2644.misc.rst new file mode 100644 index 0000000000..1aa9fbbe4e --- /dev/null +++ b/changelog.d/2644.misc.rst @@ -0,0 +1 @@ +Fixed ``DeprecationWarning`` due to ``threading.Thread.setDaemon`` in tests -- by :user:`tirkarthi` diff --git a/changelog.d/2654.misc.rst b/changelog.d/2654.misc.rst new file mode 100644 index 0000000000..268c4a2e46 --- /dev/null +++ b/changelog.d/2654.misc.rst @@ -0,0 +1,2 @@ +Made the changelog generator compatible +with Towncrier >= 19.9 -- :user:`webknjaz` diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html new file mode 100644 index 0000000000..ce48f46b8a --- /dev/null +++ b/docs/_templates/tidelift-sidebar.html @@ -0,0 +1,6 @@ +

For Enterprise

+ +

+Professionally-supported {{ project }} is available with the +Tidelift Subscription. +

diff --git a/docs/build_meta.rst b/docs/build_meta.rst index c36e2bab38..2ad5ae267e 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -7,7 +7,7 @@ What is it? Python packaging has come `a long way `_. -The traditional ``setuptools`` way of packgaging Python modules +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. @@ -67,14 +67,11 @@ specify the package information:: [options] packages = find: -Now generate the distribution. Although the PyPA is still working to -`provide a recommended tool `_ -to build packages, the `pep517 package `_ -provides this functionality. To build the package:: +Now generate the distribution. To build the package, use +`PyPA build `_:: - $ pip install -q pep517 - $ mkdir dist - $ python -m pep517.build . + $ pip install -q build + $ python -m build And now it's done! The ``.whl`` file and ``.tar.gz`` can then be distributed and installed:: diff --git a/docs/conf.py b/docs/conf.py index de9fe3f892..2f0d3cb3d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,81 +1,6 @@ -import subprocess -import sys -import os +extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] - -# hack to run the bootstrap script so that jaraco.packaging.sphinx -# can invoke setup.py -'READTHEDOCS' in os.environ and subprocess.check_call( - [sys.executable, '-m', 'bootstrap'], - cwd=os.path.join(os.path.dirname(__file__), os.path.pardir), -) - -# -- Project information ----------------------------------------------------- - -github_url = 'https://github.com' -github_sponsors_url = f'{github_url}/sponsors' - -# -- General configuration -- - -extensions = [ - 'sphinx.ext.extlinks', # allows to create custom roles easily - 'sphinx.ext.intersphinx', # allows interlinking external docs sites - 'jaraco.packaging.sphinx', - 'rst.linker', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The master toctree document. -master_doc = 'index' - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# -- Options for extlinks extension --------------------------------------- -extlinks = { - 'user': (f'{github_sponsors_url}/%s', '@'), # noqa: WPS323 -} - -# -- Options for HTML output -- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_theme'] - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -html_sidebars = { - 'index': [ - 'relations.html', 'sourcelink.html', 'indexsidebar.html', - 'searchbox.html']} - -# If false, no module index is generated. -html_use_modindex = False - -# If false, no index is generated. -html_use_index = False - -# -- Options for LaTeX output -- - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, -# documentclass [howto/manual]). -latex_documents = [( - 'index', 'Setuptools.tex', 'Setuptools Documentation', - 'The fellowship of the packaging', 'manual', -)] +master_doc = "index" link_files = { '../CHANGES.rst': dict( @@ -148,10 +73,35 @@ ), } +intersphinx_mapping = { + 'pypa-build': ('https://pypa-build.readthedocs.io/en/latest/', None) +} + +# Add support for linking usernames +github_url = 'https://github.com' +github_sponsors_url = f'{github_url}/sponsors' +extlinks = { + 'user': (f'{github_sponsors_url}/%s', '@'), # noqa: WPS323 +} +extensions += ['sphinx.ext.extlinks', 'sphinx.ext.intersphinx'] # Be strict about any broken references: nitpicky = True +# Ref: https://github.com/python-attrs/attrs/pull/571/files\ +# #diff-85987f48f1258d9ee486e3191495582dR82 +default_role = 'any' + +# Custom sidebar templates, maps document names to template names. +html_theme = 'alabaster' +templates_path = ['_templates'] +html_sidebars = {'index': ['tidelift-sidebar.html']} + +# Add support for inline tabs +extensions += ['sphinx_inline_tabs'] + +# Support for distutils + # Ref: https://stackoverflow.com/a/30624034/595220 nitpick_ignore = [ ('c:func', 'SHGetSpecialFolderPath'), # ref to MS docs @@ -188,12 +138,6 @@ ('py:mod', 'docutils'), # there's no Sphinx site documenting this ] - -# Ref: https://github.com/python-attrs/attrs/pull/571/files\ -# #diff-85987f48f1258d9ee486e3191495582dR82 -default_role = 'any' - - # Allow linking objects on other Sphinx sites seamlessly: intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), diff --git a/docs/pkg_resources.rst b/docs/pkg_resources.rst index 364e218328..994bea6f2b 100644 --- a/docs/pkg_resources.rst +++ b/docs/pkg_resources.rst @@ -10,6 +10,13 @@ eggs, support for merging packages that have separately-distributed modules or subpackages, and APIs for managing Python's current "working set" of active packages. +Use of ``pkg_resources`` is discouraged in favor of +`importlib.resources `_, +`importlib.metadata `_, +and their backports (`resources `_, +`metadata `_). +Please consider using those libraries instead of pkg_resources. + .. contents:: **Table of Contents** @@ -703,7 +710,7 @@ entry point group and look for entry points named "pre_process" and To advertise an entry point, a project needs to use ``setuptools`` and provide an ``entry_points`` argument to ``setup()`` in its setup script, so that the entry points will be included in the distribution's metadata. For more -details, see the [``setuptools`` documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins). +details, see :ref:`Advertising Behavior`. Each project distribution can advertise at most one entry point of a given name within the same entry point group. For example, a distutils extension @@ -1939,4 +1946,3 @@ History 0.3a1 * Initial release. - diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst index 03ce9fa23a..619b2d1493 100644 --- a/docs/references/keywords.rst +++ b/docs/references/keywords.rst @@ -76,6 +76,17 @@ Keywords ``license`` A string specifying the license of the package. +``license_file`` + + .. warning:: + ``license_file`` is deprecated. Use ``license_files`` instead. + +``license_files`` + + A list of glob patterns for license related files that should be included. + If neither ``license_file`` nor ``license_files`` is specified, this option + defaults to ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, and ``AUTHORS*``. + ``keywords`` A list of strings or a comma-separated string providing descriptive meta-data. See: `PEP 0314`_. diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 104d68faef..0000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# keep these in sync with setup.cfg -sphinx -jaraco.packaging>=6.1 -rst.linker>=1.9 -pygments-github-lexers==0.0.5 - -setuptools>=34 diff --git a/docs/setuptools.rst b/docs/setuptools.rst index 1000a0cebf..541bec515a 100644 --- a/docs/setuptools.rst +++ b/docs/setuptools.rst @@ -54,7 +54,7 @@ Feature Highlights: Developer's Guide ----------------- - +The developer's guide has been updated. See the :doc:`most recent version `. @@ -157,7 +157,7 @@ To use this feature: ] build-backend = "setuptools.build_meta" -* Use a :pep:`517` compatible build frontend, such as ``pip >= 19`` or ``pep517``. +* Use a :pep:`517` compatible build frontend, such as ``pip >= 19`` or ``build``. .. warning:: diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index bc66869b6e..7c97ca1cfa 100644 --- a/docs/userguide/declarative_config.rst +++ b/docs/userguide/declarative_config.rst @@ -184,7 +184,7 @@ maintainer_email maintainer-email str classifiers classifier file:, list-comma license str license_file str -license_files list-comma +license_files list-comma 42.0.0 description summary file:, str long_description long-description file:, str long_description_content_type str 38.6.0 @@ -243,6 +243,19 @@ data_files dict 40.6.0 **find_namespace directive** - The ``find_namespace:`` directive is supported since Python >=3.3. + Notes: 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``. + +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] + rest = docutils>=0.3; pack ==1.1, ==1.3 + pdf = + ReportLab>=1.2 + RXP + importlib-metadata; python_version < "3.8" diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 354a9f8c36..188083e0fb 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -3,7 +3,7 @@ Dependencies Management in Setuptools ===================================== There are three types of dependency styles offered by setuptools: -1) build system requirement, required dependency and 3) optional +1) build system requirement, 2) required dependency and 3) optional dependency. .. Note:: @@ -19,8 +19,8 @@ Build system requirement Package requirement ------------------- After organizing all the scripts and files and getting ready for packaging, -there needs to be a way to tell Python what programs it need to actually -do the packgaging (in our case, ``setuptools`` of course). Usually, +there needs to be a way to tell Python what programs it needs to actually +do the packaging (in our case, ``setuptools`` of course). Usually, you also need the ``wheel`` package as well since it is recommended that you upload a ``.whl`` file to PyPI alongside your ``.tar.gz`` file. Unlike the other two types of dependency keyword, this one is specified in your @@ -47,32 +47,36 @@ Declaring required dependency This is where a package declares its core dependencies, without which it won't be able to run. ``setuptools`` support automatically download and install these dependencies when the package is installed. Although there is more -finess to it, let's start with a simple example. +finesse to it, let's start with a simple example. -.. code-block:: ini +.. tab:: setup.cfg - [options] - #... - install_requires = - docutils - BazSpam ==1.1 + .. code-block:: ini + + [options] + #... + install_requires = + docutils + BazSpam ==1.1 + +.. tab:: setup.py -.. code-block:: python + .. code-block:: python - setup( - #..., - install_requires = [ - 'docutils', - 'BazSpam ==1.1' - ] - ) + setup( + #..., + install_requires = [ + 'docutils', + 'BazSpam ==1.1' + ] + ) When your project is installed (e.g. using pip), all of the dependencies not already installed will be located (via PyPI), downloaded, built (if necessary), and installed and 2) Any scripts in your project will be installed with wrappers that verify the availability of the specified dependencies at runtime. - + Platform specific dependencies ------------------------------ @@ -82,41 +86,49 @@ specific dependencies. For example, the ``enum`` package was added in Python 3.4, therefore, package that depends on it can elect to install it only when the Python version is older than 3.4. To accomplish this -.. code-block:: ini +.. tab:: setup.cfg - [options] - #... - install_requires = - enum34;python_version<'3.4' + .. code-block:: ini -.. code-block:: python - - setup( + [options] #... - install_requires=[ - "enum34;python_version<'3.4'",] - ) + install_requires = + enum34;python_version<'3.4' + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + install_requires=[ + "enum34;python_version<'3.4'",] + ) Similarly, if you also wish to declare ``pywin32`` with a minimal version of 1.0 and only install it if the user is using a Windows operating system: -.. code-block:: ini - - [options] - #... - install_requires = - enum34;python_version<'3.4' - pywin32 >= 1.0;platform_system=='Windows' +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - install_requires=[ - "enum34;python_version<'3.4'", - "pywin32 >= 1.0;platform_system=='Windows'" - ] - ) + install_requires = + enum34;python_version<'3.4' + pywin32 >= 1.0;platform_system=='Windows' + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + install_requires=[ + "enum34;python_version<'3.4'", + "pywin32 >= 1.0;platform_system=='Windows'" + ] + ) The environmental markers that may be used for testing platform types are detailed in `PEP 508 `_. @@ -181,20 +193,24 @@ The ``dependency_links`` option takes the form of a list of URL strings. For example, this will cause a search of the specified page for eggs or source distributions, if the package's dependencies aren't already installed: -.. code-block:: ini - - [options] - #... - dependency_links = http://peak.telecommunity.com/snapshots/ +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - dependency_links=[ - "http://peak.telecommunity.com/snapshots/" - ], - ) + dependency_links = http://peak.telecommunity.com/snapshots/ + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + dependency_links=[ + "http://peak.telecommunity.com/snapshots/" + ], + ) Optional dependencies @@ -202,7 +218,7 @@ Optional dependencies Setuptools allows you to declare dependencies that only get installed under specific circumstances. These dependencies are specified with ``extras_require`` keyword and are only installed if another package depends on it (either -directly or indirectly) This makes it convenient to declare dependencies for +directly or indirectly) This makes it convenient to declare dependencies for ancillary functions such as "tests" and "docs". .. note:: @@ -211,24 +227,28 @@ ancillary functions such as "tests" and "docs". For example, Package-A offers optional PDF support and requires two other dependencies for it to work: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Package-A + .. code-block:: ini - [options.extras_require] - PDF = ReportLab>=1.2; RXP + [metadata] + name = Package-A + [options.extras_require] + PDF = ReportLab>=1.2; RXP -.. code-block:: python - setup( - name="Project-A", - #... - extras_require={ - "PDF": ["ReportLab>=1.2", "RXP"], - } - ) +.. tab:: setup.py + + .. code-block:: python + + setup( + name="Project-A", + #... + extras_require={ + "PDF": ["ReportLab>=1.2", "RXP"], + } + ) The name ``PDF`` is an arbitary identifier of such a list of dependencies, to which other components can refer and have them installed. There are two common @@ -236,57 +256,69 @@ use cases. First is the console_scripts entry point: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Project A - #... + .. code-block:: ini - [options] - #... - entry_points= - [console_scripts] - rst2pdf = project_a.tools.pdfgen [PDF] - rst2html = project_a.tools.htmlgen - -.. code-block:: python - - setup( - name = "Project-A" - #..., - entry_points={ - "console_scripts": [ - "rst2pdf = project_a.tools.pdfgen [PDF]", - "rst2html = project_a.tools.htmlgen", - ], - } - ) + [metadata] + name = Project A + #... -When the script ``rst2pdf`` is run, it will trigger the installation of -the two dependencies ``PDF`` maps to. + [options] + #... + entry_points= + [console_scripts] + rst2pdf = project_a.tools.pdfgen [PDF] + rst2html = project_a.tools.htmlgen + +.. tab:: setup.py + + .. code-block:: python + + setup( + name = "Project-A" + #..., + entry_points={ + "console_scripts": [ + "rst2pdf = project_a.tools.pdfgen [PDF]", + "rst2html = project_a.tools.htmlgen", + ], + } + ) + +This syntax indicates that the entry point (in this case a console script) +is only valid when the PDF extra is installed. It is up to the installer +to determine how to handle the situation where PDF was not indicated +(e.g. omit the console script, provide a warning when attempting to load +the entry point, assume the extras are present and let the implementation +fail later). The second use case is that other package can use this "extra" for their own dependencies. For example, if "Project-B" needs "project A" with PDF support installed, it might declare the dependency like this: -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = Project-B - #... + .. code-block:: ini - [options] - #... - install_requires = - Project-A[PDF] + [metadata] + name = Project-B + #... + + [options] + #... + install_requires = + Project-A[PDF] + +.. tab:: setup.py -.. code-block:: python + .. code-block:: python - setup( - name="Project-B", - install_requires=["Project-A[PDF]"], - ... - ) + setup( + name="Project-B", + install_requires=["Project-A[PDF]"], + ... + ) This will cause ReportLab to be installed along with project A, if project B is installed -- even if project A was already installed. In this way, a project diff --git a/docs/userguide/development_mode.rst b/docs/userguide/development_mode.rst index bce724a79f..3c477ec114 100644 --- a/docs/userguide/development_mode.rst +++ b/docs/userguide/development_mode.rst @@ -3,9 +3,9 @@ Under normal circumstances, the ``distutils`` assume that you are going to build a distribution of your project, not use it in its "raw" or "unbuilt" -form. If you were to use the ``distutils`` that way, you would have to rebuild -and reinstall your project every time you made a change to it during -development. +form. However, if you were to use the ``distutils`` to build a distribution, +you would have to rebuild and reinstall your project every time you made a +change to it during development. Another problem that sometimes comes up with the ``distutils`` is that you may need to do development on two related projects at the same time. You may need diff --git a/docs/userguide/entry_point.rst b/docs/userguide/entry_point.rst index edab446502..63d30a4893 100644 --- a/docs/userguide/entry_point.rst +++ b/docs/userguide/entry_point.rst @@ -28,7 +28,7 @@ with ``__init__.py`` as: .. code-block:: python - def helloworld(): + def hello_world(): print("Hello world") and ``__main__.py`` providing a hook: @@ -64,7 +64,7 @@ After installing the package, a user may invoke that function by simply calling The syntax for entry points is specified as follows: -.. code-block:: +.. code-block:: ini = [.[.]][:.] diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index de4ef6682f..0a8070ae43 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -19,36 +19,44 @@ Package Discovery and Namespace Package support for namespace package. Normally, you would specify the package to be included manually in the following manner: -.. code-block:: ini - - [options] - #... - packages = - mypkg1 - mypkg2 +.. tab:: setup.cfg -.. code-block:: python + .. code-block:: ini - setup( + [options] #... - packages = ['mypkg1', 'mypkg2'] - ) + packages = + mypkg1 + mypkg2 + +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + packages = ['mypkg1', 'mypkg2'] + ) This can get tiresome reallly quickly. To speed things up, we introduce two functions provided by setuptools: -.. code-block:: ini +.. tab:: setup.cfg - [options] - packages = find: - #or - packages = find_namespace: + .. code-block:: ini -.. code-block:: python + [options] + packages = find: + #or + packages = find_namespace: + +.. tab:: setup.py - from setuptools import find_packages - #or - from setuptools import find_namespace_packages + .. code-block:: python + + from setuptools import find_packages + #or + from setuptools import find_namespace_packages Using ``find:`` or ``find_packages`` @@ -71,30 +79,34 @@ it, consider the following directory To have your setup.cfg or setup.py to automatically include packages found in ``src`` that starts with the name ``pkg`` and not ``additional``: -.. code-block:: ini +.. tab:: setup.cfg - [options] - packages = find: - package_dir = - =src + .. code-block:: ini - [options.packages.find] - where = src - include = pkg* - exclude = additional + [options] + packages = find: + package_dir = + =src -.. code-block:: python + [options.packages.find] + where = src + include = pkg* + exclude = additional - setup( - #... - packages = find_packages( - where = 'src', - include = ['pkg*',], - exclude = ['additional',] - ), - package_dir = {"":"src"} - #... - ) +.. tab:: setup.py + + .. code-block:: python + + setup( + #... + packages = find_packages( + where = 'src', + include = ['pkg*',], + exclude = ['additional',] + ), + package_dir = {"":"src"} + #... + ) .. _Namespace Packages: @@ -144,7 +156,7 @@ to use ``find_namespace:``: =src packages = find_namespace: - [options.packages.find_namespace] + [options.packages.find] where = src When you install the zipped distribution, ``timmins.foo`` would become @@ -195,17 +207,21 @@ following: And the ``namespace_packages`` keyword in your ``setup.cfg`` or ``setup.py``: -.. code-block:: ini +.. tab:: setup.cfg - [options] - namespace_packages = timmins + .. code-block:: ini -.. code-block:: python + [options] + namespace_packages = timmins + +.. tab:: setup.py + + .. code-block:: python - setup( - # ... - namespace_packages = ['timmins'] - ) + setup( + # ... + namespace_packages = ['timmins'] + ) And your directory should look like this diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 697087edb4..2807f59bee 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -21,7 +21,7 @@ the backend (build system) it wants to use. The distribution can then be generated with whatever tools that provides a ``build sdist``-alike functionality. While this may appear cumbersome, given the added pieces, it in fact tremendously enhances the portability of your package. The -change is driven under :pep:`517 <517#build-requirements>`. To learn more about Python packaging in general, +change is driven under :pep:`PEP 517 <517#build-requirements>`. To learn more about Python packaging in general, navigate to the `bottom `_ of this page. @@ -37,33 +37,52 @@ package your project: requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" -Then, you will need a ``setup.cfg`` to specify your package information, -such as metadata, contents, dependencies, etc. Here we demonstrate the minimum +Then, you will need a ``setup.cfg`` or ``setup.py`` to specify your package +information, such as metadata, contents, dependencies, etc. Here we demonstrate +the minimum -.. code-block:: ini +.. tab:: setup.cfg - [metadata] - name = "mypackage" - version = 0.0.1 + .. code-block:: ini - [options] - packages = "mypackage" - install_requires = - requests - importlib; python_version == "2.6" + [metadata] + name = mypackage + version = 0.0.1 + + [options] + packages = mypackage + install_requires = + requests + importlib; python_version == "2.6" + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + name='mypackage', + version='0.0.1', + packages=['mypackage'], + install_requires=[ + 'requests', + 'importlib; python_version == "2.6"', + ], + ) This is what your project would look like:: ~/mypackage/ pyproject.toml - setup.cfg + setup.cfg # or setup.py mypackage/__init__.py -Then, you need an installer, such as `pep517 `_ -which you can obtain via ``pip install pep517``. After downloading it, invoke -the installer:: +Then, you need an builder, such as :std:doc:`PyPA build ` +which you can obtain via ``pip install build``. After downloading it, invoke +the builder:: - python -m pep517.build + python -m build You now have your distribution ready (e.g. a ``tar.gz`` file and a ``.whl`` file in the ``dist`` directory), which you can upload to PyPI! @@ -71,7 +90,7 @@ file in the ``dist`` directory), which you can upload to PyPI! Of course, before you release your project to PyPI, you'll want to add a bit more information to your setup script to help people find or learn about your project. And maybe your project will have grown by then to include a few -dependencies, and perhaps some data files and scripts. In the next few section, +dependencies, and perhaps some data files and scripts. In the next few sections, we will walk through those additional but essential information you need to specify to properly package your project. diff --git a/easy_install.py b/easy_install.py deleted file mode 100644 index d87e984034..0000000000 --- a/easy_install.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Run the EasyInstall command""" - -if __name__ == '__main__': - from setuptools.command.easy_install import main - main() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..976ba02946 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 5828132edd..0000000000 --- a/netlify.toml +++ /dev/null @@ -1,7 +0,0 @@ -# Configuration for pull request documentation previews via Netlify - -# Netlify relies on there being a ./runtime.txt to indicate Python 3. - -[build] - publish = "build/html" - command = "pip install tox && tox -e docs" diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 737f4d5fad..c84f1dd9e8 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -38,6 +38,7 @@ import inspect import ntpath import posixpath +import importlib from pkgutil import get_importer try: @@ -696,7 +697,8 @@ def add(self, dist, entry=None, insert=True, replace=False): keys2.append(dist.key) self._added_new(dist) - def resolve(self, requirements, env=None, installer=None, + # FIXME: 'WorkingSet.resolve' is too complex (11) + def resolve(self, requirements, env=None, installer=None, # noqa: C901 replace_conflicting=False, extras=None): """List all distributions needed to (recursively) meet `requirements` @@ -1745,7 +1747,8 @@ def _get_date_and_size(zip_stat): timestamp = time.mktime(date_time) return timestamp, size - def _extract_resource(self, manager, zip_path): + # FIXME: 'ZipProvider._extract_resource' is too complex (12) + def _extract_resource(self, manager, zip_path): # noqa: C901 if zip_path in self._index(): for name in self._index()[zip_path]: @@ -1983,7 +1986,7 @@ def find_eggs_in_zip(importer, path_item, only=False): dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath) for dist in dists: yield dist - elif subitem.lower().endswith('.dist-info'): + elif subitem.lower().endswith(('.dist-info', '.egg-info')): subpath = os.path.join(path_item, subitem) submeta = EggMetadata(zipimport.zipimporter(subpath)) submeta.egg_info = subpath @@ -2209,7 +2212,7 @@ def _handle_ns(packageName, path_item): if subpath is not None: path = module.__path__ path.append(subpath) - loader.load_module(packageName) + importlib.import_module(packageName) _rebuild_mod_path(path, packageName, module) return subpath @@ -2858,7 +2861,8 @@ def get_entry_info(self, group, name): """Return the EntryPoint object for `group`+`name`, or ``None``""" return self.get_entry_map(group).get(name) - def insert_on(self, path, loc=None, replace=False): + # FIXME: 'Distribution.insert_on' is too complex (13) + def insert_on(self, path, loc=None, replace=False): # noqa: C901 """Ensure self.location is on path If replace=False (default): diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py index 4dc3beb2fa..fed5929540 100644 --- a/pkg_resources/extern/__init__.py +++ b/pkg_resources/extern/__init__.py @@ -1,3 +1,4 @@ +import importlib.util import sys @@ -20,17 +21,10 @@ def search_path(self): yield self.vendor_pkg + '.' yield '' - def find_module(self, fullname, path=None): - """ - Return self when fullname starts with root_name and the - target module is one vendored through this importer. - """ + def _module_matches_namespace(self, fullname): + """Figure out if the target module is vendored.""" root, base, target = fullname.partition(self.root_name + '.') - if root: - return - if not any(map(target.startswith, self.vendored_names)): - return - return self + return not root and any(map(target.startswith, self.vendored_names)) def load_module(self, fullname): """ @@ -54,6 +48,19 @@ def load_module(self, fullname): "distribution.".format(**locals()) ) + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + + def find_spec(self, fullname, path=None, target=None): + """Return a module spec for vendored names.""" + return ( + importlib.util.spec_from_loader(fullname, self) + if self._module_matches_namespace(fullname) else None + ) + def install(self): """ Install this importer into sys.meta_path if not already present. diff --git a/pkg_resources/tests/data/my-test-package-zip/my-test-package.zip b/pkg_resources/tests/data/my-test-package-zip/my-test-package.zip new file mode 100644 index 0000000000..81f9a0170f Binary files /dev/null and b/pkg_resources/tests/data/my-test-package-zip/my-test-package.zip differ diff --git a/pkg_resources/tests/test_find_distributions.py b/pkg_resources/tests/test_find_distributions.py index f9594422f2..b01b4827a7 100644 --- a/pkg_resources/tests/test_find_distributions.py +++ b/pkg_resources/tests/test_find_distributions.py @@ -32,3 +32,12 @@ def test_zipped_egg(self, target_dir): assert [dist.project_name for dist in dists] == ['my-test-package'] dists = pkg_resources.find_distributions(str(target_dir), only=True) assert not list(dists) + + def test_zipped_sdist_one_level_removed(self, target_dir): + (TESTS_DATA_DIR / 'my-test-package-zip').copy(target_dir) + dists = pkg_resources.find_distributions( + str(target_dir / "my-test-package.zip")) + assert [dist.project_name for dist in dists] == ['my-test-package'] + dists = pkg_resources.find_distributions( + str(target_dir / "my-test-package.zip"), only=True) + assert not list(dists) diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index b08bb293ef..965a7c0089 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -773,7 +773,7 @@ class TestNamespaces: ns_str = "__import__('pkg_resources').declare_namespace(__name__)\n" - @pytest.yield_fixture + @pytest.fixture def symlinked_tmpdir(self, tmpdir): """ Where available, return the tempdir as a symlink, @@ -791,7 +791,7 @@ def symlinked_tmpdir(self, tmpdir): finally: os.unlink(link_name) - @pytest.yield_fixture(autouse=True) + @pytest.fixture(autouse=True) def patched_path(self, tmpdir): """ Patch sys.path to include the 'site-pkgs' dir. Also diff --git a/pyproject.toml b/pyproject.toml index 2d36286555..70e3473d44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,30 @@ [build-system] requires = [ - # avoid self install on Python 2; ref #1996 - "setuptools >= 40.8; python_version > '3'", "wheel", ] build-backend = "setuptools.build_meta" backend-path = ["."] +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] + +[pytest.enabler.black] +#addopts = "--black" + +[pytest.enabler.mypy] +#addopts = "--mypy" + +[pytest.enabler.flake8] +addopts = "--flake8" + +[pytest.enabler.cov] +addopts = "--cov" + +[pytest.enabler.xdist] +addopts = "-n auto" + [tool.towncrier] package = "setuptools" package_dir = "setuptools" @@ -41,9 +59,3 @@ backend-path = ["."] directory = "misc" name = "Misc" showcontent = true - -[tool.jaraco.pytest.plugins.flake8] -addopts = "--flake8" - -[tool.jaraco.pytest.plugins.cov] -addopts = "--cov" diff --git a/pytest.ini b/pytest.ini index 162ad8737b..9aa1b17347 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,8 +1,13 @@ [pytest] -addopts=--doctest-modules --doctest-glob=pkg_resources/api_tests.txt -r sxX norecursedirs=dist build .tox .eggs -doctest_optionflags=ELLIPSIS ALLOW_UNICODE -filterwarnings = +addopts= + --doctest-modules + --doctest-glob=pkg_resources/api_tests.txt + -r sxX +doctest_optionflags=ALLOW_UNICODE ELLIPSIS +# workaround for warning pytest-dev/pytest#6178 +junit_family=xunit2 +filterwarnings= # Fail on warnings error # https://github.com/pypa/setuptools/issues/1823 diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index 475ba515c0..0000000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -3.7 diff --git a/setup.cfg b/setup.cfg index 5ee3a8e98a..ebdc2c638c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,84 +1,92 @@ -[egg_info] -tag_build = .post -tag_date = 1 - -[aliases] -clean_egg_info = egg_info -Db '' -release = clean_egg_info sdist bdist_wheel -source = register sdist binary -binary = bdist_egg upload --show-response - -[upload] -repository = https://upload.pypi.org/legacy/ - -[sdist] -formats = zip - [metadata] +license_files = + LICENSE name = setuptools -version = 50.3.2 -description = Easily download, build, install, upgrade, and uninstall Python packages +version = 56.0.0 author = Python Packaging Authority author_email = distutils-sig@python.org -long_description = file: README.rst -long_description_content_type = text/x-rst; charset=UTF-8 -license_file = LICENSE -keywords = CPAN PyPI distutils eggs package management +description = Easily download, build, install, upgrade, and uninstall Python packages +long_description = file:README.rst url = https://github.com/pypa/setuptools -project_urls = - Documentation = https://setuptools.readthedocs.io/ classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Topic :: Software Development :: Libraries :: Python Modules - Topic :: System :: Archiving :: Packaging - Topic :: System :: Systems Administration - Topic :: Utilities + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Topic :: Software Development :: Libraries :: Python Modules + Topic :: System :: Archiving :: Packaging + Topic :: System :: Systems Administration + Topic :: Utilities +keywords = CPAN PyPI distutils eggs package management +project_urls = + Documentation = https://setuptools.readthedocs.io/ [options] -zip_safe = True -python_requires = >=3.5 -py_modules = easy_install -packages = find: +packages = find_namespace: +# disabled as it causes tests to be included #2505 +# include_package_data = true +python_requires = >=3.6 +install_requires = [options.packages.find] -exclude = *.tests +exclude = + build* + dist* + docs* + tests* + *.tests + tools* [options.extras_require] +testing = + # upstream + pytest >= 4.6 + pytest-checkdocs >= 2.4 + pytest-flake8 + # python_implementation: workaround for jaraco/skeleton#22 + # python_version: workaround for python/typed_ast#156 + pytest-black >= 0.3.7; python_implementation != "PyPy" and python_version < "3.10" + pytest-cov + # python_implementation: workaround for jaraco/skeleton#22 + # python_version: workaround for python/typed_ast#156 + pytest-mypy; python_implementation != "PyPy" and python_version < "3.10" + pytest-enabler >= 1.0.1 + + # local + mock + flake8-2020 + virtualenv>=13.0.0 + pytest-virtualenv>=1.2.7 + wheel + paver + pip>=19.1 # For proper file:// URLs support. + jaraco.envs + pytest-xdist + sphinx + jaraco.path>=3.2.0 + +docs = + # upstream + sphinx + jaraco.packaging >= 8.2 + rst.linker >= 1.9 + + # local + pygments-github-lexers==0.0.5 + sphinx-inline-tabs + ssl = - wincertstore==0.2; sys_platform=='win32' + wincertstore==0.2; sys_platform=='win32' certs = - certifi==2016.9.26 + certifi==2016.9.26 -tests = - 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 - # Coverage is unbearably slow on PyPy - pytest-cov>=2.5.1; python_implementation != "PyPy" - paver; python_version>="3.6" - pip>=19.1 # For proper file:// URLs support. - jaraco.envs - jaraco.test >= 3.1.1; python_version >= "3.6" +[options.entry_points] -docs = - # Keep these in sync with docs/requirements.txt - sphinx - jaraco.packaging>=6.1 - rst.linker>=1.9 - pygments-github-lexers==0.0.5 +[egg_info] +tag_build = .post +tag_date = 1 + +[sdist] +formats = zip diff --git a/setup.py b/setup.py index 2bd48daa67..f5cbff31e5 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,4 @@ #!/usr/bin/env python -""" -Distutils setup file, used to install or test 'setuptools' -""" import os import sys @@ -13,17 +10,6 @@ here = os.path.dirname(__file__) -def require_metadata(): - "Prevent improper installs without necessary metadata. See #659" - egg_info_dir = os.path.join(here, 'setuptools.egg-info') - if not os.path.exists(egg_info_dir): - msg = ( - "Cannot build setuptools without metadata. " - "Run `bootstrap.py`." - ) - raise RuntimeError(msg) - - def read_commands(): command_ns = {} cmd_module_path = 'setuptools/command/__init__.py' @@ -33,22 +19,6 @@ def read_commands(): return command_ns['__all__'] -def _gen_console_scripts(): - yield "easy_install = setuptools.command.easy_install:main" - - # Gentoo distributions manage the python-version-specific scripts - # themselves, so those platforms define an environment variable to - # suppress the creation of the version-specific scripts. - var_names = ( - 'SETUPTOOLS_DISABLE_VERSIONED_EASY_INSTALL_SCRIPT', - 'DISTRIBUTE_DISABLE_VERSIONED_EASY_INSTALL_SCRIPT', - ) - if any(os.environ.get(var) not in (None, "", "0") for var in var_names): - return - tmpl = "easy_install-{shortver} = setuptools.command.easy_install:main" - yield tmpl.format(shortver='{}.{}'.format(*sys.version_info)) - - package_data = dict( setuptools=['script (dev).tmpl', 'script.tmpl', 'site-patch.py'], ) @@ -173,9 +143,6 @@ def _restore_install_lib(self): "depends.txt = setuptools.command.egg_info:warn_depends_obsolete", "dependency_links.txt = setuptools.command.egg_info:overwrite_arg", ], - "console_scripts": list(_gen_console_scripts()), - "setuptools.installation": - ['eggsecutable = setuptools.command.easy_install:bootstrap'], }, dependency_links=[ pypi_link( @@ -192,5 +159,4 @@ def _restore_install_lib(self): if __name__ == '__main__': # allow setup.py to run from another directory here and os.chdir(here) - require_metadata() dist = setuptools.setup(**setup_params) diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py index 0ce190b8cf..0f70284822 100644 --- a/setuptools/archive_util.py +++ b/setuptools/archive_util.py @@ -125,6 +125,56 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter): os.chmod(target, unix_attributes) +def _resolve_tar_file_or_dir(tar_obj, tar_member_obj): + """Resolve any links and extract link targets as normal files.""" + while tar_member_obj is not None and ( + tar_member_obj.islnk() or tar_member_obj.issym()): + linkpath = tar_member_obj.linkname + if tar_member_obj.issym(): + base = posixpath.dirname(tar_member_obj.name) + linkpath = posixpath.join(base, linkpath) + linkpath = posixpath.normpath(linkpath) + tar_member_obj = tar_obj._getmember(linkpath) + + is_file_or_dir = ( + tar_member_obj is not None and + (tar_member_obj.isfile() or tar_member_obj.isdir()) + ) + if is_file_or_dir: + return tar_member_obj + + raise LookupError('Got unknown file type') + + +def _iter_open_tar(tar_obj, extract_dir, progress_filter): + """Emit member-destination pairs from a tar archive.""" + # don't do any chowning! + tar_obj.chown = lambda *args: None + + with contextlib.closing(tar_obj): + for member in tar_obj: + name = member.name + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name.split('/'): + continue + + prelim_dst = os.path.join(extract_dir, *name.split('/')) + + try: + member = _resolve_tar_file_or_dir(tar_obj, member) + except LookupError: + continue + + final_dst = progress_filter(name, prelim_dst) + if not final_dst: + continue + + if final_dst.endswith(os.sep): + final_dst = final_dst[:-1] + + yield member, final_dst + + def unpack_tarfile(filename, extract_dir, progress_filter=default_filter): """Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir` @@ -138,38 +188,18 @@ def unpack_tarfile(filename, extract_dir, progress_filter=default_filter): raise UnrecognizedFormat( "%s is not a compressed or uncompressed tar file" % (filename,) ) from e - with contextlib.closing(tarobj): - # don't do any chowning! - tarobj.chown = lambda *args: None - for member in tarobj: - name = member.name - # don't extract absolute paths or ones with .. in them - if not name.startswith('/') and '..' not in name.split('/'): - prelim_dst = os.path.join(extract_dir, *name.split('/')) - - # resolve any links and to extract the link targets as normal - # files - while member is not None and ( - member.islnk() or member.issym()): - linkpath = member.linkname - if member.issym(): - base = posixpath.dirname(member.name) - linkpath = posixpath.join(base, linkpath) - linkpath = posixpath.normpath(linkpath) - member = tarobj._getmember(linkpath) - - if member is not None and (member.isfile() or member.isdir()): - final_dst = progress_filter(name, prelim_dst) - if final_dst: - if final_dst.endswith(os.sep): - final_dst = final_dst[:-1] - try: - # XXX Ugh - tarobj._extract_member(member, final_dst) - except tarfile.ExtractError: - # chown/chmod/mkfifo/mknode/makedev failed - pass - return True + + for member, final_dst in _iter_open_tar( + tarobj, extract_dir, progress_filter, + ): + try: + # XXX Ugh + tarobj._extract_member(member, final_dst) + except tarfile.ExtractError: + # chown/chmod/mkfifo/mknode/makedev failed + pass + + return True extraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index b9e8a2b3fa..9dfb2f24b5 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -101,7 +101,12 @@ def _file_with_extension(directory, extension): f for f in os.listdir(directory) if f.endswith(extension) ) - file, = matching + try: + file, = matching + except ValueError: + raise ValueError( + 'No distribution was found. Ensure that `setup.py` ' + 'is not empty and that it calls `setup()`.') return file diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index 743f5588fa..570e69576e 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -2,7 +2,7 @@ 'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop', 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', - 'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info', + 'upload_docs', 'build_clib', 'dist_info', ] from distutils.command.bdist import bdist diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index a88efb45b8..e6b1609f7b 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -2,7 +2,6 @@ Build .egg distributions""" -from distutils.errors import DistutilsSetupError from distutils.dir_util import remove_tree, mkpath from distutils import log from types import CodeType @@ -11,12 +10,10 @@ import re import textwrap import marshal -import warnings from pkg_resources import get_build_platform, Distribution, ensure_directory -from pkg_resources import EntryPoint from setuptools.extension import Library -from setuptools import Command, SetuptoolsDeprecationWarning +from setuptools import Command from sysconfig import get_path, get_python_version @@ -153,7 +150,7 @@ def call_command(self, cmdname, **kw): self.run_command(cmdname) return cmd - def run(self): + def run(self): # noqa: C901 # is too complex (14) # FIXME # Generate metadata first self.run_command("egg_info") # We run install_lib before install_data, because some data hacks @@ -268,49 +265,7 @@ def zip_safe(self): return analyze_egg(self.bdist_dir, self.stubs) def gen_header(self): - epm = EntryPoint.parse_map(self.distribution.entry_points or '') - ep = epm.get('setuptools.installation', {}).get('eggsecutable') - if ep is None: - return 'w' # not an eggsecutable, do it the usual way. - - warnings.warn( - "Eggsecutables are deprecated and will be removed in a future " - "version.", - SetuptoolsDeprecationWarning - ) - - if not ep.attrs or ep.extras: - raise DistutilsSetupError( - "eggsecutable entry point (%r) cannot have 'extras' " - "or refer to a module" % (ep,) - ) - - pyver = '{}.{}'.format(*sys.version_info) - pkg = ep.module_name - full = '.'.join(ep.attrs) - base = ep.attrs[0] - basename = os.path.basename(self.egg_output) - - header = ( - "#!/bin/sh\n" - 'if [ `basename $0` = "%(basename)s" ]\n' - 'then exec python%(pyver)s -c "' - "import sys, os; sys.path.insert(0, os.path.abspath('$0')); " - "from %(pkg)s import %(base)s; sys.exit(%(full)s())" - '" "$@"\n' - 'else\n' - ' echo $0 is not the correct name for this egg file.\n' - ' echo Please rename it back to %(basename)s and try again.\n' - ' exec false\n' - 'fi\n' - ) % locals() - - if not self.dry_run: - mkpath(os.path.dirname(self.egg_output), dry_run=self.dry_run) - f = open(self.egg_output, 'w') - f.write(header) - f.close() - return 'a' + return 'w' def copy_metadata_to(self, target_dir): "Copy metadata (egg info) to the target_dir" diff --git a/setuptools/command/bdist_wininst.py b/setuptools/command/bdist_wininst.py deleted file mode 100644 index ff4b634592..0000000000 --- a/setuptools/command/bdist_wininst.py +++ /dev/null @@ -1,30 +0,0 @@ -import distutils.command.bdist_wininst as orig -import warnings - -from setuptools import SetuptoolsDeprecationWarning - - -class bdist_wininst(orig.bdist_wininst): - def reinitialize_command(self, command, reinit_subcommands=0): - """ - Supplement reinitialize_command to work around - http://bugs.python.org/issue20819 - """ - cmd = self.distribution.reinitialize_command( - command, reinit_subcommands) - if command in ('install', 'install_lib'): - cmd.install_lib = None - return cmd - - def run(self): - warnings.warn( - "bdist_wininst is deprecated and will be removed in a future " - "version. Use bdist_wheel (wheel packages) instead.", - SetuptoolsDeprecationWarning - ) - - self._is_running = True - try: - orig.bdist_wininst.run(self) - finally: - self._is_running = False diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 9ec83b7d8b..45adb6a1ea 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -67,7 +67,7 @@ __all__ = [ 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg', - 'main', 'get_exe_prefixes', + 'get_exe_prefixes', ] @@ -226,7 +226,7 @@ def _render_version(): print(tmpl.format(**locals())) raise SystemExit() - def finalize_options(self): + def finalize_options(self): # noqa: C901 # is too complex (25) # FIXME self.version and self._render_version() py_version = sys.version.split()[0] @@ -437,7 +437,7 @@ def pseudo_tempname(self): def warn_deprecated_options(self): pass - def check_site_dir(self): + def check_site_dir(self): # noqa: C901 # is too complex (12) # FIXME """Verify that self.install_dir is .pth-capable dir, if needed""" instdir = normalize_path(self.install_dir) @@ -713,7 +713,10 @@ def select_scheme(self, name): if getattr(self, attrname) is None: setattr(self, attrname, scheme[key]) - def process_distribution(self, requirement, dist, deps=True, *info): + # FIXME: 'easy_install.process_distribution' is too complex (12) + def process_distribution( # noqa: C901 + self, requirement, dist, deps=True, *info, + ): self.update_pth(dist) self.package_index.add(dist) if dist in self.local_index[dist.key]: @@ -837,12 +840,19 @@ def write_script(self, script_name, contents, mode="t", blockers=()): def install_eggs(self, spec, dist_filename, tmpdir): # .egg dirs or files are already built, so just return them - if dist_filename.lower().endswith('.egg'): - return [self.install_egg(dist_filename, tmpdir)] - elif dist_filename.lower().endswith('.exe'): - return [self.install_exe(dist_filename, tmpdir)] - elif dist_filename.lower().endswith('.whl'): - return [self.install_wheel(dist_filename, tmpdir)] + installer_map = { + '.egg': self.install_egg, + '.exe': self.install_exe, + '.whl': self.install_wheel, + } + try: + install_dist = installer_map[ + dist_filename.lower()[-4:] + ] + except KeyError: + pass + else: + return [install_dist(dist_filename, tmpdir)] # Anything else, try to extract and build setup_base = tmpdir @@ -887,7 +897,8 @@ def egg_distribution(self, egg_path): metadata = EggMetadata(zipimport.zipimporter(egg_path)) return Distribution.from_filename(egg_path, metadata=metadata) - def install_egg(self, egg_path, tmpdir): + # FIXME: 'easy_install.install_egg' is too complex (11) + def install_egg(self, egg_path, tmpdir): # noqa: C901 destination = os.path.join( self.install_dir, os.path.basename(egg_path), @@ -986,7 +997,8 @@ def install_exe(self, dist_filename, tmpdir): # install the .egg return self.install_egg(egg_path, tmpdir) - def exe_to_egg(self, dist_filename, egg_tmp): + # FIXME: 'easy_install.exe_to_egg' is too complex (12) + def exe_to_egg(self, dist_filename, egg_tmp): # noqa: C901 """Extract a bdist_wininst to the directories an egg would use""" # Check for .pth file and set up prefix translations prefixes = get_exe_prefixes(dist_filename) @@ -1178,22 +1190,24 @@ def _set_fetcher_options(self, base): for key, val in ei_opts.items(): if key not in fetch_directives: continue - fetch_options[key.replace('_', '-')] = val[1] + fetch_options[key] = val[1] # create a settings dictionary suitable for `edit_config` settings = dict(easy_install=fetch_options) cfg_filename = os.path.join(base, 'setup.cfg') setopt.edit_config(cfg_filename, settings) - def update_pth(self, dist): + def update_pth(self, dist): # noqa: C901 # is too complex (11) # FIXME if self.pth_file is None: return for d in self.pth_file[dist.key]: # drop old entries - if self.multi_version or d.location != dist.location: - log.info("Removing %s from easy-install.pth file", d) - self.pth_file.remove(d) - if d.location in self.shadow_path: - self.shadow_path.remove(d.location) + if not self.multi_version and d.location == dist.location: + continue + + log.info("Removing %s from easy-install.pth file", d) + self.pth_file.remove(d) + if d.location in self.shadow_path: + self.shadow_path.remove(d.location) if not self.multi_version: if dist.location in self.pth_file.paths: @@ -1207,19 +1221,21 @@ def update_pth(self, dist): if dist.location not in self.shadow_path: self.shadow_path.append(dist.location) - if not self.dry_run: + if self.dry_run: + return - self.pth_file.save() + self.pth_file.save() - if dist.key == 'setuptools': - # Ensure that setuptools itself never becomes unavailable! - # XXX should this check for latest version? - filename = os.path.join(self.install_dir, 'setuptools.pth') - if os.path.islink(filename): - os.unlink(filename) - f = open(filename, 'wt') - f.write(self.pth_file.make_relative(dist.location) + '\n') - f.close() + if dist.key != 'setuptools': + return + + # Ensure that setuptools itself never becomes unavailable! + # XXX should this check for latest version? + filename = os.path.join(self.install_dir, 'setuptools.pth') + if os.path.islink(filename): + os.unlink(filename) + with open(filename, 'wt') as f: + f.write(self.pth_file.make_relative(dist.location) + '\n') def unpack_progress(self, src, dst): # Progress filter for unpacking @@ -1360,58 +1376,63 @@ def get_site_dirs(): if sys.exec_prefix != sys.prefix: prefixes.append(sys.exec_prefix) for prefix in prefixes: - if prefix: - if sys.platform in ('os2emx', 'riscos'): - sitedirs.append(os.path.join(prefix, "Lib", "site-packages")) - elif os.sep == '/': - sitedirs.extend([ - os.path.join( - prefix, - "lib", - "python{}.{}".format(*sys.version_info), - "site-packages", - ), - os.path.join(prefix, "lib", "site-python"), - ]) - else: - sitedirs.extend([ + if not prefix: + continue + + if sys.platform in ('os2emx', 'riscos'): + sitedirs.append(os.path.join(prefix, "Lib", "site-packages")) + elif os.sep == '/': + sitedirs.extend([ + os.path.join( prefix, - os.path.join(prefix, "lib", "site-packages"), - ]) - if sys.platform == 'darwin': - # for framework builds *only* we add the standard Apple - # locations. Currently only per-user, but /Library and - # /Network/Library could be added too - if 'Python.framework' in prefix: - home = os.environ.get('HOME') - if home: - home_sp = os.path.join( - home, - 'Library', - 'Python', - '{}.{}'.format(*sys.version_info), - 'site-packages', - ) - sitedirs.append(home_sp) + "lib", + "python{}.{}".format(*sys.version_info), + "site-packages", + ), + os.path.join(prefix, "lib", "site-python"), + ]) + else: + sitedirs.extend([ + prefix, + os.path.join(prefix, "lib", "site-packages"), + ]) + if sys.platform != 'darwin': + continue + + # for framework builds *only* we add the standard Apple + # locations. Currently only per-user, but /Library and + # /Network/Library could be added too + if 'Python.framework' not in prefix: + continue + + home = os.environ.get('HOME') + if not home: + continue + + home_sp = os.path.join( + home, + 'Library', + 'Python', + '{}.{}'.format(*sys.version_info), + 'site-packages', + ) + sitedirs.append(home_sp) lib_paths = get_path('purelib'), get_path('platlib') - for site_lib in lib_paths: - if site_lib not in sitedirs: - sitedirs.append(site_lib) + + sitedirs.extend(s for s in lib_paths if s not in sitedirs) if site.ENABLE_USER_SITE: sitedirs.append(site.USER_SITE) - try: + with contextlib.suppress(AttributeError): sitedirs.extend(site.getsitepackages()) - except AttributeError: - pass sitedirs = list(map(normalize_path, sitedirs)) return sitedirs -def expand_paths(inputs): +def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME """Yield sys.path directories that might contain "old-style" packages""" seen = {} @@ -1443,13 +1464,18 @@ def expand_paths(inputs): # Yield existing non-dupe, non-import directory lines from it for line in lines: - if not line.startswith("import"): - line = normalize_path(line.rstrip()) - if line not in seen: - seen[line] = 1 - if not os.path.isdir(line): - continue - yield line, os.listdir(line) + if line.startswith("import"): + continue + + line = normalize_path(line.rstrip()) + if line in seen: + continue + + seen[line] = 1 + if not os.path.isdir(line): + continue + + yield line, os.listdir(line) def extract_wininst_cfg(dist_filename): @@ -2167,7 +2193,7 @@ def _get_script_args(cls, type_, name, header, script_text): @classmethod def _adjust_header(cls, type_, orig_header): """ - Make sure 'pythonw' is used for gui and and 'python' is used for + Make sure 'pythonw' is used for gui and 'python' is used for console (regardless of what sys.executable is). """ pattern = 'pythonw.exe' @@ -2258,60 +2284,6 @@ def current_umask(): return tmp -def bootstrap(): - # This function is called when setuptools*.egg is run using /bin/sh - import setuptools - - argv0 = os.path.dirname(setuptools.__path__[0]) - sys.argv[0] = argv0 - sys.argv.append(argv0) - main() - - -def main(argv=None, **kw): - from setuptools import setup - from setuptools.dist import Distribution - - class DistributionWithoutHelpCommands(Distribution): - common_usage = "" - - def _show_help(self, *args, **kw): - with _patch_usage(): - Distribution._show_help(self, *args, **kw) - - if argv is None: - argv = sys.argv[1:] - - with _patch_usage(): - setup( - script_args=['-q', 'easy_install', '-v'] + argv, - script_name=sys.argv[0] or 'easy_install', - distclass=DistributionWithoutHelpCommands, - **kw - ) - - -@contextlib.contextmanager -def _patch_usage(): - import distutils.core - USAGE = textwrap.dedent(""" - usage: %(script)s [options] requirement_or_url ... - or: %(script)s --help - """).lstrip() - - def gen_usage(script_name): - return USAGE % dict( - script=os.path.basename(script_name), - ) - - saved = distutils.core.gen_usage - distutils.core.gen_usage = gen_usage - try: - yield - finally: - distutils.core.gen_usage = saved - - class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): """ Warning for EasyInstall deprecations, bypassing suppression. diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 0b7ad677f2..1f120b67d1 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -8,6 +8,7 @@ from distutils import log import distutils.errors import distutils.filelist +import functools import os import re import sys @@ -31,7 +32,7 @@ from setuptools import SetuptoolsDeprecationWarning -def translate_pattern(glob): +def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME """ Translate a file path glob like '*.txt' in to a regular expression. This differs from fnmatch.translate which allows wildcards to match @@ -332,70 +333,74 @@ def process_template_line(self, line): # patterns, (dir and patterns), or (dir_pattern). (action, patterns, dir, dir_pattern) = self._parse_template_line(line) + action_map = { + 'include': self.include, + 'exclude': self.exclude, + 'global-include': self.global_include, + 'global-exclude': self.global_exclude, + 'recursive-include': functools.partial( + self.recursive_include, dir, + ), + 'recursive-exclude': functools.partial( + self.recursive_exclude, dir, + ), + 'graft': self.graft, + 'prune': self.prune, + } + log_map = { + 'include': "warning: no files found matching '%s'", + 'exclude': ( + "warning: no previously-included files found " + "matching '%s'" + ), + 'global-include': ( + "warning: no files found matching '%s' " + "anywhere in distribution" + ), + 'global-exclude': ( + "warning: no previously-included files matching " + "'%s' found anywhere in distribution" + ), + 'recursive-include': ( + "warning: no files found matching '%s' " + "under directory '%s'" + ), + 'recursive-exclude': ( + "warning: no previously-included files matching " + "'%s' found under directory '%s'" + ), + 'graft': "warning: no directories found matching '%s'", + 'prune': "no previously-included directories found matching '%s'", + } + + try: + process_action = action_map[action] + except KeyError: + raise DistutilsInternalError( + "this cannot happen: invalid action '{action!s}'". + format(action=action), + ) + # OK, now we know that the action is valid and we have the # right number of words on the line for that action -- so we # can proceed with minimal error-checking. - if action == 'include': - self.debug_print("include " + ' '.join(patterns)) - for pattern in patterns: - if not self.include(pattern): - log.warn("warning: no files found matching '%s'", pattern) - - elif action == 'exclude': - self.debug_print("exclude " + ' '.join(patterns)) - for pattern in patterns: - if not self.exclude(pattern): - log.warn(("warning: no previously-included files " - "found matching '%s'"), pattern) - - elif action == 'global-include': - self.debug_print("global-include " + ' '.join(patterns)) - for pattern in patterns: - if not self.global_include(pattern): - log.warn(("warning: no files found matching '%s' " - "anywhere in distribution"), pattern) - - elif action == 'global-exclude': - self.debug_print("global-exclude " + ' '.join(patterns)) - for pattern in patterns: - if not self.global_exclude(pattern): - log.warn(("warning: no previously-included files matching " - "'%s' found anywhere in distribution"), - pattern) - - elif action == 'recursive-include': - self.debug_print("recursive-include %s %s" % - (dir, ' '.join(patterns))) - for pattern in patterns: - if not self.recursive_include(dir, pattern): - log.warn(("warning: no files found matching '%s' " - "under directory '%s'"), - pattern, dir) - - elif action == 'recursive-exclude': - self.debug_print("recursive-exclude %s %s" % - (dir, ' '.join(patterns))) - for pattern in patterns: - if not self.recursive_exclude(dir, pattern): - log.warn(("warning: no previously-included files matching " - "'%s' found under directory '%s'"), - pattern, dir) - - elif action == 'graft': - self.debug_print("graft " + dir_pattern) - if not self.graft(dir_pattern): - log.warn("warning: no directories found matching '%s'", - dir_pattern) - - elif action == 'prune': - self.debug_print("prune " + dir_pattern) - if not self.prune(dir_pattern): - log.warn(("no previously-included directories found " - "matching '%s'"), dir_pattern) - - else: - raise DistutilsInternalError( - "this cannot happen: invalid action '%s'" % action) + + action_is_recursive = action.startswith('recursive-') + if action in {'graft', 'prune'}: + patterns = [dir_pattern] + extra_log_args = (dir, ) if action_is_recursive else () + log_tmpl = log_map[action] + + self.debug_print( + ' '.join( + [action] + + ([dir] if action_is_recursive else []) + + patterns, + ) + ) + for pattern in patterns: + if not process_action(pattern): + log.warn(log_tmpl, pattern, *extra_log_args) def _remove_files(self, predicate): """ diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index 8c9a15e2bb..9cd8eb0627 100644 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -1,5 +1,6 @@ from distutils import log import distutils.command.install_scripts as orig +from distutils.errors import DistutilsModuleError import os import sys @@ -35,7 +36,7 @@ def run(self): try: bw_cmd = self.get_finalized_command("bdist_wininst") is_wininst = getattr(bw_cmd, '_is_running', False) - except ImportError: + except (ImportError, DistutilsModuleError): is_wininst = False writer = ei.ScriptWriter if is_wininst: diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 887b7efa05..a6ea814a30 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -4,6 +4,7 @@ import sys import io import contextlib +from glob import iglob from setuptools.extern import ordered_set @@ -194,29 +195,41 @@ def check_license(self): """Checks if license_file' or 'license_files' is configured and adds any valid paths to 'self.filelist'. """ - - files = ordered_set.OrderedSet() - opts = self.distribution.get_option_dict('metadata') - # ignore the source of the value - _, license_file = opts.get('license_file', (None, None)) - - if license_file is None: - log.debug("'license_file' option was not specified") - else: - files.add(license_file) - + files = ordered_set.OrderedSet() try: - files.update(self.distribution.metadata.license_files) + license_files = self.distribution.metadata.license_files except TypeError: log.warn("warning: 'license_files' option is malformed") - - for f in files: - if not os.path.exists(f): - log.warn( - "warning: Failed to find the configured license file '%s'", - f) - files.remove(f) - - self.filelist.extend(files) + license_files = ordered_set.OrderedSet() + patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \ + else ordered_set.OrderedSet(license_files) + + if 'license_file' in opts: + log.warn( + "warning: the 'license_file' option is deprecated, " + "use 'license_files' instead") + patterns.append(opts['license_file'][1]) + + if 'license_file' not in opts and 'license_files' not in opts: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + + for pattern in patterns: + for path in iglob(pattern): + if path.endswith('~'): + log.debug( + "ignoring license file '%s' as it looks like a backup", + path) + continue + + if path not in files and os.path.isfile(path): + log.info( + "adding license file '%s' (matched pattern '%s')", + path, pattern) + files.add(path) + + self.filelist.extend(sorted(files)) diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 2559458a1d..845bff4421 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -2,7 +2,7 @@ """upload_docs Implements a Distutils 'upload_docs' subcommand (upload documentation to -PyPI's pythonhosted.org). +sites other than PyPi such as devpi). """ from base64 import standard_b64encode @@ -31,7 +31,7 @@ class upload_docs(upload): # supported by Warehouse (and won't be). DEFAULT_REPOSITORY = 'https://pypi.python.org/pypi/' - description = 'Upload documentation to PyPI' + description = 'Upload documentation to sites other than PyPi such as devpi' user_options = [ ('repository=', 'r', @@ -59,7 +59,7 @@ def finalize_options(self): if self.upload_dir is None: if self.has_sphinx(): build_sphinx = self.get_finalized_command('build_sphinx') - self.target_dir = build_sphinx.builder_target_dir + self.target_dir = dict(build_sphinx.builder_target_dirs)['html'] else: build = self.get_finalized_command('build') self.target_dir = os.path.join(build.build_base, 'docs') @@ -67,7 +67,7 @@ def finalize_options(self): self.ensure_dirname('upload_dir') self.target_dir = self.upload_dir if 'pypi.python.org' in self.repository: - log.warn("Upload_docs command is deprecated. Use RTD instead.") + log.warn("Upload_docs command is deprecated for PyPi. Use RTD instead.") self.announce('Using upload directory %s' % self.target_dir) def create_zipfile(self, filename): diff --git a/setuptools/config.py b/setuptools/config.py index af3a3bcbd5..4a6cd4694b 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -574,6 +574,7 @@ def parsers(self): parse_list_semicolon = partial(self._parse_list, separator=';') parse_bool = self._parse_bool parse_dict = self._parse_dict + parse_cmdclass = self._parse_cmdclass return { 'zip_safe': parse_bool, @@ -594,6 +595,22 @@ def parsers(self): 'entry_points': self._parse_file, 'py_modules': parse_list, 'python_requires': SpecifierSet, + 'cmdclass': parse_cmdclass, + } + + def _parse_cmdclass(self, value): + def resolve_class(qualified_class_name): + idx = qualified_class_name.rfind('.') + class_name = qualified_class_name[idx+1:] + pkg_name = qualified_class_name[:idx] + + module = __import__(pkg_name) + + return getattr(module, class_name) + + return { + k: resolve_class(v) + for k, v in self._parse_dict(value).items() } def _parse_packages(self, value): diff --git a/setuptools/dist.py b/setuptools/dist.py index 2c088ef8cb..c7af35dc99 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -11,10 +11,13 @@ import distutils.core import distutils.cmd import distutils.dist +import distutils.command from distutils.util import strtobool from distutils.debug import DEBUG from distutils.fancy_getopt import translate_longopt import itertools +import textwrap +from typing import List, Optional, TYPE_CHECKING from collections import defaultdict from email import message_from_file @@ -29,11 +32,15 @@ from . import SetuptoolsDeprecationWarning import setuptools +import setuptools.command from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration import pkg_resources +if TYPE_CHECKING: + from email.message import Message + __import__('setuptools.extern.packaging.specifiers') __import__('setuptools.extern.packaging.version') @@ -65,61 +72,92 @@ def get_metadata_version(self): return mv -def read_pkg_file(self, file): - """Reads the metadata values from a file object.""" - msg = message_from_file(file) +def rfc822_unescape(content: str) -> str: + """Reverse RFC-822 escaping by removing leading whitespaces from content.""" + lines = content.splitlines() + if len(lines) == 1: + return lines[0].lstrip() + return '\n'.join( + (lines[0].lstrip(), + textwrap.dedent('\n'.join(lines[1:])))) + + +def _read_field_from_msg(msg: "Message", field: str) -> Optional[str]: + """Read Message header field.""" + value = msg[field] + if value == 'UNKNOWN': + return None + return value + - def _read_field(name): - value = msg[name] - if value == 'UNKNOWN': - return None +def _read_field_unescaped_from_msg(msg: "Message", field: str) -> Optional[str]: + """Read Message header field and apply rfc822_unescape.""" + value = _read_field_from_msg(msg, field) + if value is None: return value + return rfc822_unescape(value) - def _read_list(name): - values = msg.get_all(name, None) - if values == []: - return None - return values + +def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]: + """Read Message header field and return all results as list.""" + values = msg.get_all(field, None) + if values == []: + return None + return values + + +def read_pkg_file(self, file): + """Reads the metadata values from a file object.""" + msg = message_from_file(file) self.metadata_version = StrictVersion(msg['metadata-version']) - self.name = _read_field('name') - self.version = _read_field('version') - self.description = _read_field('summary') + self.name = _read_field_from_msg(msg, 'name') + self.version = _read_field_from_msg(msg, 'version') + self.description = _read_field_from_msg(msg, 'summary') # we are filling author only. - self.author = _read_field('author') + self.author = _read_field_from_msg(msg, 'author') self.maintainer = None - self.author_email = _read_field('author-email') + self.author_email = _read_field_from_msg(msg, 'author-email') self.maintainer_email = None - self.url = _read_field('home-page') - self.license = _read_field('license') + self.url = _read_field_from_msg(msg, 'home-page') + self.license = _read_field_from_msg(msg, 'license') if 'download-url' in msg: - self.download_url = _read_field('download-url') + self.download_url = _read_field_from_msg(msg, 'download-url') else: self.download_url = None - self.long_description = _read_field('description') - self.description = _read_field('summary') + self.long_description = _read_field_unescaped_from_msg(msg, 'description') + self.description = _read_field_from_msg(msg, 'summary') if 'keywords' in msg: - self.keywords = _read_field('keywords').split(',') + self.keywords = _read_field_from_msg(msg, 'keywords').split(',') - self.platforms = _read_list('platform') - self.classifiers = _read_list('classifier') + self.platforms = _read_list_from_msg(msg, 'platform') + self.classifiers = _read_list_from_msg(msg, 'classifier') # PEP 314 - these fields only exist in 1.1 if self.metadata_version == StrictVersion('1.1'): - self.requires = _read_list('requires') - self.provides = _read_list('provides') - self.obsoletes = _read_list('obsoletes') + self.requires = _read_list_from_msg(msg, 'requires') + self.provides = _read_list_from_msg(msg, 'provides') + self.obsoletes = _read_list_from_msg(msg, 'obsoletes') else: self.requires = None self.provides = None self.obsoletes = None +def single_line(val): + # quick and dirty validation for description pypa/setuptools#1390 + if '\n' in val: + # TODO after 2021-07-31: Replace with `raise ValueError("newlines not allowed")` + warnings.warn("newlines not allowed and will break in the future") + val = val.replace('\n', ' ') + return val + + # Based on Python 3.5 version -def write_pkg_file(self, file): +def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME """Write the PKG-INFO format data to a file object. """ version = self.get_metadata_version() @@ -130,7 +168,7 @@ def write_field(key, value): write_field('Metadata-Version', str(version)) write_field('Name', self.get_name()) write_field('Version', self.get_version()) - write_field('Summary', self.get_description()) + write_field('Summary', single_line(self.get_description())) write_field('Home-page', self.get_url()) if version < StrictVersion('1.2'): @@ -283,7 +321,7 @@ def check_specifier(dist, attr, value): """Verify that value is a valid version specifier""" try: packaging.specifiers.SpecifierSet(value) - except packaging.specifiers.InvalidSpecifier as error: + except (packaging.specifiers.InvalidSpecifier, AttributeError) as error: tmpl = ( "{attr!r} must be a string " "containing valid version specifiers; {error}" @@ -548,7 +586,8 @@ def _clean_req(self, req): req.marker = None return req - def _parse_config_files(self, filenames=None): + # FIXME: 'Distribution._parse_config_files' is too complex (14) + def _parse_config_files(self, filenames=None): # noqa: C901 """ Adapted from distutils.dist.Distribution.parse_config_files, this method provides the same functionality in subtly-improved @@ -557,14 +596,12 @@ def _parse_config_files(self, filenames=None): from configparser import ConfigParser # Ignore install directory options if we have a venv - if sys.prefix != sys.base_prefix: - ignore_options = [ - 'install-base', 'install-platbase', 'install-lib', - 'install-platlib', 'install-purelib', 'install-headers', - 'install-scripts', 'install-data', 'prefix', 'exec-prefix', - 'home', 'user', 'root'] - else: - ignore_options = [] + ignore_options = [] if sys.prefix == sys.base_prefix else [ + 'install-base', 'install-platbase', 'install-lib', + 'install-platlib', 'install-purelib', 'install-headers', + 'install-scripts', 'install-data', 'prefix', 'exec-prefix', + 'home', 'user', 'root', + ] ignore_options = frozenset(ignore_options) @@ -575,6 +612,7 @@ def _parse_config_files(self, filenames=None): self.announce("Distribution.parse_config_files():") parser = ConfigParser() + parser.optionxform = str for filename in filenames: with io.open(filename, encoding='utf-8') as reader: if DEBUG: @@ -585,32 +623,69 @@ def _parse_config_files(self, filenames=None): opt_dict = self.get_option_dict(section) for opt in options: - if opt != '__name__' and opt not in ignore_options: - val = parser.get(section, opt) - opt = opt.replace('-', '_') - opt_dict[opt] = (filename, val) + if opt == '__name__' or opt in ignore_options: + continue + + val = parser.get(section, opt) + opt = self.warn_dash_deprecation(opt, section) + opt = self.make_option_lowercase(opt, section) + opt_dict[opt] = (filename, val) # Make the ConfigParser forget everything (so we retain # the original filenames that options come from) parser.__init__() + if 'global' not in self.command_options: + return + # If there was a "global" section in the config file, use it # to set Distribution options. - if 'global' in self.command_options: - for (opt, (src, val)) in self.command_options['global'].items(): - alias = self.negative_opt.get(opt) - try: - if alias: - setattr(self, alias, not strtobool(val)) - elif opt in ('verbose', 'dry_run'): # ugh! - setattr(self, opt, strtobool(val)) - else: - setattr(self, opt, val) - except ValueError as e: - raise DistutilsOptionError(e) from e + for (opt, (src, val)) in self.command_options['global'].items(): + alias = self.negative_opt.get(opt) + if alias: + val = not strtobool(val) + elif opt in ('verbose', 'dry_run'): # ugh! + val = strtobool(val) + + try: + setattr(self, alias or opt, val) + except ValueError as e: + raise DistutilsOptionError(e) from e + + def warn_dash_deprecation(self, opt, section): + if section in ( + 'options.extras_require', 'options.data_files', + ): + return opt + + underscore_opt = opt.replace('-', '_') + commands = distutils.command.__all__ + setuptools.command.__all__ + if (not section.startswith('options') and section != 'metadata' + and section not in commands): + return underscore_opt + + if '-' in opt: + warnings.warn( + "Usage of dash-separated '%s' will not be supported in future " + "versions. Please use the underscore name '%s' instead" + % (opt, underscore_opt)) + return underscore_opt + + def make_option_lowercase(self, opt, section): + if section != 'metadata' or opt.islower(): + return opt + + lowercase_opt = opt.lower() + warnings.warn( + "Usage of uppercase key '%s' in '%s' will be deprecated in future " + "versions. Please use lowercase '%s' instead" + % (opt, section, lowercase_opt) + ) + return lowercase_opt - def _set_command_options(self, command_obj, option_dict=None): + # FIXME: 'Distribution._set_command_options' is too complex (14) + def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 """ Set the options for 'command_obj' from 'option_dict'. Basically this means copying elements of a dictionary ('option_dict') to diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index b7f30dc2e3..7df32fdea2 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -1,3 +1,4 @@ +import importlib.util import sys @@ -20,17 +21,10 @@ def search_path(self): yield self.vendor_pkg + '.' yield '' - def find_module(self, fullname, path=None): - """ - Return self when fullname starts with root_name and the - target module is one vendored through this importer. - """ + def _module_matches_namespace(self, fullname): + """Figure out if the target module is vendored.""" root, base, target = fullname.partition(self.root_name + '.') - if root: - return - if not any(map(target.startswith, self.vendored_names)): - return - return self + return not root and any(map(target.startswith, self.vendored_names)) def load_module(self, fullname): """ @@ -54,6 +48,19 @@ def load_module(self, fullname): "distribution.".format(**locals()) ) + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + + def find_spec(self, fullname, path=None, target=None): + """Return a module spec for vendored names.""" + return ( + importlib.util.spec_from_loader(fullname, self) + if self._module_matches_namespace(fullname) else None + ) + def install(self): """ Install this importer into sys.meta_path if not already present. diff --git a/setuptools/glob.py b/setuptools/glob.py index 9d7cbc5da6..87062b8187 100644 --- a/setuptools/glob.py +++ b/setuptools/glob.py @@ -47,6 +47,8 @@ def iglob(pathname, recursive=False): def _iglob(pathname, recursive): dirname, basename = os.path.split(pathname) + glob_in_dir = glob2 if recursive and _isrecursive(basename) else glob1 + if not has_magic(pathname): if basename: if os.path.lexists(pathname): @@ -56,13 +58,9 @@ def _iglob(pathname, recursive): if os.path.isdir(dirname): yield pathname return + if not dirname: - if recursive and _isrecursive(basename): - for x in glob2(dirname, basename): - yield x - else: - for x in glob1(dirname, basename): - yield x + yield from glob_in_dir(dirname, basename) return # `os.path.split()` returns the argument itself as a dirname if it is a # drive or UNC path. Prevent an infinite recursion if a drive or UNC path @@ -71,12 +69,7 @@ def _iglob(pathname, recursive): dirs = _iglob(dirname, recursive) else: dirs = [dirname] - if has_magic(basename): - if recursive and _isrecursive(basename): - glob_in_dir = glob2 - else: - glob_in_dir = glob1 - else: + if not has_magic(basename): glob_in_dir = glob0 for dirname in dirs: for name in glob_in_dir(dirname, basename): diff --git a/setuptools/installer.py b/setuptools/installer.py index e630b87479..57e2b587aa 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -7,7 +7,6 @@ from distutils.errors import DistutilsError import pkg_resources -from setuptools.command.easy_install import easy_install from setuptools.wheel import Wheel @@ -19,54 +18,11 @@ def _fixup_find_links(find_links): return find_links -def _legacy_fetch_build_egg(dist, req): - """Fetch an egg needed for building. - - Legacy path using EasyInstall. - """ - tmp_dist = dist.__class__({'script_args': ['easy_install']}) - opts = tmp_dist.get_option_dict('easy_install') - opts.clear() - opts.update( - (k, v) - for k, v in dist.get_option_dict('easy_install').items() - if k in ( - # don't use any other settings - 'find_links', 'site_dirs', 'index_url', - 'optimize', 'site_dirs', 'allow_hosts', - )) - if dist.dependency_links: - links = dist.dependency_links[:] - if 'find_links' in opts: - links = _fixup_find_links(opts['find_links'][1]) + links - opts['find_links'] = ('setup', links) - install_dir = dist.get_egg_cache_dir() - cmd = easy_install( - tmp_dist, args=["x"], install_dir=install_dir, - exclude_scripts=True, - always_copy=False, build_directory=None, editable=False, - upgrade=False, multi_version=True, no_report=True, user=False - ) - cmd.ensure_finalized() - return cmd.easy_install(req) - - -def fetch_build_egg(dist, req): +def fetch_build_egg(dist, req): # noqa: C901 # is too complex (16) # FIXME """Fetch an egg needed for building. Use pip/wheel to fetch/build a wheel.""" - # Check pip is available. - try: - pkg_resources.get_distribution('pip') - except pkg_resources.DistributionNotFound: - dist.announce( - 'WARNING: The pip package is not available, falling back ' - 'to EasyInstall for handling setup_requires/test_requires; ' - 'this is deprecated and will be removed in a future version.', - log.WARN - ) - return _legacy_fetch_build_egg(dist, req) - # Warn if wheel is not. + # Warn if wheel is not available try: pkg_resources.get_distribution('wheel') except pkg_resources.DistributionNotFound: @@ -80,20 +36,17 @@ def fetch_build_egg(dist, req): if 'allow_hosts' in opts: raise DistutilsError('the `allow-hosts` option is not supported ' 'when using pip to install requirements.') - if 'PIP_QUIET' in os.environ or 'PIP_VERBOSE' in os.environ: - quiet = False - else: - quiet = True + quiet = 'PIP_QUIET' not in os.environ and 'PIP_VERBOSE' not in os.environ if 'PIP_INDEX_URL' in os.environ: index_url = None elif 'index_url' in opts: index_url = opts['index_url'][1] else: index_url = None - if 'find_links' in opts: - find_links = _fixup_find_links(opts['find_links'][1])[:] - else: - find_links = [] + find_links = ( + _fixup_find_links(opts['find_links'][1])[:] if 'find_links' in opts + else [] + ) if dist.dependency_links: find_links.extend(dist.dependency_links) eggs_dir = os.path.realpath(dist.get_egg_cache_dir()) @@ -112,16 +65,12 @@ def fetch_build_egg(dist, req): cmd.append('--quiet') if index_url is not None: cmd.extend(('--index-url', index_url)) - if find_links is not None: - for link in find_links: - cmd.extend(('--find-links', link)) + for link in find_links or []: + cmd.extend(('--find-links', link)) # If requirement is a PEP 508 direct URL, directly pass # the URL to pip, as `req @ url` does not work on the # command line. - if req.url: - cmd.append(req.url) - else: - cmd.append(str(req)) + cmd.append(req.url or str(req)) try: subprocess.check_call(cmd) except subprocess.CalledProcessError as e: diff --git a/setuptools/msvc.py b/setuptools/msvc.py index 1ead72b421..d5e0a95232 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -24,6 +24,7 @@ from os import listdir, pathsep from os.path import join, isfile, isdir, dirname import sys +import contextlib import platform import itertools import subprocess @@ -724,28 +725,23 @@ def find_reg_vs_vers(self): ms = self.ri.microsoft vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) vs_vers = [] - for hkey in self.ri.HKEYS: - for key in vckeys: - try: - bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) - except (OSError, IOError): - continue - with bkey: - subkeys, values, _ = winreg.QueryInfoKey(bkey) - for i in range(values): - try: - ver = float(winreg.EnumValue(bkey, i)[0]) - if ver not in vs_vers: - vs_vers.append(ver) - except ValueError: - pass - for i in range(subkeys): - try: - ver = float(winreg.EnumKey(bkey, i)) - if ver not in vs_vers: - vs_vers.append(ver) - except ValueError: - pass + for hkey, key in itertools.product(self.ri.HKEYS, vckeys): + try: + bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) + except (OSError, IOError): + continue + with bkey: + subkeys, values, _ = winreg.QueryInfoKey(bkey) + for i in range(values): + with contextlib.suppress(ValueError): + ver = float(winreg.EnumValue(bkey, i)[0]) + if ver not in vs_vers: + vs_vers.append(ver) + for i in range(subkeys): + with contextlib.suppress(ValueError): + ver = float(winreg.EnumKey(bkey, i)) + if ver not in vs_vers: + vs_vers.append(ver) return sorted(vs_vers) def find_programdata_vs_vers(self): @@ -925,8 +921,8 @@ def WindowsSdkLastVersion(self): """ return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib')) - @property - def WindowsSdkDir(self): + @property # noqa: C901 + def WindowsSdkDir(self): # noqa: C901 # is too complex (12) # FIXME """ Microsoft Windows SDK directory. diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 3979b131b5..123e9582b5 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -320,7 +320,8 @@ def __init__( else: self.opener = urllib.request.urlopen - def process_url(self, url, retrieve=False): + # FIXME: 'PackageIndex.process_url' is too complex (14) + def process_url(self, url, retrieve=False): # noqa: C901 """Evaluate a URL as a possible download, and maybe retrieve it""" if url in self.scanned_urls and not retrieve: return @@ -428,49 +429,53 @@ def scan_egg_link(self, path, entry): dist.precedence = SOURCE_DIST self.add(dist) + def _scan(self, link): + # Process a URL to see if it's for a package page + NO_MATCH_SENTINEL = None, None + if not link.startswith(self.index_url): + return NO_MATCH_SENTINEL + + parts = list(map( + urllib.parse.unquote, link[len(self.index_url):].split('/') + )) + if len(parts) != 2 or '#' in parts[1]: + return NO_MATCH_SENTINEL + + # it's a package page, sanitize and index it + pkg = safe_name(parts[0]) + ver = safe_version(parts[1]) + self.package_pages.setdefault(pkg.lower(), {})[link] = True + return to_filename(pkg), to_filename(ver) + def process_index(self, url, page): """Process the contents of a PyPI page""" - def scan(link): - # Process a URL to see if it's for a package page - if link.startswith(self.index_url): - parts = list(map( - urllib.parse.unquote, link[len(self.index_url):].split('/') - )) - if len(parts) == 2 and '#' not in parts[1]: - # it's a package page, sanitize and index it - pkg = safe_name(parts[0]) - ver = safe_version(parts[1]) - self.package_pages.setdefault(pkg.lower(), {})[link] = True - return to_filename(pkg), to_filename(ver) - return None, None - # process an index page into the package-page index for match in HREF.finditer(page): try: - scan(urllib.parse.urljoin(url, htmldecode(match.group(1)))) + self._scan(urllib.parse.urljoin(url, htmldecode(match.group(1)))) except ValueError: pass - pkg, ver = scan(url) # ensure this page is in the page index - if pkg: - # process individual package page - for new_url in find_external_links(url, page): - # Process the found URL - base, frag = egg_info_for_url(new_url) - if base.endswith('.py') and not frag: - if ver: - new_url += '#egg=%s-%s' % (pkg, ver) - else: - self.need_version_info(url) - self.scan_url(new_url) - - return PYPI_MD5.sub( - lambda m: '%s' % m.group(1, 3, 2), page - ) - else: + pkg, ver = self._scan(url) # ensure this page is in the page index + if not pkg: return "" # no sense double-scanning non-package pages + # process individual package page + for new_url in find_external_links(url, page): + # Process the found URL + base, frag = egg_info_for_url(new_url) + if base.endswith('.py') and not frag: + if ver: + new_url += '#egg=%s-%s' % (pkg, ver) + else: + self.need_version_info(url) + self.scan_url(new_url) + + return PYPI_MD5.sub( + lambda m: '%s' % m.group(1, 3, 2), page + ) + def need_version_info(self, url): self.scan_all( "Page at %s links to .py file(s) without version info; an index " @@ -591,7 +596,7 @@ def download(self, spec, tmpdir): spec = parse_requirement_arg(spec) return getattr(self.fetch_distribution(spec, tmpdir), 'location', None) - def fetch_distribution( + def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME self, requirement, tmpdir, force_scan=False, source=False, develop_ok=False, local_index=None): """Obtain a distribution suitable for fulfilling `requirement` @@ -762,7 +767,8 @@ def _download_to(self, url, filename): def reporthook(self, url, filename, blocknum, blksize, size): pass # no-op - def open_url(self, url, warning=None): + # FIXME: + def open_url(self, url, warning=None): # noqa: C901 # is too complex (12) if url.startswith('file:'): return local_open(url) try: diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index eac5e65608..b58cca37c9 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -56,7 +56,7 @@ class CertificateError(ValueError): pass -if not match_hostname: +if not match_hostname: # noqa: C901 # 'If 59' is too complex (21) # FIXME def _dnsname_match(dn, hostname, max_wildcards=1): """Matching according to RFC 6125, section 6.4.3 diff --git a/setuptools/tests/files.py b/setuptools/tests/files.py deleted file mode 100644 index 71194b9de0..0000000000 --- a/setuptools/tests/files.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - - -def build_files(file_defs, prefix=""): - """ - Build a set of files/directories, as described by the - file_defs dictionary. - - Each key/value pair in the dictionary is interpreted as - a filename/contents - pair. If the contents value is a dictionary, a directory - is created, and the - dictionary interpreted as the files within it, recursively. - - For example: - - {"README.txt": "A README file", - "foo": { - "__init__.py": "", - "bar": { - "__init__.py": "", - }, - "baz.py": "# Some code", - } - } - """ - for name, contents in file_defs.items(): - full_name = os.path.join(prefix, name) - if isinstance(contents, dict): - os.makedirs(full_name, exist_ok=True) - build_files(contents, prefix=full_name) - else: - if isinstance(contents, bytes): - with open(full_name, 'wb') as f: - f.write(contents) - else: - with open(full_name, 'w') as f: - f.write(contents) diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 5204c8d126..a5a172e0f9 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -1,9 +1,14 @@ +import contextlib +import sys +import shutil +import subprocess + import pytest from . import contexts -@pytest.yield_fixture +@pytest.fixture def user_override(monkeypatch): """ Override site.USER_BASE and site.USER_SITE with temporary directories in @@ -17,7 +22,53 @@ def user_override(monkeypatch): yield -@pytest.yield_fixture +@pytest.fixture def tmpdir_cwd(tmpdir): with tmpdir.as_cwd() as orig: yield orig + + +@pytest.fixture +def tmp_src(request, tmp_path): + """Make a copy of the source dir under `$tmp/src`. + + This fixture is useful whenever it's necessary to run `setup.py` + or `pip install` against the source directory when there's no + control over the number of simultaneous invocations. Such + concurrent runs create and delete directories with the same names + under the target directory and so they influence each other's runs + when they are not being executed sequentially. + """ + tmp_src_path = tmp_path / 'src' + shutil.copytree(request.config.rootdir, tmp_src_path) + return tmp_src_path + + +@pytest.fixture(autouse=True, scope="session") +def workaround_xdist_376(request): + """ + Workaround pytest-dev/pytest-xdist#376 + + ``pytest-xdist`` tends to inject '' into ``sys.path``, + which may break certain isolation expectations. + Remove the entry so the import + machinery behaves the same irrespective of xdist. + """ + if not request.config.pluginmanager.has_plugin('xdist'): + return + + with contextlib.suppress(ValueError): + sys.path.remove('') + + +@pytest.fixture +def sample_project(tmp_path): + """ + Clone the 'sampleproject' and return a path to it. + """ + cmd = ['git', 'clone', 'https://github.com/pypa/sampleproject'] + try: + subprocess.check_call(cmd, cwd=str(tmp_path)) + except Exception: + pytest.skip("Unable to clone sampleproject") + return tmp_path / 'sampleproject' diff --git a/setuptools/tests/requirements.txt b/setuptools/tests/requirements.txt index d0d07f70c0..b2d84a941e 100644 --- a/setuptools/tests/requirements.txt +++ b/setuptools/tests/requirements.txt @@ -11,3 +11,4 @@ paver; python_version>="3.6" futures; python_version=="2.7" pip>=19.1 # For proper file:// URLs support. jaraco.envs +sphinx diff --git a/setuptools/tests/server.py b/setuptools/tests/server.py index 7e2132301b..6717c05358 100644 --- a/setuptools/tests/server.py +++ b/setuptools/tests/server.py @@ -65,7 +65,7 @@ def __init__( http.server.HTTPServer.__init__( self, server_address, RequestHandlerClass) threading.Thread.__init__(self) - self.setDaemon(True) + self.daemon = True self.requests = [] def run(self): diff --git a/setuptools/tests/test_bdist_deprecations.py b/setuptools/tests/test_bdist_deprecations.py deleted file mode 100644 index 704164aacb..0000000000 --- a/setuptools/tests/test_bdist_deprecations.py +++ /dev/null @@ -1,23 +0,0 @@ -"""develop tests -""" -import mock - -import pytest - -from setuptools.dist import Distribution -from setuptools import SetuptoolsDeprecationWarning - - -@mock.patch("distutils.command.bdist_wininst.bdist_wininst") -def test_bdist_wininst_warning(distutils_cmd): - dist = Distribution(dict( - script_name='setup.py', - script_args=['bdist_wininst'], - name='foo', - py_modules=['hi'], - )) - dist.parse_command_line() - with pytest.warns(SetuptoolsDeprecationWarning): - dist.run_commands() - - distutils_cmd.run.assert_called_once() diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index 8760ea304c..fb5b90b1a3 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -7,7 +7,6 @@ import pytest from setuptools.dist import Distribution -from setuptools import SetuptoolsDeprecationWarning from . import contexts @@ -65,17 +64,3 @@ def test_exclude_source_files(self, setup_context, user_override): names = list(zi.filename for zi in zip.filelist) assert 'hi.pyc' in names assert 'hi.py' not in names - - def test_eggsecutable_warning(self, setup_context, user_override): - dist = Distribution(dict( - script_name='setup.py', - script_args=['bdist_egg'], - name='foo', - py_modules=['hi'], - entry_points={ - 'setuptools.installation': - ['eggsecutable = my_package.some_module:main_func']}, - )) - dist.parse_command_line() - with pytest.warns(SetuptoolsDeprecationWarning): - dist.run_commands() diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index 838fdb4299..b6deebe4e2 100644 --- a/setuptools/tests/test_build_ext.py +++ b/setuptools/tests/test_build_ext.py @@ -2,12 +2,13 @@ import distutils.command.build_ext as orig from distutils.sysconfig import get_config_var +from jaraco import path + from setuptools.command.build_ext import build_ext, get_abi3_suffix from setuptools.dist import Distribution from setuptools.extension import Extension from . import environment -from .files import build_files from .textwrap import DALS @@ -103,10 +104,10 @@ def test_build_ext_config_handling(tmpdir_cwd): 'setup.cfg': DALS( """ [build] - build-base = foo_build + build_base = foo_build """), } - build_files(files) + path.build(files) code, output = environment.run_setup_py( cmd=['build'], data_stream=(0, 2), ) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 5462b26a8a..ab75a1896c 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -3,15 +3,16 @@ import tarfile import importlib from concurrent import futures +import re import pytest +from jaraco import path -from .files import build_files from .textwrap import DALS class BuildBackendBase: - def __init__(self, cwd=None, env={}, backend_name='setuptools.build_meta'): + def __init__(self, cwd='.', env={}, backend_name='setuptools.build_meta'): self.cwd = cwd self.env = env self.backend_name = backend_name @@ -108,7 +109,7 @@ def run(): 'setup.cfg': DALS(""" [metadata] name = foo - version='0.0.0' + version = 0.0.0 [options] py_modules=hello @@ -126,11 +127,11 @@ class TestBuildMetaBackend: backend_name = 'setuptools.build_meta' def get_build_backend(self): - return BuildBackend(cwd='.', backend_name=self.backend_name) + return BuildBackend(backend_name=self.backend_name) @pytest.fixture(params=defns) def build_backend(self, tmpdir, request): - build_files(request.param, prefix=str(tmpdir)) + path.build(request.param, prefix=str(tmpdir)) with tmpdir.as_cwd(): yield self.get_build_backend() @@ -170,7 +171,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): """), } - build_files(files) + path.build(files) dist_dir = os.path.abspath('preexisting-' + build_type) @@ -262,7 +263,7 @@ def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd): build-backend = "setuptools.build_meta """), } - build_files(files) + path.build(files) build_backend = self.get_build_backend() targz_path = build_backend.build_sdist("temp") with tarfile.open(os.path.join("temp", targz_path)) as tar: @@ -271,7 +272,7 @@ def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd): def test_build_sdist_setup_py_exists(self, tmpdir_cwd): # If build_sdist is called from a script other than setup.py, # ensure setup.py is included - build_files(defns[0]) + path.build(defns[0]) build_backend = self.get_build_backend() targz_path = build_backend.build_sdist("temp") @@ -293,7 +294,7 @@ def test_build_sdist_setup_py_manifest_excluded(self, tmpdir_cwd): """) } - build_files(files) + path.build(files) build_backend = self.get_build_backend() targz_path = build_backend.build_sdist("temp") @@ -315,7 +316,7 @@ def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd): """) } - build_files(files) + path.build(files) build_backend = self.get_build_backend() build_backend.build_sdist("temp") @@ -335,9 +336,9 @@ def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd): } def test_build_sdist_relative_path_import(self, tmpdir_cwd): - build_files(self._relative_path_import_files) + path.build(self._relative_path_import_files) build_backend = self.get_build_backend() - with pytest.raises(ImportError): + with pytest.raises(ImportError, match="^No module named 'hello'$"): build_backend.build_sdist("temp") @pytest.mark.parametrize('setup_literal, requirements', [ @@ -374,7 +375,7 @@ def run(): """), } - build_files(files) + path.build(files) build_backend = self.get_build_backend() @@ -409,7 +410,7 @@ def run(): """), } - build_files(files) + path.build(files) build_backend = self.get_build_backend() @@ -437,11 +438,21 @@ def run(): } def test_sys_argv_passthrough(self, tmpdir_cwd): - build_files(self._sys_argv_0_passthrough) + path.build(self._sys_argv_0_passthrough) build_backend = self.get_build_backend() with pytest.raises(AssertionError): build_backend.build_sdist("temp") + @pytest.mark.parametrize('build_hook', ('build_sdist', 'build_wheel')) + def test_build_with_empty_setuppy(self, build_backend, build_hook): + files = {'setup.py': ''} + path.build(files) + + with pytest.raises( + ValueError, + match=re.escape('No distribution was found.')): + getattr(build_backend, build_hook)("temp") + class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta:__legacy__' @@ -449,13 +460,13 @@ class TestBuildMetaLegacyBackend(TestBuildMetaBackend): # build_meta_legacy-specific tests def test_build_sdist_relative_path_import(self, tmpdir_cwd): # This must fail in build_meta, but must pass in build_meta_legacy - build_files(self._relative_path_import_files) + path.build(self._relative_path_import_files) build_backend = self.get_build_backend() build_backend.build_sdist("temp") def test_sys_argv_passthrough(self, tmpdir_cwd): - build_files(self._sys_argv_0_passthrough) + path.build(self._sys_argv_0_passthrough) build_backend = self.get_build_backend() build_backend.build_sdist("temp") diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 1dee12718f..21f1becd49 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,3 +1,6 @@ +import types +import sys + import contextlib import configparser @@ -7,6 +10,7 @@ from mock import patch from setuptools.dist import Distribution, _Distribution from setuptools.config import ConfigHandler, read_configuration +from distutils.core import Command from .textwrap import DALS @@ -206,8 +210,8 @@ def test_aliases(self, tmpdir): fake_env( tmpdir, '[metadata]\n' - 'author-email = test@test.com\n' - 'home-page = http://test.test.com/test/\n' + 'author_email = test@test.com\n' + 'home_page = http://test.test.com/test/\n' 'summary = Short summary\n' 'platform = a, b\n' 'classifier =\n' @@ -503,6 +507,44 @@ def test_not_utf8(self, tmpdir): with get_dist(tmpdir): pass + def test_warn_dash_deprecation(self, tmpdir): + # warn_dash_deprecation() is a method in setuptools.dist + # remove this test and the method when no longer needed + fake_env( + tmpdir, + '[metadata]\n' + 'author-email = test@test.com\n' + 'maintainer_email = foo@foo.com\n' + ) + msg = ("Usage of dash-separated 'author-email' will not be supported " + "in future versions. " + "Please use the underscore name 'author_email' instead") + with pytest.warns(UserWarning, match=msg): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.author_email == 'test@test.com' + assert metadata.maintainer_email == 'foo@foo.com' + + def test_make_option_lowercase(self, tmpdir): + # remove this test and the method make_option_lowercase() in setuptools.dist + # when no longer needed + fake_env( + tmpdir, + '[metadata]\n' + 'Name = foo\n' + 'description = Some description\n' + ) + msg = ("Usage of uppercase key 'Name' in 'metadata' will be deprecated in " + "future versions. " + "Please use lowercase 'name' instead") + with pytest.warns(UserWarning, match=msg): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.name == 'foo' + assert metadata.description == 'Some description' + class TestOptions: @@ -768,6 +810,20 @@ def test_extras_require(self, tmpdir): } assert dist.metadata.provides_extras == set(['pdf', 'rest']) + def test_dash_preserved_extras_require(self, tmpdir): + fake_env( + tmpdir, + '[options.extras_require]\n' + 'foo-a = foo\n' + 'foo_b = test\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.extras_require == { + 'foo-a': ['foo'], + 'foo_b': ['test'] + } + def test_entry_points(self, tmpdir): _, config = fake_env( tmpdir, @@ -802,6 +858,24 @@ def test_entry_points(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.entry_points == expected + def test_case_sensitive_entry_points(self, tmpdir): + _, config = fake_env( + tmpdir, + '[options.entry_points]\n' + 'GROUP1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'GROUP1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'] + } + def test_data_files(self, tmpdir): fake_env( tmpdir, @@ -853,6 +927,26 @@ def test_python_requires_invalid(self, tmpdir): with get_dist(tmpdir) as dist: dist.parse_config_files() + def test_cmdclass(self, tmpdir): + class CustomCmd(Command): + pass + + m = types.ModuleType('custom_build', 'test package') + + m.__dict__['CustomCmd'] = CustomCmd + + sys.modules['custom_build'] = m + + fake_env( + tmpdir, + '[options]\n' + 'cmdclass =\n' + ' customcmd = custom_build.CustomCmd\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.cmdclass == {'customcmd': CustomCmd} + saved_dist_init = _Distribution.__init__ diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index 9854420e6b..df8db4e281 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -7,6 +7,8 @@ import io import subprocess import platform +import pathlib +import textwrap from setuptools.command import test @@ -31,7 +33,7 @@ """ -@pytest.yield_fixture +@pytest.fixture def temp_user(monkeypatch): with contexts.tempdir() as user_base: with contexts.tempdir() as user_site: @@ -40,7 +42,7 @@ def temp_user(monkeypatch): yield -@pytest.yield_fixture +@pytest.fixture def test_env(tmpdir, temp_user): target = tmpdir foo = target.mkdir('foo') @@ -199,3 +201,55 @@ def test_namespace_package_importable(self, tmpdir): ] 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?)", + ) + def test_editable_prefix(self, tmp_path, sample_project): + """ + 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( + pathlib.Path(path).relative_to(sys.prefix) + for path in sys.path + if 'site-packages' in path + and path.startswith(sys.prefix) + ) + + # install the workaround + self.install_workaround(site_packages) + + env = dict(os.environ, PYTHONPATH=str(site_packages)) + cmd = [ + sys.executable, + '-m', 'pip', + 'install', + '--editable', + str(sample_project), + '--prefix', str(prefix), + '--no-build-isolation', + ] + subprocess.check_call(cmd, env=env) + + # now run 'sample' with the prefix on the PYTHONPATH + bin = 'Scripts' if platform.system() == 'Windows' else 'bin' + exe = prefix / bin / 'sample' + if sys.version_info < (3, 7) and platform.system() == 'Windows': + exe = str(exe) + subprocess.check_call([exe], env=env) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index cb47fb5848..dcec1734a9 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -9,6 +9,9 @@ _get_unpatched, check_package_data, DistDeprecationWarning, + check_specifier, + rfc822_escape, + rfc822_unescape, ) from setuptools import sic from setuptools import Distribution @@ -84,6 +87,9 @@ def __read_test_cases(): ('Metadata version 1.1: Provides', params( provides=['package'], )), + ('Metadata Version 1.0: Short long description', params( + long_description='Short long description', + )), ('Metadata version 1.1: Obsoletes', params( obsoletes=['foo'], )), @@ -161,6 +167,7 @@ def test_read_metadata(name, attrs): ('metadata_version', dist_class.get_metadata_version), ('provides', dist_class.get_provides), ('description', dist_class.get_description), + ('long_description', dist_class.get_long_description), ('download_url', dist_class.get_download_url), ('keywords', dist_class.get_keywords), ('platforms', dist_class.get_platforms), @@ -323,3 +330,49 @@ def test_check_package_data(package_data, expected_message): with pytest.raises( DistutilsSetupError, match=re.escape(expected_message)): check_package_data(None, str('package_data'), package_data) + + +def test_check_specifier(): + # valid specifier value + attrs = {'name': 'foo', 'python_requires': '>=3.0, !=3.1'} + dist = Distribution(attrs) + check_specifier(dist, attrs, attrs['python_requires']) + + # invalid specifier value + attrs = {'name': 'foo', 'python_requires': ['>=3.0', '!=3.1']} + with pytest.raises(DistutilsSetupError): + dist = Distribution(attrs) + + +@pytest.mark.parametrize( + 'content, result', + ( + pytest.param( + "Just a single line", + None, + id="single_line", + ), + pytest.param( + "Multiline\nText\nwithout\nextra indents\n", + None, + id="multiline", + ), + pytest.param( + "Multiline\n With\n\nadditional\n indentation", + None, + id="multiline_with_indentation", + ), + pytest.param( + " Leading whitespace", + "Leading whitespace", + id="remove_leading_whitespace", + ), + pytest.param( + " Leading whitespace\nIn\n Multiline comment", + "Leading whitespace\nIn\n Multiline comment", + id="remove_leading_whitespace_multiline", + ), + ) +) +def test_rfc822_unescape(content, result): + assert (result or content) == rfc822_unescape(rfc822_escape(content)) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index a53773df8c..0e89921c90 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -21,10 +21,10 @@ def run(self, cmd, *args, **kwargs): @pytest.fixture -def venv(tmpdir): +def venv(tmp_path, tmp_src): env = VirtualEnv() - env.root = path.Path(tmpdir) - env.req = os.getcwd() + env.root = path.Path(tmp_path / 'venv') + env.req = str(tmp_src) return env.create() diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 26a5e9a6ba..a3b2d6e66c 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -15,8 +15,10 @@ import mock import time import re +import subprocess import pytest +from jaraco import path from setuptools import sandbox from setuptools.sandbox import run_setup @@ -25,7 +27,6 @@ EasyInstallDeprecationWarning, ScriptWriter, PthDistributions, WindowsScriptWriter, ) -from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution from pkg_resources import normalize_path, working_set from pkg_resources import Distribution as PRDistribution @@ -34,10 +35,19 @@ import pkg_resources from . import contexts -from .files import build_files from .textwrap import DALS +@pytest.fixture(autouse=True) +def pip_disable_index(monkeypatch): + """ + Important: Disable the default index for pip to avoid + querying packages in the index and potentially resolving + and installing packages there. + """ + monkeypatch.setenv('PIP_NO_INDEX', 'true') + + class FakeDist: def get_entry_map(self, group): if group != 'console_scripts': @@ -305,7 +315,7 @@ def test_add_from_site_is_ignored(self): assert not pth.dirty -@pytest.yield_fixture +@pytest.fixture def setup_context(tmpdir): with (tmpdir / 'setup.py').open('w') as f: f.write(SETUP_PY) @@ -361,7 +371,7 @@ def foo_package(self, tmpdir): f.write('Name: foo\n') return str(tmpdir) - @pytest.yield_fixture() + @pytest.fixture() def install_target(self, tmpdir): target = str(tmpdir) with mock.patch('sys.path', sys.path + [target]): @@ -406,7 +416,7 @@ def patched_setup_context(self): ) -@pytest.yield_fixture +@pytest.fixture def distutils_package(): distutils_setup_py = SETUP_PY.replace( 'from setuptools import setup', @@ -445,22 +455,22 @@ def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch): """ monkeypatch.setenv(str('PIP_RETRIES'), str('0')) monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv('PIP_NO_INDEX', 'false') with contexts.quiet(): # create an sdist that has a build-time dependency. with TestSetupRequires.create_sdist() as dist_file: with contexts.tempdir() as temp_install_dir: with contexts.environment(PYTHONPATH=temp_install_dir): - ei_params = [ + cmd = [ + sys.executable, + '-m', 'setup', + 'easy_install', '--index-url', mock_index.url, '--exclude-scripts', '--install-dir', temp_install_dir, dist_file, ] - with sandbox.save_argv(['easy_install']): - # attempt to install the dist. It should - # fail because it doesn't exist. - with pytest.raises(SystemExit): - easy_install_pkg.main(ei_params) + subprocess.Popen(cmd).wait() # there should have been one requests to the server assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] @@ -618,6 +628,7 @@ def make_dependency_sdist(dist_path, distname, version): def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): monkeypatch.setenv(str('PIP_RETRIES'), str('0')) monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv('PIP_NO_INDEX', 'false') monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url) with contexts.save_pkg_resources_state(): with contexts.tempdir() as temp_dir: @@ -730,10 +741,10 @@ def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir): assert eggs == ['dep 1.0'] @pytest.mark.parametrize( - 'use_legacy_installer,with_dependency_links_in_setup_py', - itertools.product((False, True), (False, True))) + 'with_dependency_links_in_setup_py', + (False, True)) def test_setup_requires_with_find_links_in_setup_cfg( - self, monkeypatch, use_legacy_installer, + self, monkeypatch, with_dependency_links_in_setup_py): monkeypatch.setenv(str('PIP_RETRIES'), str('0')) monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) @@ -755,11 +766,9 @@ def test_setup_requires_with_find_links_in_setup_cfg( fp.write(DALS( ''' from setuptools import installer, setup - if {use_legacy_installer}: - installer.fetch_build_egg = installer._legacy_fetch_build_egg setup(setup_requires='python-xlib==42', dependency_links={dependency_links!r}) - ''').format(use_legacy_installer=use_legacy_installer, # noqa + ''').format( dependency_links=dependency_links)) with open(test_setup_cfg, 'w') as fp: fp.write(DALS( @@ -785,7 +794,7 @@ def test_setup_requires_with_transitive_extra_dependency( # Create source tree for `dep`. dep_pkg = os.path.join(temp_dir, 'dep') os.mkdir(dep_pkg) - build_files({ + path.build({ 'setup.py': DALS(""" import setuptools diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index dc472af4c8..80d3577424 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -6,15 +6,15 @@ import stat import time +import pytest +from jaraco import path + from setuptools.command.egg_info import ( egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision, ) from setuptools.dist import Distribution -import pytest - from . import environment -from .files import build_files from .textwrap import DALS from . import contexts @@ -37,7 +37,7 @@ class TestEggInfo: """) def _create_project(self): - build_files({ + path.build({ 'setup.py': self.setup_script, 'hello.py': DALS(""" def run(): @@ -45,7 +45,7 @@ def run(): """) }) - @pytest.yield_fixture + @pytest.fixture def env(self): with contexts.tempdir(prefix='setuptools-test.') as env_dir: env = Environment(env_dir) @@ -56,7 +56,7 @@ def env(self): for dirname in subs ) list(map(os.mkdir, env.paths.values())) - build_files({ + path.build({ env.paths['home']: { '.pydistutils.cfg': DALS(""" [egg_info] @@ -106,7 +106,7 @@ def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): the file should remain unchanged. """ setup_cfg = os.path.join(env.paths['home'], 'setup.cfg') - build_files({ + path.build({ setup_cfg: DALS(""" [egg_info] tag_build = @@ -159,8 +159,10 @@ def test_license_is_a_string(self, tmpdir_cwd, env): setup() """) - build_files({'setup.py': setup_script, - 'setup.cfg': setup_config}) + path.build({ + 'setup.py': setup_script, + 'setup.cfg': setup_config, + }) # This command should fail with a ValueError, but because it's # currently configured to use a subprocess, the actual traceback @@ -193,7 +195,7 @@ def test_rebuilt(self, tmpdir_cwd, env): def test_manifest_template_is_read(self, tmpdir_cwd, env): self._create_project() - build_files({ + path.build({ 'MANIFEST.in': DALS(""" recursive-include docs *.rst """), @@ -216,8 +218,10 @@ def _setup_script_with_requires(self, requires, use_setup_cfg=False): ''' ) % ('' if use_setup_cfg else requires) setup_config = requires if use_setup_cfg else '' - build_files({'setup.py': setup_script, - 'setup.cfg': setup_config}) + path.build({ + 'setup.py': setup_script, + 'setup.cfg': setup_config, + }) mismatch_marker = "python_version<'{this_ver}'".format( this_ver=sys.version_info[0], @@ -533,7 +537,7 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env): 'setup.cfg': DALS(""" """), 'LICENSE': "Test license" - }, False), # no license_file attribute + }, True), # no license_file attribute, LICENSE auto-included ({ 'setup.cfg': DALS(""" [metadata] @@ -541,12 +545,20 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env): """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, False) # license file is manually excluded + }, False), # license file is manually excluded + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICEN[CS]E* + """), + 'LICENSE': "Test license", + }, True, + id="glob_pattern"), ]) def test_setup_cfg_license_file( self, tmpdir_cwd, env, files, license_in_sources): self._create_project() - build_files(files) + path.build(files) environment.run_setup_py( cmd=['egg_info'], @@ -621,7 +633,7 @@ def test_setup_cfg_license_file( 'setup.cfg': DALS(""" """), 'LICENSE': "Test license" - }, [], ['LICENSE']), # no license_files attribute + }, ['LICENSE'], []), # no license_files attribute, LICENSE auto-included ({ 'setup.cfg': DALS(""" [metadata] @@ -640,12 +652,41 @@ def test_setup_cfg_license_file( 'MANIFEST.in': "exclude LICENSE-XYZ", 'LICENSE-ABC': "ABC license", 'LICENSE-XYZ': "XYZ license" - }, ['LICENSE-ABC'], ['LICENSE-XYZ']) # subset is manually excluded + }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # subset is manually excluded + pytest.param({ + 'setup.cfg': "", + 'LICENSE-ABC': "ABC license", + 'COPYING-ABC': "ABC copying", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + 'LICENCE-XYZ': "XYZ license", + 'LICENSE': "License", + 'INVALID-LICENSE': "Invalid license", + }, [ + 'LICENSE-ABC', + 'COPYING-ABC', + 'NOTICE-ABC', + 'AUTHORS-ABC', + 'LICENCE-XYZ', + 'LICENSE', + ], ['INVALID-LICENSE'], + # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + id="default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, ['LICENSE-ABC'], ['NOTICE-XYZ'], + id="no_default_glob_patterns"), ]) def test_setup_cfg_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): self._create_project() - build_files(files) + path.build(files) environment.run_setup_py( cmd=['egg_info'], @@ -745,12 +786,33 @@ def test_setup_cfg_license_files( 'LICENSE-PQR': "PQR license", 'LICENSE-XYZ': "XYZ license" # manually excluded - }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']) + }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, ['LICENSE-ABC'], ['NOTICE-XYZ'], + id="no_default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE* + license_files = + NOTICE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + }, ['LICENSE-ABC', 'NOTICE-ABC'], ['AUTHORS-ABC'], + id="combined_glob_patterrns"), ]) def test_setup_cfg_license_file_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): self._create_project() - build_files(files) + path.build(files) environment.run_setup_py( cmd=['egg_info'], @@ -886,7 +948,7 @@ def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None): def test_egg_info_tag_only_once(self, tmpdir_cwd, env): self._create_project() - build_files({ + path.build({ 'setup.cfg': DALS(""" [egg_info] tag_build = dev diff --git a/setuptools/tests/test_glob.py b/setuptools/tests/test_glob.py index a0728c5d12..e99587f568 100644 --- a/setuptools/tests/test_glob.py +++ b/setuptools/tests/test_glob.py @@ -1,9 +1,8 @@ import pytest +from jaraco import path from setuptools.glob import glob -from .files import build_files - @pytest.mark.parametrize('tree, pattern, matches', ( ('', b'', []), @@ -31,5 +30,5 @@ )) def test_glob(monkeypatch, tmpdir, tree, pattern, matches): monkeypatch.chdir(tmpdir) - build_files({name: '' for name in tree.split()}) + path.build({name: '' for name in tree.split()}) assert list(sorted(glob(pattern))) == list(sorted(matches)) diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 24cef480ea..b557831216 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -6,11 +6,6 @@ import glob import os import sys -import re -import subprocess -import functools -import tarfile -import zipfile import urllib.request import pytest @@ -20,6 +15,13 @@ from setuptools.dist import Distribution +pytestmark = pytest.mark.skipif( + 'platform.python_implementation() == "PyPy" and ' + 'platform.system() == "Windows"', + reason="pypa/setuptools#2496", +) + + def setup_module(module): packages = 'stevedore', 'virtualenvwrapper', 'pbr', 'novaclient' for pkg in packages: @@ -117,56 +119,3 @@ def test_pyuri(install_context): # The package data should be installed. assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex')) - - -build_deps = ['appdirs', 'packaging', 'pyparsing', 'six'] - - -@pytest.mark.parametrize("build_dep", build_deps) -@pytest.mark.skipif( - sys.version_info < (3, 6), reason='run only on late versions') -def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): - """ - All setuptools build dependencies must build without - setuptools. - """ - if 'pyparsing' in build_dep: - pytest.xfail(reason="Project imports setuptools unconditionally") - build_target = tmpdir_factory.mktemp('source') - build_dir = download_and_extract(request, build_dep, build_target) - install_target = tmpdir_factory.mktemp('target') - output = install(build_dir, install_target) - for line in output.splitlines(): - match = re.search('Unknown distribution option: (.*)', line) - allowed_unknowns = [ - 'test_suite', - 'tests_require', - 'python_requires', - 'install_requires', - 'long_description_content_type', - ] - assert not match or match.group(1).strip('"\'') in allowed_unknowns - - -def install(pkg_dir, install_dir): - with open(os.path.join(pkg_dir, 'setuptools.py'), 'w') as breaker: - breaker.write('raise ImportError()') - cmd = [sys.executable, 'setup.py', 'install', '--prefix', str(install_dir)] - env = dict(os.environ, PYTHONPATH=str(pkg_dir)) - output = subprocess.check_output( - cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) - return output.decode('utf-8') - - -def download_and_extract(request, req, target): - cmd = [ - sys.executable, '-m', 'pip', 'download', '--no-deps', - '--no-binary', ':all:', req, - ] - output = subprocess.check_output(cmd, encoding='utf-8') - filename = re.search('Saved (.*)', output).group(1) - request.addfinalizer(functools.partial(os.remove, filename)) - opener = zipfile.ZipFile if filename.endswith('.zip') else tarfile.open - with opener(filename) as archive: - archive.extractall(target) - return os.path.join(target, os.listdir(target)[0]) diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 82bdb9c643..589cefb2c0 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -55,6 +55,7 @@ def touch(filename): default_files = frozenset(map(make_local_path, [ 'README.rst', 'MANIFEST.in', + 'LICENSE', 'setup.py', 'app.egg-info/PKG-INFO', 'app.egg-info/SOURCES.txt', diff --git a/setuptools/tests/test_msvc.py b/setuptools/tests/test_msvc.py index 24e38ea880..d1527bfa46 100644 --- a/setuptools/tests/test_msvc.py +++ b/setuptools/tests/test_msvc.py @@ -88,7 +88,7 @@ def test_no_registry_entries_means_nothing_found(self): assert isinstance(exc, expected) assert 'aka.ms/vcpython27' in str(exc) - @pytest.yield_fixture + @pytest.fixture def user_preferred_setting(self): """ Set up environment with different install dirs for user vs. system @@ -116,7 +116,7 @@ def test_prefer_current_user(self, user_preferred_setting): expected = os.path.join(user_preferred_setting, 'vcvarsall.bat') assert expected == result - @pytest.yield_fixture + @pytest.fixture def local_machine_setting(self): """ Set up environment with only the system environment configured. @@ -138,7 +138,7 @@ def test_local_machine_recognized(self, local_machine_setting): expected = os.path.join(local_machine_setting, 'vcvarsall.bat') assert expected == result - @pytest.yield_fixture + @pytest.fixture def x64_preferred_setting(self): """ Set up environment with 64-bit and 32-bit system settings configured diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index 6c8c522dc0..270f90c98b 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -62,8 +62,9 @@ def test_pkg_resources_import(self, tmpdir): target.mkdir() install_cmd = [ sys.executable, - '-m', 'easy_install', - '-d', str(target), + '-m', 'pip', + 'install', + '-t', str(target), str(pkg), ] with test.test.paths_on_pythonpath([str(target)]): diff --git a/setuptools/tests/test_sphinx_upload_docs.py b/setuptools/tests/test_sphinx_upload_docs.py new file mode 100644 index 0000000000..cc5b8293bf --- /dev/null +++ b/setuptools/tests/test_sphinx_upload_docs.py @@ -0,0 +1,38 @@ +import pytest + +from jaraco import path + +from setuptools.command.upload_docs import upload_docs +from setuptools.dist import Distribution + + +@pytest.fixture +def sphinx_doc_sample_project(tmpdir_cwd): + path.build({ + 'setup.py': 'from setuptools import setup; setup()', + 'build': { + 'docs': { + 'conf.py': 'project="test"', + 'index.rst': ".. toctree::\ + :maxdepth: 2\ + :caption: Contents:", + }, + }, + }) + + +@pytest.mark.usefixtures('sphinx_doc_sample_project') +class TestSphinxUploadDocs: + def test_sphinx_doc(self): + params = dict( + name='foo', + packages=['test'], + ) + dist = Distribution(params) + + cmd = upload_docs(dist) + + cmd.initialize_options() + assert cmd.upload_dir is None + assert cmd.has_sphinx() is True + cmd.finalize_options() diff --git a/setuptools/tests/test_upload_docs.py b/setuptools/tests/test_upload_docs.py index a26e32a61d..55978aadc7 100644 --- a/setuptools/tests/test_upload_docs.py +++ b/setuptools/tests/test_upload_docs.py @@ -3,6 +3,7 @@ import contextlib import pytest +from jaraco import path from setuptools.command.upload_docs import upload_docs from setuptools.dist import Distribution @@ -10,28 +11,20 @@ from .textwrap import DALS from . import contexts -SETUP_PY = DALS( - """ - from setuptools import setup - - setup(name='foo') - """) - @pytest.fixture def sample_project(tmpdir_cwd): - # setup.py - with open('setup.py', 'wt') as f: - f.write(SETUP_PY) - - os.mkdir('build') - - # A test document. - with open('build/index.html', 'w') as f: - f.write("Hello world.") - - # An empty folder. - os.mkdir('build/empty') + path.build({ + 'setup.py': DALS(""" + from setuptools import setup + + setup(name='foo') + """), + 'build': { + 'index.html': 'Hello world.', + 'empty': {}, + } + }) @pytest.mark.usefixtures('sample_project') diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index c8ed9e57ea..399dbaf0bb 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -1,9 +1,11 @@ import glob import os import sys +import itertools + +import pathlib import pytest -from pytest import yield_fixture from pytest_fixture_config import yield_requires_config import pytest_virtualenv @@ -27,7 +29,7 @@ def pytest_virtualenv_works(virtualenv): @yield_requires_config(pytest_virtualenv.CONFIG, ['virtualenv_executable']) -@yield_fixture(scope='function') +@pytest.fixture(scope='function') def bare_virtualenv(): """ Bare virtualenv (no pip/setuptools/wheel). """ @@ -39,14 +41,11 @@ def bare_virtualenv(): yield venv -SOURCE_DIR = os.path.join(os.path.dirname(__file__), '../..') - - -def test_clean_env_install(bare_virtualenv): +def test_clean_env_install(bare_virtualenv, tmp_src): """ Check setuptools can be installed in a clean environment. """ - bare_virtualenv.run(['python', 'setup.py', 'install'], cd=SOURCE_DIR) + bare_virtualenv.run(['python', 'setup.py', 'install'], cd=tmp_src) def _get_pip_versions(): @@ -67,24 +66,38 @@ def _get_pip_versions(): # No network, disable most of these tests network = False + def mark(param, *marks): + if not isinstance(param, type(pytest.param(''))): + param = pytest.param(param) + return param._replace(marks=param.marks + marks) + + def skip_network(param): + return param if network else mark(param, pytest.mark.skip(reason="no network")) + + issue2599 = pytest.mark.skipif( + sys.version_info > (3, 10), + reason="pypa/setuptools#2599", + ) + network_versions = [ - 'pip==9.0.3', - 'pip==10.0.1', - 'pip==18.1', - 'pip==19.0.1', - 'https://github.com/pypa/pip/archive/master.zip', + mark('pip==9.0.3', issue2599), + mark('pip==10.0.1', issue2599), + mark('pip==18.1', issue2599), + mark('pip==19.3.1', pytest.mark.xfail(reason='pypa/pip#6599')), + 'pip==20.0.2', + 'https://github.com/pypa/pip/archive/main.zip', ] - versions = [None] + [ - pytest.param(v, **({} if network else {'marks': pytest.mark.skip})) - for v in network_versions - ] + versions = itertools.chain( + [None], + map(skip_network, network_versions) + ) - return versions + return list(versions) @pytest.mark.parametrize('pip_version', _get_pip_versions()) -def test_pip_upgrade_from_source(pip_version, virtualenv): +def test_pip_upgrade_from_source(pip_version, tmp_src, virtualenv): """ Check pip can upgrade setuptools from source. """ @@ -103,7 +116,7 @@ def test_pip_upgrade_from_source(pip_version, virtualenv): virtualenv.run(' && '.join(( 'python setup.py -q sdist -d {dist}', 'python setup.py -q bdist_wheel -d {dist}', - )).format(dist=dist_dir), cd=SOURCE_DIR) + )).format(dist=dist_dir), cd=tmp_src) sdist = glob.glob(os.path.join(dist_dir, '*.zip'))[0] wheel = glob.glob(os.path.join(dist_dir, '*.whl'))[0] # Then update from wheel. @@ -112,19 +125,19 @@ def test_pip_upgrade_from_source(pip_version, virtualenv): virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) -def _check_test_command_install_requirements(virtualenv, tmpdir): +def _check_test_command_install_requirements(virtualenv, tmpdir, cwd): """ Check the test command will install all required dependencies. """ # Install setuptools. - virtualenv.run('python setup.py develop', cd=SOURCE_DIR) + virtualenv.run('python setup.py develop', cd=cwd) def sdist(distname, version): dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) make_nspkg_sdist(str(dist_path), distname, version) return dist_path dependency_links = [ - str(dist_path) + pathlib.Path(str(dist_path)).as_uri() for dist_path in ( sdist('foobar', '2.4'), sdist('bits', '4.2'), @@ -174,22 +187,21 @@ def sdist(distname, version): assert tmpdir.join('success').check() -def test_test_command_install_requirements(virtualenv, tmpdir): +def test_test_command_install_requirements(virtualenv, tmpdir, request): # Ensure pip/wheel packages are installed. virtualenv.run( "python -c \"__import__('pkg_resources').require(['pip', 'wheel'])\"") - _check_test_command_install_requirements(virtualenv, tmpdir) - - -def test_test_command_install_requirements_when_using_easy_install( - bare_virtualenv, tmpdir): - _check_test_command_install_requirements(bare_virtualenv, tmpdir) + # uninstall setuptools so that 'setup.py develop' works + virtualenv.run("python -m pip uninstall -y setuptools") + # disable index URL so bits and bobs aren't requested from PyPI + virtualenv.env['PIP_NO_INDEX'] = '1' + _check_test_command_install_requirements(virtualenv, tmpdir, request.config.rootdir) -def test_no_missing_dependencies(bare_virtualenv): +def test_no_missing_dependencies(bare_virtualenv, request): """ Quick and dirty test to ensure all external dependencies are vendored. """ for command in ('upload',): # sorted(distutils.command.__all__): - bare_virtualenv.run( - ['python', 'setup.py', command, '-h'], cd=SOURCE_DIR) + cmd = ['python', 'setup.py', command, '-h'] + bare_virtualenv.run(cmd, cd=request.config.rootdir) diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index e56eac14d1..7345b135fd 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -15,6 +15,7 @@ import zipfile import pytest +from jaraco import path from pkg_resources import Distribution, PathMetadata, PY_MAJOR from setuptools.extern.packaging.utils import canonicalize_name @@ -22,7 +23,6 @@ from setuptools.wheel import Wheel from .contexts import tempdir -from .files import build_files from .textwrap import DALS @@ -91,7 +91,7 @@ def build_wheel(extra_file_defs=None, **kwargs): if extra_file_defs: file_defs.update(extra_file_defs) with tempdir() as source_dir: - build_files(file_defs, source_dir) + path.build(file_defs, source_dir) subprocess.check_call((sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir) yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0] diff --git a/skeleton.md b/skeleton.md new file mode 100644 index 0000000000..0938f8920d --- /dev/null +++ b/skeleton.md @@ -0,0 +1,166 @@ +# Overview + +This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. + +## An SCM-Managed Approach + +While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a Git repo capturing the evolution and culmination of these best practices. + +It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. + +The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. + +Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. + +# Usage + +## new projects + +To use skeleton for a new project, simply pull the skeleton into a new project: + +``` +$ git init my-new-project +$ cd my-new-project +$ git pull gh://jaraco/skeleton +``` + +Now customize the project to suit your individual project needs. + +## existing projects + +If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. + +``` +$ git merge skeleton --allow-unrelated-histories +``` + +The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. + +## Updating + +Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar Git operations. + +For example, here's a session of the [path project](https://pypi.org/project/path) pulling non-conflicting changes from the skeleton: + + + +Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. + +## Periodic Collapse + +In late 2020, this project [introduced](https://github.com/jaraco/skeleton/issues/27) the idea of a periodic but infrequent (O(years)) collapse of commits to limit the number of commits a new consumer will need to accept to adopt the skeleton. + +The full history of commits is collapsed into a single commit and that commit becomes the new mainline head. + +When one of these collapse operations happens, any project that previously pulled from the skeleton will no longer have a related history with that new main branch. For those projects, the skeleton provides a "handoff" branch that reconciles the two branches. Any project that has previously merged with the skeleton but now gets an error "fatal: refusing to merge unrelated histories" should instead use the handoff branch once to incorporate the new main branch. + +``` +$ git pull https://github.com/jaraco/skeleton 2020-handoff +``` + +This handoff needs to be pulled just once and thereafter the project can pull from the main head. + +The archive and handoff branches from prior collapses are indicate here: + +| refresh | archive | handoff | +|---------|-----------------|--------------| +| 2020-12 | archive/2020-12 | 2020-handoff | + +# Features + +The features/techniques employed by the skeleton include: + +- PEP 517/518-based build relying on Setuptools as the build tool +- Setuptools declarative configuration using setup.cfg +- tox for running tests +- A README.rst as reStructuredText with some popular badges, but with Read the Docs and AppVeyor badges commented out +- A CHANGES.rst file intended for publishing release notes about the project +- Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) +- Integrated type checking through [mypy](https://github.com/python/mypy/). + +## Packaging Conventions + +A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on Setuptools (a minimum version compatible with setup.cfg declarative config). + +The setup.cfg file implements the following features: + +- Assumes universal wheel for release +- Advertises the project's LICENSE file (MIT by default) +- Reads the README.rst file into the long description +- Some common Trove classifiers +- Includes all packages discovered in the repo +- Data files in the package are also included (not just Python files) +- Declares the required Python versions +- Declares install requirements (empty by default) +- Declares setup requirements for legacy environments +- Supplies two 'extras': + - testing: requirements for running tests + - docs: requirements for building docs + - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts +- Placeholder for defining entry points + +Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: + +- derive the project version from SCM tags +- ensure that all files committed to the repo are automatically included in releases + +## Running Tests + +The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). + +Other environments (invoked with `tox -e {name}`) supplied include: + + - a `docs` environment to build the documentation + - a `release` environment to publish the package to PyPI + +A pytest.ini is included to define common options around running tests. In particular: + +- rely on default test discovery in the current directory +- avoid recursing into common directories not containing tests +- run doctests on modules and invoke Flake8 tests +- in doctests, allow Unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. +- filters out known warnings caused by libraries/functionality included by the skeleton + +Relies on a .flake8 file to correct some default behaviors: + +- disable mutually incompatible rules W503 and W504 +- support for Black format + +## Continuous Integration + +The project is pre-configured to run Continuous Integration tests. + +### Github Actions + +[Github Actions](https://docs.github.com/en/free-pro-team@latest/actions) are the preferred provider as they provide free, fast, multi-platform services with straightforward configuration. Configured in `.github/workflows`. + +Features include: +- test against multiple Python versions +- run on late (and updated) platform versions +- automated releases of tagged commits +- [automatic merging of PRs](https://github.com/marketplace/actions/merge-pull-requests) (requires [protecting branches with required status checks](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/enabling-required-status-checks), [not possible through API](https://github.community/t/set-all-status-checks-to-be-required-as-branch-protection-using-the-github-api/119493)) + + +### Continuous Deployments + +In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: + +``` +pip-run -q jaraco.develop -- -m jaraco.develop.add-github-secrets +``` + +## Building Documentation + +Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. + +In addition to building the Sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. + +## Cutting releases + +By default, tagged commits are released through the continuous integration deploy stage. + +Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: + +``` +TWINE_PASSWORD={token} tox -e release +``` diff --git a/tools/tox_pip.py b/tools/tox_pip.py deleted file mode 100644 index be2ff1d01d..0000000000 --- a/tools/tox_pip.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import subprocess -import sys -import re - - -def remove_setuptools(): - """ - Remove setuptools from the current environment. - """ - print("Removing setuptools") - cmd = [sys.executable, '-m', 'pip', 'uninstall', '-y', 'setuptools'] - # set cwd to something other than '.' to avoid detecting - # '.' as the installed package. - subprocess.check_call(cmd, cwd=os.environ['TOX_WORK_DIR']) - - -def bootstrap(): - print("Running bootstrap") - cmd = [sys.executable, '-m', 'bootstrap'] - subprocess.check_call(cmd) - - -def is_install_self(args): - """ - Do the args represent an install of .? - """ - def strip_extras(arg): - match = re.match(r'(.*)?\[.*\]$', arg) - return match.group(1) if match else arg - - return ( - 'install' in args - and any( - arg in ['.', os.getcwd()] - for arg in map(strip_extras, args) - ) - ) - - -def pip(*args): - cmd = [sys.executable, '-m', 'pip'] + list(args) - return subprocess.check_call(cmd) - - -def test_dependencies(): - from ConfigParser import ConfigParser - - def clean(dep): - spec, _, _ = dep.partition('#') - return spec.strip() - - parser = ConfigParser() - parser.read('setup.cfg') - raw = parser.get('options.extras_require', 'tests').split('\n') - return filter(None, map(clean, raw)) - - -def run(args): - os.environ['PIP_USE_PEP517'] = 'true' - - if is_install_self(args): - remove_setuptools() - bootstrap() - - pip(*args) - - -if __name__ == '__main__': - run(sys.argv[1:]) diff --git a/towncrier_template.rst b/towncrier_template.rst index 31098b7cf6..7f507342d7 100644 --- a/towncrier_template.rst +++ b/towncrier_template.rst @@ -1,3 +1,7 @@ +{% if top_line %} +{{ top_line }} +{{ top_underline * ((top_line)|length)}} +{% endif %} {% for section, _ in sections.items() %} {% set underline = underlines[0] %}{% if section %}{{section}} {{ underline * section|length }} diff --git a/tox.ini b/tox.ini index 828d2c02e3..04e77ed527 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,20 @@ -# To run Tox against all supported Python interpreters, you can set: -# -# export TOXENV='py3{5,6,7,8},pypy,pypy3' - [tox] -envlist=python +envlist = python minversion = 3.2 -requires = - tox-pip-version >= 0.0.6 - -[helpers] -# Custom pip behavior -pip = python {toxinidir}/tools/tox_pip.py +# https://github.com/jaraco/skeleton/issues/6 +tox_pip_extensions_ext_venv_update = true +toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] -pip_version = pip -install_command = {[helpers]pip} install {opts} {packages} -list_dependencies_command = {[helpers]pip} freeze --all +deps = +commands = + pytest {posargs} +usedevelop = True +extras = testing setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} -# TODO: The passed environment variables came from copying other tox.ini files -# These should probably be individually annotated to explain what needs them. -passenv=APPDATA HOMEDRIVE HOMEPATH windir Program* CommonProgram* VS* APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED -commands = pytest {posargs} -usedevelop=True -extras = - tests - +passenv = + windir # required for test_pkg_resources [testenv:coverage] description=Combine coverage data and create report @@ -45,20 +34,11 @@ commands=codecov -X gcov --file {toxworkdir}/coverage.xml [testenv:docs] extras = - docs - testing + docs + testing changedir = docs commands = - {envpython} -m sphinx \ - -j auto \ - -b html \ - --color \ - -a \ - -n \ - -W \ - -d "{temp_dir}/.doctrees" \ - . \ - "{toxinidir}/build/html" + python -m sphinx -W . {toxinidir}/build/html [testenv:finalize] skip_install = True @@ -72,21 +52,20 @@ commands = [testenv:release] skip_install = True deps = - wheel - twine[keyring]>=1.13 + build + twine>=3 path jaraco.develop>=7.1 - jaraco.tidelift passenv = TWINE_PASSWORD GITHUB_TOKEN - TIDELIFT_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -m bootstrap python -c "import path; path.Path('dist').rmtree_p()" - python setup.py release + # unset tag_build and tag_date pypa/setuptools#2500 + python setup.py egg_info -Db "" saveopts + python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release - python -m jaraco.tidelift.publish-release-notes