diff --git a/.cirrus.yml b/.cirrus.yml index 15eb2df3..eed71be5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -2,7 +2,7 @@ freebsd_instance: image_family: freebsd-13-0-snap test_task: - only_if: "$CIRRUS_BRANCH == 'master' || $CIRRUS_PR != ''" + only_if: "$CIRRUS_BRANCH == 'main' || $CIRRUS_PR != ''" skip: "!changesInclude('.cirrus.yml', '**.py', 'pyproject.toml', 'setup.*')" pip_cache: diff --git a/.github/workflows/codeqa-test.yml b/.github/workflows/codeqa-test.yml deleted file mode 100644 index feece779..00000000 --- a/.github/workflows/codeqa-test.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Python codeqa/test - -on: - push: - branches: [main] - pull_request: - -jobs: - flake8: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Check code style with Flake8 - uses: TrueBrain/actions-flake8@v2 - with: - path: src tests - - test: - needs: [flake8] - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-10.15, macos-11, windows-latest] - python-version: ["2.7", "3.6", "3.9", "3.10", "pypy-2.7", "pypy-3.7"] - exclude: - - os: macos-11 - python-version: pypy-2.7 - - os: windows-latest - python-version: "2.7" - - os: windows-latest - python-version: pypy-2.7 - - os: windows-latest - python-version: pypy-3.7 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Upgrade setuptools - run: pip install "setuptools >= 40.9" - - name: Install the project - run: "pip install --no-binary=:all: ." - - name: Install test dependencies - run: pip install .[test] - - name: Test with pytest - run: python -b -m pytest -W always --cov-report=xml:pytest-cov.xml - - name: Send coverage data to Codecov - uses: codecov/codecov-action@v1 - with: - file: pytest-cov.xml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 50d896e0..b3e521d7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,18 +9,16 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.x - name: Install dependencies - run: | - pip install "setuptools >= 40.9" - pip install . + run: pip install build -e . - name: Create packages - run: python setup.py sdist bdist_wheel + run: python -m build -n -s -w . - name: Upload packages - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c1bca612 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,53 @@ +name: Run the test suite + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + schedule: + - cron: '0 6 * * 1' + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8", "pypy-3.9"] + include: + - os: macos-latest + python-version: "3.7" + - os: windows-latest + python-version: "3.7" + - os: macos-latest + python-version: "3.11" + - os: windows-latest + python-version: "3.11" + - os: macos-latest + python-version: "pypy-3.9" + - os: windows-latest + python-version: "pypy-3.9" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: pip-test-${{ matrix.python-version }}-${{ matrix.os }} + - name: Install the project + run: "pip install --no-binary=:all: ." + - name: Install test dependencies + run: pip install .[test] coverage[toml] + - name: Test with pytest + run: | + coverage run -m pytest -W always + coverage xml + - name: Send coverage data to Codecov + uses: codecov/codecov-action@v3 + with: + file: coverage.xml diff --git a/.gitignore b/.gitignore index ae5be45e..54defebb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.egg-info *.dist-info *.pyc +*.so +*.dll build dist docs/_build @@ -12,4 +14,3 @@ __pycache__ .idea .cache .eggs -.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b9db00ab --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +exclude: ^src/wheel/vendored + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - id: requirements-txt-fixer + - id: trailing-whitespace + +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["-a", "from __future__ import annotations"] + +- repo: https://github.com/asottile/pyupgrade + rev: v3.2.0 + hooks: + - id: pyupgrade + args: ["--py37-plus"] + +- repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + args: [--target-version=py37] + +- repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] + +- repo: https://github.com/codespell-project/codespell + rev: v2.2.2 + hooks: + - id: codespell + +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-blanket-type-ignore + - id: python-no-eval + - id: python-use-type-annotations + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal diff --git a/.readthedocs.yml b/.readthedocs.yml index 582db994..0d6b0b44 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,7 @@ version: 2 formats: [htmlzip, pdf] python: - version: 3.6 + version: "3.7" install: - method: pip path: . diff --git a/LICENSE.txt b/LICENSE.txt index c3441e6c..a31470f1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,6 @@ -"wheel" copyright (c) 2012-2014 Daniel Holth and -contributors. +MIT License -The MIT License +Copyright (c) 2012 Daniel Holth and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/README.rst b/README.rst index 6538f4f0..1cf194ec 100644 --- a/README.rst +++ b/README.rst @@ -30,4 +30,3 @@ Everyone interacting in the wheel project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - diff --git a/docs/conf.py b/docs/conf.py index 42153417..19b11f4e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # wheel documentation build configuration file, created by # sphinx-quickstart on Thu Jul 12 00:14:09 2012. @@ -10,49 +9,50 @@ # # All configuration values have a default; values that are commented out # serve to show the default. -import io +from __future__ import annotations + import os import re # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx'] +extensions = ["sphinx.ext.intersphinx"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'wheel' -copyright = u'2012, Daniel Holth' +project = "wheel" +copyright = "2012, Daniel Holth" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # here = os.path.abspath(os.path.dirname(__file__)) -with io.open(os.path.join(here, '..', 'src', 'wheel', '__init__.py'), - encoding='utf8') as version_file: - match = re.search(r"__version__ = '((\d+\.\d+\.\d+).*)'", - version_file.read()) +with open( + os.path.join(here, "..", "src", "wheel", "__init__.py"), encoding="utf8" +) as version_file: + match = re.search(r'__version__ = "((\d+\.\d+\.\d+).*)"', version_file.read()) # The short X.Y version. version = match.group(2) @@ -61,176 +61,168 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' -highlight_language = 'bash' +pygments_style = "sphinx" +highlight_language = "bash" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -intersphinx_mapping = { - 'python': ('https://docs.python.org/', None) -} +intersphinx_mapping = {"python": ("https://docs.python.org/", None)} # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'wheeldoc' +htmlhelp_basename = "wheeldoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'wheel.tex', u'wheel Documentation', - u'Daniel Holth', 'manual'), + ("index", "wheel.tex", "wheel Documentation", "Daniel Holth", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'wheel', u'wheel Documentation', - [u'Daniel Holth'], 1) -] +man_pages = [("index", "wheel", "wheel Documentation", ["Daniel Holth"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -239,16 +231,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'wheel', u'wheel Documentation', - u'Daniel Holth', 'wheel', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "wheel", + "wheel Documentation", + "Daniel Holth", + "wheel", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/development.rst b/docs/development.rst index 0100e53c..75b3a793 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -4,7 +4,7 @@ Development Pull Requests ------------- -- Submit Pull Requests against the ``master`` branch. +- Submit Pull Requests against the ``main`` branch. - Provide a good description of what you're doing and why. - Provide tests that cover your changes and try to run the tests locally first. @@ -35,13 +35,13 @@ click "New pull request". That's it. Automated Testing ----------------- -All pull requests and merges to 'master' branch are tested in `Github Actions`_ +All pull requests and merges to ``main`` branch are tested in `GitHub Actions`_ based on the workflows in the ``.github`` directory. The only way to trigger the test suite to run again for a pull request is to submit another change to the pull branch. -.. _Github Actions: https://github.com/actions +.. _GitHub Actions: https://github.com/actions Running Tests Locally --------------------- diff --git a/docs/installing.rst b/docs/installing.rst index 53a0f5d2..2f94efa4 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -12,7 +12,6 @@ If you prefer using your system package manager to install Python packages, you can typically find the wheel package under one of the following package names: * python-wheel -* python2-wheel * python3-wheel .. _pip: https://pip.pypa.io/en/stable/ @@ -22,4 +21,4 @@ Python and OS Compatibility --------------------------- wheel should work on any Python implementation and operating system and is -compatible with Python version 2.7 and upwards of 3.4. +compatible with Python version 3.7 and upwards. diff --git a/docs/news.rst b/docs/news.rst index eddfcf62..77a96610 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -1,6 +1,25 @@ Release Notes ============= +**0.38.1 (2022-11-04)** + +- Removed install dependency on setuptools +- The future-proof fix in 0.36.0 for converting PyPy's SOABI into a abi tag was + faulty. Fixed so that future changes in the SOABI will not change the tag. + +**0.38.0 (2022-10-21)** + +- Dropped support for Python < 3.7 +- Updated vendored ``packaging`` to 21.3 +- Replaced all uses of ``distutils`` with ``setuptools`` +- The handling of ``license_files`` (including glob patterns and default + values) is now delegated to ``setuptools>=57.0.0`` (#466). + The package dependencies were updated to reflect this change. +- Fixed potential DoS attack via the ``WHEEL_INFO_RE`` regular expression +- Fixed ``ValueError: ZIP does not support timestamps before 1980`` when using + ``SOURCE_DATE_EPOCH=0`` or when on-disk timestamps are earlier than 1980-01-01. Such + timestamps are now changed to the minimum value before packaging. + **0.37.1 (2021-12-22)** - Fixed ``wheel pack`` duplicating the ``WHEEL`` contents when the build number has changed (#415) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a8fe813b..13c16a35 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,9 +1,10 @@ Quickstart ========== -To build a wheel for your setuptools based project:: +To build a wheel for your project:: - python setup.py bdist_wheel + python -m pip install build + python -m build --wheel The wheel will go to ``dist/yourproject-.whl``. diff --git a/docs/reference/wheel_convert.rst b/docs/reference/wheel_convert.rst index bff25b23..ca625b53 100644 --- a/docs/reference/wheel_convert.rst +++ b/docs/reference/wheel_convert.rst @@ -44,4 +44,3 @@ Examples "pycharm-debug.egg" is not a valid egg name (must match at least name-version-pyX.Y.egg) $ echo $? 1 - diff --git a/docs/reference/wheel_unpack.rst b/docs/reference/wheel_unpack.rst index 420f6103..e51a093f 100644 --- a/docs/reference/wheel_unpack.rst +++ b/docs/reference/wheel_unpack.rst @@ -44,4 +44,3 @@ Examples wheel.install.BadWheelFile: Bad hash for file 'mypackage/module.py' $ echo $? 1 - diff --git a/docs/story.rst b/docs/story.rst index 102f98b3..101c8cd8 100644 --- a/docs/story.rst +++ b/docs/story.rst @@ -5,28 +5,28 @@ I was impressed with Tarek’s packaging talk at PyCon 2010, and I admire PEP 345 (Metadata for Python Software Packages 1.2) and PEP 376 (Database of Installed Python Distributions) which standardize a richer metadata format and show how distributions should be installed on disk. So -naturally with all the hubbub about `packaging` in Python 3.3, I decided +naturally with all the hubbub about ``packaging`` in Python 3.3, I decided to try it to reap the benefits of a more standardized and predictable Python packaging experience. -I began by converting `cryptacular`, a password hashing package which -has a simple C extension, to use setup.cfg. I downloaded the Python 3.3 -source, struggled with the difference between setup.py and setup.cfg -syntax, fixed the `define_macros` feature, stopped using the missing -`extras` functionality, and several hours later I was able to generate my -`METADATA` from `setup.cfg`. I rejoiced at my newfound freedom from the +I began by converting ``cryptacular``, a password hashing package which +has a simple C extension, to use ``setup.cfg``. I downloaded the Python 3.3 +source, struggled with the difference between ``setup.py`` and ``setup.cfg`` +syntax, fixed the ``define_macros`` feature, stopped using the missing +``extras`` functionality, and several hours later I was able to generate my +``METADATA`` from ``setup.cfg``. I rejoiced at my newfound freedom from the tyranny of arbitrary code execution during the build and install process. It was a lot of work. The package is worse off than before, and it can’t be built or installed without patching the Python source code itself. It was about that time that distutils-sig had a discussion about the -need to include a generated setup.cfg from setup.cfg because setup.cfg -wasn’t static enough. Wait, what? +need to include a generated ``setup.cfg`` from ``setup.cfg`` because +``setup.cfg`` wasn’t static enough. Wait, what? Of course there is a different way to massively simplify the install process. It’s called built or binary packages. You never have to run -`setup.py` because there is no `setup.py`. There is only METADATA aka +``setup.py`` because there is no ``setup.py``. There is only METADATA aka PKG-INFO. Installation has two steps: ‘build package’; ‘install package’, and you can skip the first step, have someone else do it for you, do it on another machine, or install the build system from a @@ -36,12 +36,12 @@ is still complicated, but installation is simple. With the binary package strategy people who want to install use a simple, compatible installer, and people who want to package use whatever is convenient for them for as long as it meets their needs. No one has -to rewrite `setup.py` for their own or the 20k+ other packages on PyPI +to rewrite ``setup.py`` for their own or the 20k+ other packages on PyPI unless a different build system does a better job. Wheel is my attempt to benefit from the excellent distutils-sig work -without having to fix the intractable `distutils` software itself. Like -METADATA and .dist-info directories but unlike Extension(), it’s +without having to fix the intractable ``distutils`` software itself. Like +``METADATA`` and ``.dist-info`` directories but unlike Extension(), it’s simple enough that there really could be alternate implementations; the simplest (but less than ideal) installer is nothing more than “unzip archive.whl” somewhere on sys.path. @@ -51,11 +51,11 @@ of eggs. Some comparisons: * Wheel is an installation format; egg is importable. Wheel archives do not need to include .pyc and are less tied to a specific Python version or implementation. Wheel can install (pure Python) packages built with previous versions of Python so you don’t always have to wait for the packager to catch up. -* Wheel uses .dist-info directories; egg uses .egg-info. Wheel is compatible with the new world of Python `packaging` and the new concepts it brings. +* Wheel uses .dist-info directories; egg uses .egg-info. Wheel is compatible with the new world of Python ``packaging`` and the new concepts it brings. * Wheel has a richer file naming convention for today’s multi-implementation world. A single wheel archive can indicate its compatibility with a number of Python language versions and implementations, ABIs, and system architectures. Historically the ABI has been specific to a CPython release, but when we get a longer-term ABI, wheel will be ready. -* Wheel is lossless. The first wheel implementation `bdist_wheel` always generates `egg-info`, and then converts it to a `.whl`. Later tools will allow for the conversion of existing eggs and bdist_wininst distributions. +* Wheel is lossless. The first wheel implementation ``bdist_wheel`` always generates ``egg-info``, and then converts it to a ``.whl``. Later tools will allow for the conversion of existing eggs and bdist_wininst distributions. * Wheel is versioned. Every wheel file contains the version of the wheel specification and the implementation that packaged it. Hopefully the next migration can simply be to Wheel 2.0. diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 6517258d..c8b5a347 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -4,9 +4,10 @@ User Guide Building Wheels --------------- -Building wheels from a setuptools_ based project is simple:: +To build a wheel for your project:: - python setup.py bdist_wheel + python -m pip install build + python -m build --wheel This will build any C extensions in the project and then package those and the pure Python code into a ``.whl`` file in the ``dist`` directory. @@ -20,7 +21,6 @@ adding this to your ``setup.cfg`` file: [bdist_wheel] universal = 1 -.. _setuptools: https://pypi.org/project/setuptools/ Including license files in the generated wheel file --------------------------------------------------- diff --git a/setup.cfg b/setup.cfg index 7f1a86fb..b4a7b147 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,18 +9,15 @@ classifiers = Topic :: System :: Archiving :: Packaging License :: OSI Approved :: MIT License Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 author = Daniel Holth author_email = dholth@fastmail.fm -maintainer = Alex Gronholm +maintainer = Alex Grönholm maintainer_email = alex.gronholm@nextday.fi url = https://github.com/pypa/wheel project_urls = @@ -34,8 +31,8 @@ license = MIT package_dir= = src packages = find: -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* -setup_requires = setuptools >= 40.9.0 +python_requires = >=3.7 +setup_requires = setuptools >= 45.2.0 zip_safe = False [options.packages.find] @@ -44,7 +41,6 @@ where = src [options.extras_require] test = pytest >= 3.0.0 - pytest-cov [options.entry_points] console_scripts = @@ -52,8 +48,15 @@ console_scripts = distutils.commands = bdist_wheel = wheel.bdist_wheel:bdist_wheel +[tool:isort] +src_paths = src +profile = black +skip_gitignore = true + +[flake8] +max-line-length = 88 + [tool:pytest] -addopts = --cov --cov-config=setup.cfg testpaths = tests [coverage:run] @@ -62,10 +65,3 @@ omit = */vendored/* [coverage:report] show_missing = true - -[flake8] -max-line-length = 99 - -[bdist_wheel] -# use py2.py3 tag for pure-python dist: -universal = 1 diff --git a/setup.py b/setup.py index 628efe3b..a03590f5 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ -# coding: utf-8 +from __future__ import annotations + from setuptools import setup -setup(maintainer=u'Alex Grönholm') +setup() diff --git a/src/wheel/__init__.py b/src/wheel/__init__.py index a4b38359..db16b91c 100644 --- a/src/wheel/__init__.py +++ b/src/wheel/__init__.py @@ -1 +1,3 @@ -__version__ = '0.37.1' +from __future__ import annotations + +__version__ = "0.38.1" diff --git a/src/wheel/__main__.py b/src/wheel/__main__.py index b3773a20..0be74537 100644 --- a/src/wheel/__main__.py +++ b/src/wheel/__main__.py @@ -2,16 +2,20 @@ Wheel command line tool (enable python -m wheel syntax) """ +from __future__ import annotations + import sys def main(): # needed for console script - if __package__ == '': + if __package__ == "": # To be able to run 'python wheel-0.9.whl/wheel': import os.path + path = os.path.dirname(os.path.dirname(__file__)) sys.path[0:0] = [path] import wheel.cli + sys.exit(wheel.cli.main()) diff --git a/src/wheel/_setuptools_logging.py b/src/wheel/_setuptools_logging.py new file mode 100644 index 00000000..006c0985 --- /dev/null +++ b/src/wheel/_setuptools_logging.py @@ -0,0 +1,26 @@ +# copied from setuptools.logging, omitting monkeypatching +from __future__ import annotations + +import logging +import sys + + +def _not_warning(record): + return record.levelno < logging.WARNING + + +def configure(): + """ + Configure logging to emit warning and above to stderr + and everything else to stdout. This behavior is provided + for compatibility with distutils.log but may change in + the future. + """ + err_handler = logging.StreamHandler() + err_handler.setLevel(logging.WARNING) + out_handler = logging.StreamHandler(sys.stdout) + out_handler.addFilter(_not_warning) + handlers = err_handler, out_handler + logging.basicConfig( + format="{message}", style="{", handlers=handlers, level=logging.DEBUG + ) diff --git a/src/wheel/bdist_wheel.py b/src/wheel/bdist_wheel.py index 80e43d0a..4754fd11 100644 --- a/src/wheel/bdist_wheel.py +++ b/src/wheel/bdist_wheel.py @@ -4,116 +4,112 @@ A wheel is a built archive format. """ -import distutils +from __future__ import annotations + import os +import re import shutil import stat import sys -import re +import sysconfig import warnings from collections import OrderedDict -from distutils.core import Command -from distutils import log as logger -from io import BytesIO +from email.generator import BytesGenerator, Generator from glob import iglob +from io import BytesIO from shutil import rmtree -from sysconfig import get_config_var from zipfile import ZIP_DEFLATED, ZIP_STORED import pkg_resources +from setuptools import Command -from .pkginfo import write_pkg_info +from . import __version__ as wheel_version from .macosx_libfile import calculate_macosx_platform_tag from .metadata import pkginfo_to_metadata +from .util import log from .vendored.packaging import tags from .wheelfile import WheelFile -from . import __version__ as wheel_version - -if sys.version_info < (3,): - from email.generator import Generator as BytesGenerator -else: - from email.generator import BytesGenerator safe_name = pkg_resources.safe_name safe_version = pkg_resources.safe_version +setuptools_major_version = int( + pkg_resources.get_distribution("setuptools").version.split(".")[0] +) -PY_LIMITED_API_PATTERN = r'cp3\d' +PY_LIMITED_API_PATTERN = r"cp3\d" def python_tag(): - return 'py{}'.format(sys.version_info[0]) + return f"py{sys.version_info[0]}" def get_platform(archive_root): """Return our platform name 'win32', 'linux_x86_64'""" - # XXX remove distutils dependency - result = distutils.util.get_platform() + result = sysconfig.get_platform() if result.startswith("macosx") and archive_root is not None: result = calculate_macosx_platform_tag(archive_root, result) - if result == "linux_x86_64" and sys.maxsize == 2147483647: + elif result == "linux-x86_64" and sys.maxsize == 2147483647: # pip pull request #3497 - result = "linux_i686" - return result + result = "linux-i686" + + return result.replace("-", "_") def get_flag(var, fallback, expected=True, warn=True): """Use a fallback value for determining SOABI flags if the needed config var is unset or unavailable.""" - val = get_config_var(var) + val = sysconfig.get_config_var(var) if val is None: if warn: - warnings.warn("Config variable '{0}' is unset, Python ABI tag may " - "be incorrect".format(var), RuntimeWarning, 2) + warnings.warn( + "Config variable '{}' is unset, Python ABI tag may " + "be incorrect".format(var), + RuntimeWarning, + 2, + ) return fallback return val == expected def get_abi_tag(): - """Return the ABI tag based on SOABI (if available) or emulate SOABI - (CPython 2, PyPy).""" - soabi = get_config_var('SOABI') + """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2).""" + soabi = sysconfig.get_config_var("SOABI") impl = tags.interpreter_name() - if not soabi and impl in ('cp', 'pp') and hasattr(sys, 'maxunicode'): - d = '' - m = '' - u = '' - if get_flag('Py_DEBUG', - hasattr(sys, 'gettotalrefcount'), - warn=(impl == 'cp')): - d = 'd' - if get_flag('WITH_PYMALLOC', - impl == 'cp', - warn=(impl == 'cp' and - sys.version_info < (3, 8))) \ - and sys.version_info < (3, 8): - m = 'm' - if get_flag('Py_UNICODE_SIZE', - sys.maxunicode == 0x10ffff, - expected=4, - warn=(impl == 'cp' and - sys.version_info < (3, 3))) \ - and sys.version_info < (3, 3): - u = 'u' - abi = '%s%s%s%s%s' % (impl, tags.interpreter_version(), d, m, u) - elif soabi and soabi.startswith('cpython-'): - abi = 'cp' + soabi.split('-')[1] - elif soabi and soabi.startswith('pypy-'): + if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"): + d = "" + m = "" + u = "" + if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")): + d = "d" + + if get_flag( + "WITH_PYMALLOC", + impl == "cp", + warn=(impl == "cp" and sys.version_info < (3, 8)), + ) and sys.version_info < (3, 8): + m = "m" + + abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}" + elif soabi and impl == "cp": + abi = "cp" + soabi.split("-")[1] + elif soabi and impl == "pp": # we want something like pypy36-pp73 - abi = '-'.join(soabi.split('-')[:2]) - abi = abi.replace('.', '_').replace('-', '_') + abi = "-".join(soabi.split("-")[:2]) + abi = abi.replace(".", "_").replace("-", "_") elif soabi: - abi = soabi.replace('.', '_').replace('-', '_') + abi = soabi.replace(".", "_").replace("-", "_") else: abi = None + return abi def safer_name(name): - return safe_name(name).replace('-', '_') + return safe_name(name).replace("-", "_") def safer_version(version): - return safe_version(version).replace('-', '_') + return safe_version(version).replace("-", "_") def remove_readonly(func, path, excinfo): @@ -124,61 +120,78 @@ def remove_readonly(func, path, excinfo): class bdist_wheel(Command): - description = 'create a wheel distribution' - - supported_compressions = OrderedDict([ - ('stored', ZIP_STORED), - ('deflated', ZIP_DEFLATED) - ]) - - user_options = [('bdist-dir=', 'b', - "temporary directory for creating the distribution"), - ('plat-name=', 'p', - "platform name to embed in generated filenames " - "(default: %s)" % get_platform(None)), - ('keep-temp', 'k', - "keep the pseudo-installation tree around after " + - "creating the distribution archive"), - ('dist-dir=', 'd', - "directory to put final built distributions in"), - ('skip-build', None, - "skip rebuilding everything (for testing/debugging)"), - ('relative', None, - "build the archive using relative paths " - "(default: false)"), - ('owner=', 'u', - "Owner name used when creating a tar file" - " [default: current user]"), - ('group=', 'g', - "Group name used when creating a tar file" - " [default: current group]"), - ('universal', None, - "make a universal wheel" - " (default: false)"), - ('compression=', None, - "zipfile compression (one of: {})" - " (default: 'deflated')" - .format(', '.join(supported_compressions))), - ('python-tag=', None, - "Python implementation compatibility tag" - " (default: '%s')" % (python_tag())), - ('build-number=', None, - "Build number for this particular version. " - "As specified in PEP-0427, this must start with a digit. " - "[default: None]"), - ('py-limited-api=', None, - "Python tag (cp32|cp33|cpNN) for abi3 wheel tag" - " (default: false)"), - ] - - boolean_options = ['keep-temp', 'skip-build', 'relative', 'universal'] + description = "create a wheel distribution" + + supported_compressions = OrderedDict( + [("stored", ZIP_STORED), ("deflated", ZIP_DEFLATED)] + ) + + user_options = [ + ("bdist-dir=", "b", "temporary directory for creating the distribution"), + ( + "plat-name=", + "p", + "platform name to embed in generated filenames " + "(default: %s)" % get_platform(None), + ), + ( + "keep-temp", + "k", + "keep the pseudo-installation tree around after " + + "creating the distribution archive", + ), + ("dist-dir=", "d", "directory to put final built distributions in"), + ("skip-build", None, "skip rebuilding everything (for testing/debugging)"), + ( + "relative", + None, + "build the archive using relative paths " "(default: false)", + ), + ( + "owner=", + "u", + "Owner name used when creating a tar file" " [default: current user]", + ), + ( + "group=", + "g", + "Group name used when creating a tar file" " [default: current group]", + ), + ("universal", None, "make a universal wheel" " (default: false)"), + ( + "compression=", + None, + "zipfile compression (one of: {})" + " (default: 'deflated')".format(", ".join(supported_compressions)), + ), + ( + "python-tag=", + None, + "Python implementation compatibility tag" + " (default: '%s')" % (python_tag()), + ), + ( + "build-number=", + None, + "Build number for this particular version. " + "As specified in PEP-0427, this must start with a digit. " + "[default: None]", + ), + ( + "py-limited-api=", + None, + "Python tag (cp32|cp33|cpNN) for abi3 wheel tag" " (default: false)", + ), + ] + + boolean_options = ["keep-temp", "skip-build", "relative", "universal"] def initialize_options(self): self.bdist_dir = None self.data_dir = None self.plat_name = None self.plat_tag = None - self.format = 'zip' + self.format = "zip" self.keep_temp = False self.dist_dir = None self.egginfo_dir = None @@ -188,7 +201,7 @@ def initialize_options(self): self.owner = None self.group = None self.universal = False - self.compression = 'deflated' + self.compression = "deflated" self.python_tag = python_tag() self.build_number = None self.py_limited_api = False @@ -196,35 +209,39 @@ def initialize_options(self): def finalize_options(self): if self.bdist_dir is None: - bdist_base = self.get_finalized_command('bdist').bdist_base - self.bdist_dir = os.path.join(bdist_base, 'wheel') + bdist_base = self.get_finalized_command("bdist").bdist_base + self.bdist_dir = os.path.join(bdist_base, "wheel") - self.data_dir = self.wheel_dist_name + '.data' + self.data_dir = self.wheel_dist_name + ".data" self.plat_name_supplied = self.plat_name is not None try: self.compression = self.supported_compressions[self.compression] except KeyError: - raise ValueError('Unsupported compression: {}'.format(self.compression)) + raise ValueError(f"Unsupported compression: {self.compression}") - need_options = ('dist_dir', 'plat_name', 'skip_build') + need_options = ("dist_dir", "plat_name", "skip_build") - self.set_undefined_options('bdist', - *zip(need_options, need_options)) + self.set_undefined_options("bdist", *zip(need_options, need_options)) - self.root_is_pure = not (self.distribution.has_ext_modules() - or self.distribution.has_c_libraries()) + self.root_is_pure = not ( + self.distribution.has_ext_modules() or self.distribution.has_c_libraries() + ) - if self.py_limited_api and not re.match(PY_LIMITED_API_PATTERN, self.py_limited_api): + if self.py_limited_api and not re.match( + PY_LIMITED_API_PATTERN, self.py_limited_api + ): raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN) # Support legacy [wheel] section for setting universal - wheel = self.distribution.get_option_dict('wheel') - if 'universal' in wheel: + wheel = self.distribution.get_option_dict("wheel") + if "universal" in wheel: # please don't define this in your global configs - logger.warn('The [wheel] section is deprecated. Use [bdist_wheel] instead.') - val = wheel['universal'][1].strip() - if val.lower() in ('1', 'true', 'yes'): + log.warning( + "The [wheel] section is deprecated. Use [bdist_wheel] instead.", + ) + val = wheel["universal"][1].strip() + if val.lower() in ("1", "true", "yes"): self.universal = True if self.build_number is not None and not self.build_number[:1].isdigit(): @@ -233,11 +250,13 @@ def finalize_options(self): @property def wheel_dist_name(self): """Return distribution full name with - replaced with _""" - components = (safer_name(self.distribution.get_name()), - safer_version(self.distribution.get_version())) + components = ( + safer_name(self.distribution.get_name()), + safer_version(self.distribution.get_version()), + ) if self.build_number: components += (self.build_number,) - return '-'.join(components) + return "-".join(components) def get_tag(self): # bdist sets self.plat_name if unset, we should only use it for purepy @@ -245,7 +264,7 @@ def get_tag(self): if self.plat_name_supplied: plat_name = self.plat_name elif self.root_is_pure: - plat_name = 'any' + plat_name = "any" else: # macosx contains system version in platform name so need special handle if self.plat_name and not self.plat_name.startswith("macosx"): @@ -259,47 +278,52 @@ def get_tag(self): # modules, use the default platform name. plat_name = get_platform(self.bdist_dir) - if plat_name in ('linux-x86_64', 'linux_x86_64') and sys.maxsize == 2147483647: - plat_name = 'linux_i686' + if ( + plat_name in ("linux-x86_64", "linux_x86_64") + and sys.maxsize == 2147483647 + ): + plat_name = "linux_i686" - plat_name = plat_name.lower().replace('-', '_').replace('.', '_') + plat_name = plat_name.lower().replace("-", "_").replace(".", "_") if self.root_is_pure: if self.universal: - impl = 'py2.py3' + impl = "py2.py3" else: impl = self.python_tag - tag = (impl, 'none', plat_name) + tag = (impl, "none", plat_name) else: impl_name = tags.interpreter_name() impl_ver = tags.interpreter_version() impl = impl_name + impl_ver # We don't work on CPython 3.1, 3.0. - if self.py_limited_api and (impl_name + impl_ver).startswith('cp3'): + if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"): impl = self.py_limited_api - abi_tag = 'abi3' + abi_tag = "abi3" else: abi_tag = str(get_abi_tag()).lower() tag = (impl, abi_tag, plat_name) # issue gh-374: allow overriding plat_name - supported_tags = [(t.interpreter, t.abi, plat_name) - for t in tags.sys_tags()] - assert tag in supported_tags, "would build wheel with unsupported tag {}".format(tag) + supported_tags = [ + (t.interpreter, t.abi, plat_name) for t in tags.sys_tags() + ] + assert ( + tag in supported_tags + ), f"would build wheel with unsupported tag {tag}" return tag def run(self): - build_scripts = self.reinitialize_command('build_scripts') - build_scripts.executable = 'python' + build_scripts = self.reinitialize_command("build_scripts") + build_scripts.executable = "python" build_scripts.force = True - build_ext = self.reinitialize_command('build_ext') + build_ext = self.reinitialize_command("build_ext") build_ext.inplace = False if not self.skip_build: - self.run_command('build') + self.run_command("build") - install = self.reinitialize_command('install', - reinit_subcommands=True) + install = self.reinitialize_command("install", reinit_subcommands=True) install.root = self.bdist_dir install.compile = False install.skip_build = self.skip_build @@ -308,45 +332,46 @@ def run(self): # A wheel without setuptools scripts is more cross-platform. # Use the (undocumented) `no_ep` option to setuptools' # install_scripts command to avoid creating entry point scripts. - install_scripts = self.reinitialize_command('install_scripts') + install_scripts = self.reinitialize_command("install_scripts") install_scripts.no_ep = True # Use a custom scheme for the archive, because we have to decide # at installation time which scheme to use. - for key in ('headers', 'scripts', 'data', 'purelib', 'platlib'): - setattr(install, - 'install_' + key, - os.path.join(self.data_dir, key)) + for key in ("headers", "scripts", "data", "purelib", "platlib"): + setattr(install, "install_" + key, os.path.join(self.data_dir, key)) - basedir_observed = '' + basedir_observed = "" - if os.name == 'nt': + if os.name == "nt": # win32 barfs if any of these are ''; could be '.'? # (distutils.command.install:change_roots bug) - basedir_observed = os.path.normpath(os.path.join(self.data_dir, '..')) + basedir_observed = os.path.normpath(os.path.join(self.data_dir, "..")) self.install_libbase = self.install_lib = basedir_observed - setattr(install, - 'install_purelib' if self.root_is_pure else 'install_platlib', - basedir_observed) + setattr( + install, + "install_purelib" if self.root_is_pure else "install_platlib", + basedir_observed, + ) - logger.info("installing to %s", self.bdist_dir) + log.info(f"installing to {self.bdist_dir}") - self.run_command('install') + self.run_command("install") impl_tag, abi_tag, plat_tag = self.get_tag() - archive_basename = "{}-{}-{}-{}".format(self.wheel_dist_name, impl_tag, abi_tag, plat_tag) + archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}" if not self.relative: archive_root = self.bdist_dir else: archive_root = os.path.join( - self.bdist_dir, - self._ensure_relative(install.install_base)) + self.bdist_dir, self._ensure_relative(install.install_base) + ) - self.set_undefined_options('install_egg_info', ('target', 'egginfo_dir')) - distinfo_dirname = '{}-{}.dist-info'.format( + self.set_undefined_options("install_egg_info", ("target", "egginfo_dir")) + distinfo_dirname = "{}-{}.dist-info".format( safer_name(self.distribution.get_name()), - safer_version(self.distribution.get_version())) + safer_version(self.distribution.get_version()), + ) distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname) self.egg2dist(self.egginfo_dir, distinfo_dir) @@ -356,48 +381,49 @@ def run(self): if not os.path.exists(self.dist_dir): os.makedirs(self.dist_dir) - wheel_path = os.path.join(self.dist_dir, archive_basename + '.whl') - with WheelFile(wheel_path, 'w', self.compression) as wf: + wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl") + with WheelFile(wheel_path, "w", self.compression) as wf: wf.write_files(archive_root) # Add to 'Distribution.dist_files' so that the "upload" command works - getattr(self.distribution, 'dist_files', []).append( - ('bdist_wheel', - '{}.{}'.format(*sys.version_info[:2]), # like 3.7 - wheel_path)) + getattr(self.distribution, "dist_files", []).append( + ( + "bdist_wheel", + "{}.{}".format(*sys.version_info[:2]), # like 3.7 + wheel_path, + ) + ) if not self.keep_temp: - logger.info('removing %s', self.bdist_dir) + log.info(f"removing {self.bdist_dir}") if not self.dry_run: rmtree(self.bdist_dir, onerror=remove_readonly) - def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel_version + ')'): + def write_wheelfile( + self, wheelfile_base, generator="bdist_wheel (" + wheel_version + ")" + ): from email.message import Message - # Workaround for Python 2.7 for when "generator" is unicode - if sys.version_info < (3,) and not isinstance(generator, str): - generator = generator.encode('utf-8') - msg = Message() - msg['Wheel-Version'] = '1.0' # of the spec - msg['Generator'] = generator - msg['Root-Is-Purelib'] = str(self.root_is_pure).lower() + msg["Wheel-Version"] = "1.0" # of the spec + msg["Generator"] = generator + msg["Root-Is-Purelib"] = str(self.root_is_pure).lower() if self.build_number is not None: - msg['Build'] = self.build_number + msg["Build"] = self.build_number # Doesn't work for bdist_wininst impl_tag, abi_tag, plat_tag = self.get_tag() - for impl in impl_tag.split('.'): - for abi in abi_tag.split('.'): - for plat in plat_tag.split('.'): - msg['Tag'] = '-'.join((impl, abi, plat)) + for impl in impl_tag.split("."): + for abi in abi_tag.split("."): + for plat in plat_tag.split("."): + msg["Tag"] = "-".join((impl, abi, plat)) - wheelfile_path = os.path.join(wheelfile_base, 'WHEEL') - logger.info('creating %s', wheelfile_path) + wheelfile_path = os.path.join(wheelfile_base, "WHEEL") + log.info(f"creating {wheelfile_path}") buffer = BytesIO() BytesGenerator(buffer, maxheaderlen=0).flatten(msg) - with open(wheelfile_path, 'wb') as f: - f.write(buffer.getvalue().replace(b'\r\n', b'\r')) + with open(wheelfile_path, "wb") as f: + f.write(buffer.getvalue().replace(b"\r\n", b"\r")) def _ensure_relative(self, path): # copied from dir_util, deleted @@ -408,34 +434,51 @@ def _ensure_relative(self, path): @property def license_paths(self): - metadata = self.distribution.get_option_dict('metadata') + if setuptools_major_version >= 57: + # Setuptools has resolved any patterns to actual file names + return self.distribution.metadata.license_files or () + files = set() - patterns = sorted({ - option for option in metadata.get('license_files', ('', ''))[1].split() - }) + metadata = self.distribution.get_option_dict("metadata") + if setuptools_major_version >= 42: + # Setuptools recognizes the license_files option but does not do globbing + patterns = self.distribution.metadata.license_files + else: + # Prior to those, wheel is entirely responsible for handling license files + if "license_files" in metadata: + patterns = metadata["license_files"][1].split() + else: + patterns = () - if 'license_file' in metadata: - warnings.warn('The "license_file" option is deprecated. Use ' - '"license_files" instead.', DeprecationWarning) - files.add(metadata['license_file'][1]) + if "license_file" in metadata: + warnings.warn( + 'The "license_file" option is deprecated. Use "license_files" instead.', + DeprecationWarning, + ) + files.add(metadata["license_file"][1]) - if 'license_file' not in metadata and 'license_files' not in metadata: - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + if not files and not patterns and not isinstance(patterns, list): + patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") for pattern in patterns: for path in iglob(pattern): - if path.endswith('~'): - logger.debug('ignoring license file "%s" as it looks like a backup', path) + if path.endswith("~"): + log.debug( + f'ignoring license file "{path}" as it looks like a backup' + ) continue if path not in files and os.path.isfile(path): - logger.info('adding license file "%s" (matched pattern "%s")', path, pattern) + log.info( + f'adding license file "{path}" (matched pattern "{pattern}")' + ) files.add(path) return files def egg2dist(self, egginfo_path, distinfo_path): """Convert an .egg-info directory into a .dist-info directory""" + def adios(p): """Appropriately delete directory, file or link.""" if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p): @@ -451,12 +494,13 @@ def adios(p): # to name the archive file. Check for this case and report # accordingly. import glob - pat = os.path.join(os.path.dirname(egginfo_path), '*.egg-info') + + pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info") possible = glob.glob(pat) - err = "Egg metadata expected at %s but not found" % (egginfo_path,) + err = f"Egg metadata expected at {egginfo_path} but not found" if possible: alt = os.path.basename(possible[0]) - err += " (%s found - possible misnamed archive file?)" % (alt,) + err += f" ({alt} found - possible misnamed archive file?)" raise ValueError(err) @@ -467,23 +511,31 @@ def adios(p): os.mkdir(distinfo_path) else: # .egg-info is a directory - pkginfo_path = os.path.join(egginfo_path, 'PKG-INFO') + pkginfo_path = os.path.join(egginfo_path, "PKG-INFO") pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path) # ignore common egg metadata that is useless to wheel - shutil.copytree(egginfo_path, distinfo_path, - ignore=lambda x, y: {'PKG-INFO', 'requires.txt', 'SOURCES.txt', - 'not-zip-safe'} - ) + shutil.copytree( + egginfo_path, + distinfo_path, + ignore=lambda x, y: { + "PKG-INFO", + "requires.txt", + "SOURCES.txt", + "not-zip-safe", + }, + ) # delete dependency_links if it is only whitespace - dependency_links_path = os.path.join(distinfo_path, 'dependency_links.txt') - with open(dependency_links_path, 'r') as dependency_links_file: + dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt") + with open(dependency_links_path) as dependency_links_file: dependency_links = dependency_links_file.read().strip() if not dependency_links: adios(dependency_links_path) - write_pkg_info(os.path.join(distinfo_path, 'METADATA'), pkg_info) + pkg_info_path = os.path.join(distinfo_path, "METADATA") + with open(pkg_info_path, "w", encoding="utf-8") as out: + Generator(out, mangle_from_=False, maxheaderlen=0).flatten(pkg_info) for license_path in self.license_paths: filename = os.path.basename(license_path) diff --git a/src/wheel/cli/__init__.py b/src/wheel/cli/__init__.py index 95740bfb..c0fb8c44 100644 --- a/src/wheel/cli/__init__.py +++ b/src/wheel/cli/__init__.py @@ -2,41 +2,38 @@ Wheel command-line utility. """ -from __future__ import print_function +from __future__ import annotations import argparse import os import sys -def require_pkgresources(name): - try: - import pkg_resources # noqa: F401 - except ImportError: - raise RuntimeError("'{0}' needs pkg_resources (part of setuptools).".format(name)) - - class WheelError(Exception): pass def unpack_f(args): from .unpack import unpack + unpack(args.wheelfile, args.dest) def pack_f(args): from .pack import pack + pack(args.directory, args.dest_dir, args.build_number) def convert_f(args): from .convert import convert + convert(args.files, args.dest_dir, args.verbose) def version_f(args): from .. import __version__ + print("wheel %s" % __version__) @@ -44,30 +41,41 @@ def parser(): p = argparse.ArgumentParser() s = p.add_subparsers(help="commands") - unpack_parser = s.add_parser('unpack', help='Unpack wheel') - unpack_parser.add_argument('--dest', '-d', help='Destination directory', - default='.') - unpack_parser.add_argument('wheelfile', help='Wheel file') + unpack_parser = s.add_parser("unpack", help="Unpack wheel") + unpack_parser.add_argument( + "--dest", "-d", help="Destination directory", default="." + ) + unpack_parser.add_argument("wheelfile", help="Wheel file") unpack_parser.set_defaults(func=unpack_f) - repack_parser = s.add_parser('pack', help='Repack wheel') - repack_parser.add_argument('directory', help='Root directory of the unpacked wheel') - repack_parser.add_argument('--dest-dir', '-d', default=os.path.curdir, - help="Directory to store the wheel (default %(default)s)") - repack_parser.add_argument('--build-number', help="Build tag to use in the wheel name") + repack_parser = s.add_parser("pack", help="Repack wheel") + repack_parser.add_argument("directory", help="Root directory of the unpacked wheel") + repack_parser.add_argument( + "--dest-dir", + "-d", + default=os.path.curdir, + help="Directory to store the wheel (default %(default)s)", + ) + repack_parser.add_argument( + "--build-number", help="Build tag to use in the wheel name" + ) repack_parser.set_defaults(func=pack_f) - convert_parser = s.add_parser('convert', help='Convert egg or wininst to wheel') - convert_parser.add_argument('files', nargs='*', help='Files to convert') - convert_parser.add_argument('--dest-dir', '-d', default=os.path.curdir, - help="Directory to store wheels (default %(default)s)") - convert_parser.add_argument('--verbose', '-v', action='store_true') + convert_parser = s.add_parser("convert", help="Convert egg or wininst to wheel") + convert_parser.add_argument("files", nargs="*", help="Files to convert") + convert_parser.add_argument( + "--dest-dir", + "-d", + default=os.path.curdir, + help="Directory to store wheels (default %(default)s)", + ) + convert_parser.add_argument("--verbose", "-v", action="store_true") convert_parser.set_defaults(func=convert_f) - version_parser = s.add_parser('version', help='Print version and exit') + version_parser = s.add_parser("version", help="Print version and exit") version_parser.set_defaults(func=version_f) - help_parser = s.add_parser('help', help='Show this help') + help_parser = s.add_parser("help", help="Show this help") help_parser.set_defaults(func=lambda args: p.print_help()) return p @@ -76,7 +84,7 @@ def parser(): def main(): p = parser() args = p.parse_args() - if not hasattr(args, 'func'): + if not hasattr(args, "func"): p.print_help() else: try: diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index 154f1b1e..1287059d 100755 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -1,21 +1,29 @@ +from __future__ import annotations + import os.path import re import shutil -import sys import tempfile import zipfile -from distutils import dist from glob import iglob from ..bdist_wheel import bdist_wheel from ..wheelfile import WheelFile -from . import WheelError, require_pkgresources +from . import WheelError + +try: + from setuptools import Distribution +except ImportError: + from distutils.dist import Distribution -egg_info_re = re.compile(r''' +egg_info_re = re.compile( + r""" (?P.+?)-(?P.+?) (-(?Ppy\d\.\d+) (-(?P.+?))? - )?.egg$''', re.VERBOSE) + )?.egg$""", + re.VERBOSE, +) class _bdist_wheel_tag(bdist_wheel): @@ -34,11 +42,11 @@ def get_tag(self): return bdist_wheel.get_tag(self) -def egg2wheel(egg_path, dest_dir): +def egg2wheel(egg_path: str, dest_dir: str): filename = os.path.basename(egg_path) match = egg_info_re.match(filename) if not match: - raise WheelError('Invalid egg file name: {}'.format(filename)) + raise WheelError(f"Invalid egg file name: {filename}") egg_info = match.groupdict() dir = tempfile.mkdtemp(suffix="_e2w") @@ -55,34 +63,34 @@ def egg2wheel(egg_path, dest_dir): else: shutil.copytree(src, os.path.join(dir, pth)) - pyver = egg_info['pyver'] + pyver = egg_info["pyver"] if pyver: - pyver = egg_info['pyver'] = pyver.replace('.', '') + pyver = egg_info["pyver"] = pyver.replace(".", "") - arch = (egg_info['arch'] or 'any').replace('.', '_').replace('-', '_') + arch = (egg_info["arch"] or "any").replace(".", "_").replace("-", "_") # assume all binary eggs are for CPython - abi = 'cp' + pyver[2:] if arch != 'any' else 'none' + abi = "cp" + pyver[2:] if arch != "any" else "none" - root_is_purelib = egg_info['arch'] is None + root_is_purelib = egg_info["arch"] is None if root_is_purelib: - bw = bdist_wheel(dist.Distribution()) + bw = bdist_wheel(Distribution()) else: - bw = _bdist_wheel_tag(dist.Distribution()) + bw = _bdist_wheel_tag(Distribution()) bw.root_is_pure = root_is_purelib bw.python_tag = pyver bw.plat_name_supplied = True - bw.plat_name = egg_info['arch'] or 'any' + bw.plat_name = egg_info["arch"] or "any" if not root_is_purelib: bw.full_tag_supplied = True bw.full_tag = (pyver, abi, arch) - dist_info_dir = os.path.join(dir, '{name}-{ver}.dist-info'.format(**egg_info)) - bw.egg2dist(os.path.join(dir, 'EGG-INFO'), dist_info_dir) - bw.write_wheelfile(dist_info_dir, generator='egg2wheel') - wheel_name = '{name}-{ver}-{pyver}-{}-{}.whl'.format(abi, arch, **egg_info) - with WheelFile(os.path.join(dest_dir, wheel_name), 'w') as wf: + dist_info_dir = os.path.join(dir, "{name}-{ver}.dist-info".format(**egg_info)) + bw.egg2dist(os.path.join(dir, "EGG-INFO"), dist_info_dir) + bw.write_wheelfile(dist_info_dir, generator="egg2wheel") + wheel_name = "{name}-{ver}-{pyver}-{}-{}.whl".format(abi, arch, **egg_info) + with WheelFile(os.path.join(dest_dir, wheel_name), "w") as wf: wf.write_files(dir) shutil.rmtree(dir) @@ -125,38 +133,38 @@ def parse_wininst_info(wininfo_name, egginfo_name): if egginfo_name: egginfo = egg_info_re.search(egginfo_name) if not egginfo: - raise ValueError("Egg info filename %s is not valid" % (egginfo_name,)) + raise ValueError(f"Egg info filename {egginfo_name} is not valid") # Parse the wininst filename # 1. Distribution name (up to the first '-') - w_name, sep, rest = wininfo_name.partition('-') + w_name, sep, rest = wininfo_name.partition("-") if not sep: - raise ValueError("Installer filename %s is not valid" % (wininfo_name,)) + raise ValueError(f"Installer filename {wininfo_name} is not valid") # Strip '.exe' rest = rest[:-4] # 2. Python version (from the last '-', must start with 'py') - rest2, sep, w_pyver = rest.rpartition('-') - if sep and w_pyver.startswith('py'): + rest2, sep, w_pyver = rest.rpartition("-") + if sep and w_pyver.startswith("py"): rest = rest2 - w_pyver = w_pyver.replace('.', '') + w_pyver = w_pyver.replace(".", "") else: # Not version specific - use py2.py3. While it is possible that # pure-Python code is not compatible with both Python 2 and 3, there # is no way of knowing from the wininst format, so we assume the best # here (the user can always manually rename the wheel to be more # restrictive if needed). - w_pyver = 'py2.py3' + w_pyver = "py2.py3" # 3. Version and architecture - w_ver, sep, w_arch = rest.rpartition('.') + w_ver, sep, w_arch = rest.rpartition(".") if not sep: - raise ValueError("Installer filename %s is not valid" % (wininfo_name,)) + raise ValueError(f"Installer filename {wininfo_name} is not valid") if egginfo: - w_name = egginfo.group('name') - w_ver = egginfo.group('ver') + w_name = egginfo.group("name") + w_ver = egginfo.group("ver") - return {'name': w_name, 'ver': w_ver, 'arch': w_arch, 'pyver': w_pyver} + return {"name": w_name, "ver": w_ver, "arch": w_arch, "pyver": w_pyver} def wininst2wheel(path, dest_dir): @@ -164,7 +172,7 @@ def wininst2wheel(path, dest_dir): # Search for egg-info in the archive egginfo_name = None for filename in bdw.namelist(): - if '.egg-info' in filename: + if ".egg-info" in filename: egginfo_name = filename break @@ -172,13 +180,13 @@ def wininst2wheel(path, dest_dir): root_is_purelib = True for zipinfo in bdw.infolist(): - if zipinfo.filename.startswith('PLATLIB'): + if zipinfo.filename.startswith("PLATLIB"): root_is_purelib = False break if root_is_purelib: - paths = {'purelib': ''} + paths = {"purelib": ""} else: - paths = {'platlib': ''} + paths = {"platlib": ""} dist_info = "%(name)s-%(ver)s" % info datadir = "%s.data/" % dist_info @@ -186,13 +194,13 @@ def wininst2wheel(path, dest_dir): # rewrite paths to trick ZipFile into extracting an egg # XXX grab wininst .ini - between .exe, padding, and first zip file. members = [] - egginfo_name = '' + egginfo_name = "" for zipinfo in bdw.infolist(): - key, basename = zipinfo.filename.split('/', 1) + key, basename = zipinfo.filename.split("/", 1) key = key.lower() basepath = paths.get(key, None) if basepath is None: - basepath = datadir + key.lower() + '/' + basepath = datadir + key.lower() + "/" oldname = zipinfo.filename newname = basepath + basename zipinfo.filename = newname @@ -203,66 +211,62 @@ def wininst2wheel(path, dest_dir): members.append(newname) # Remember egg-info name for the egg2dist call below if not egginfo_name: - if newname.endswith('.egg-info'): + if newname.endswith(".egg-info"): egginfo_name = newname - elif '.egg-info/' in newname: - egginfo_name, sep, _ = newname.rpartition('/') + elif ".egg-info/" in newname: + egginfo_name, sep, _ = newname.rpartition("/") dir = tempfile.mkdtemp(suffix="_b2w") bdw.extractall(dir, members) # egg2wheel - abi = 'none' - pyver = info['pyver'] - arch = (info['arch'] or 'any').replace('.', '_').replace('-', '_') + abi = "none" + pyver = info["pyver"] + arch = (info["arch"] or "any").replace(".", "_").replace("-", "_") # Wininst installers always have arch even if they are not # architecture-specific (because the format itself is). # So, assume the content is architecture-neutral if root is purelib. if root_is_purelib: - arch = 'any' + arch = "any" # If the installer is architecture-specific, it's almost certainly also # CPython-specific. - if arch != 'any': - pyver = pyver.replace('py', 'cp') - wheel_name = '-'.join((dist_info, pyver, abi, arch)) + if arch != "any": + pyver = pyver.replace("py", "cp") + wheel_name = "-".join((dist_info, pyver, abi, arch)) if root_is_purelib: - bw = bdist_wheel(dist.Distribution()) + bw = bdist_wheel(Distribution()) else: - bw = _bdist_wheel_tag(dist.Distribution()) + bw = _bdist_wheel_tag(Distribution()) bw.root_is_pure = root_is_purelib bw.python_tag = pyver bw.plat_name_supplied = True - bw.plat_name = info['arch'] or 'any' + bw.plat_name = info["arch"] or "any" if not root_is_purelib: bw.full_tag_supplied = True bw.full_tag = (pyver, abi, arch) - dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info) + dist_info_dir = os.path.join(dir, "%s.dist-info" % dist_info) bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir) - bw.write_wheelfile(dist_info_dir, generator='wininst2wheel') + bw.write_wheelfile(dist_info_dir, generator="wininst2wheel") wheel_path = os.path.join(dest_dir, wheel_name) - with WheelFile(wheel_path, 'w') as wf: + with WheelFile(wheel_path, "w") as wf: wf.write_files(dir) shutil.rmtree(dir) def convert(files, dest_dir, verbose): - # Only support wheel convert if pkg_resources is present - require_pkgresources('wheel convert') - for pat in files: for installer in iglob(pat): - if os.path.splitext(installer)[1] == '.egg': + if os.path.splitext(installer)[1] == ".egg": conv = egg2wheel else: conv = wininst2wheel if verbose: - print("{}... ".format(installer)) - sys.stdout.flush() + print(f"{installer}... ", flush=True) conv(installer, dest_dir) if verbose: diff --git a/src/wheel/cli/pack.py b/src/wheel/cli/pack.py index 9403c51d..1949d4cf 100644 --- a/src/wheel/cli/pack.py +++ b/src/wheel/cli/pack.py @@ -1,17 +1,16 @@ -from __future__ import print_function +from __future__ import annotations import os.path import re -import sys from wheel.cli import WheelError from wheel.wheelfile import WheelFile DIST_INFO_RE = re.compile(r"^(?P(?P.+?)-(?P\d.*?))\.dist-info$") -BUILD_NUM_RE = re.compile(br'Build: (\d\w*)$') +BUILD_NUM_RE = re.compile(rb"Build: (\d\w*)$") -def pack(directory, dest_dir, build_number): +def pack(directory: str, dest_dir: str, build_number: str | None): """Repack a previously unpacked wheel directory into a new wheel file. The .dist-info/WHEEL file must contain one or more tags so that the target @@ -21,44 +20,54 @@ def pack(directory, dest_dir, build_number): :param dest_dir: Destination directory (defaults to the current directory) """ # Find the .dist-info directory - dist_info_dirs = [fn for fn in os.listdir(directory) - if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn)] + dist_info_dirs = [ + fn + for fn in os.listdir(directory) + if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn) + ] if len(dist_info_dirs) > 1: - raise WheelError('Multiple .dist-info directories found in {}'.format(directory)) + raise WheelError(f"Multiple .dist-info directories found in {directory}") elif not dist_info_dirs: - raise WheelError('No .dist-info directories found in {}'.format(directory)) + raise WheelError(f"No .dist-info directories found in {directory}") # Determine the target wheel filename dist_info_dir = dist_info_dirs[0] - name_version = DIST_INFO_RE.match(dist_info_dir).group('namever') + name_version = DIST_INFO_RE.match(dist_info_dir).group("namever") # Read the tags and the existing build number from .dist-info/WHEEL existing_build_number = None - wheel_file_path = os.path.join(directory, dist_info_dir, 'WHEEL') + wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") with open(wheel_file_path) as f: tags = [] for line in f: - if line.startswith('Tag: '): - tags.append(line.split(' ')[1].rstrip()) - elif line.startswith('Build: '): - existing_build_number = line.split(' ')[1].rstrip() + if line.startswith("Tag: "): + tags.append(line.split(" ")[1].rstrip()) + elif line.startswith("Build: "): + existing_build_number = line.split(" ")[1].rstrip() if not tags: - raise WheelError('No tags present in {}/WHEEL; cannot determine target wheel filename' - .format(dist_info_dir)) + raise WheelError( + "No tags present in {}/WHEEL; cannot determine target wheel " + "filename".format(dist_info_dir) + ) # Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL build_number = build_number if build_number is not None else existing_build_number if build_number is not None: if build_number: - name_version += '-' + build_number + name_version += "-" + build_number if build_number != existing_build_number: - replacement = ('Build: %s\r\n' % build_number).encode('ascii') if build_number else b'' - with open(wheel_file_path, 'rb+') as f: + replacement = ( + ("Build: %s\r\n" % build_number).encode("ascii") + if build_number + else b"" + ) + with open(wheel_file_path, "rb+") as f: wheel_file_content = f.read() - wheel_file_content, num_replaced = BUILD_NUM_RE.subn(replacement, - wheel_file_content) + wheel_file_content, num_replaced = BUILD_NUM_RE.subn( + replacement, wheel_file_content + ) if not num_replaced: wheel_file_content += replacement @@ -67,16 +76,15 @@ def pack(directory, dest_dir, build_number): f.write(wheel_file_content) # Reassemble the tags for the wheel file - impls = sorted({tag.split('-')[0] for tag in tags}) - abivers = sorted({tag.split('-')[1] for tag in tags}) - platforms = sorted({tag.split('-')[2] for tag in tags}) - tagline = '-'.join(['.'.join(impls), '.'.join(abivers), '.'.join(platforms)]) + impls = sorted({tag.split("-")[0] for tag in tags}) + abivers = sorted({tag.split("-")[1] for tag in tags}) + platforms = sorted({tag.split("-")[2] for tag in tags}) + tagline = "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)]) # Repack the wheel - wheel_path = os.path.join(dest_dir, '{}-{}.whl'.format(name_version, tagline)) - with WheelFile(wheel_path, 'w') as wf: - print("Repacking wheel as {}...".format(wheel_path), end='') - sys.stdout.flush() + wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl") + with WheelFile(wheel_path, "w") as wf: + print(f"Repacking wheel as {wheel_path}...", end="", flush=True) wf.write_files(directory) - print('OK') + print("OK") diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index 2e9857a3..c6409d4b 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -1,12 +1,11 @@ -from __future__ import print_function +from __future__ import annotations -import os.path -import sys +from pathlib import Path from ..wheelfile import WheelFile -def unpack(path, dest='.'): +def unpack(path: str, dest: str = ".") -> None: """Unpack a wheel. Wheel content will be unpacked to {dest}/{name}-{ver}, where {name} @@ -16,10 +15,9 @@ def unpack(path, dest='.'): :param dest: Destination directory (default to current directory). """ with WheelFile(path) as wf: - namever = wf.parsed_filename.group('namever') - destination = os.path.join(dest, namever) - print("Unpacking to: {}...".format(destination), end='') - sys.stdout.flush() + namever = wf.parsed_filename.group("namever") + destination = Path(dest) / namever + print(f"Unpacking to: {destination}...", end="", flush=True) wf.extractall(destination) - print('OK') + print("OK") diff --git a/src/wheel/macosx_libfile.py b/src/wheel/macosx_libfile.py index 39006fb0..4d085742 100644 --- a/src/wheel/macosx_libfile.py +++ b/src/wheel/macosx_libfile.py @@ -38,32 +38,37 @@ target when the arm64 target is 11.0. """ +from __future__ import annotations + import ctypes import os import sys """here the needed const and struct from mach-o header files""" -FAT_MAGIC = 0xcafebabe -FAT_CIGAM = 0xbebafeca -FAT_MAGIC_64 = 0xcafebabf -FAT_CIGAM_64 = 0xbfbafeca -MH_MAGIC = 0xfeedface -MH_CIGAM = 0xcefaedfe -MH_MAGIC_64 = 0xfeedfacf -MH_CIGAM_64 = 0xcffaedfe +FAT_MAGIC = 0xCAFEBABE +FAT_CIGAM = 0xBEBAFECA +FAT_MAGIC_64 = 0xCAFEBABF +FAT_CIGAM_64 = 0xBFBAFECA +MH_MAGIC = 0xFEEDFACE +MH_CIGAM = 0xCEFAEDFE +MH_MAGIC_64 = 0xFEEDFACF +MH_CIGAM_64 = 0xCFFAEDFE LC_VERSION_MIN_MACOSX = 0x24 LC_BUILD_VERSION = 0x32 -CPU_TYPE_ARM64 = 0x0100000c +CPU_TYPE_ARM64 = 0x0100000C mach_header_fields = [ - ("magic", ctypes.c_uint32), ("cputype", ctypes.c_int), - ("cpusubtype", ctypes.c_int), ("filetype", ctypes.c_uint32), - ("ncmds", ctypes.c_uint32), ("sizeofcmds", ctypes.c_uint32), - ("flags", ctypes.c_uint32) - ] + ("magic", ctypes.c_uint32), + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("filetype", ctypes.c_uint32), + ("ncmds", ctypes.c_uint32), + ("sizeofcmds", ctypes.c_uint32), + ("flags", ctypes.c_uint32), +] """ struct mach_header { uint32_t magic; /* mach magic number identifier */ @@ -101,9 +106,11 @@ """ fat_arch_fields = [ - ("cputype", ctypes.c_int), ("cpusubtype", ctypes.c_int), - ("offset", ctypes.c_uint32), ("size", ctypes.c_uint32), - ("align", ctypes.c_uint32) + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("offset", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("align", ctypes.c_uint32), ] """ struct fat_arch { @@ -116,9 +123,12 @@ """ fat_arch_64_fields = [ - ("cputype", ctypes.c_int), ("cpusubtype", ctypes.c_int), - ("offset", ctypes.c_uint64), ("size", ctypes.c_uint64), - ("align", ctypes.c_uint32), ("reserved", ctypes.c_uint32) + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("offset", ctypes.c_uint64), + ("size", ctypes.c_uint64), + ("align", ctypes.c_uint32), + ("reserved", ctypes.c_uint32), ] """ struct fat_arch_64 { @@ -135,13 +145,18 @@ """base for reading segment info""" segment_command_fields = [ - ("cmd", ctypes.c_uint32), ("cmdsize", ctypes.c_uint32), - ("segname", ctypes.c_char * 16), ("vmaddr", ctypes.c_uint32), - ("vmsize", ctypes.c_uint32), ("fileoff", ctypes.c_uint32), - ("filesize", ctypes.c_uint32), ("maxprot", ctypes.c_int), - ("initprot", ctypes.c_int), ("nsects", ctypes.c_uint32), + ("cmd", ctypes.c_uint32), + ("cmdsize", ctypes.c_uint32), + ("segname", ctypes.c_char * 16), + ("vmaddr", ctypes.c_uint32), + ("vmsize", ctypes.c_uint32), + ("fileoff", ctypes.c_uint32), + ("filesize", ctypes.c_uint32), + ("maxprot", ctypes.c_int), + ("initprot", ctypes.c_int), + ("nsects", ctypes.c_uint32), ("flags", ctypes.c_uint32), - ] +] """ struct segment_command { /* for 32-bit architectures */ uint32_t cmd; /* LC_SEGMENT */ @@ -160,13 +175,18 @@ """ segment_command_fields_64 = [ - ("cmd", ctypes.c_uint32), ("cmdsize", ctypes.c_uint32), - ("segname", ctypes.c_char * 16), ("vmaddr", ctypes.c_uint64), - ("vmsize", ctypes.c_uint64), ("fileoff", ctypes.c_uint64), - ("filesize", ctypes.c_uint64), ("maxprot", ctypes.c_int), - ("initprot", ctypes.c_int), ("nsects", ctypes.c_uint32), + ("cmd", ctypes.c_uint32), + ("cmdsize", ctypes.c_uint32), + ("segname", ctypes.c_char * 16), + ("vmaddr", ctypes.c_uint64), + ("vmsize", ctypes.c_uint64), + ("fileoff", ctypes.c_uint64), + ("filesize", ctypes.c_uint64), + ("maxprot", ctypes.c_int), + ("initprot", ctypes.c_int), + ("nsects", ctypes.c_uint32), ("flags", ctypes.c_uint32), - ] +] """ struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ @@ -183,8 +203,10 @@ }; """ -version_min_command_fields = segment_base_fields + \ - [("version", ctypes.c_uint32), ("sdk", ctypes.c_uint32)] +version_min_command_fields = segment_base_fields + [ + ("version", ctypes.c_uint32), + ("sdk", ctypes.c_uint32), +] """ struct version_min_command { uint32_t cmd; /* LC_VERSION_MIN_MACOSX or @@ -197,9 +219,12 @@ }; """ -build_version_command_fields = segment_base_fields + \ - [("platform", ctypes.c_uint32), ("minos", ctypes.c_uint32), - ("sdk", ctypes.c_uint32), ("ntools", ctypes.c_uint32)] +build_version_command_fields = segment_base_fields + [ + ("platform", ctypes.c_uint32), + ("minos", ctypes.c_uint32), + ("sdk", ctypes.c_uint32), + ("ntools", ctypes.c_uint32), +] """ struct build_version_command { uint32_t cmd; /* LC_BUILD_VERSION */ @@ -214,10 +239,12 @@ def swap32(x): - return (((x << 24) & 0xFF000000) | - ((x << 8) & 0x00FF0000) | - ((x >> 8) & 0x0000FF00) | - ((x >> 24) & 0x000000FF)) + return ( + ((x << 24) & 0xFF000000) + | ((x << 8) & 0x00FF0000) + | ((x >> 8) & 0x0000FF00) + | ((x >> 24) & 0x000000FF) + ) def get_base_class_and_magic_number(lib_file, seek=None): @@ -226,7 +253,8 @@ def get_base_class_and_magic_number(lib_file, seek=None): else: lib_file.seek(seek) magic_number = ctypes.c_uint32.from_buffer_copy( - lib_file.read(ctypes.sizeof(ctypes.c_uint32))).value + lib_file.read(ctypes.sizeof(ctypes.c_uint32)) + ).value # Handle wrong byte order if magic_number in [FAT_CIGAM, FAT_CIGAM_64, MH_CIGAM, MH_CIGAM_64]: @@ -244,8 +272,7 @@ def get_base_class_and_magic_number(lib_file, seek=None): def read_data(struct_class, lib_file): - return struct_class.from_buffer_copy(lib_file.read( - ctypes.sizeof(struct_class))) + return struct_class.from_buffer_copy(lib_file.read(ctypes.sizeof(struct_class))) def extract_macosx_min_system_version(path_to_lib): @@ -255,6 +282,7 @@ def extract_macosx_min_system_version(path_to_lib): return if magic_number in [FAT_MAGIC, FAT_CIGAM_64]: + class FatHeader(BaseClass): _fields_ = fat_header_fields @@ -263,12 +291,15 @@ class FatHeader(BaseClass): class FatArch(BaseClass): _fields_ = fat_arch_fields + else: class FatArch(BaseClass): _fields_ = fat_arch_64_fields - fat_arch_list = [read_data(FatArch, lib_file) for _ in range(fat_header.nfat_arch)] + fat_arch_list = [ + read_data(FatArch, lib_file) for _ in range(fat_header.nfat_arch) + ] versions_list = [] for el in fat_arch_list: @@ -333,12 +364,14 @@ class MachHeader(base_class): segment_base = read_data(SegmentBase, lib_file) lib_file.seek(pos) if segment_base.cmd == LC_VERSION_MIN_MACOSX: + class VersionMinCommand(base_class): _fields_ = version_min_command_fields version_info = read_data(VersionMinCommand, lib_file) return parse_version(version_info.version) elif segment_base.cmd == LC_BUILD_VERSION: + class VersionBuild(base_class): _fields_ = build_version_command_fields @@ -350,9 +383,9 @@ class VersionBuild(base_class): def parse_version(version): - x = (version & 0xffff0000) >> 16 - y = (version & 0x0000ff00) >> 8 - z = (version & 0x000000ff) + x = (version & 0xFFFF0000) >> 16 + y = (version & 0x0000FF00) >> 8 + z = version & 0x000000FF return x, y, z @@ -362,34 +395,37 @@ def calculate_macosx_platform_tag(archive_root, platform_tag): Example platform tag `macosx-10.14-x86_64` """ - prefix, base_version, suffix = platform_tag.split('-') - base_version = tuple([int(x) for x in base_version.split(".")]) + prefix, base_version, suffix = platform_tag.split("-") + base_version = tuple(int(x) for x in base_version.split(".")) base_version = base_version[:2] if base_version[0] > 10: base_version = (base_version[0], 0) assert len(base_version) == 2 if "MACOSX_DEPLOYMENT_TARGET" in os.environ: - deploy_target = tuple([int(x) for x in os.environ[ - "MACOSX_DEPLOYMENT_TARGET"].split(".")]) + deploy_target = tuple( + int(x) for x in os.environ["MACOSX_DEPLOYMENT_TARGET"].split(".") + ) deploy_target = deploy_target[:2] if deploy_target[0] > 10: deploy_target = (deploy_target[0], 0) if deploy_target < base_version: sys.stderr.write( - "[WARNING] MACOSX_DEPLOYMENT_TARGET is set to a lower value ({}) than the " - "version on which the Python interpreter was compiled ({}), and will be " - "ignored.\n".format('.'.join(str(x) for x in deploy_target), - '.'.join(str(x) for x in base_version)) + "[WARNING] MACOSX_DEPLOYMENT_TARGET is set to a lower value ({}) than " + "the version on which the Python interpreter was compiled ({}), and " + "will be ignored.\n".format( + ".".join(str(x) for x in deploy_target), + ".".join(str(x) for x in base_version), ) + ) else: base_version = deploy_target assert len(base_version) == 2 start_version = base_version versions_dict = {} - for (dirpath, dirnames, filenames) in os.walk(archive_root): + for (dirpath, _dirnames, filenames) in os.walk(archive_root): for filename in filenames: - if filename.endswith('.dylib') or filename.endswith('.so'): + if filename.endswith(".dylib") or filename.endswith(".so"): lib_path = os.path.join(dirpath, filename) min_ver = extract_macosx_min_system_version(lib_path) if min_ver is not None: @@ -410,17 +446,24 @@ def calculate_macosx_platform_tag(archive_root, platform_tag): files_form = "this file" else: files_form = "these files" - error_message = \ - "[WARNING] This wheel needs a higher macOS version than {} " \ - "To silence this warning, set MACOSX_DEPLOYMENT_TARGET to at least " +\ - fin_base_version + " or recreate " + files_form + " with lower " \ + error_message = ( + "[WARNING] This wheel needs a higher macOS version than {} " + "To silence this warning, set MACOSX_DEPLOYMENT_TARGET to at least " + + fin_base_version + + " or recreate " + + files_form + + " with lower " "MACOSX_DEPLOYMENT_TARGET: \n" + problematic_files + ) if "MACOSX_DEPLOYMENT_TARGET" in os.environ: - error_message = error_message.format("is set in MACOSX_DEPLOYMENT_TARGET variable.") + error_message = error_message.format( + "is set in MACOSX_DEPLOYMENT_TARGET variable." + ) else: error_message = error_message.format( - "the version your Python interpreter is compiled against.") + "the version your Python interpreter is compiled against." + ) sys.stderr.write(error_message) diff --git a/src/wheel/metadata.py b/src/wheel/metadata.py index 37efa743..159ff0aa 100644 --- a/src/wheel/metadata.py +++ b/src/wheel/metadata.py @@ -1,133 +1,109 @@ """ Tools for converting old- to new-style metadata. """ +from __future__ import annotations import os.path import textwrap +from email.message import Message +from email.parser import Parser +from typing import Iterator -import pkg_resources +from pkg_resources import Requirement, safe_extra, split_sections -from .pkginfo import read_pkg_info - -def requires_to_requires_dist(requirement): +def requires_to_requires_dist(requirement: Requirement) -> str: """Return the version specifier for a requirement in PEP 345/566 fashion.""" - if getattr(requirement, 'url', None): + if getattr(requirement, "url", None): return " @ " + requirement.url requires_dist = [] for op, ver in requirement.specs: requires_dist.append(op + ver) - if not requires_dist: - return '' - return " (%s)" % ','.join(sorted(requires_dist)) + + if requires_dist: + return " (" + ",".join(sorted(requires_dist)) + ")" + else: + return "" -def convert_requirements(requirements): +def convert_requirements(requirements: list[str]) -> Iterator[str]: """Yield Requires-Dist: strings for parsed requirements strings.""" for req in requirements: - parsed_requirement = pkg_resources.Requirement.parse(req) + parsed_requirement = Requirement.parse(req) spec = requires_to_requires_dist(parsed_requirement) extras = ",".join(sorted(parsed_requirement.extras)) if extras: - extras = "[%s]" % extras - yield (parsed_requirement.project_name + extras + spec) + extras = f"[{extras}]" + + yield parsed_requirement.project_name + extras + spec -def generate_requirements(extras_require): +def generate_requirements( + extras_require: dict[str, list[str]] +) -> Iterator[tuple[str, str]]: """ - Convert requirements from a setup()-style dictionary to ('Requires-Dist', 'requirement') - and ('Provides-Extra', 'extra') tuples. + Convert requirements from a setup()-style dictionary to + ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples. extras_require is a dictionary of {extra: [requirements]} as passed to setup(), using the empty extra {'': [requirements]} to hold install_requires. """ for extra, depends in extras_require.items(): - condition = '' - extra = extra or '' - if ':' in extra: # setuptools extra:condition syntax - extra, condition = extra.split(':', 1) + condition = "" + extra = extra or "" + if ":" in extra: # setuptools extra:condition syntax + extra, condition = extra.split(":", 1) - extra = pkg_resources.safe_extra(extra) + extra = safe_extra(extra) if extra: - yield 'Provides-Extra', extra + yield "Provides-Extra", extra if condition: condition = "(" + condition + ") and " condition += "extra == '%s'" % extra if condition: - condition = ' ; ' + condition + condition = " ; " + condition for new_req in convert_requirements(depends): - yield 'Requires-Dist', new_req + condition + yield "Requires-Dist", new_req + condition -def pkginfo_to_metadata(egg_info_path, pkginfo_path): +def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message: """ Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format """ - pkg_info = read_pkg_info(pkginfo_path) - pkg_info.replace_header('Metadata-Version', '2.1') + with open(pkginfo_path, encoding="utf-8") as headers: + pkg_info = Parser().parse(headers) + + pkg_info.replace_header("Metadata-Version", "2.1") # Those will be regenerated from `requires.txt`. - del pkg_info['Provides-Extra'] - del pkg_info['Requires-Dist'] - requires_path = os.path.join(egg_info_path, 'requires.txt') + del pkg_info["Provides-Extra"] + del pkg_info["Requires-Dist"] + requires_path = os.path.join(egg_info_path, "requires.txt") if os.path.exists(requires_path): with open(requires_path) as requires_file: requires = requires_file.read() - parsed_requirements = sorted(pkg_resources.split_sections(requires), - key=lambda x: x[0] or '') + parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "") for extra, reqs in parsed_requirements: for key, value in generate_requirements({extra: reqs}): if (key, value) not in pkg_info.items(): pkg_info[key] = value - description = pkg_info['Description'] + description = pkg_info["Description"] if description: - pkg_info.set_payload(dedent_description(pkg_info)) - del pkg_info['Description'] + description_lines = pkg_info["Description"].splitlines() + dedented_description = "\n".join( + # if the first line of long_description is blank, + # the first line here will be indented. + ( + description_lines[0].lstrip(), + textwrap.dedent("\n".join(description_lines[1:])), + "\n", + ) + ) + pkg_info.set_payload(dedented_description) + del pkg_info["Description"] return pkg_info - - -def pkginfo_unicode(pkg_info, field): - """Hack to coax Unicode out of an email Message() - Python 3.3+""" - text = pkg_info[field] - field = field.lower() - if not isinstance(text, str): - for item in pkg_info.raw_items(): - if item[0].lower() == field: - text = item[1].encode('ascii', 'surrogateescape') \ - .decode('utf-8') - break - - return text - - -def dedent_description(pkg_info): - """ - Dedent and convert pkg_info['Description'] to Unicode. - """ - description = pkg_info['Description'] - - # Python 3 Unicode handling, sorta. - surrogates = False - if not isinstance(description, str): - surrogates = True - description = pkginfo_unicode(pkg_info, 'Description') - - description_lines = description.splitlines() - description_dedent = '\n'.join( - # if the first line of long_description is blank, - # the first line here will be indented. - (description_lines[0].lstrip(), - textwrap.dedent('\n'.join(description_lines[1:])), - '\n')) - - if surrogates: - description_dedent = description_dedent \ - .encode("utf8") \ - .decode("ascii", "surrogateescape") - - return description_dedent diff --git a/src/wheel/pkginfo.py b/src/wheel/pkginfo.py deleted file mode 100644 index 115be45b..00000000 --- a/src/wheel/pkginfo.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Tools for reading and writing PKG-INFO / METADATA without caring -about the encoding.""" - -from email.parser import Parser - -try: - unicode - _PY3 = False -except NameError: - _PY3 = True - -if not _PY3: - from email.generator import Generator - - def read_pkg_info_bytes(bytestr): - return Parser().parsestr(bytestr) - - def read_pkg_info(path): - with open(path, "r") as headers: - message = Parser().parse(headers) - return message - - def write_pkg_info(path, message): - with open(path, 'w') as metadata: - Generator(metadata, mangle_from_=False, maxheaderlen=0).flatten(message) -else: - from email.generator import BytesGenerator - - def read_pkg_info_bytes(bytestr): - headers = bytestr.decode(encoding="ascii", errors="surrogateescape") - message = Parser().parsestr(headers) - return message - - def read_pkg_info(path): - with open(path, "r", - encoding="ascii", - errors="surrogateescape") as headers: - message = Parser().parse(headers) - return message - - def write_pkg_info(path, message): - with open(path, "wb") as out: - BytesGenerator(out, mangle_from_=False, maxheaderlen=0).flatten(message) diff --git a/src/wheel/util.py b/src/wheel/util.py index 3ae2b445..d98d98cb 100644 --- a/src/wheel/util.py +++ b/src/wheel/util.py @@ -1,46 +1,26 @@ -import base64 -import io -import sys - +from __future__ import annotations -if sys.version_info[0] < 3: - text_type = unicode # noqa: F821 - - StringIO = io.BytesIO +import base64 +import logging - def native(s, encoding='utf-8'): - if isinstance(s, unicode): # noqa: F821 - return s.encode(encoding) - return s -else: - text_type = str +log = logging.getLogger("wheel") - StringIO = io.StringIO +# ensure Python logging is configured +try: + __import__("setuptools.logging") +except ImportError: + # setuptools < ?? + from . import _setuptools_logging - def native(s, encoding='utf-8'): - if isinstance(s, bytes): - return s.decode(encoding) - return s + _setuptools_logging.configure() -def urlsafe_b64encode(data): +def urlsafe_b64encode(data: bytes) -> bytes: """urlsafe_b64encode without padding""" - return base64.urlsafe_b64encode(data).rstrip(b'=') + return base64.urlsafe_b64encode(data).rstrip(b"=") -def urlsafe_b64decode(data): +def urlsafe_b64decode(data: bytes) -> bytes: """urlsafe_b64decode without padding""" - pad = b'=' * (4 - (len(data) & 3)) + pad = b"=" * (4 - (len(data) & 3)) return base64.urlsafe_b64decode(data + pad) - - -def as_unicode(s): - if isinstance(s, bytes): - return s.decode('utf-8') - return s - - -def as_bytes(s): - if isinstance(s, text_type): - return s.encode('utf-8') - return s diff --git a/src/wheel/vendored/packaging/_manylinux.py b/src/wheel/vendored/packaging/_manylinux.py new file mode 100644 index 00000000..4934ba8b --- /dev/null +++ b/src/wheel/vendored/packaging/_manylinux.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +import collections +import functools +import os +import re +import struct +import sys +import warnings +from typing import IO, Iterator, NamedTuple + + +# Python does not provide platform information at sufficient granularity to +# identify the architecture of the running executable in some cases, so we +# determine it dynamically by reading the information from the running +# process. This only applies on Linux, which uses the ELF format. +class _ELFFileHeader: + # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + class _InvalidELFFileHeader(ValueError): + """ + An invalid ELF file header was found. + """ + + ELF_MAGIC_NUMBER = 0x7F454C46 + ELFCLASS32 = 1 + ELFCLASS64 = 2 + ELFDATA2LSB = 1 + ELFDATA2MSB = 2 + EM_386 = 3 + EM_S390 = 22 + EM_ARM = 40 + EM_X86_64 = 62 + EF_ARM_ABIMASK = 0xFF000000 + EF_ARM_ABI_VER5 = 0x05000000 + EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + def __init__(self, file: IO[bytes]) -> None: + def unpack(fmt: str) -> int: + try: + data = file.read(struct.calcsize(fmt)) + result: tuple[int, ...] = struct.unpack(fmt, data) + except struct.error: + raise _ELFFileHeader._InvalidELFFileHeader() + return result[0] + + self.e_ident_magic = unpack(">I") + if self.e_ident_magic != self.ELF_MAGIC_NUMBER: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_class = unpack("B") + if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_data = unpack("B") + if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_version = unpack("B") + self.e_ident_osabi = unpack("B") + self.e_ident_abiversion = unpack("B") + self.e_ident_pad = file.read(7) + format_h = "H" + format_i = "I" + format_q = "Q" + format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q + self.e_type = unpack(format_h) + self.e_machine = unpack(format_h) + self.e_version = unpack(format_i) + self.e_entry = unpack(format_p) + self.e_phoff = unpack(format_p) + self.e_shoff = unpack(format_p) + self.e_flags = unpack(format_i) + self.e_ehsize = unpack(format_h) + self.e_phentsize = unpack(format_h) + self.e_phnum = unpack(format_h) + self.e_shentsize = unpack(format_h) + self.e_shnum = unpack(format_h) + self.e_shstrndx = unpack(format_h) + + +def _get_elf_header() -> _ELFFileHeader | None: + try: + with open(sys.executable, "rb") as f: + elf_header = _ELFFileHeader(f) + except (OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): + return None + return elf_header + + +def _is_linux_armhf() -> bool: + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_ARM + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABIMASK + ) == elf_header.EF_ARM_ABI_VER5 + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD + ) == elf_header.EF_ARM_ABI_FLOAT_HARD + return result + + +def _is_linux_i686() -> bool: + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_386 + return result + + +def _have_compatible_abi(arch: str) -> bool: + if arch == "armv7l": + return _is_linux_armhf() + if arch == "i686": + return _is_linux_i686() + return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"} + + +# If glibc ever changes its major version, we need to know what the last +# minor version was, so we can build the complete list of all versions. +# For now, guess what the highest minor version might be, assume it will +# be 50 for testing. Once this actually happens, update the dictionary +# with the actual value. +_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50) + + +class _GLibCVersion(NamedTuple): + major: int + minor: int + + +def _glibc_version_string_confstr() -> str | None: + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 + try: + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". + version_string = os.confstr("CS_GNU_LIBC_VERSION") + assert version_string is not None + _, version = version_string.split() + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes() -> str | None: + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + # + # We must also handle the special case where the executable is not a + # dynamically linked executable. This can occur when using musl libc, + # for example. In this situation, dlopen() will error, leading to an + # OSError. Interestingly, at least in the case of musl, there is no + # errno set on the OSError. The single string argument used to construct + # OSError comes from libc itself and is therefore not portable to + # hard code here. In any case, failure to call dlopen() means we + # can proceed, so we bail on our attempt. + try: + process_namespace = ctypes.CDLL(None) + except OSError: + return None + + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str: str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +def _glibc_version_string() -> str | None: + """Returns glibc version string, or None if not using glibc.""" + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _parse_glibc_version(version_str: str) -> tuple[int, int]: + """Parse glibc version. + + We use a regexp instead of str.split because we want to discard any + random junk that might come after the minor version -- this might happen + in patched/forked versions of glibc (e.g. Linaro's version of glibc + uses version strings like "2.20-2014.11"). See gh-3588. + """ + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return -1, -1 + return int(m.group("major")), int(m.group("minor")) + + +@functools.lru_cache() +def _get_glibc_version() -> tuple[int, int]: + version_str = _glibc_version_string() + if version_str is None: + return (-1, -1) + return _parse_glibc_version(version_str) + + +# From PEP 513, PEP 600 +def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool: + sys_glibc = _get_glibc_version() + if sys_glibc < version: + return False + # Check for presence of _manylinux module. + try: + import _manylinux # noqa + except ImportError: + return True + if hasattr(_manylinux, "manylinux_compatible"): + result = _manylinux.manylinux_compatible(version[0], version[1], arch) + if result is not None: + return bool(result) + return True + if version == _GLibCVersion(2, 5): + if hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if version == _GLibCVersion(2, 12): + if hasattr(_manylinux, "manylinux2010_compatible"): + return bool(_manylinux.manylinux2010_compatible) + if version == _GLibCVersion(2, 17): + if hasattr(_manylinux, "manylinux2014_compatible"): + return bool(_manylinux.manylinux2014_compatible) + return True + + +_LEGACY_MANYLINUX_MAP = { + # CentOS 7 w/ glibc 2.17 (PEP 599) + (2, 17): "manylinux2014", + # CentOS 6 w/ glibc 2.12 (PEP 571) + (2, 12): "manylinux2010", + # CentOS 5 w/ glibc 2.5 (PEP 513) + (2, 5): "manylinux1", +} + + +def platform_tags(linux: str, arch: str) -> Iterator[str]: + if not _have_compatible_abi(arch): + return + # Oldest glibc to be supported regardless of architecture is (2, 17). + too_old_glibc2 = _GLibCVersion(2, 16) + if arch in {"x86_64", "i686"}: + # On x86/i686 also oldest glibc to be supported is (2, 5). + too_old_glibc2 = _GLibCVersion(2, 4) + current_glibc = _GLibCVersion(*_get_glibc_version()) + glibc_max_list = [current_glibc] + # We can assume compatibility across glibc major versions. + # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 + # + # Build a list of maximum glibc versions so that we can + # output the canonical list of all glibc from current_glibc + # down to too_old_glibc2, including all intermediary versions. + for glibc_major in range(current_glibc.major - 1, 1, -1): + glibc_minor = _LAST_GLIBC_MINOR[glibc_major] + glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_compatible(tag, arch, glibc_version): + yield linux.replace("linux", tag) + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_compatible(legacy_tag, arch, glibc_version): + yield linux.replace("linux", legacy_tag) diff --git a/src/wheel/vendored/packaging/_musllinux.py b/src/wheel/vendored/packaging/_musllinux.py new file mode 100644 index 00000000..7946c9b2 --- /dev/null +++ b/src/wheel/vendored/packaging/_musllinux.py @@ -0,0 +1,138 @@ +"""PEP 656 support. + +This module implements logic to detect if the currently running Python is +linked against musl, and what musl version is used. +""" + +from __future__ import annotations + +import contextlib +import functools +import operator +import os +import re +import struct +import subprocess +import sys +from typing import IO, Iterator, NamedTuple + + +def _read_unpacked(f: IO[bytes], fmt: str) -> tuple[int, ...]: + return struct.unpack(fmt, f.read(struct.calcsize(fmt))) + + +def _parse_ld_musl_from_elf(f: IO[bytes]) -> str | None: + """Detect musl libc location by parsing the Python executable. + + Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca + ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + f.seek(0) + try: + ident = _read_unpacked(f, "16B") + except struct.error: + return None + if ident[:4] != tuple(b"\x7fELF"): # Invalid magic, not ELF. + return None + f.seek(struct.calcsize("HHI"), 1) # Skip file type, machine, and version. + + try: + # e_fmt: Format for program header. + # p_fmt: Format for section header. + # p_idx: Indexes to find p_type, p_offset, and p_filesz. + e_fmt, p_fmt, p_idx = { + 1: ("IIIIHHH", "IIIIIIII", (0, 1, 4)), # 32-bit. + 2: ("QQQIHHH", "IIQQQQQQ", (0, 2, 5)), # 64-bit. + }[ident[4]] + except KeyError: + return None + else: + p_get = operator.itemgetter(*p_idx) + + # Find the interpreter section and return its content. + try: + _, e_phoff, _, _, _, e_phentsize, e_phnum = _read_unpacked(f, e_fmt) + except struct.error: + return None + for i in range(e_phnum + 1): + f.seek(e_phoff + e_phentsize * i) + try: + p_type, p_offset, p_filesz = p_get(_read_unpacked(f, p_fmt)) + except struct.error: + return None + if p_type != 3: # Not PT_INTERP. + continue + f.seek(p_offset) + interpreter = os.fsdecode(f.read(p_filesz)).strip("\0") + if "musl" not in interpreter: + return None + return interpreter + return None + + +class _MuslVersion(NamedTuple): + major: int + minor: int + + +def _parse_musl_version(output: str) -> _MuslVersion | None: + lines = [n for n in (n.strip() for n in output.splitlines()) if n] + if len(lines) < 2 or lines[0][:4] != "musl": + return None + m = re.match(r"Version (\d+)\.(\d+)", lines[1]) + if not m: + return None + return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) + + +@functools.lru_cache() +def _get_musl_version(executable: str) -> _MuslVersion | None: + """Detect currently-running musl runtime version. + + This is done by checking the specified executable's dynamic linking + information, and invoking the loader to parse its output for a version + string. If the loader is musl, the output would be something like:: + + musl libc (x86_64) + Version 1.2.2 + Dynamic Program Loader + """ + with contextlib.ExitStack() as stack: + try: + f = stack.enter_context(open(executable, "rb")) + except OSError: + return None + ld = _parse_ld_musl_from_elf(f) + if not ld: + return None + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) + return _parse_musl_version(proc.stderr) + + +def platform_tags(arch: str) -> Iterator[str]: + """Generate musllinux tags compatible to the current platform. + + :param arch: Should be the part of platform tag after the ``linux_`` + prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a + prerequisite for the current platform to be musllinux-compatible. + + :returns: An iterator of compatible musllinux tags. + """ + sys_musl = _get_musl_version(sys.executable) + if sys_musl is None: # Python not dynamically linked against musl. + return + for minor in range(sys_musl.minor, -1, -1): + yield f"musllinux_{sys_musl.major}_{minor}_{arch}" + + +if __name__ == "__main__": # pragma: no cover + import sysconfig + + plat = sysconfig.get_platform() + assert plat.startswith("linux-"), "not linux" + + print("plat:", plat) + print("musl:", _get_musl_version(sys.executable)) + print("tags:", end=" ") + for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): + print(t, end="\n ") diff --git a/src/wheel/vendored/packaging/_typing.py b/src/wheel/vendored/packaging/_typing.py deleted file mode 100644 index 77a8b918..00000000 --- a/src/wheel/vendored/packaging/_typing.py +++ /dev/null @@ -1,48 +0,0 @@ -"""For neatly implementing static typing in packaging. - -`mypy` - the static type analysis tool we use - uses the `typing` module, which -provides core functionality fundamental to mypy's functioning. - -Generally, `typing` would be imported at runtime and used in that fashion - -it acts as a no-op at runtime and does not have any run-time overhead by -design. - -As it turns out, `typing` is not vendorable - it uses separate sources for -Python 2/Python 3. Thus, this codebase can not expect it to be present. -To work around this, mypy allows the typing import to be behind a False-y -optional to prevent it from running at runtime and type-comments can be used -to remove the need for the types to be accessible directly during runtime. - -This module provides the False-y guard in a nicely named fashion so that a -curious maintainer can reach here to read this. - -In packaging, all static-typing related imports should be guarded as follows: - - from packaging._typing import TYPE_CHECKING - - if TYPE_CHECKING: - from typing import ... - -Ref: https://github.com/python/mypy/issues/3216 -""" - -__all__ = ["TYPE_CHECKING", "cast"] - -# The TYPE_CHECKING constant defined by the typing module is False at runtime -# but True while type checking. -if False: # pragma: no cover - from typing import TYPE_CHECKING -else: - TYPE_CHECKING = False - -# typing's cast syntax requires calling typing.cast at runtime, but we don't -# want to import typing at runtime. Here, we inform the type checkers that -# we're importing `typing.cast` as `cast` and re-implement typing.cast's -# runtime behavior in a block that is ignored by type checkers. -if TYPE_CHECKING: # pragma: no cover - # not executed at runtime - from typing import cast -else: - # executed at runtime - def cast(type_, value): # noqa - return value diff --git a/src/wheel/vendored/packaging/tags.py b/src/wheel/vendored/packaging/tags.py index c2a140c2..4e003a95 100644 --- a/src/wheel/vendored/packaging/tags.py +++ b/src/wheel/vendored/packaging/tags.py @@ -2,81 +2,35 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import +from __future__ import annotations -import distutils.util - -try: - from importlib.machinery import EXTENSION_SUFFIXES -except ImportError: # pragma: no cover - import imp - - EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] - del imp -import collections import logging -import os import platform -import re -import struct import sys import sysconfig -import warnings - -from ._typing import TYPE_CHECKING, cast - -if TYPE_CHECKING: # pragma: no cover - from typing import ( - Dict, - FrozenSet, - IO, - Iterable, - Iterator, - List, - Optional, - Sequence, - Tuple, - Union, - ) - - PythonVersion = Sequence[int] - MacVersion = Tuple[int, int] - GlibcVersion = Tuple[int, int] +from importlib.machinery import EXTENSION_SUFFIXES +from typing import Iterable, Iterator, Sequence, Tuple, cast +from . import _manylinux, _musllinux logger = logging.getLogger(__name__) -INTERPRETER_SHORT_NAMES = { +PythonVersion = Sequence[int] +MacVersion = Tuple[int, int] + +INTERPRETER_SHORT_NAMES: dict[str, str] = { "python": "py", # Generic. "cpython": "cp", "pypy": "pp", "ironpython": "ip", "jython": "jy", -} # type: Dict[str, str] +} _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 -_LEGACY_MANYLINUX_MAP = { - # CentOS 7 w/ glibc 2.17 (PEP 599) - (2, 17): "manylinux2014", - # CentOS 6 w/ glibc 2.12 (PEP 571) - (2, 12): "manylinux2010", - # CentOS 5 w/ glibc 2.5 (PEP 513) - (2, 5): "manylinux1", -} - -# If glibc ever changes its major version, we need to know what the last -# minor version was, so we can build the complete list of all versions. -# For now, guess what the highest minor version might be, assume it will -# be 50 for testing. Once this actually happens, update the dictionary -# with the actual value. -_LAST_GLIBC_MINOR = collections.defaultdict(lambda: 50) # type: Dict[int, int] -glibcVersion = collections.namedtuple("Version", ["major", "minor"]) - - -class Tag(object): +class Tag: """ A representation of the tag triple for a wheel. @@ -86,8 +40,7 @@ class Tag(object): __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] - def __init__(self, interpreter, abi, platform): - # type: (str, str, str) -> None + def __init__(self, interpreter: str, abi: str, platform: str) -> None: self._interpreter = interpreter.lower() self._abi = abi.lower() self._platform = platform.lower() @@ -99,46 +52,39 @@ def __init__(self, interpreter, abi, platform): self._hash = hash((self._interpreter, self._abi, self._platform)) @property - def interpreter(self): - # type: () -> str + def interpreter(self) -> str: return self._interpreter @property - def abi(self): - # type: () -> str + def abi(self) -> str: return self._abi @property - def platform(self): - # type: () -> str + def platform(self) -> str: return self._platform - def __eq__(self, other): - # type: (object) -> bool + def __eq__(self, other: object) -> bool: if not isinstance(other, Tag): return NotImplemented return ( - (self.platform == other.platform) - and (self.abi == other.abi) - and (self.interpreter == other.interpreter) + (self._hash == other._hash) # Short-circuit ASAP for perf reasons. + and (self._platform == other._platform) + and (self._abi == other._abi) + and (self._interpreter == other._interpreter) ) - def __hash__(self): - # type: () -> int + def __hash__(self) -> int: return self._hash - def __str__(self): - # type: () -> str - return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + def __str__(self) -> str: + return f"{self._interpreter}-{self._abi}-{self._platform}" - def __repr__(self): - # type: () -> str - return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + def __repr__(self) -> str: + return f"<{self} @ {id(self)}>" -def parse_tag(tag): - # type: (str) -> FrozenSet[Tag] +def parse_tag(tag: str) -> frozenset[Tag]: """ Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. @@ -154,24 +100,7 @@ def parse_tag(tag): return frozenset(tags) -def _warn_keyword_parameter(func_name, kwargs): - # type: (str, Dict[str, bool]) -> bool - """ - Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only. - """ - if not kwargs: - return False - elif len(kwargs) > 1 or "warn" not in kwargs: - kwargs.pop("warn", None) - arg = next(iter(kwargs.keys())) - raise TypeError( - "{}() got an unexpected keyword argument {!r}".format(func_name, arg) - ) - return kwargs["warn"] - - -def _get_config_var(name, warn=False): - # type: (str, bool) -> Union[int, str, None] +def _get_config_var(name: str, warn: bool = False) -> int | str | None: value = sysconfig.get_config_var(name) if value is None and warn: logger.debug( @@ -180,13 +109,11 @@ def _get_config_var(name, warn=False): return value -def _normalize_string(string): - # type: (str) -> str +def _normalize_string(string: str) -> str: return string.replace(".", "_").replace("-", "_") -def _abi3_applies(python_version): - # type: (PythonVersion) -> bool +def _abi3_applies(python_version: PythonVersion) -> bool: """ Determine if the Python version supports abi3. @@ -195,8 +122,7 @@ def _abi3_applies(python_version): return len(python_version) > 1 and tuple(python_version) >= (3, 2) -def _cpython_abis(py_version, warn=False): - # type: (PythonVersion, bool) -> List[str] +def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] version = _version_nodot(py_version[:2]) @@ -222,7 +148,7 @@ def _cpython_abis(py_version, warn=False): elif debug: # Debug builds can also load "normal" extension modules. # We can also assume no UCS-4 or pymalloc requirement. - abis.append("cp{version}".format(version=version)) + abis.append(f"cp{version}") abis.insert( 0, "cp{version}{debug}{pymalloc}{ucs4}".format( @@ -233,12 +159,12 @@ def _cpython_abis(py_version, warn=False): def cpython_tags( - python_version=None, # type: Optional[PythonVersion] - abis=None, # type: Optional[Iterable[str]] - platforms=None, # type: Optional[Iterable[str]] - **kwargs # type: bool -): - # type: (...) -> Iterator[Tag] + python_version: PythonVersion | None = None, + abis: Iterable[str] | None = None, + platforms: Iterable[str] | None = None, + *, + warn: bool = False, +) -> Iterator[Tag]: """ Yields the tags for a CPython interpreter. @@ -254,11 +180,10 @@ def cpython_tags( If 'abi3' or 'none' are specified in 'abis' then they will be yielded at their normal position and not at the beginning. """ - warn = _warn_keyword_parameter("cpython_tags", kwargs) if not python_version: python_version = sys.version_info[:2] - interpreter = "cp{}".format(_version_nodot(python_version[:2])) + interpreter = f"cp{_version_nodot(python_version[:2])}" if abis is None: if len(python_version) > 1: @@ -273,15 +198,13 @@ def cpython_tags( except ValueError: pass - platforms = list(platforms or _platform_tags()) + platforms = list(platforms or platform_tags()) for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) if _abi3_applies(python_version): - for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): - yield tag - for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): - yield tag + yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) if _abi3_applies(python_version): for minor_version in range(python_version[1] - 1, 1, -1): @@ -292,20 +215,19 @@ def cpython_tags( yield Tag(interpreter, "abi3", platform_) -def _generic_abi(): - # type: () -> Iterator[str] +def _generic_abi() -> Iterator[str]: abi = sysconfig.get_config_var("SOABI") if abi: yield _normalize_string(abi) def generic_tags( - interpreter=None, # type: Optional[str] - abis=None, # type: Optional[Iterable[str]] - platforms=None, # type: Optional[Iterable[str]] - **kwargs # type: bool -): - # type: (...) -> Iterator[Tag] + interpreter: str | None = None, + abis: Iterable[str] | None = None, + platforms: Iterable[str] | None = None, + *, + warn: bool = False, +) -> Iterator[Tag]: """ Yields the tags for a generic interpreter. @@ -314,14 +236,13 @@ def generic_tags( The "none" ABI will be added if it was not explicitly provided. """ - warn = _warn_keyword_parameter("generic_tags", kwargs) if not interpreter: interp_name = interpreter_name() interp_version = interpreter_version(warn=warn) interpreter = "".join([interp_name, interp_version]) if abis is None: abis = _generic_abi() - platforms = list(platforms or _platform_tags()) + platforms = list(platforms or platform_tags()) abis = list(abis) if "none" not in abis: abis.append("none") @@ -330,8 +251,7 @@ def generic_tags( yield Tag(interpreter, abi, platform_) -def _py_interpreter_range(py_version): - # type: (PythonVersion) -> Iterator[str] +def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: """ Yields Python versions in descending order. @@ -339,19 +259,18 @@ def _py_interpreter_range(py_version): all previous versions of that major version. """ if len(py_version) > 1: - yield "py{version}".format(version=_version_nodot(py_version[:2])) - yield "py{major}".format(major=py_version[0]) + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" if len(py_version) > 1: for minor in range(py_version[1] - 1, -1, -1): - yield "py{version}".format(version=_version_nodot((py_version[0], minor))) + yield f"py{_version_nodot((py_version[0], minor))}" def compatible_tags( - python_version=None, # type: Optional[PythonVersion] - interpreter=None, # type: Optional[str] - platforms=None, # type: Optional[Iterable[str]] -): - # type: (...) -> Iterator[Tag] + python_version: PythonVersion | None = None, + interpreter: str | None = None, + platforms: Iterable[str] | None = None, +) -> Iterator[Tag]: """ Yields the sequence of tags that are compatible with a specific version of Python. @@ -362,7 +281,7 @@ def compatible_tags( """ if not python_version: python_version = sys.version_info[:2] - platforms = list(platforms or _platform_tags()) + platforms = list(platforms or platform_tags()) for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) @@ -372,8 +291,7 @@ def compatible_tags( yield Tag(version, "none", "any") -def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): - # type: (str, bool) -> str +def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: if not is_32bit: return arch @@ -383,8 +301,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): return "i386" -def _mac_binary_formats(version, cpu_arch): - # type: (MacVersion, str) -> List[str] +def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]: formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -416,8 +333,9 @@ def _mac_binary_formats(version, cpu_arch): return formats -def mac_platforms(version=None, arch=None): - # type: (Optional[MacVersion], Optional[str]) -> Iterator[str] +def mac_platforms( + version: MacVersion | None = None, arch: str | None = None +) -> Iterator[str]: """ Yields the platform tags for a macOS system. @@ -426,7 +344,7 @@ def mac_platforms(version=None, arch=None): generate platform tags for. Both parameters default to the appropriate value for the current system. """ - version_str, _, cpu_arch = platform.mac_ver() # type: ignore + version_str, _, cpu_arch = platform.mac_ver() if version is None: version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) else: @@ -487,320 +405,24 @@ def mac_platforms(version=None, arch=None): ) -# From PEP 513, PEP 600 -def _is_manylinux_compatible(name, arch, glibc_version): - # type: (str, str, GlibcVersion) -> bool - sys_glibc = _get_glibc_version() - if sys_glibc < glibc_version: - return False - # Check for presence of _manylinux module. - try: - import _manylinux # noqa - except ImportError: - pass - else: - if hasattr(_manylinux, "manylinux_compatible"): - result = _manylinux.manylinux_compatible( - glibc_version[0], glibc_version[1], arch - ) - if result is not None: - return bool(result) - else: - if glibc_version == (2, 5): - if hasattr(_manylinux, "manylinux1_compatible"): - return bool(_manylinux.manylinux1_compatible) - if glibc_version == (2, 12): - if hasattr(_manylinux, "manylinux2010_compatible"): - return bool(_manylinux.manylinux2010_compatible) - if glibc_version == (2, 17): - if hasattr(_manylinux, "manylinux2014_compatible"): - return bool(_manylinux.manylinux2014_compatible) - return True - - -def _glibc_version_string(): - # type: () -> Optional[str] - # Returns glibc version string, or None if not using glibc. - return _glibc_version_string_confstr() or _glibc_version_string_ctypes() - - -def _glibc_version_string_confstr(): - # type: () -> Optional[str] - """ - Primary implementation of glibc_version_string using os.confstr. - """ - # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely - # to be broken or missing. This strategy is used in the standard library - # platform module. - # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 - try: - # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". - version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 - "CS_GNU_LIBC_VERSION" - ) - assert version_string is not None - _, version = version_string.split() # type: Tuple[str, str] - except (AssertionError, AttributeError, OSError, ValueError): - # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... - return None - return version - - -def _glibc_version_string_ctypes(): - # type: () -> Optional[str] - """ - Fallback implementation of glibc_version_string using ctypes. - """ - try: - import ctypes - except ImportError: - return None - - # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen - # manpage says, "If filename is NULL, then the returned handle is for the - # main program". This way we can let the linker do the work to figure out - # which libc our process is actually using. - # - # We must also handle the special case where the executable is not a - # dynamically linked executable. This can occur when using musl libc, - # for example. In this situation, dlopen() will error, leading to an - # OSError. Interestingly, at least in the case of musl, there is no - # errno set on the OSError. The single string argument used to construct - # OSError comes from libc itself and is therefore not portable to - # hard code here. In any case, failure to call dlopen() means we - # can proceed, so we bail on our attempt. - try: - # Note: typeshed is wrong here so we are ignoring this line. - process_namespace = ctypes.CDLL(None) # type: ignore - except OSError: - return None - - try: - gnu_get_libc_version = process_namespace.gnu_get_libc_version - except AttributeError: - # Symbol doesn't exist -> therefore, we are not linked to - # glibc. - return None - - # Call gnu_get_libc_version, which returns a string like "2.5" - gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() # type: str - # py2 / py3 compatibility: - if not isinstance(version_str, str): - version_str = version_str.decode("ascii") - - return version_str - - -def _parse_glibc_version(version_str): - # type: (str) -> Tuple[int, int] - # Parse glibc version. - # - # We use a regexp instead of str.split because we want to discard any - # random junk that might come after the minor version -- this might happen - # in patched/forked versions of glibc (e.g. Linaro's version of glibc - # uses version strings like "2.20-2014.11"). See gh-3588. - m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) - if not m: - warnings.warn( - "Expected glibc version with 2 components major.minor," - " got: %s" % version_str, - RuntimeWarning, - ) - return -1, -1 - return (int(m.group("major")), int(m.group("minor"))) - - -_glibc_version = [] # type: List[Tuple[int, int]] - - -def _get_glibc_version(): - # type: () -> Tuple[int, int] - if _glibc_version: - return _glibc_version[0] - version_str = _glibc_version_string() - if version_str is None: - _glibc_version.append((-1, -1)) - else: - _glibc_version.append(_parse_glibc_version(version_str)) - return _glibc_version[0] - - -# Python does not provide platform information at sufficient granularity to -# identify the architecture of the running executable in some cases, so we -# determine it dynamically by reading the information from the running -# process. This only applies on Linux, which uses the ELF format. -class _ELFFileHeader(object): - # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header - class _InvalidELFFileHeader(ValueError): - """ - An invalid ELF file header was found. - """ - - ELF_MAGIC_NUMBER = 0x7F454C46 - ELFCLASS32 = 1 - ELFCLASS64 = 2 - ELFDATA2LSB = 1 - ELFDATA2MSB = 2 - EM_386 = 3 - EM_S390 = 22 - EM_ARM = 40 - EM_X86_64 = 62 - EF_ARM_ABIMASK = 0xFF000000 - EF_ARM_ABI_VER5 = 0x05000000 - EF_ARM_ABI_FLOAT_HARD = 0x00000400 - - def __init__(self, file): - # type: (IO[bytes]) -> None - def unpack(fmt): - # type: (str) -> int - try: - (result,) = struct.unpack( - fmt, file.read(struct.calcsize(fmt)) - ) # type: (int, ) - except struct.error: - raise _ELFFileHeader._InvalidELFFileHeader() - return result - - self.e_ident_magic = unpack(">I") - if self.e_ident_magic != self.ELF_MAGIC_NUMBER: - raise _ELFFileHeader._InvalidELFFileHeader() - self.e_ident_class = unpack("B") - if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: - raise _ELFFileHeader._InvalidELFFileHeader() - self.e_ident_data = unpack("B") - if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: - raise _ELFFileHeader._InvalidELFFileHeader() - self.e_ident_version = unpack("B") - self.e_ident_osabi = unpack("B") - self.e_ident_abiversion = unpack("B") - self.e_ident_pad = file.read(7) - format_h = "H" - format_i = "I" - format_q = "Q" - format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q - self.e_type = unpack(format_h) - self.e_machine = unpack(format_h) - self.e_version = unpack(format_i) - self.e_entry = unpack(format_p) - self.e_phoff = unpack(format_p) - self.e_shoff = unpack(format_p) - self.e_flags = unpack(format_i) - self.e_ehsize = unpack(format_h) - self.e_phentsize = unpack(format_h) - self.e_phnum = unpack(format_h) - self.e_shentsize = unpack(format_h) - self.e_shnum = unpack(format_h) - self.e_shstrndx = unpack(format_h) - - -def _get_elf_header(): - # type: () -> Optional[_ELFFileHeader] - try: - with open(sys.executable, "rb") as f: - elf_header = _ELFFileHeader(f) - except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): - return None - return elf_header - - -def _is_linux_armhf(): - # type: () -> bool - # hard-float ABI can be detected from the ELF header of the running - # process - # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf - elf_header = _get_elf_header() - if elf_header is None: - return False - result = elf_header.e_ident_class == elf_header.ELFCLASS32 - result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB - result &= elf_header.e_machine == elf_header.EM_ARM - result &= ( - elf_header.e_flags & elf_header.EF_ARM_ABIMASK - ) == elf_header.EF_ARM_ABI_VER5 - result &= ( - elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD - ) == elf_header.EF_ARM_ABI_FLOAT_HARD - return result - - -def _is_linux_i686(): - # type: () -> bool - elf_header = _get_elf_header() - if elf_header is None: - return False - result = elf_header.e_ident_class == elf_header.ELFCLASS32 - result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB - result &= elf_header.e_machine == elf_header.EM_386 - return result - - -def _have_compatible_manylinux_abi(arch): - # type: (str) -> bool - if arch == "armv7l": - return _is_linux_armhf() - if arch == "i686": - return _is_linux_i686() - return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"} - - -def _manylinux_tags(linux, arch): - # type: (str, str) -> Iterator[str] - # Oldest glibc to be supported regardless of architecture is (2, 17). - too_old_glibc2 = glibcVersion(2, 16) - if arch in {"x86_64", "i686"}: - # On x86/i686 also oldest glibc to be supported is (2, 5). - too_old_glibc2 = glibcVersion(2, 4) - current_glibc = glibcVersion(*_get_glibc_version()) - glibc_max_list = [current_glibc] - # We can assume compatibility across glibc major versions. - # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 - # - # Build a list of maximum glibc versions so that we can - # output the canonical list of all glibc from current_glibc - # down to too_old_glibc2, including all intermediary versions. - for glibc_major in range(current_glibc.major - 1, 1, -1): - glibc_max_list.append(glibcVersion(glibc_major, _LAST_GLIBC_MINOR[glibc_major])) - for glibc_max in glibc_max_list: - if glibc_max.major == too_old_glibc2.major: - min_minor = too_old_glibc2.minor - else: - # For other glibc major versions oldest supported is (x, 0). - min_minor = -1 - for glibc_minor in range(glibc_max.minor, min_minor, -1): - glibc_version = (glibc_max.major, glibc_minor) - tag = "manylinux_{}_{}".format(*glibc_version) - if _is_manylinux_compatible(tag, arch, glibc_version): - yield linux.replace("linux", tag) - # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. - if glibc_version in _LEGACY_MANYLINUX_MAP: - legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] - if _is_manylinux_compatible(legacy_tag, arch, glibc_version): - yield linux.replace("linux", legacy_tag) - - -def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): - # type: (bool) -> Iterator[str] - linux = _normalize_string(distutils.util.get_platform()) +def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: + linux = _normalize_string(sysconfig.get_platform()) if is_32bit: if linux == "linux_x86_64": linux = "linux_i686" elif linux == "linux_aarch64": linux = "linux_armv7l" _, arch = linux.split("_", 1) - if _have_compatible_manylinux_abi(arch): - for tag in _manylinux_tags(linux, arch): - yield tag + yield from _manylinux.platform_tags(linux, arch) + yield from _musllinux.platform_tags(arch) yield linux -def _generic_platforms(): - # type: () -> Iterator[str] - yield _normalize_string(distutils.util.get_platform()) +def _generic_platforms() -> Iterator[str]: + yield _normalize_string(sysconfig.get_platform()) -def _platform_tags(): - # type: () -> Iterator[str] +def platform_tags() -> Iterator[str]: """ Provides the platform tags for this installation. """ @@ -812,25 +434,18 @@ def _platform_tags(): return _generic_platforms() -def interpreter_name(): - # type: () -> str +def interpreter_name() -> str: """ Returns the name of the running interpreter. """ - try: - name = sys.implementation.name # type: ignore - except AttributeError: # pragma: no cover - # Python 2.7 compatibility. - name = platform.python_implementation().lower() + name = sys.implementation.name return INTERPRETER_SHORT_NAMES.get(name) or name -def interpreter_version(**kwargs): - # type: (bool) -> str +def interpreter_version(*, warn: bool = False) -> str: """ Returns the version of the running interpreter. """ - warn = _warn_keyword_parameter("interpreter_version", kwargs) version = _get_config_var("py_version_nodot", warn=warn) if version: version = str(version) @@ -839,28 +454,25 @@ def interpreter_version(**kwargs): return version -def _version_nodot(version): - # type: (PythonVersion) -> str +def _version_nodot(version: PythonVersion) -> str: return "".join(map(str, version)) -def sys_tags(**kwargs): - # type: (bool) -> Iterator[Tag] +def sys_tags(*, warn: bool = False) -> Iterator[Tag]: """ Returns the sequence of tag triples for the running interpreter. The order of the sequence corresponds to priority order for the interpreter, from most to least important. """ - warn = _warn_keyword_parameter("sys_tags", kwargs) interp_name = interpreter_name() if interp_name == "cp": - for tag in cpython_tags(warn=warn): - yield tag + yield from cpython_tags(warn=warn) else: - for tag in generic_tags(): - yield tag + yield from generic_tags() - for tag in compatible_tags(): - yield tag + if interp_name == "pp": + yield from compatible_tags(interpreter="pp3") + else: + yield from compatible_tags() diff --git a/src/wheel/vendored/vendor.txt b/src/wheel/vendored/vendor.txt index 57233e13..df56c238 100644 --- a/src/wheel/vendored/vendor.txt +++ b/src/wheel/vendored/vendor.txt @@ -1 +1 @@ -packaging==20.9 +packaging==21.3 diff --git a/src/wheel/wheelfile.py b/src/wheel/wheelfile.py index 21e73619..f55fc737 100644 --- a/src/wheel/wheelfile.py +++ b/src/wheel/wheelfile.py @@ -1,41 +1,33 @@ -from __future__ import print_function +from __future__ import annotations import csv import hashlib import os.path import re import stat -import sys import time from collections import OrderedDict -from distutils import log as logger -from zipfile import ZIP_DEFLATED, ZipInfo, ZipFile +from io import StringIO, TextIOWrapper +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo from wheel.cli import WheelError -from wheel.util import urlsafe_b64decode, as_unicode, native, urlsafe_b64encode, as_bytes, StringIO - -if sys.version_info >= (3,): - from io import TextIOWrapper - - def read_csv(fp): - return csv.reader(TextIOWrapper(fp, newline='', encoding='utf-8')) -else: - def read_csv(fp): - for line in csv.reader(fp): - yield [column.decode('utf-8') for column in line] +from wheel.util import log, urlsafe_b64decode, urlsafe_b64encode # Non-greedy matching of an optional build number may be too clever (more # invalid wheel filenames will match). Separate regex for .dist-info? WHEEL_INFO_RE = re.compile( - r"""^(?P(?P.+?)-(?P.+?))(-(?P\d[^-]*))? - -(?P.+?)-(?P.+?)-(?P.+?)\.whl$""", - re.VERBOSE) + r"""^(?P(?P[^-]+?)-(?P[^-]+?))(-(?P\d[^-]*))? + -(?P[^-]+?)-(?P[^-]+?)-(?P[^.]+?)\.whl$""", + re.VERBOSE, +) +MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC def get_zipinfo_datetime(timestamp=None): - # Some applications need reproducible .whl files, but they can't do this without forcing - # the timestamp of the individual ZipInfo objects. See issue #143. - timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', timestamp or time.time())) + # Some applications need reproducible .whl files, but they can't do this without + # forcing the timestamp of the individual ZipInfo objects. See issue #143. + timestamp = int(os.environ.get("SOURCE_DATE_EPOCH", timestamp or time.time())) + timestamp = max(timestamp, MINIMUM_TIMESTAMP) return time.gmtime(timestamp)[0:6] @@ -46,79 +38,88 @@ class WheelFile(ZipFile): _default_algorithm = hashlib.sha256 - def __init__(self, file, mode='r', compression=ZIP_DEFLATED): + def __init__(self, file, mode="r", compression=ZIP_DEFLATED): basename = os.path.basename(file) self.parsed_filename = WHEEL_INFO_RE.match(basename) - if not basename.endswith('.whl') or self.parsed_filename is None: - raise WheelError("Bad wheel filename {!r}".format(basename)) + if not basename.endswith(".whl") or self.parsed_filename is None: + raise WheelError(f"Bad wheel filename {basename!r}") ZipFile.__init__(self, file, mode, compression=compression, allowZip64=True) - self.dist_info_path = '{}.dist-info'.format(self.parsed_filename.group('namever')) - self.record_path = self.dist_info_path + '/RECORD' + self.dist_info_path = "{}.dist-info".format( + self.parsed_filename.group("namever") + ) + self.record_path = self.dist_info_path + "/RECORD" self._file_hashes = OrderedDict() self._file_sizes = {} - if mode == 'r': + if mode == "r": # Ignore RECORD and any embedded wheel signatures self._file_hashes[self.record_path] = None, None - self._file_hashes[self.record_path + '.jws'] = None, None - self._file_hashes[self.record_path + '.p7s'] = None, None + self._file_hashes[self.record_path + ".jws"] = None, None + self._file_hashes[self.record_path + ".p7s"] = None, None # Fill in the expected hashes by reading them from RECORD try: record = self.open(self.record_path) except KeyError: - raise WheelError('Missing {} file'.format(self.record_path)) + raise WheelError(f"Missing {self.record_path} file") with record: - for line in read_csv(record): + for line in csv.reader( + TextIOWrapper(record, newline="", encoding="utf-8") + ): path, hash_sum, size = line if not hash_sum: continue - algorithm, hash_sum = hash_sum.split(u'=') + algorithm, hash_sum = hash_sum.split("=") try: hashlib.new(algorithm) except ValueError: - raise WheelError('Unsupported hash algorithm: {}'.format(algorithm)) + raise WheelError(f"Unsupported hash algorithm: {algorithm}") - if algorithm.lower() in {'md5', 'sha1'}: + if algorithm.lower() in {"md5", "sha1"}: raise WheelError( - 'Weak hash algorithm ({}) is not permitted by PEP 427' - .format(algorithm)) + "Weak hash algorithm ({}) is not permitted by PEP " + "427".format(algorithm) + ) self._file_hashes[path] = ( - algorithm, urlsafe_b64decode(hash_sum.encode('ascii'))) + algorithm, + urlsafe_b64decode(hash_sum.encode("ascii")), + ) def open(self, name_or_info, mode="r", pwd=None): - def _update_crc(newdata, eof=None): - if eof is None: - eof = ef._eof - update_crc_orig(newdata) - else: # Python 2 - update_crc_orig(newdata, eof) - + def _update_crc(newdata): + eof = ef._eof + update_crc_orig(newdata) running_hash.update(newdata) if eof and running_hash.digest() != expected_hash: - raise WheelError("Hash mismatch for file '{}'".format(native(ef_name))) - - ef_name = as_unicode(name_or_info.filename if isinstance(name_or_info, ZipInfo) - else name_or_info) - if mode == 'r' and not ef_name.endswith('/') and ef_name not in self._file_hashes: - raise WheelError("No hash found for file '{}'".format(native(ef_name))) + raise WheelError(f"Hash mismatch for file '{ef_name}'") + + ef_name = ( + name_or_info.filename if isinstance(name_or_info, ZipInfo) else name_or_info + ) + if ( + mode == "r" + and not ef_name.endswith("/") + and ef_name not in self._file_hashes + ): + raise WheelError(f"No hash found for file '{ef_name}'") ef = ZipFile.open(self, name_or_info, mode, pwd) - if mode == 'r' and not ef_name.endswith('/'): + if mode == "r" and not ef_name.endswith("/"): algorithm, expected_hash = self._file_hashes[ef_name] if expected_hash is not None: - # Monkey patch the _update_crc method to also check for the hash from RECORD + # Monkey patch the _update_crc method to also check for the hash from + # RECORD running_hash = hashlib.new(algorithm) update_crc_orig, ef._update_crc = ef._update_crc, _update_crc return ef def write_files(self, base_dir): - logger.info("creating '%s' and adding '%s' to it", self.filename, base_dir) + log.info(f"creating '{self.filename}' and adding '{base_dir}' to it") deferred = [] for root, dirnames, filenames in os.walk(base_dir): # Sort the directory names so that `os.walk` will walk them in a @@ -127,10 +128,10 @@ def write_files(self, base_dir): for name in sorted(filenames): path = os.path.normpath(os.path.join(root, name)) if os.path.isfile(path): - arcname = os.path.relpath(path, base_dir).replace(os.path.sep, '/') + arcname = os.path.relpath(path, base_dir).replace(os.path.sep, "/") if arcname == self.record_path: pass - elif root.endswith('.dist-info'): + elif root.endswith(".dist-info"): deferred.append((path, arcname)) else: self.write(path, arcname) @@ -140,42 +141,51 @@ def write_files(self, base_dir): self.write(path, arcname) def write(self, filename, arcname=None, compress_type=None): - with open(filename, 'rb') as f: + with open(filename, "rb") as f: st = os.fstat(f.fileno()) data = f.read() - zinfo = ZipInfo(arcname or filename, date_time=get_zipinfo_datetime(st.st_mtime)) + zinfo = ZipInfo( + arcname or filename, date_time=get_zipinfo_datetime(st.st_mtime) + ) zinfo.external_attr = (stat.S_IMODE(st.st_mode) | stat.S_IFMT(st.st_mode)) << 16 zinfo.compress_type = compress_type or self.compression self.writestr(zinfo, data, compress_type) - def writestr(self, zinfo_or_arcname, bytes, compress_type=None): - ZipFile.writestr(self, zinfo_or_arcname, bytes, compress_type) - fname = (zinfo_or_arcname.filename if isinstance(zinfo_or_arcname, ZipInfo) - else zinfo_or_arcname) - logger.info("adding '%s'", fname) + def writestr(self, zinfo_or_arcname, data, compress_type=None): + if isinstance(data, str): + data = data.encode("utf-8") + + ZipFile.writestr(self, zinfo_or_arcname, data, compress_type) + fname = ( + zinfo_or_arcname.filename + if isinstance(zinfo_or_arcname, ZipInfo) + else zinfo_or_arcname + ) + log.info(f"adding '{fname}'") if fname != self.record_path: - hash_ = self._default_algorithm(bytes) - self._file_hashes[fname] = hash_.name, native(urlsafe_b64encode(hash_.digest())) - self._file_sizes[fname] = len(bytes) + hash_ = self._default_algorithm(data) + self._file_hashes[fname] = ( + hash_.name, + urlsafe_b64encode(hash_.digest()).decode("ascii"), + ) + self._file_sizes[fname] = len(data) def close(self): # Write RECORD - if self.fp is not None and self.mode == 'w' and self._file_hashes: + if self.fp is not None and self.mode == "w" and self._file_hashes: data = StringIO() - writer = csv.writer(data, delimiter=',', quotechar='"', lineterminator='\n') - writer.writerows(( + writer = csv.writer(data, delimiter=",", quotechar='"', lineterminator="\n") + writer.writerows( ( - fname, - algorithm + "=" + hash_, - self._file_sizes[fname] + (fname, algorithm + "=" + hash_, self._file_sizes[fname]) + for fname, (algorithm, hash_) in self._file_hashes.items() ) - for fname, (algorithm, hash_) in self._file_hashes.items() - )) + ) writer.writerow((format(self.record_path), "", "")) - zinfo = ZipInfo(native(self.record_path), date_time=get_zipinfo_datetime()) + zinfo = ZipInfo(self.record_path, date_time=get_zipinfo_datetime()) zinfo.compress_type = self.compression zinfo.external_attr = 0o664 << 16 - self.writestr(zinfo, as_bytes(data.getvalue())) + self.writestr(zinfo, data.getvalue()) ZipFile.close(self) diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 7d711aa8..1dc86c0d 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import re @@ -7,7 +9,7 @@ def test_egg_re(): """Make sure egg_info_re matches.""" - egg_names_path = os.path.join(os.path.dirname(__file__), 'eggnames.txt') + egg_names_path = os.path.join(os.path.dirname(__file__), "eggnames.txt") with open(egg_names_path) as egg_names: for line in egg_names: line = line.strip() @@ -20,5 +22,7 @@ def test_convert_egg(egg_paths, tmpdir): wheel_names = [path.basename for path in tmpdir.listdir()] assert len(wheel_names) == len(egg_paths) assert all(WHEEL_INFO_RE.match(filename) for filename in wheel_names) - assert all(re.match(r'^[\w\d.]+-\d\.\d-\w+\d+-[\w\d]+-[\w\d]+\.whl$', fname) - for fname in wheel_names) + assert all( + re.match(r"^[\w\d.]+-\d\.\d-\w+\d+-[\w\d]+-[\w\d]+\.whl$", fname) + for fname in wheel_names + ) diff --git a/tests/cli/test_pack.py b/tests/cli/test_pack.py index 3f689a47..9264eb9e 100644 --- a/tests/cli/test_pack.py +++ b/tests/cli/test_pack.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from textwrap import dedent from zipfile import ZipFile @@ -7,31 +9,38 @@ from wheel.cli.pack import pack THISDIR = os.path.dirname(__file__) -TESTWHEEL_NAME = 'test-1.0-py2.py3-none-any.whl' -TESTWHEEL_PATH = os.path.join(THISDIR, '..', 'testdata', TESTWHEEL_NAME) +TESTWHEEL_NAME = "test-1.0-py2.py3-none-any.whl" +TESTWHEEL_PATH = os.path.join(THISDIR, "..", "testdata", TESTWHEEL_NAME) -@pytest.mark.filterwarnings('error:Duplicate name') -@pytest.mark.parametrize('build_tag_arg, existing_build_tag, filename', [ - (None, None, 'test-1.0-py2.py3-none-any.whl'), - ('2b', None, 'test-1.0-2b-py2.py3-none-any.whl'), - (None, '3', 'test-1.0-3-py2.py3-none-any.whl'), - ('', '3', 'test-1.0-py2.py3-none-any.whl'), -], ids=['nobuildnum', 'newbuildarg', 'oldbuildnum', 'erasebuildnum']) +@pytest.mark.filterwarnings("error:Duplicate name") +@pytest.mark.parametrize( + "build_tag_arg, existing_build_tag, filename", + [ + (None, None, "test-1.0-py2.py3-none-any.whl"), + ("2b", None, "test-1.0-2b-py2.py3-none-any.whl"), + (None, "3", "test-1.0-3-py2.py3-none-any.whl"), + ("", "3", "test-1.0-py2.py3-none-any.whl"), + ], + ids=["nobuildnum", "newbuildarg", "oldbuildnum", "erasebuildnum"], +) def test_pack(tmpdir_factory, tmpdir, build_tag_arg, existing_build_tag, filename): - unpack_dir = tmpdir_factory.mktemp('wheeldir') + unpack_dir = tmpdir_factory.mktemp("wheeldir") with ZipFile(TESTWHEEL_PATH) as zf: - old_record = zf.read('test-1.0.dist-info/RECORD') - old_record_lines = sorted(line.rstrip() for line in old_record.split(b'\n') - if line and not line.startswith(b'test-1.0.dist-info/WHEEL,')) + old_record = zf.read("test-1.0.dist-info/RECORD") + old_record_lines = sorted( + line.rstrip() + for line in old_record.split(b"\n") + if line and not line.startswith(b"test-1.0.dist-info/WHEEL,") + ) zf.extractall(str(unpack_dir)) if existing_build_tag: # Add the build number to WHEEL - wheel_file_path = unpack_dir.join('test-1.0.dist-info').join('WHEEL') + wheel_file_path = unpack_dir.join("test-1.0.dist-info").join("WHEEL") wheel_file_content = wheel_file_path.read_binary() - assert b'Build' not in wheel_file_content - wheel_file_content += b'Build: 3\r\n' + assert b"Build" not in wheel_file_content + wheel_file_content += b"Build: 3\r\n" wheel_file_path.write_binary(wheel_file_content) pack(str(unpack_dir), str(tmpdir), build_tag_arg) @@ -39,24 +48,31 @@ def test_pack(tmpdir_factory, tmpdir, build_tag_arg, existing_build_tag, filenam assert new_wheel_path.isfile() with ZipFile(str(new_wheel_path)) as zf: - new_record = zf.read('test-1.0.dist-info/RECORD') - new_record_lines = sorted(line.rstrip() for line in new_record.split(b'\n') - if line and not line.startswith(b'test-1.0.dist-info/WHEEL,')) + new_record = zf.read("test-1.0.dist-info/RECORD") + new_record_lines = sorted( + line.rstrip() + for line in new_record.split(b"\n") + if line and not line.startswith(b"test-1.0.dist-info/WHEEL,") + ) - new_wheel_file_content = zf.read('test-1.0.dist-info/WHEEL') + new_wheel_file_content = zf.read("test-1.0.dist-info/WHEEL") assert new_record_lines == old_record_lines expected_build_num = build_tag_arg or existing_build_tag - expected_wheel_content = dedent("""\ + expected_wheel_content = dedent( + """\ Wheel-Version: 1.0 Generator: bdist_wheel (0.30.0) Root-Is-Purelib: false Tag: py2-none-any Tag: py3-none-any - """.replace('\n', '\r\n')) + """.replace( + "\n", "\r\n" + ) + ) if expected_build_num: - expected_wheel_content += 'Build: %s\r\n' % expected_build_num + expected_wheel_content += "Build: %s\r\n" % expected_build_num - expected_wheel_content = expected_wheel_content.encode('ascii') + expected_wheel_content = expected_wheel_content.encode("ascii") assert new_wheel_file_content == expected_wheel_content diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py index 531ca7de..9505caea 100644 --- a/tests/cli/test_unpack.py +++ b/tests/cli/test_unpack.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from wheel.cli.unpack import unpack diff --git a/tests/conftest.py b/tests/conftest.py index d9821b83..d5a99d85 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ pytest local configuration plug-in """ +from __future__ import annotations + import os.path import subprocess import sys @@ -9,38 +11,55 @@ import pytest -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def wheels_and_eggs(tmpdir_factory): """Build wheels and eggs from test distributions.""" - test_distributions = "complex-dist", "simple.dist", "headers.dist", "commasinfilenames.dist" - if sys.version_info >= (3, 6): - # Only Python 3.6+ can handle packaging unicode file names reliably - # across different platforms - test_distributions += ("unicode.dist",) + test_distributions = ( + "complex-dist", + "simple.dist", + "headers.dist", + "commasinfilenames.dist", + "unicode.dist", + ) - if sys.platform != 'win32': + if sys.platform != "win32": # ABI3 extensions don't really work on Windows test_distributions += ("abi3extension.dist",) pwd = os.path.abspath(os.curdir) this_dir = os.path.dirname(__file__) - build_dir = tmpdir_factory.mktemp('build') - dist_dir = tmpdir_factory.mktemp('dist') + build_dir = tmpdir_factory.mktemp("build") + dist_dir = tmpdir_factory.mktemp("dist") for dist in test_distributions: - os.chdir(os.path.join(this_dir, 'testdata', dist)) - subprocess.check_call([sys.executable, 'setup.py', - 'bdist_egg', '-b', str(build_dir), '-d', str(dist_dir), - 'bdist_wheel', '-b', str(build_dir), '-d', str(dist_dir)]) + os.chdir(os.path.join(this_dir, "testdata", dist)) + subprocess.check_call( + [ + sys.executable, + "setup.py", + "bdist_egg", + "-b", + str(build_dir), + "-d", + str(dist_dir), + "bdist_wheel", + "-b", + str(build_dir), + "-d", + str(dist_dir), + ] + ) os.chdir(pwd) - return sorted(str(fname) for fname in dist_dir.listdir() if fname.ext in ('.whl', '.egg')) + return sorted( + str(fname) for fname in dist_dir.listdir() if fname.ext in (".whl", ".egg") + ) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def wheel_paths(wheels_and_eggs): - return [fname for fname in wheels_and_eggs if fname.endswith('.whl')] + return [fname for fname in wheels_and_eggs if fname.endswith(".whl")] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def egg_paths(wheels_and_eggs): - return [fname for fname in wheels_and_eggs if fname.endswith('.egg')] + return [fname for fname in wheels_and_eggs if fname.endswith(".egg")] diff --git a/tests/test_bdist_wheel.py b/tests/test_bdist_wheel.py index 651c0345..531d9e6f 100644 --- a/tests/test_bdist_wheel.py +++ b/tests/test_bdist_wheel.py @@ -1,149 +1,210 @@ -# coding: utf-8 +from __future__ import annotations + import os.path import shutil import stat import subprocess import sys +import sysconfig from zipfile import ZipFile import pytest -from wheel.bdist_wheel import bdist_wheel +from wheel.bdist_wheel import bdist_wheel, get_abi_tag +from wheel.vendored.packaging import tags from wheel.wheelfile import WheelFile DEFAULT_FILES = { - 'dummy_dist-1.0.dist-info/top_level.txt', - 'dummy_dist-1.0.dist-info/METADATA', - 'dummy_dist-1.0.dist-info/WHEEL', - 'dummy_dist-1.0.dist-info/RECORD' + "dummy_dist-1.0.dist-info/top_level.txt", + "dummy_dist-1.0.dist-info/METADATA", + "dummy_dist-1.0.dist-info/WHEEL", + "dummy_dist-1.0.dist-info/RECORD", } DEFAULT_LICENSE_FILES = { - 'LICENSE', 'LICENSE.txt', 'LICENCE', 'LICENCE.txt', 'COPYING', - 'COPYING.md', 'NOTICE', 'NOTICE.rst', 'AUTHORS', 'AUTHORS.txt' + "LICENSE", + "LICENSE.txt", + "LICENCE", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "NOTICE", + "NOTICE.rst", + "AUTHORS", + "AUTHORS.txt", } OTHER_IGNORED_FILES = { - 'LICENSE~', 'AUTHORS~', + "LICENSE~", + "AUTHORS~", } - - -@pytest.fixture -def dummy_dist(tmpdir_factory): - basedir = tmpdir_factory.mktemp('dummy_dist') - basedir.join('setup.py').write("""\ +SETUPPY_EXAMPLE = """\ from setuptools import setup setup( name='dummy_dist', - version='1.0' + version='1.0', ) -""") +""" + + +@pytest.fixture +def dummy_dist(tmpdir_factory): + basedir = tmpdir_factory.mktemp("dummy_dist") + basedir.join("setup.py").write(SETUPPY_EXAMPLE) for fname in DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES: - basedir.join(fname).write('') + basedir.join(fname).write("") - basedir.join('licenses').mkdir().join('DUMMYFILE').write('') + basedir.join("licenses").mkdir().join("DUMMYFILE").write("") return basedir def test_no_scripts(wheel_paths): """Make sure entry point scripts are not generated.""" - path = next(path for path in wheel_paths if 'complex_dist' in path) + path = next(path for path in wheel_paths if "complex_dist" in path) for entry in ZipFile(path).infolist(): - assert '.data/scripts/' not in entry.filename + assert ".data/scripts/" not in entry.filename -@pytest.mark.skipif(sys.version_info < (3, 6), - reason='Packaging unicode file names only works reliably on Python 3.6+') def test_unicode_record(wheel_paths): - path = next(path for path in wheel_paths if 'unicode.dist' in path) + path = next(path for path in wheel_paths if "unicode.dist" in path) with ZipFile(path) as zf: - record = zf.read('unicode.dist-0.1.dist-info/RECORD') + record = zf.read("unicode.dist-0.1.dist-info/RECORD") - assert u'åäö_日本語.py'.encode('utf-8') in record + assert "åäö_日本語.py".encode() in record def test_licenses_default(dummy_dist, monkeypatch, tmpdir): monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal']) - with WheelFile('dist/dummy_dist-1.0-py2.py3-none-any.whl') as wf: - license_files = {'dummy_dist-1.0.dist-info/' + fname for fname in DEFAULT_LICENSE_FILES} + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel", "-b", str(tmpdir), "--universal"] + ) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/" + fname for fname in DEFAULT_LICENSE_FILES + } assert set(wf.namelist()) == DEFAULT_FILES | license_files def test_licenses_deprecated(dummy_dist, monkeypatch, tmpdir): - dummy_dist.join('setup.cfg').write('[metadata]\nlicense_file=licenses/DUMMYFILE') + dummy_dist.join("setup.cfg").write("[metadata]\nlicense_file=licenses/DUMMYFILE") monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal']) - with WheelFile('dist/dummy_dist-1.0-py2.py3-none-any.whl') as wf: - license_files = {'dummy_dist-1.0.dist-info/DUMMYFILE'} + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel", "-b", str(tmpdir), "--universal"] + ) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"} assert set(wf.namelist()) == DEFAULT_FILES | license_files -def test_licenses_override(dummy_dist, monkeypatch, tmpdir): - dummy_dist.join('setup.cfg').write('[metadata]\nlicense_files=licenses/*\n LICENSE') +@pytest.mark.parametrize( + "config_file, config", + [ + ("setup.cfg", "[metadata]\nlicense_files=licenses/*\n LICENSE"), + ("setup.cfg", "[metadata]\nlicense_files=licenses/*, LICENSE"), + ( + "setup.py", + SETUPPY_EXAMPLE.replace( + ")", " license_files=['licenses/DUMMYFILE', 'LICENSE'])" + ), + ), + ], +) +def test_licenses_override(dummy_dist, monkeypatch, tmpdir, config_file, config): + dummy_dist.join(config_file).write(config) monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal']) - with WheelFile('dist/dummy_dist-1.0-py2.py3-none-any.whl') as wf: - license_files = {'dummy_dist-1.0.dist-info/' + fname for fname in {'DUMMYFILE', 'LICENSE'}} + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel", "-b", str(tmpdir), "--universal"] + ) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/" + fname for fname in {"DUMMYFILE", "LICENSE"} + } assert set(wf.namelist()) == DEFAULT_FILES | license_files def test_licenses_disabled(dummy_dist, monkeypatch, tmpdir): - dummy_dist.join('setup.cfg').write('[metadata]\nlicense_files=\n') + dummy_dist.join("setup.cfg").write("[metadata]\nlicense_files=\n") monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal']) - with WheelFile('dist/dummy_dist-1.0-py2.py3-none-any.whl') as wf: + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel", "-b", str(tmpdir), "--universal"] + ) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: assert set(wf.namelist()) == DEFAULT_FILES def test_build_number(dummy_dist, monkeypatch, tmpdir): monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal', '--build-number=2']) - with WheelFile('dist/dummy_dist-1.0-2-py2.py3-none-any.whl') as wf: + subprocess.check_call( + [ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmpdir), + "--universal", + "--build-number=2", + ] + ) + with WheelFile("dist/dummy_dist-1.0-2-py2.py3-none-any.whl") as wf: filenames = set(wf.namelist()) - assert 'dummy_dist-1.0.dist-info/RECORD' in filenames - assert 'dummy_dist-1.0.dist-info/METADATA' in filenames + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames -@pytest.mark.skipif(sys.version_info[0] < 3, reason='The limited ABI only works on Python 3+') def test_limited_abi(monkeypatch, tmpdir): """Test that building a binary wheel with the limited ABI works.""" this_dir = os.path.dirname(__file__) - source_dir = os.path.join(this_dir, 'testdata', 'extension.dist') - build_dir = tmpdir.join('build') - dist_dir = tmpdir.join('dist') + source_dir = os.path.join(this_dir, "testdata", "extension.dist") + build_dir = tmpdir.join("build") + dist_dir = tmpdir.join("dist") monkeypatch.chdir(source_dir) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(build_dir), - '-d', str(dist_dir)]) + subprocess.check_call( + [ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(build_dir), + "-d", + str(dist_dir), + ] + ) def test_build_from_readonly_tree(dummy_dist, monkeypatch, tmpdir): - basedir = str(tmpdir.join('dummy')) + basedir = str(tmpdir.join("dummy")) shutil.copytree(str(dummy_dist), basedir) monkeypatch.chdir(basedir) # Make the tree read-only - for root, dirs, files in os.walk(basedir): + for root, _dirs, files in os.walk(basedir): for fname in files: os.chmod(os.path.join(root, fname), stat.S_IREAD) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel']) + subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"]) -@pytest.mark.parametrize('option, compress_type', list(bdist_wheel.supported_compressions.items()), - ids=list(bdist_wheel.supported_compressions)) +@pytest.mark.parametrize( + "option, compress_type", + list(bdist_wheel.supported_compressions.items()), + ids=list(bdist_wheel.supported_compressions), +) def test_compression(dummy_dist, monkeypatch, tmpdir, option, compress_type): monkeypatch.chdir(dummy_dist) - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '-b', str(tmpdir), - '--universal', '--compression={}'.format(option)]) - with WheelFile('dist/dummy_dist-1.0-py2.py3-none-any.whl') as wf: + subprocess.check_call( + [ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmpdir), + "--universal", + f"--compression={option}", + ] + ) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: filenames = set(wf.namelist()) - assert 'dummy_dist-1.0.dist-info/RECORD' in filenames - assert 'dummy_dist-1.0.dist-info/METADATA' in filenames + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames for zinfo in wf.filelist: assert zinfo.compress_type == compress_type @@ -151,6 +212,34 @@ def test_compression(dummy_dist, monkeypatch, tmpdir, option, compress_type): def test_wheelfile_line_endings(wheel_paths): for path in wheel_paths: with WheelFile(path) as wf: - wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith('WHEEL')) + wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith("WHEEL")) wheelfile_contents = wf.read(wheelfile) - assert b'\r' not in wheelfile_contents + assert b"\r" not in wheelfile_contents + + +def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmpdir): + monkeypatch.setenv("SOURCE_DATE_EPOCH", "0") + monkeypatch.chdir(dummy_dist) + subprocess.check_call( + [ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmpdir), + "--universal", + "--build-number=2", + ] + ) + + +def test_get_abi_tag_old(monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73") + assert get_abi_tag() == "pypy36_pp73" + + +def test_get_abi_tag_new(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin") + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + assert get_abi_tag() == "pypy37_pp73" diff --git a/tests/test_macosx_libfile.py b/tests/test_macosx_libfile.py index 614e723c..fed3ebbe 100644 --- a/tests/test_macosx_libfile.py +++ b/tests/test_macosx_libfile.py @@ -1,15 +1,16 @@ +from __future__ import annotations + import os import sys -import distutils.util +import sysconfig -from wheel.macosx_libfile import extract_macosx_min_system_version from wheel.bdist_wheel import get_platform +from wheel.macosx_libfile import extract_macosx_min_system_version def test_read_from_dylib(): dirname = os.path.dirname(__file__) - dylib_dir = os.path.join(dirname, "testdata", - "macosx_minimal_system_version") + dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") versions = [ ("test_lib_10_6_fat.dylib", "10.6.0"), ("test_lib_10_10_fat.dylib", "10.10.0"), @@ -31,12 +32,12 @@ def test_read_from_dylib(): ) str_ver = ".".join([str(x) for x in extracted]) assert str_ver == ver - assert extract_macosx_min_system_version( - os.path.join(dylib_dir, "test_lib.c") - ) is None - assert extract_macosx_min_system_version( - os.path.join(dylib_dir, "libb.dylib") - ) is None + assert ( + extract_macosx_min_system_version(os.path.join(dylib_dir, "test_lib.c")) is None + ) + assert ( + extract_macosx_min_system_version(os.path.join(dylib_dir, "libb.dylib")) is None + ) def return_factory(return_val): @@ -50,38 +51,60 @@ class TestGetPlatformMacosx: def test_simple(self, monkeypatch): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-11.0-x86_64")) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-11.0-x86_64") + ) assert get_platform(dylib_dir) == "macosx_11_0_x86_64" def test_version_bump(self, monkeypatch, capsys): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) assert get_platform(dylib_dir) == "macosx_11_0_x86_64" captured = capsys.readouterr() assert "[WARNING] This wheel needs a higher macOS version than" in captured.err - def test_information_about_problematic_files_python_version(self, monkeypatch, capsys): + def test_information_about_problematic_files_python_version( + self, monkeypatch, capsys + ): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] - )) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_10_10_x86_64" captured = capsys.readouterr() assert "[WARNING] This wheel needs a higher macOS version than" in captured.err - assert "the version your Python interpreter is compiled against." in captured.err + assert ( + "the version your Python interpreter is compiled against." in captured.err + ) assert "test_lib_10_10_fat.dylib" in captured.err - def test_information_about_problematic_files_env_variable(self, monkeypatch, capsys): + def test_information_about_problematic_files_env_variable( + self, monkeypatch, capsys + ): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.8") - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] - )) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_10_10_x86_64" captured = capsys.readouterr() assert "[WARNING] This wheel needs a higher macOS version than" in captured.err @@ -91,10 +114,16 @@ def test_information_about_problematic_files_env_variable(self, monkeypatch, cap def test_bump_platform_tag_by_env_variable(self, monkeypatch, capsys): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_6_fat.dylib"])] - )) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_6_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_10_9_x86_64" monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10") assert get_platform(dylib_dir) == "macosx_10_10_x86_64" @@ -104,11 +133,26 @@ def test_bump_platform_tag_by_env_variable(self, monkeypatch, capsys): def test_bugfix_release_platform_tag(self, monkeypatch, capsys): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_6_fat.dylib", - "test_lib_10_10_10.dylib"])] - )) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) + monkeypatch.setattr( + os, + "walk", + return_factory( + [ + ( + dylib_dir, + [], + [ + "test_lib_10_6.dylib", + "test_lib_10_6_fat.dylib", + "test_lib_10_10_10.dylib", + ], + ) + ] + ), + ) assert get_platform(dylib_dir) == "macosx_10_10_x86_64" captured = capsys.readouterr() assert "This wheel needs a higher macOS version than" in captured.err @@ -120,36 +164,57 @@ def test_bugfix_release_platform_tag(self, monkeypatch, capsys): def test_warning_on_to_low_env_variable(self, monkeypatch, capsys): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.8") - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_6_fat.dylib"])] - )) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_6_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_10_9_x86_64" captured = capsys.readouterr() - assert "MACOSX_DEPLOYMENT_TARGET is set to a lower value (10.8) than the" in captured.err + assert ( + "MACOSX_DEPLOYMENT_TARGET is set to a lower value (10.8) than the" + in captured.err + ) def test_get_platform_bigsur_env(self, monkeypatch): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-10.9-x86_64")) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-10.9-x86_64") + ) monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "11") - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] - )) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_11_0_x86_64" def test_get_platform_bigsur_platform(self, monkeypatch): dirname = os.path.dirname(__file__) dylib_dir = os.path.join(dirname, "testdata", "macosx_minimal_system_version") - monkeypatch.setattr(distutils.util, "get_platform", return_factory("macosx-11-x86_64")) - monkeypatch.setattr(os, "walk", return_factory( - [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] - )) + monkeypatch.setattr( + sysconfig, "get_platform", return_factory("macosx-11-x86_64") + ) + monkeypatch.setattr( + os, + "walk", + return_factory( + [(dylib_dir, [], ["test_lib_10_6.dylib", "test_lib_10_10_fat.dylib"])] + ), + ) assert get_platform(dylib_dir) == "macosx_11_0_x86_64" def test_get_platform_linux(monkeypatch): - monkeypatch.setattr(distutils.util, "get_platform", return_factory("linux_x86_64")) + monkeypatch.setattr(sysconfig, "get_platform", return_factory("linux-x86_64")) monkeypatch.setattr(sys, "maxsize", 2147483647) assert get_platform(None) == "linux_i686" diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 2e4f24c8..96461c40 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,32 +1,38 @@ +from __future__ import annotations + from wheel.metadata import pkginfo_to_metadata def test_pkginfo_to_metadata(tmpdir): expected_metadata = [ - ('Metadata-Version', '2.1'), - ('Name', 'spam'), - ('Version', '0.1'), - ('Requires-Dist', "pip @ https://github.com/pypa/pip/archive/1.3.1.zip"), - ('Requires-Dist', 'pywin32 ; sys_platform=="win32"'), - ('Requires-Dist', 'foo @ http://host/foo.zip ; sys_platform=="win32"'), - ('Provides-Extra', 'signatures'), - ('Requires-Dist', 'pyxdg ; (sys_platform!="win32") and extra == \'signatures\''), - ('Provides-Extra', 'empty_extra'), - ('Provides-Extra', 'extra'), - ('Requires-Dist', 'bar @ http://host/bar.zip ; extra == \'extra\''), - ('Provides-Extra', 'faster-signatures'), - ('Requires-Dist', "ed25519ll ; extra == 'faster-signatures'"), - ('Provides-Extra', 'rest'), - ('Requires-Dist', "docutils (>=0.8) ; extra == 'rest'"), - ('Requires-Dist', "keyring ; extra == 'signatures'"), - ('Requires-Dist', "keyrings.alt ; extra == 'signatures'"), - ('Provides-Extra', 'test'), - ('Requires-Dist', "pytest (>=3.0.0) ; extra == 'test'"), - ('Requires-Dist', "pytest-cov ; extra == 'test'"), + ("Metadata-Version", "2.1"), + ("Name", "spam"), + ("Version", "0.1"), + ("Requires-Dist", "pip @ https://github.com/pypa/pip/archive/1.3.1.zip"), + ("Requires-Dist", 'pywin32 ; sys_platform=="win32"'), + ("Requires-Dist", 'foo @ http://host/foo.zip ; sys_platform=="win32"'), + ("Provides-Extra", "signatures"), + ( + "Requires-Dist", + "pyxdg ; (sys_platform!=\"win32\") and extra == 'signatures'", + ), + ("Provides-Extra", "empty_extra"), + ("Provides-Extra", "extra"), + ("Requires-Dist", "bar @ http://host/bar.zip ; extra == 'extra'"), + ("Provides-Extra", "faster-signatures"), + ("Requires-Dist", "ed25519ll ; extra == 'faster-signatures'"), + ("Provides-Extra", "rest"), + ("Requires-Dist", "docutils (>=0.8) ; extra == 'rest'"), + ("Requires-Dist", "keyring ; extra == 'signatures'"), + ("Requires-Dist", "keyrings.alt ; extra == 'signatures'"), + ("Provides-Extra", "test"), + ("Requires-Dist", "pytest (>=3.0.0) ; extra == 'test'"), + ("Requires-Dist", "pytest-cov ; extra == 'test'"), ] - pkg_info = tmpdir.join('PKG-INFO') - pkg_info.write("""\ + pkg_info = tmpdir.join("PKG-INFO") + pkg_info.write( + """\ Metadata-Version: 0.0 Name: spam Version: 0.1 @@ -35,10 +41,12 @@ def test_pkginfo_to_metadata(tmpdir): Provides-Extra: reST Provides-Extra: signatures Provides-Extra: Signatures -Provides-Extra: faster-signatures""") +Provides-Extra: faster-signatures""" + ) - egg_info_dir = tmpdir.ensure_dir('test.egg-info') - egg_info_dir.join('requires.txt').write("""\ + egg_info_dir = tmpdir.ensure_dir("test.egg-info") + egg_info_dir.join("requires.txt").write( + """\ pip@https://github.com/pypa/pip/archive/1.3.1.zip [extra] @@ -65,7 +73,10 @@ def test_pkginfo_to_metadata(tmpdir): [test] pytest>=3.0.0 -pytest-cov""") +pytest-cov""" + ) - message = pkginfo_to_metadata(egg_info_path=str(egg_info_dir), pkginfo_path=str(pkg_info)) + message = pkginfo_to_metadata( + egg_info_path=str(egg_info_dir), pkginfo_path=str(pkg_info) + ) assert message.items() == expected_metadata diff --git a/tests/test_pkginfo.py b/tests/test_pkginfo.py deleted file mode 100644 index a7e07a8f..00000000 --- a/tests/test_pkginfo.py +++ /dev/null @@ -1,22 +0,0 @@ -from email.parser import Parser - -from wheel.pkginfo import write_pkg_info - - -def test_pkginfo_mangle_from(tmpdir): - """Test that write_pkginfo() will not prepend a ">" to a line starting with "From".""" - metadata = """\ -Metadata-Version: 2.1 -Name: foo - -From blahblah - -==== -Test -==== - -""" - message = Parser().parsestr(metadata) - pkginfo_file = tmpdir.join('PKGINFO') - write_pkg_info(str(pkginfo_file), message) - assert pkginfo_file.read_text('ascii') == metadata diff --git a/tests/test_tagopt.py b/tests/test_tagopt.py index d40b078f..59a18b0f 100644 --- a/tests/test_tagopt.py +++ b/tests/test_tagopt.py @@ -3,6 +3,8 @@ --plat-name) """ +from __future__ import annotations + import subprocess import sys @@ -25,176 +27,185 @@ @pytest.fixture def temp_pkg(request, tmpdir): - tmpdir.join('test.py').write('print("Hello, world")') + tmpdir.join("test.py").write('print("Hello, world")') - ext = getattr(request, 'param', [False, '']) + ext = getattr(request, "param", [False, ""]) if ext[0]: # if ext[1] is not '', it will write a bad header and fail to compile - tmpdir.join('test.c').write('#include ' % ext[1]) + tmpdir.join("test.c").write("#include " % ext[1]) setup_py = SETUP_PY.format(ext_modules=EXT_MODULES) else: - setup_py = SETUP_PY.format(ext_modules='') + setup_py = SETUP_PY.format(ext_modules="") - tmpdir.join('setup.py').write(setup_py) + tmpdir.join("setup.py").write(setup_py) if ext[0]: try: subprocess.check_call( - [sys.executable, 'setup.py', 'build_ext'], cwd=str(tmpdir)) + [sys.executable, "setup.py", "build_ext"], cwd=str(tmpdir) + ) except subprocess.CalledProcessError: - pytest.skip('Cannot compile C extensions') + pytest.skip("Cannot compile C extensions") return tmpdir -@pytest.mark.parametrize('temp_pkg', [[True, 'xxx']], indirect=['temp_pkg']) +@pytest.mark.parametrize("temp_pkg", [[True, "xxx"]], indirect=["temp_pkg"]) def test_nocompile_skips(temp_pkg): - assert False # should have skipped with a "Cannot compile" message + assert False # noqa: B011 - should have skipped with a "Cannot compile" message def test_default_tag(temp_pkg): - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel'], cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename == 'Test-1.0-py%s-none-any.whl' % (sys.version_info[0],) - assert wheels[0].ext == '.whl' + assert wheels[0].basename == f"Test-1.0-py{sys.version_info[0]}-none-any.whl" + assert wheels[0].ext == ".whl" def test_build_number(temp_pkg): - subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel', '--build-number=1'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + subprocess.check_call( + [sys.executable, "setup.py", "bdist_wheel", "--build-number=1"], + cwd=str(temp_pkg), + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert (wheels[0].basename == 'Test-1.0-1-py%s-none-any.whl' % (sys.version_info[0],)) - assert wheels[0].ext == '.whl' + assert wheels[0].basename == f"Test-1.0-1-py{sys.version_info[0]}-none-any.whl" + assert wheels[0].ext == ".whl" def test_explicit_tag(temp_pkg): subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel', '--python-tag=py32'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel", "--python-tag=py32"], + cwd=str(temp_pkg), + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py32-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py32-") + assert wheels[0].ext == ".whl" def test_universal_tag(temp_pkg): subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel', '--universal'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel", "--universal"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py2.py3-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py2.py3-") + assert wheels[0].ext == ".whl" def test_universal_beats_explicit_tag(temp_pkg): subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel', '--universal', '--python-tag=py32'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel", "--universal", "--python-tag=py32"], + cwd=str(temp_pkg), + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py2.py3-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py2.py3-") + assert wheels[0].ext == ".whl" def test_universal_in_setup_cfg(temp_pkg): - temp_pkg.join('setup.cfg').write('[bdist_wheel]\nuniversal=1') + temp_pkg.join("setup.cfg").write("[bdist_wheel]\nuniversal=1") subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py2.py3-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py2.py3-") + assert wheels[0].ext == ".whl" def test_pythontag_in_setup_cfg(temp_pkg): - temp_pkg.join('setup.cfg').write('[bdist_wheel]\npython_tag=py32') + temp_pkg.join("setup.cfg").write("[bdist_wheel]\npython_tag=py32") subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py32-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py32-") + assert wheels[0].ext == ".whl" def test_legacy_wheel_section_in_setup_cfg(temp_pkg): - temp_pkg.join('setup.cfg').write('[wheel]\nuniversal=1') + temp_pkg.join("setup.cfg").write("[wheel]\nuniversal=1") subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.startswith('Test-1.0-py2.py3-') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.startswith("Test-1.0-py2.py3-") + assert wheels[0].ext == ".whl" def test_plat_name_purepy(temp_pkg): subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel', '--plat-name=testplat.pure'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel", "--plat-name=testplat.pure"], + cwd=str(temp_pkg), + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.endswith('-testplat_pure.whl') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.endswith("-testplat_pure.whl") + assert wheels[0].ext == ".whl" -@pytest.mark.parametrize('temp_pkg', [[True, '']], indirect=['temp_pkg']) +@pytest.mark.parametrize("temp_pkg", [[True, ""]], indirect=["temp_pkg"]) def test_plat_name_ext(temp_pkg): subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel', '--plat-name=testplat.arch'], - cwd=str(temp_pkg)) + [sys.executable, "setup.py", "bdist_wheel", "--plat-name=testplat.arch"], + cwd=str(temp_pkg), + ) - dist_dir = temp_pkg.join('dist') + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.endswith('-testplat_arch.whl') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.endswith("-testplat_arch.whl") + assert wheels[0].ext == ".whl" def test_plat_name_purepy_in_setupcfg(temp_pkg): - temp_pkg.join('setup.cfg').write('[bdist_wheel]\nplat_name=testplat.pure') + temp_pkg.join("setup.cfg").write("[bdist_wheel]\nplat_name=testplat.pure") subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel'], - cwd=str(temp_pkg)) - dist_dir = temp_pkg.join('dist') + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.endswith('-testplat_pure.whl') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.endswith("-testplat_pure.whl") + assert wheels[0].ext == ".whl" -@pytest.mark.parametrize('temp_pkg', [[True, '']], indirect=['temp_pkg']) +@pytest.mark.parametrize("temp_pkg", [[True, ""]], indirect=["temp_pkg"]) def test_plat_name_ext_in_setupcfg(temp_pkg): - temp_pkg.join('setup.cfg').write('[bdist_wheel]\nplat_name=testplat.arch') + temp_pkg.join("setup.cfg").write("[bdist_wheel]\nplat_name=testplat.arch") subprocess.check_call( - [sys.executable, 'setup.py', 'bdist_wheel'], - cwd=str(temp_pkg)) + [sys.executable, "setup.py", "bdist_wheel"], cwd=str(temp_pkg) + ) - dist_dir = temp_pkg.join('dist') + dist_dir = temp_pkg.join("dist") assert dist_dir.check(dir=1) wheels = dist_dir.listdir() assert len(wheels) == 1 - assert wheels[0].basename.endswith('-testplat_arch.whl') - assert wheels[0].ext == '.whl' + assert wheels[0].basename.endswith("-testplat_arch.whl") + assert wheels[0].ext == ".whl" diff --git a/tests/test_wheelfile.py b/tests/test_wheelfile.py index db11bcd2..9ff5a328 100644 --- a/tests/test_wheelfile.py +++ b/tests/test_wheelfile.py @@ -1,174 +1,185 @@ -# coding: utf-8 -from __future__ import unicode_literals +from __future__ import annotations import sys -from zipfile import ZipFile, ZIP_DEFLATED +from zipfile import ZIP_DEFLATED, ZipFile import pytest from wheel.cli import WheelError -from wheel.util import native, as_bytes from wheel.wheelfile import WheelFile @pytest.fixture def wheel_path(tmpdir): - return str(tmpdir.join('test-1.0-py2.py3-none-any.whl')) + return str(tmpdir.join("test-1.0-py2.py3-none-any.whl")) def test_wheelfile_re(tmpdir): # Regression test for #208 - path = tmpdir.join('foo-2-py3-none-any.whl') - with WheelFile(str(path), 'w') as wf: - assert wf.parsed_filename.group('namever') == 'foo-2' - - -@pytest.mark.parametrize('filename', [ - 'test.whl', - 'test-1.0.whl', - 'test-1.0-py2.whl', - 'test-1.0-py2-none.whl', - 'test-1.0-py2-none-any' -]) + path = tmpdir.join("foo-2-py3-none-any.whl") + with WheelFile(str(path), "w") as wf: + assert wf.parsed_filename.group("namever") == "foo-2" + + +@pytest.mark.parametrize( + "filename", + [ + "test.whl", + "test-1.0.whl", + "test-1.0-py2.whl", + "test-1.0-py2-none.whl", + "test-1.0-py2-none-any", + ], +) def test_bad_wheel_filename(filename): exc = pytest.raises(WheelError, WheelFile, filename) - exc.match('^Bad wheel filename {!r}$'.format(filename)) + exc.match(f"^Bad wheel filename {filename!r}$") def test_missing_record(wheel_path): - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, w0rld!")\n')) + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n') exc = pytest.raises(WheelError, WheelFile, wheel_path) exc.match("^Missing test-1.0.dist-info/RECORD file$") def test_unsupported_hash_algorithm(wheel_path): - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, w0rld!")\n')) + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n') zf.writestr( - 'test-1.0.dist-info/RECORD', - as_bytes('hello/héllö.py,sha000=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25')) + "test-1.0.dist-info/RECORD", + "hello/héllö.py,sha000=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25", + ) exc = pytest.raises(WheelError, WheelFile, wheel_path) exc.match("^Unsupported hash algorithm: sha000$") -@pytest.mark.parametrize('algorithm, digest', [ - ('md5', '4J-scNa2qvSgy07rS4at-Q'), - ('sha1', 'QjCnGu5Qucb6-vir1a6BVptvOA4') -], ids=['md5', 'sha1']) +@pytest.mark.parametrize( + "algorithm, digest", + [("md5", "4J-scNa2qvSgy07rS4at-Q"), ("sha1", "QjCnGu5Qucb6-vir1a6BVptvOA4")], + ids=["md5", "sha1"], +) def test_weak_hash_algorithm(wheel_path, algorithm, digest): - hash_string = '{}={}'.format(algorithm, digest) - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, w0rld!")\n')) - zf.writestr('test-1.0.dist-info/RECORD', - as_bytes('hello/héllö.py,{},25'.format(hash_string))) + hash_string = f"{algorithm}={digest}" + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n') + zf.writestr("test-1.0.dist-info/RECORD", f"hello/héllö.py,{hash_string},25") exc = pytest.raises(WheelError, WheelFile, wheel_path) - exc.match(r"^Weak hash algorithm \({}\) is not permitted by PEP 427$".format(algorithm)) - - -@pytest.mark.parametrize('algorithm, digest', [ - ('sha256', 'bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo'), - ('sha384', 'cDXriAy_7i02kBeDkN0m2RIDz85w6pwuHkt2PZ4VmT2PQc1TZs8Ebvf6eKDFcD_S'), - ('sha512', 'kdX9CQlwNt4FfOpOKO_X0pn_v1opQuksE40SrWtMyP1NqooWVWpzCE3myZTfpy8g2azZON_' - 'iLNpWVxTwuDWqBQ') -], ids=['sha256', 'sha384', 'sha512']) + exc.match(rf"^Weak hash algorithm \({algorithm}\) is not permitted by PEP 427$") + + +@pytest.mark.parametrize( + "algorithm, digest", + [ + ("sha256", "bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo"), + ("sha384", "cDXriAy_7i02kBeDkN0m2RIDz85w6pwuHkt2PZ4VmT2PQc1TZs8Ebvf6eKDFcD_S"), + ( + "sha512", + "kdX9CQlwNt4FfOpOKO_X0pn_v1opQuksE40SrWtMyP1NqooWVWpzCE3myZTfpy8g2azZON_" + "iLNpWVxTwuDWqBQ", + ), + ], + ids=["sha256", "sha384", "sha512"], +) def test_testzip(wheel_path, algorithm, digest): - hash_string = '{}={}'.format(algorithm, digest) - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, world!")\n')) - zf.writestr('test-1.0.dist-info/RECORD', - as_bytes('hello/héllö.py,{},25'.format(hash_string))) + hash_string = f"{algorithm}={digest}" + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, world!")\n') + zf.writestr("test-1.0.dist-info/RECORD", f"hello/héllö.py,{hash_string},25") with WheelFile(wheel_path) as wf: wf.testzip() def test_testzip_missing_hash(wheel_path): - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, world!")\n')) - zf.writestr('test-1.0.dist-info/RECORD', '') + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, world!")\n') + zf.writestr("test-1.0.dist-info/RECORD", "") with WheelFile(wheel_path) as wf: exc = pytest.raises(WheelError, wf.testzip) - exc.match(native("^No hash found for file 'hello/héllö.py'$")) + exc.match("^No hash found for file 'hello/héllö.py'$") def test_testzip_bad_hash(wheel_path): - with ZipFile(wheel_path, 'w') as zf: - zf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, w0rld!")\n')) + with ZipFile(wheel_path, "w") as zf: + zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n') zf.writestr( - 'test-1.0.dist-info/RECORD', - as_bytes('hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25')) + "test-1.0.dist-info/RECORD", + "hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25", + ) with WheelFile(wheel_path) as wf: exc = pytest.raises(WheelError, wf.testzip) - exc.match(native("^Hash mismatch for file 'hello/héllö.py'$")) + exc.match("^Hash mismatch for file 'hello/héllö.py'$") def test_write_str(wheel_path): - with WheelFile(wheel_path, 'w') as wf: - wf.writestr(native('hello/héllö.py'), as_bytes('print("Héllö, world!")\n')) - wf.writestr(native('hello/h,ll,.py'), as_bytes('print("Héllö, world!")\n')) + with WheelFile(wheel_path, "w") as wf: + wf.writestr("hello/héllö.py", 'print("Héllö, world!")\n') + wf.writestr("hello/h,ll,.py", 'print("Héllö, world!")\n') - with ZipFile(wheel_path, 'r') as zf: + with ZipFile(wheel_path, "r") as zf: infolist = zf.infolist() assert len(infolist) == 3 - assert infolist[0].filename == native('hello/héllö.py') + assert infolist[0].filename == "hello/héllö.py" assert infolist[0].file_size == 25 - assert infolist[1].filename == native('hello/h,ll,.py') + assert infolist[1].filename == "hello/h,ll,.py" assert infolist[1].file_size == 25 - assert infolist[2].filename == 'test-1.0.dist-info/RECORD' + assert infolist[2].filename == "test-1.0.dist-info/RECORD" - record = zf.read('test-1.0.dist-info/RECORD') - assert record == as_bytes( - 'hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25\n' + record = zf.read("test-1.0.dist-info/RECORD") + assert record.decode("utf-8") == ( + "hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25\n" '"hello/h,ll,.py",sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25\n' - 'test-1.0.dist-info/RECORD,,\n') + "test-1.0.dist-info/RECORD,,\n" + ) def test_timestamp(tmpdir_factory, wheel_path, monkeypatch): # An environment variable can be used to influence the timestamp on # TarInfo objects inside the zip. See issue #143. - build_dir = tmpdir_factory.mktemp('build') - for filename in ('one', 'two', 'three'): - build_dir.join(filename).write(filename + '\n') + build_dir = tmpdir_factory.mktemp("build") + for filename in ("one", "two", "three"): + build_dir.join(filename).write(filename + "\n") # The earliest date representable in TarInfos, 1980-01-01 - monkeypatch.setenv(native('SOURCE_DATE_EPOCH'), native('315576060')) + monkeypatch.setenv("SOURCE_DATE_EPOCH", "315576060") - with WheelFile(wheel_path, 'w') as wf: + with WheelFile(wheel_path, "w") as wf: wf.write_files(str(build_dir)) - with ZipFile(wheel_path, 'r') as zf: + with ZipFile(wheel_path, "r") as zf: for info in zf.infolist(): assert info.date_time[:3] == (1980, 1, 1) assert info.compress_type == ZIP_DEFLATED -@pytest.mark.skipif(sys.platform == 'win32', - reason='Windows does not support UNIX-like permissions') +@pytest.mark.skipif( + sys.platform == "win32", reason="Windows does not support UNIX-like permissions" +) def test_attributes(tmpdir_factory, wheel_path): # With the change from ZipFile.write() to .writestr(), we need to manually # set member attributes. - build_dir = tmpdir_factory.mktemp('build') - files = (('foo', 0o644), ('bar', 0o755)) + build_dir = tmpdir_factory.mktemp("build") + files = (("foo", 0o644), ("bar", 0o755)) for filename, mode in files: path = build_dir.join(filename) - path.write(filename + '\n') + path.write(filename + "\n") path.chmod(mode) - with WheelFile(wheel_path, 'w') as wf: + with WheelFile(wheel_path, "w") as wf: wf.write_files(str(build_dir)) - with ZipFile(wheel_path, 'r') as zf: + with ZipFile(wheel_path, "r") as zf: for filename, mode in files: info = zf.getinfo(filename) assert info.external_attr == (mode | 0o100000) << 16 assert info.compress_type == ZIP_DEFLATED - info = zf.getinfo('test-1.0.dist-info/RECORD') + info = zf.getinfo("test-1.0.dist-info/RECORD") permissions = (info.external_attr >> 16) & 0o777 assert permissions == 0o664 diff --git a/tests/testdata/abi3extension.dist/setup.py b/tests/testdata/abi3extension.dist/setup.py index 3ffd8394..5962bd15 100644 --- a/tests/testdata/abi3extension.dist/setup.py +++ b/tests/testdata/abi3extension.dist/setup.py @@ -1,11 +1,12 @@ -from setuptools import setup, Extension +from __future__ import annotations -setup(name='extension.dist', - version='0.1', - description=u'A testing distribution \N{SNOWMAN}', - ext_modules=[ - Extension(name='extension', - sources=['extension.c'], - py_limited_api=True) - ], - ) +from setuptools import Extension, setup + +setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[ + Extension(name="extension", sources=["extension.c"], py_limited_api=True) + ], +) diff --git a/tests/testdata/commasinfilenames.dist/setup.py b/tests/testdata/commasinfilenames.dist/setup.py index 8cf9e4ec..a2783a3b 100644 --- a/tests/testdata/commasinfilenames.dist/setup.py +++ b/tests/testdata/commasinfilenames.dist/setup.py @@ -1,12 +1,12 @@ +from __future__ import annotations + from setuptools import setup setup( - name='testrepo', - version='0.1', + name="testrepo", + version="0.1", packages=["mypackage"], - description='A test package with commas in file names', + description="A test package with commas in file names", include_package_data=True, - package_data={ - "mypackage.data": ["*"] - }, + package_data={"mypackage.data": ["*"]}, ) diff --git a/tests/testdata/complex-dist/complexdist/__init__.py b/tests/testdata/complex-dist/complexdist/__init__.py index 559fbb7d..88aa7b76 100644 --- a/tests/testdata/complex-dist/complexdist/__init__.py +++ b/tests/testdata/complex-dist/complexdist/__init__.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + def main(): return diff --git a/tests/testdata/complex-dist/setup.py b/tests/testdata/complex-dist/setup.py index 41cfb04f..e0439d9e 100644 --- a/tests/testdata/complex-dist/setup.py +++ b/tests/testdata/complex-dist/setup.py @@ -1,21 +1,24 @@ +from __future__ import annotations + from setuptools import setup -setup(name='complex-dist', - version='0.1', - description=u'Another testing distribution \N{SNOWMAN}', - long_description=u'Another testing distribution \N{SNOWMAN}', - author="Illustrious Author", - author_email="illustrious@example.org", - url="http://example.org/exemplary", - packages=['complexdist'], - setup_requires=["wheel", "setuptools"], - install_requires=["quux", "splort"], - extras_require={'simple': ['simple.dist']}, - tests_require=["foo", "bar>=10.0.0"], - entry_points={ - 'console_scripts': [ - 'complex-dist=complexdist:main', - 'complex-dist2=complexdist:main', - ], - }, - ) +setup( + name="complex-dist", + version="0.1", + description="Another testing distribution \N{SNOWMAN}", + long_description="Another testing distribution \N{SNOWMAN}", + author="Illustrious Author", + author_email="illustrious@example.org", + url="http://example.org/exemplary", + packages=["complexdist"], + setup_requires=["wheel", "setuptools"], + install_requires=["quux", "splort"], + extras_require={"simple": ["simple.dist"]}, + tests_require=["foo", "bar>=10.0.0"], + entry_points={ + "console_scripts": [ + "complex-dist=complexdist:main", + "complex-dist2=complexdist:main", + ], + }, +) diff --git a/tests/testdata/extension.dist/setup.py b/tests/testdata/extension.dist/setup.py index ae225254..9a6eed8c 100644 --- a/tests/testdata/extension.dist/setup.py +++ b/tests/testdata/extension.dist/setup.py @@ -1,10 +1,10 @@ -from setuptools import setup, Extension +from __future__ import annotations -setup(name='extension.dist', - version='0.1', - description=u'A testing distribution \N{SNOWMAN}', - ext_modules=[ - Extension(name='extension', - sources=['extension.c']) - ], - ) +from setuptools import Extension, setup + +setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[Extension(name="extension", sources=["extension.c"])], +) diff --git a/tests/testdata/headers.dist/setup.py b/tests/testdata/headers.dist/setup.py index 67cada32..6cf9b46f 100644 --- a/tests/testdata/headers.dist/setup.py +++ b/tests/testdata/headers.dist/setup.py @@ -1,7 +1,10 @@ +from __future__ import annotations + from setuptools import setup -setup(name='headers.dist', - version='0.1', - description=u'A distribution with headers', - headers=['header.h'] - ) +setup( + name="headers.dist", + version="0.1", + description="A distribution with headers", + headers=["header.h"], +) diff --git a/tests/testdata/macosx_minimal_system_version/test_lib.c b/tests/testdata/macosx_minimal_system_version/test_lib.c index ca24f6c0..dfa22681 100644 --- a/tests/testdata/macosx_minimal_system_version/test_lib.c +++ b/tests/testdata/macosx_minimal_system_version/test_lib.c @@ -10,4 +10,4 @@ int num_of_letters(char* text){ lett += 1; } return num; -} \ No newline at end of file +} diff --git a/tests/testdata/simple.dist/setup.py b/tests/testdata/simple.dist/setup.py index d2aaac92..1e7a78a2 100644 --- a/tests/testdata/simple.dist/setup.py +++ b/tests/testdata/simple.dist/setup.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from setuptools import setup -setup(name='simple.dist', - version='0.1', - description=u'A testing distribution \N{SNOWMAN}', - packages=['simpledist'], - extras_require={'voting': ['beaglevote']}, - ) +setup( + name="simple.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + packages=["simpledist"], + extras_require={"voting": ["beaglevote"]}, +) diff --git a/tests/testdata/unicode.dist/setup.py b/tests/testdata/unicode.dist/setup.py index bb0ff60b..89c4be12 100644 --- a/tests/testdata/unicode.dist/setup.py +++ b/tests/testdata/unicode.dist/setup.py @@ -1,8 +1,10 @@ -# coding: utf-8 +from __future__ import annotations + from setuptools import setup -setup(name='unicode.dist', - version='0.1', - description=u'A testing distribution \N{SNOWMAN}', - packages=['unicodedist'] - ) +setup( + name="unicode.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + packages=["unicodedist"], +) diff --git a/tox.ini b/tox.ini index 54c73dd2..1d170e60 100644 --- a/tox.ini +++ b/tox.ini @@ -4,16 +4,18 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, py39, py310, pypy, pypy3, flake8 +envlist = py37, py38, py39, py310, py311, pypy3, lint minversion = 3.3.0 skip_missing_interpreters = true [testenv] +depends = lint commands = {envpython} -b -m pytest -W always {posargs} extras = test -[testenv:flake8] +[testenv:lint] +depends = basepython = python3 -deps = flake8 -commands = flake8 src tests +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure skip_install = true