././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7291741 wheel-0.46.1/LICENSE.txt0000644000000000000000000000212314775306464011475 0ustar00MIT 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"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7291741 wheel-0.46.1/README.rst0000644000000000000000000000211414775306464011341 0ustar00wheel ===== This is a command line tool for manipulating Python wheel files, as defined in `PEP 427`_. It contains the following functionality: * Convert ``.egg`` archives into ``.whl`` * Unpack wheel archives * Repack wheel archives * Add or remove tags in existing wheel archives .. _PEP 427: https://www.python.org/dev/peps/pep-0427/ Historical note --------------- This project used to contain the implementation of the setuptools_ ``bdist_wheel`` command, but as of setuptools v70.1, it no longer needs ``wheel`` installed for that to work. Thus, you should install this **only** if you intend to use the ``wheel`` command line tool! .. _setuptools: https://pypi.org/project/setuptools/ Documentation ------------- The documentation_ can be found on Read The Docs. .. _documentation: https://wheel.readthedocs.io/ Code of Conduct --------------- 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/Makefile0000644000000000000000000001267014775306464012252 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/wheel.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wheel.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/wheel" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wheel" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/conf.py0000644000000000000000000002005414775306464012104 0ustar00# # wheel documentation build configuration file, created by # sphinx-quickstart on Thu Jul 12 00:14:09 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. 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('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # 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"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. 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 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) # The full version, including alpha/beta/rc tags. release = match.group(1) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # 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"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # 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 # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" highlight_language = "bash" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] 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" # 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 = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # 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 # 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 # 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'] # 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' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is 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 = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. 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': '', } # 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", "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 # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # 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 = [ ("manpages/wheel", "wheel", "wheel Documentation", ["Daniel Holth"], 1), ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "wheel", "wheel Documentation", "Daniel Holth", "wheel", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/development.rst0000644000000000000000000000564714775306464013674 0ustar00Development =========== Pull Requests ------------- - 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. **Example**. Assuming you set up GitHub account, forked wheel repository from https://github.com/pypa/wheel to your own page via web interface, and your fork is located at https://github.com/yourname/wheel :: $ git clone git@github.com:pypa/wheel.git $ cd wheel # ... $ git diff $ git add ... $ git status $ git commit You may reference relevant issues in commit messages (like #1259) to make GitHub link issues and commits together, and with phrase like "fixes #1259" you can even close relevant issues automatically. Now push the changes to your fork:: $ git push git@github.com:yourname/wheel.git Open Pull Requests page at https://github.com/yourname/wheel/pulls and click "New pull request". That's it. Automated Testing ----------------- 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 Running Tests Locally --------------------- Python requirements: tox_ or pytest_ To run the tests via tox against all matching interpreters:: $ tox To run the tests via tox against a specific environment:: $ tox -e py35 Alternatively, you can run the tests via pytest using your default interpreter:: $ pip install -e .[test] # Installs the test dependencies $ pytest # Runs the tests with the current interpreter The above pip install command will replace the current interpreter's installed wheel package with the development package being tested. If you use this workflow, it is recommended to run it under a virtualenv_. .. _tox: https://pypi.org/project/tox/ .. _pytest: https://pypi.org/project/pytest/ .. _virtualenv: https://pypi.org/project/virtualenv/ Getting Involved ---------------- The wheel project welcomes help in the following ways: - Making Pull Requests for code, tests, or docs. - Commenting on open issues and pull requests. - Helping to answer questions on the `mailing list`_. .. _`mailing list`: https://mail.python.org/mailman/listinfo/distutils-sig Release Process --------------- To make a new release: #. Edit ``docs/news.rst`` and replace ``**UNRELEASED**`` with a release version and date, like ``**X.Y.Z (20XX-YY-ZZ)**``. #. Replace the ``__version__`` attribute in ``src/wheel/__init__.py`` with the same version number as above (without the date of course). #. Create a new git tag matching the version exactly #. Push the new tag to GitHub Pushing a new tag to GitHub will trigger the publish workflow which package the project and publish the resulting artifacts to PyPI. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/index.rst0000644000000000000000000000122014775306464012440 0ustar00wheel ===== `GitHub `_ | `PyPI `_ | User IRC: #pypa | Dev IRC: #pypa-dev This library is the reference implementation of the Python wheel packaging standard, as defined in `PEP 427`_. It has two different roles: #. A setuptools_ extension for building wheels that provides the ``bdist_wheel`` setuptools command #. A command line tool for working with wheel files .. _PEP 427: https://www.python.org/dev/peps/pep-0427/ .. _setuptools: https://pypi.org/project/setuptools/ .. toctree:: :maxdepth: 2 quickstart installing user_guide reference/index development news ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/installing.rst0000644000000000000000000000123114775306464013477 0ustar00Installation ============ You can use pip_ to install wheel:: pip install wheel If you do not have pip_ installed, see its documentation for `installation instructions`_. 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 * python3-wheel .. _pip: https://pip.pypa.io/en/stable/ .. _installation instructions: https://pip.pypa.io/en/stable/installing/ Python and OS Compatibility --------------------------- wheel should work on any Python implementation and operating system and is compatible with Python version 3.9 and upwards. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/make.bat0000644000000000000000000001174614775306464012222 0ustar00@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\wheel.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\wheel.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/manpages/wheel.rst0000644000000000000000000000122614775306464014236 0ustar00:orphan: wheel manual page ================= Synopsis -------- **wheel** [*command*] [*options*] Description ----------- :program:`wheel` installs and operates on `PEP 427`_ format binary wheels. Commands -------- ``unpack`` Unpack wheel ``pack`` Repack a previously unpacked wheel ``convert`` Convert egg or wininst to wheel ``tags`` Change the tags on a wheel file ``version`` Print version and exit ``help`` Show this help Try ``wheel --help`` for more information. Options ------- -h, --help show this help message and exit .. _`PEP 427`: https://www.python.org/dev/peps/pep-0427/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/news.rst0000644000000000000000000005027514775306464012323 0ustar00Release Notes ============= **0.46.1 (2025-04-08)** - Temporarily restored the ``wheel.macosx_libfile`` module (`#659 `_) **0.46.0 (2025-04-03)** - Dropped support for Python 3.8 - Removed the ``bdist_wheel`` setuptools command implementation and entry point. The ``wheel.bdist_wheel`` module is now just an alias to ``setuptools.command.bdist_wheel``, emitting a deprecation warning on import. - Removed vendored ``packaging`` in favor of a run-time dependency on it - Made the ``wheel.metadata`` module private (with a deprecation warning if it's imported - Made the ``wheel.cli`` package private (no deprecation warning) - Fixed an exception when calling the ``convert`` command with an empty description field **0.45.1 (2024-11-23)** - Fixed pure Python wheels converted from eggs and wininst files having the ABI tag in the file name **0.45.0 (2024-11-08)** - Refactored the ``convert`` command to not need setuptools to be installed - Don't configure setuptools logging unless running ``bdist_wheel`` - Added a redirection from ``wheel.bdist_wheel.bdist_wheel`` to ``setuptools.command.bdist_wheel.bdist_wheel`` to improve compatibility with ``setuptools``' latest fixes. Projects are still advised to migrate away from the deprecated module and import the ``setuptools``' implementation explicitly. (PR by @abravalheri) **0.44.0 (2024-08-04)** - Canonicalized requirements in METADATA file (PR by Wim Jeantine-Glenn) - Deprecated the ``bdist_wheel`` module, as the code was migrated to ``setuptools`` itself **0.43.0 (2024-03-11)** - Dropped support for Python 3.7 - Updated vendored ``packaging`` to 24.0 **0.42.0 (2023-11-26)** - Allowed removing build tag with ``wheel tags --build ""`` - Fixed ``wheel pack`` and ``wheel tags`` writing updated ``WHEEL`` fields after a blank line, causing other tools to ignore them - Fixed ``wheel pack`` and ``wheel tags`` writing ``WHEEL`` with CRLF line endings or a mix of CRLF and LF - Fixed ``wheel pack --build-number ""`` not removing build tag from ``WHEEL`` (above changes by Benjamin Gilbert) **0.41.3 (2023-10-30)** - Updated vendored ``packaging`` to 23.2 - Fixed ABI tag generation for CPython 3.13a1 on Windows (PR by Sam Gross) **0.41.2 (2023-08-22)** - Fixed platform tag detection for GraalPy and 32-bit python running on an aarch64 kernel (PR by Matthieu Darbois) - Fixed ``wheel tags`` to not list directories in ``RECORD`` files (PR by Mike Taves) - Fixed ABI tag generation for GraalPy (PR by Michael Simacek) **0.41.1 (2023-08-05)** - Fixed naming of the ``data_dir`` directory in the presence of local version segment given via ``egg_info.tag_build`` (PR by Anderson Bravalheri) - Fixed version specifiers in ``Requires-Dist`` being wrapped in parentheses **0.41.0 (2023-07-22)** - Added full support of the build tag syntax to ``wheel tags`` (you can now set a build tag like ``123mytag``) - Fixed warning on Python 3.12 about ``onerror`` deprecation. (PR by Henry Schreiner) - Support testing on Python 3.12 betas (PR by Ewout ter Hoeven) **0.40.0 (2023-03-14)** - Added a ``wheel tags`` command to modify tags on an existing wheel (PR by Henry Schreiner) - Updated vendored ``packaging`` to 23.0 - ``wheel unpack`` now preserves the executable attribute of extracted files - Fixed spaces in platform names not being converted to underscores (PR by David Tucker) - Fixed ``RECORD`` files in generated wheels missing the regular file attribute - Fixed ``DeprecationWarning`` about the use of the deprecated ``pkg_resources`` API (PR by Thomas Grainger) - Wheel now uses flit-core as a build backend (PR by Henry Schreiner) **0.38.4 (2022-11-09)** - Fixed ``PKG-INFO`` conversion in ``bdist_wheel`` mangling UTF-8 header values in ``METADATA`` (PR by Anderson Bravalheri) **0.38.3 (2022-11-08)** - Fixed install failure when used with ``--no-binary``, reported on Ubuntu 20.04, by removing ``setup_requires`` from ``setup.cfg`` **0.38.2 (2022-11-05)** - Fixed regression introduced in v0.38.1 which broke parsing of wheel file names with multiple platform tags **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) - Fixed parsing of file names containing commas in ``RECORD`` (PR by Hood Chatham) **0.37.0 (2021-08-09)** - Added official Python 3.10 support - Updated vendored ``packaging`` library to v20.9 **0.36.2 (2020-12-13)** - Updated vendored ``packaging`` library to v20.8 - Fixed wheel sdist missing ``LICENSE.txt`` - Don't use default ``macos/arm64`` deployment target in calculating the platform tag for fat binaries (PR by Ronald Oussoren) **0.36.1 (2020-12-04)** - Fixed ``AssertionError`` when ``MACOSX_DEPLOYMENT_TARGET`` was set to ``11`` (PR by Grzegorz Bokota and François-Xavier Coudert) - Fixed regression introduced in 0.36.0 on Python 2.7 when a custom generator name was passed as unicode (Scikit-build) (``TypeError: 'unicode' does not have the buffer interface``) **0.36.0 (2020-12-01)** - Added official Python 3.9 support - Updated vendored ``packaging`` library to v20.7 - Switched to always using LF as line separator when generating ``WHEEL`` files (on Windows, CRLF was being used instead) - The ABI tag is taken from the sysconfig SOABI value. On PyPy the SOABI value is ``pypy37-pp73`` which is not compliant with PEP 3149, as it should have both the API tag and the platform tag. This change future-proofs any change in PyPy's SOABI tag to make sure only the ABI tag is used by wheel. - Fixed regression and test for ``bdist_wheel --plat-name``. It was ignored for C extensions in v0.35, but the regression was not detected by tests. **0.35.1 (2020-08-14)** - Replaced install dependency on ``packaging`` with a vendored copy of its ``tags`` module - Fixed ``bdist_wheel`` not working on FreeBSD due to mismatching platform tag name (it was not being converted to lowercase) **0.35.0 (2020-08-13)** - Switched to the packaging_ library for computing wheel tags - Fixed a resource leak in ``WheelFile.open()`` (PR by Jon Dufresne) .. _packaging: https://pypi.org/project/packaging/ **0.34.2 (2020-01-30)** - Fixed installation of ``wheel`` from sdist on environments without Unicode file name support **0.34.1 (2020-01-27)** - Fixed installation of ``wheel`` from sdist which was broken due to a chicken and egg problem with PEP 517 and setuptools_scm **0.34.0 (2020-01-27)** - Dropped Python 3.4 support - Added automatic platform tag detection for macOS binary wheels (PR by Grzegorz Bokota) - Added the ``--compression=`` option to the ``bdist_wheel`` command - Fixed PyPy tag generation to work with the updated semantics (#328) - Updated project packaging and testing configuration for :pep:`517` - Moved the contents of setup.py to setup.cfg - Fixed duplicate RECORD file when using ``wheel pack`` on Windows - Fixed bdist_wheel failing at cleanup on Windows with a read-only source tree - Fixed ``wheel pack`` not respecting the existing build tag in ``WHEEL`` - Switched the project to use the "src" layout - Switched to setuptools_scm_ for versioning .. _setuptools_scm: https://github.com/pypa/setuptools_scm/ **0.33.6 (2019-08-18)** - Fixed regression from 0.33.5 that broke building binary wheels against the limited ABI - Fixed egg2wheel compatibility with the future release of Python 3.10 (PR by Anthony Sottile) **0.33.5 (2019-08-17)** - Don't add the ``m`` ABI flag to wheel names on Python 3.8 (PR by rdb) - Updated ``MANIFEST.in`` to include many previously omitted files in the sdist **0.33.4 (2019-05-12)** - Reverted PR #289 (adding directory entries to the wheel file) due to incompatibility with ``distlib.wheel`` **0.33.3 (2019-05-10)** (redacted release) - Fixed wheel build failures on some systems due to all attributes being preserved (PR by Matt Wozniski) **0.33.2 (2019-05-08)** (redacted release) - Fixed empty directories missing from the wheel (PR by Jason R. Coombs) **0.33.1 (2019-02-19)** - Fixed the ``--build-number`` option for ``wheel pack`` not being applied **0.33.0 (2019-02-11)** - Added the ``--build-number`` option to the ``wheel pack`` command - Fixed bad shebangs sneaking into wheels - Fixed documentation issue with ``wheel pack`` erroneously being called ``wheel repack`` - Fixed filenames with "bad" characters (like commas) not being quoted in ``RECORD`` (PR by Paul Moore) - Sort requirements extras to ensure deterministic builds (PR by PoncinMatthieu) - Forced ``inplace = False`` when building a C extension for the wheel **0.32.3 (2018-11-18)** - Fixed compatibility with Python 2.7.0 – 2.7.3 - Fixed handling of direct URL requirements with markers (PR by Benoit Pierre) **0.32.2 (2018-10-20)** - Fixed build number appearing in the ``.dist-info`` directory name - Made wheel file name parsing more permissive - Fixed wrong Python tag in wheels converted from eggs (PR by John T. Wodder II) **0.32.1 (2018-10-03)** - Fixed ``AttributeError: 'Requirement' object has no attribute 'url'`` on setuptools/pkg_resources versions older than 18.8 (PR by Benoit Pierre) - Fixed ``AttributeError: 'module' object has no attribute 'algorithms_available'`` on Python < 2.7.9 (PR by Benoit Pierre) - Fixed permissions on the generated ``.dist-info/RECORD`` file **0.32.0 (2018-09-29)** - Removed wheel signing and verifying features - Removed the "wheel install" and "wheel installscripts" commands - Added the ``wheel pack`` command - Allowed multiple license files to be specified using the ``license_files`` option - Deprecated the ``license_file`` option - Eliminated duplicate lines from generated requirements in ``.dist-info/METADATA`` (thanks to Wim Glenn for the contribution) - Fixed handling of direct URL specifiers in requirements (PR by Benoit Pierre) - Fixed canonicalization of extras (PR by Benoit Pierre) - Warn when the deprecated ``[wheel]`` section is used in ``setup.cfg`` (PR by Jon Dufresne) **0.31.1 (2018-05-13)** - Fixed arch as ``None`` when converting eggs to wheels **0.31.0 (2018-04-01)** - Fixed displaying of errors on Python 3 - Fixed single digit versions in wheel files not being properly recognized - Fixed wrong character encodings being used (instead of UTF-8) to read and write ``RECORD`` (this sometimes crashed bdist_wheel too) - Enabled Zip64 support in wheels by default - Metadata-Version is now 2.1 - Dropped DESCRIPTION.rst and metadata.json from the list of generated files - Dropped support for the non-standard, undocumented ``provides-extra`` and ``requires-dist`` keywords in setup.cfg metadata - Deprecated all wheel signing and signature verification commands - Removed the (already defunct) ``tool`` extras from setup.py **0.30.0 (2017-09-10)** - Added py-limited-api {cp32|cp33|cp34|...} flag to produce cpNN.abi3.{arch} tags on CPython 3. - Documented the ``license_file`` metadata key - Improved Python, abi tagging for ``wheel convert``. Thanks Ales Erjavec. - Fixed ``>`` being prepended to lines starting with "From" in the long description - Added support for specifying a build number (as per PEP 427). Thanks Ian Cordasco. - Made the order of files in generated ZIP files deterministic. Thanks Matthias Bach. - Made the order of requirements in metadata deterministic. Thanks Chris Lamb. - Fixed ``wheel install`` clobbering existing files - Improved the error message when trying to verify an unsigned wheel file - Removed support for Python 2.6, 3.2 and 3.3. **0.29.0 (2016-02-06)** - Fix compression type of files in archive (Issue #155, Pull Request #62, thanks Xavier Fernandez) **0.28.0 (2016-02-05)** - Fix file modes in archive (Issue #154) **0.27.0 (2016-02-05)** - Support forcing a platform tag using ``--plat-name`` on pure-Python wheels, as well as nonstandard platform tags on non-pure wheels (Pull Request #60, Issue #144, thanks Andrés Díaz) - Add SOABI tags to platform-specific wheels built for Python 2.X (Pull Request #55, Issue #63, Issue #101) - Support reproducible wheel files, wheels that can be rebuilt and will hash to the same values as previous builds (Pull Request #52, Issue #143, thanks Barry Warsaw) - Support for changes in keyring >= 8.0 (Pull Request #61, thanks Jason R. Coombs) - Use the file context manager when checking if dependency_links.txt is empty, fixes problems building wheels under PyPy on Windows (Issue #150, thanks Cosimo Lupo) - Don't attempt to (recursively) create a build directory ending with ``..`` (invalid on all platforms, but code was only executed on Windows) (Issue #91) - Added the PyPA Code of Conduct (Pull Request #56) **0.26.0 (2015-09-18)** - Fix multiple entrypoint comparison failure on Python 3 (Issue #148) **0.25.0 (2015-09-16)** - Add Python 3.5 to tox configuration - Deterministic (sorted) metadata - Fix tagging for Python 3.5 compatibility - Support py2-none-'arch' and py3-none-'arch' tags - Treat data-only wheels as pure - Write to temporary file and rename when using wheel install --force **0.24.0 (2014-07-06)** - The python tag used for pure-python packages is now .pyN (major version only). This change actually occurred in 0.23.0 when the --python-tag option was added, but was not explicitly mentioned in the changelog then. - wininst2wheel and egg2wheel removed. Use "wheel convert [archive]" instead. - Wheel now supports setuptools style conditional requirements via the extras_require={} syntax. Separate 'extra' names from conditions using the : character. Wheel's own setup.py does this. (The empty-string extra is the same as install_requires.) These conditional requirements should work the same whether the package is installed by wheel or by setup.py. **0.23.0 (2014-03-31)** - Compatibility tag flags added to the bdist_wheel command - sdist should include files necessary for tests - 'wheel convert' can now also convert unpacked eggs to wheel - Rename pydist.json to metadata.json to avoid stepping on the PEP - The --skip-scripts option has been removed, and not generating scripts is now the default. The option was a temporary approach until installers could generate scripts themselves. That is now the case with pip 1.5 and later. Note that using pip 1.4 to install a wheel without scripts will leave the installation without entry-point wrappers. The "wheel install-scripts" command can be used to generate the scripts in such cases. - Thank you contributors **0.22.0 (2013-09-15)** - Include entry_points.txt, scripts a.k.a. commands, in experimental pydist.json - Improved test_requires parsing - Python 2.6 fixes, "wheel version" command courtesy pombredanne **0.21.0 (2013-07-20)** - Pregenerated scripts are the default again. - "setup.py bdist_wheel --skip-scripts" turns them off. - setuptools is no longer a listed requirement for the 'wheel' package. It is of course still required in order for bdist_wheel to work. - "python -m wheel" avoids importing pkg_resources until it's necessary. **0.20.0** - No longer include console_scripts in wheels. Ordinary scripts (shell files, standalone Python files) are included as usual. - Include new command "python -m wheel install-scripts [distribution [distribution ...]]" to install the console_scripts (setuptools-style scripts using pkg_resources) for a distribution. **0.19.0 (2013-07-19)** - pymeta.json becomes pydist.json **0.18.0 (2013-07-04)** - Python 3 Unicode improvements **0.17.0 (2013-06-23)** - Support latest PEP-426 "pymeta.json" (json-format metadata) **0.16.0 (2013-04-29)** - Python 2.6 compatibility bugfix (thanks John McFarlane) - Bugfix for C-extension tags for CPython 3.3 (using SOABI) - Bugfix for bdist_wininst converter "wheel convert" - Bugfix for dists where "is pure" is None instead of True or False - Python 3 fix for moving Unicode Description to metadata body - Include rudimentary API documentation in Sphinx (thanks Kevin Horn) **0.15.0 (2013-01-14)** - Various improvements **0.14.0 (2012-10-27)** - Changed the signature format to better comply with the current JWS spec. Breaks all existing signatures. - Include ``wheel unsign`` command to remove RECORD.jws from an archive. - Put the description in the newly allowed payload section of PKG-INFO (METADATA) files. **0.13.0 (2012-10-17)** - Use distutils instead of sysconfig to get installation paths; can install headers. - Improve WheelFile() sort. - Allow bootstrap installs without any pkg_resources. **0.12.0 (2012-10-06)** - Unit test for wheel.tool.install **0.11.0 (2012-10-17)** - API cleanup **0.10.3 (2012-10-03)** - Scripts fixer fix **0.10.2 (2012-10-02)** - Fix keygen **0.10.1 (2012-09-30)** - Preserve attributes on install. **0.10.0 (2012-09-30)** - Include a copy of pkg_resources. Wheel can now install into a virtualenv that does not have distribute (though most packages still require pkg_resources to actually work; wheel install distribute) - Define a new setup.cfg section [wheel]. universal=1 will apply the py2.py3-none-any tag for pure python wheels. **0.9.7 (2012-09-20)** - Only import dirspec when needed. dirspec is only needed to find the configuration for keygen/signing operations. **0.9.6 (2012-09-19)** - requires-dist from setup.cfg overwrites any requirements from setup.py Care must be taken that the requirements are the same in both cases, or just always install from wheel. - drop dirspec requirement on win32 - improved command line utility, adds 'wheel convert [egg or wininst]' to convert legacy binary formats to wheel **0.9.5 (2012-09-15)** - Wheel's own wheel file can be executed by Python, and can install itself: ``python wheel-0.9.5-py27-none-any/wheel install ...`` - Use argparse; basic ``wheel install`` command should run with only stdlib dependencies. - Allow requires_dist in setup.cfg's [metadata] section. In addition to dependencies in setup.py, but will only be interpreted when installing from wheel, not from sdist. Can be qualified with environment markers. **0.9.4 (2012-09-11)** - Fix wheel.signatures in sdist **0.9.3 (2012-09-10)** - Integrated digital signatures support without C extensions. - Integrated "wheel install" command (single package, no dependency resolution) including compatibility check. - Support Python 3.3 - Use Metadata 1.3 (PEP 426) **0.9.2 (2012-08-29)** - Automatic signing if WHEEL_TOOL points to the wheel binary - Even more Python 3 fixes **0.9.1 (2012-08-28)** - 'wheel sign' uses the keys generated by 'wheel keygen' (instead of generating a new key at random each time) - Python 2/3 encoding/decoding fixes - Run tests on Python 2.6 (without signature verification) **0.9 (2012-08-22)** - Updated digital signatures scheme - Python 3 support for digital signatures - Always verify RECORD hashes on extract - "wheel" command line tool to sign, verify, unpack wheel files **0.8 (2012-08-17)** - none/any draft pep tags update - improved wininst2wheel script - doc changes and other improvements **0.7 (2012-07-28)** - sort .dist-info at end of wheel archive - Windows & Python 3 fixes from Paul Moore - pep8 - scripts to convert wininst & egg to wheel **0.6 (2012-07-23)** - require distribute >= 0.6.28 - stop using verlib **0.5 (2012-07-17)** - working pretty well **0.4.2 (2012-07-12)** - hyphenated name fix **0.4 (2012-07-11)** - improve test coverage - improve Windows compatibility - include tox.ini courtesy of Marc Abramowitz - draft hmac sha-256 signing function **0.3 (2012-07-04)** - prototype egg2wheel conversion script **0.2 (2012-07-03)** - Python 3 compatibility **0.1 (2012-06-30)** - Initial version ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/quickstart.rst0000644000000000000000000000105614775306464013532 0ustar00Quickstart ========== To build a wheel for your project:: python -m pip install build python -m build --wheel The wheel will go to ``dist/yourproject-.whl``. If you want to make universal (Python 2/3 compatible, pure Python) wheels, add the following section to your ``setup.cfg``:: [bdist_wheel] universal = 1 To convert an ``.egg`` or file to a wheel:: wheel convert youreggfile.egg Similarly, to convert a Windows installer (made using ``python setup.py bdist_wininst``) to a wheel:: wheel convert yourinstaller.exe ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/reference/index.rst0000644000000000000000000000017414775306464014405 0ustar00Reference Guide =============== .. toctree:: :maxdepth: 2 wheel_convert wheel_unpack wheel_pack wheel_tags ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7301743 wheel-0.46.1/docs/reference/wheel_convert.rst0000644000000000000000000000161314775306464016141 0ustar00wheel convert ============= Usage ----- :: wheel convert [options] [egg_file_or_directory...] Description ----------- Convert one or more eggs (``.egg``; made with ``bdist_egg``) or Windows installers (``.exe``; made with ``bdist_wininst``) into wheels. Egg names must match the standard format: * ``--pyX.Y`` for pure Python wheels * ``--pyX.Y-`` for binary wheels Options ------- .. option:: -d, --dest-dir Directory to store the generated wheels in (defaults to current directory). Examples -------- * Convert a single egg file:: $ wheel convert foobar-1.2.3-py2.7.egg $ ls *.whl foobar-1.2.3-py27-none.whl * If the egg file name is invalid:: $ wheel convert pycharm-debug.egg "pycharm-debug.egg" is not a valid egg name (must match at least name-version-pyX.Y.egg) $ echo $? 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/docs/reference/wheel_pack.rst0000644000000000000000000000166114775306464015402 0ustar00wheel pack ========== Usage ----- :: wheel pack Description ----------- Repack a previously unpacked wheel file. This command can be used to repack a wheel file after its contents have been modified. This is the equivalent of ``zip -r `` except that it regenerates the ``RECORD`` file which contains hashes of all included files. Options ------- .. option:: -d, --dest-dir Directory to put the new wheel file into. .. option:: --build-number Override the build tag in the new wheel file name Examples -------- * Unpack a wheel, add a dummy module and then repack it (with a new build number):: $ wheel unpack someproject-1.5.0-py2-py3-none.whl Unpacking to: ./someproject-1.5.0 $ touch someproject-1.5.0/somepackage/module.py $ wheel pack --build-number 2 someproject-1.5.0 Repacking wheel as ./someproject-1.5.0-2-py2-py3-none.whl...OK ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/docs/reference/wheel_tags.rst0000644000000000000000000000351514775306464015422 0ustar00wheel tags ========== Usage ----- :: wheel tags [-h] [--remove] [--python-tag TAG] [--abi-tag TAG] [--platform-tag TAG] [--build NUMBER] WHEEL [...] Description ----------- Make a new wheel with given tags from an existing wheel. Any tags left unspecified will remain the same. Multiple tags are separated by a "." Starting with a "+" will append to the existing tags. Starting with a "-" will remove a tag. Be sure to use the equals syntax on the shell so that it does not get parsed as an extra option, such as ``--python-tag=-py2``. The original file will remain unless ``--remove`` is given. The output filename(s) will be displayed on stdout for further processing. Options ------- .. option:: --remove Remove the original wheel, keeping only the retagged wheel. .. option:: --python-tag=TAG Override the python tag (prepend with "+" to append, "-" to remove). Multiple tags can be separated with a dot. .. option:: --abi-tag=TAG Override the abi tag (prepend with "+" to append, "-" to remove). Multiple tags can be separated with a dot. .. option:: --platform-tag=TAG Override the platform tag (prepend with "+" to append, "-" to remove). Multiple tags can be separated with a dot. .. option:: --build=NUMBER Specify a build number. Examples -------- * Replace a wheel's Python specific tags with generic tags (if no Python extensions are present, for example):: $ wheel tags --python-tag=py2.py3 --abi-tag=none cmake-3.20.2-cp39-cp39-win_amd64.whl cmake-3.20.2-py2.py3-none-win_amd64.whl * Add compatibility tags for macOS universal wheels and older pips:: $ wheel tags \ --platform-tag=+macosx_10_9_x86_64.macosx_11_0_arm64 \ ninja-1.11.1-py2.py3-none-macosx_10_9_universal2.whl ninja-1.11.1-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.whl ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/docs/reference/wheel_unpack.rst0000644000000000000000000000145614775306464015747 0ustar00wheel unpack ============ Usage ----- :: wheel unpack Description ----------- Unpack the given wheel file. This is the equivalent of ``unzip ``, except that it also checks that the hashes and file sizes match with those in ``RECORD`` and exits with an error if it encounters a mismatch. Options ------- .. option:: -d, --dest Directory to unpack the wheel into. Examples -------- * Unpack a wheel:: $ wheel unpack someproject-1.5.0-py2-py3-none.whl Unpacking to: ./someproject-1.5.0 * If a file's hash does not match:: $ wheel unpack someproject-1.5.0-py2-py3-none.whl Unpacking to: ./someproject-1.5.0 Traceback (most recent call last): ... wheel.install.BadWheelFile: Bad hash for file 'mypackage/module.py' $ echo $? 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/docs/story.rst0000644000000000000000000000764114775306464012526 0ustar00The Story of Wheel ================== 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 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 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? 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 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 binary package and let the build system handle the building. The build 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 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 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. If you’ve made it this far you probably wonder whether I’ve heard 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 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 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. I hope you will benefit from wheel. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/docs/user_guide.rst0000644000000000000000000000466014775306464013477 0ustar00User Guide ========== Building Wheels --------------- To build a wheel for your project:: 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. If your project contains no C extensions and is expected to work on both Python 2 and 3, you will want to tell wheel to produce universal wheels by adding this to your ``setup.cfg`` file: .. code-block:: ini [bdist_wheel] universal = 1 Including license files in the generated wheel file --------------------------------------------------- Several open source licenses require the license text to be included in every distributable artifact of the project. By default, ``wheel`` conveniently includes files matching the following glob_ patterns in the ``.dist-info`` directory: * ``AUTHORS*`` * ``COPYING*`` * ``LICEN[CS]E*`` * ``NOTICE*`` This can be overridden by setting the ``license_files`` option in the ``[metadata]`` section of the project's ``setup.cfg``. For example: .. code-block:: cfg [metadata] license_files = license.txt 3rdparty/*.txt No matter the path, all the matching license files are written in the wheel in the ``.dist-info`` directory based on their file name only. By specifying an empty ``license_files`` option, you can disable this functionality entirely. .. note:: There used to be an option called ``license_file`` (singular). As of wheel v0.32, this option has been deprecated in favor of the more versatile ``license_files`` option. .. _glob: https://docs.python.org/library/glob.html Converting Eggs to Wheels ------------------------- The wheel tool is capable of converting eggs to the wheel format. It works on both ``.egg`` files and ``.egg`` directories, and you can convert multiple eggs with a single command:: wheel convert blah-1.2.3-py2.7.egg foo-2.0b1-py3.5.egg The command supports wildcard expansion as well (via :func:`~glob.iglob`) to accommodate shells that do not do such expansion natively:: wheel convert *.egg By default, the resulting wheels are written to the current working directory. This can be changed with the ``--dest-dir`` option:: wheel convert --dest-dir /tmp blah-1.2.3-py2.7.egg Installing Wheels ----------------- To install a wheel file, use pip_:: $ pip install someproject-1.5.0-py2-py3-none.whl .. _pip: https://pypi.org/project/pip/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/pyproject.toml0000644000000000000000000000713714775306464012600 0ustar00[build-system] requires = ["flit_core >=3.8,<4"] build-backend = "flit_core.buildapi" [project] name = "wheel" description = "Command line tool for manipulating wheel files" readme = "README.rst" license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: System :: Archiving :: Packaging", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] authors = [{name = "Daniel Holth", email = "dholth@fastmail.fm"}] maintainers = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}] keywords = ["wheel", "packaging"] requires-python = ">=3.9" dependencies = [ "packaging >= 24.0" ] dynamic = ["version"] [project.urls] Documentation = "https://wheel.readthedocs.io/" Changelog = "https://wheel.readthedocs.io/en/stable/news.html" "Issue Tracker" = "https://github.com/pypa/wheel/issues" Source = "https://github.com/pypa/wheel" [project.scripts] wheel = "wheel._commands:main" [project.optional-dependencies] test = [ "pytest >= 6.0.0", "setuptools >= 77", ] [tool.flit.sdist] include = [ "LICENSE*", "docs/**/*.py", "docs/**/*.rst", "docs/Makefile", "docs/make.bat", "manpages/*.rst", "tests/**/*.py", "tests/**/*.txt", "tests/**/*.c", "tests/**/*.h", "tests/**/*.cfg", "tests/testdata/macosx_minimal_system_version/*.dylib", "tests/testdata/test-1.0-py2.py3-none-any.whl", ] exclude = [ ".github/**", ".gitignore", ".pre-commit-config.yaml", ".readthedocs.yml", "**/__pycache__", ] [tool.pytest.ini_options] minversion = "6.0" addopts = ["-rsfE", "--tb=short", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", "ignore::Warning:_pytest.*", ] log_cli_level = "info" testpaths = ["test"] [tool.coverage.run] source = ["wheel"] [tool.coverage.report] show_missing = true exclude_also = [ "@abstractmethod", "@overload", "if TYPE_CHECKING:", ] [tool.ruff.lint] extend-select = [ "ANN", # flake8-annotations "B", # flake8-bugbear "G", # flake8-logging-format "I", # isort "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.extend-per-file-ignores] "tests/commands/test_convert.py" = ["W293"] "src/wheel/macosx_libfile.py" = ["ANN201"] [tool.ruff.lint.flake8-annotations] mypy-init-return = true # Tox (https://tox.wiki/) is a tool for running tests in multiple virtualenvs. # This configuration file will run the test suite on all supported python # versions. To use it, "pipx install tox" and then run "tox" from this # directory. [tool.tox] env_list = ["py39", "py310", "py311", "py312", "py313", "py314", "pypy3", "lint", "pkg"] skip_missing_interpreters = true [tool.tox.env_run_base] depends = ["lint"] package = "wheel" commands = [["pytest", { replace = "posargs", extend = true }]] extras = ["test"] set_env = { PYTHONWARNDEFAULTENCODING = "1" } [tool.tox.env.lint] depends = [] deps = ["pre-commit"] package = "skip" commands = [["pre-commit", "run", "-a"]] [tool.tox.env.pkg] deps = ["build", "flit >= 3.8"] commands = [["pytest", "tests/test_sdist.py", { replace = "posargs", extend = true }]] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/__init__.py0000644000000000000000000000007314775306464013660 0ustar00from __future__ import annotations __version__ = "0.46.1" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/__main__.py0000644000000000000000000000100014775306464013630 0ustar00""" Wheel command line tool (enables the ``python -m wheel`` syntax) """ from __future__ import annotations import sys from typing import NoReturn def main() -> NoReturn: # needed for console script 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] from ._commands import main as cli_main sys.exit(cli_main()) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/_commands/__init__.py0000644000000000000000000001052014775306464015616 0ustar00""" Wheel command-line utility. """ from __future__ import annotations import argparse import os import sys from argparse import ArgumentTypeError from ..wheelfile import WheelError def unpack_f(args: argparse.Namespace) -> None: from .unpack import unpack unpack(args.wheelfile, args.dest) def pack_f(args: argparse.Namespace) -> None: from .pack import pack pack(args.directory, args.dest_dir, args.build_number) def convert_f(args: argparse.Namespace) -> None: from .convert import convert convert(args.files, args.dest_dir, args.verbose) def tags_f(args: argparse.Namespace) -> None: from .tags import tags names = ( tags( wheel, args.python_tag, args.abi_tag, args.platform_tag, args.build, args.remove, ) for wheel in args.wheel ) for name in names: print(name) def version_f(args: argparse.Namespace) -> None: from .. import __version__ print(f"wheel {__version__}") def parse_build_tag(build_tag: str) -> str: if build_tag and not build_tag[0].isdigit(): raise ArgumentTypeError("build tag must begin with a digit") elif "-" in build_tag: raise ArgumentTypeError("invalid character ('-') in build tag") return build_tag TAGS_HELP = """\ Make a new wheel with given tags. Any tags unspecified will remain the same. Starting the tags with a "+" will append to the existing tags. Starting with a "-" will remove a tag (use --option=-TAG syntax). Multiple tags can be separated by ".". The original file will remain unless --remove is given. The output filename(s) will be displayed on stdout for further processing. """ def parser() -> argparse.ArgumentParser: 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.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.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.set_defaults(func=convert_f) tags_parser = s.add_parser( "tags", help="Add or replace the tags on a wheel", description=TAGS_HELP ) tags_parser.add_argument("wheel", nargs="*", help="Existing wheel(s) to retag") tags_parser.add_argument( "--remove", action="store_true", help="Remove the original files, keeping only the renamed ones", ) tags_parser.add_argument( "--python-tag", metavar="TAG", help="Specify an interpreter tag(s)" ) tags_parser.add_argument("--abi-tag", metavar="TAG", help="Specify an ABI tag(s)") tags_parser.add_argument( "--platform-tag", metavar="TAG", help="Specify a platform tag(s)" ) tags_parser.add_argument( "--build", type=parse_build_tag, metavar="BUILD", help="Specify a build tag" ) tags_parser.set_defaults(func=tags_f) 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.set_defaults(func=lambda args: p.print_help()) return p def main() -> int: p = parser() args = p.parse_args() if not hasattr(args, "func"): p.print_help() else: try: args.func(args) return 0 except WheelError as e: print(e, file=sys.stderr) return 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/_commands/convert.py0000644000000000000000000003070714775306464015550 0ustar00from __future__ import annotations import os.path import re from abc import ABCMeta, abstractmethod from collections import defaultdict from collections.abc import Iterator from email.message import Message from email.parser import Parser from email.policy import EmailPolicy from glob import iglob from pathlib import Path from textwrap import dedent from zipfile import ZipFile from packaging.tags import parse_tag from .. import __version__ from .._metadata import generate_requirements from ..wheelfile import WheelFile egg_filename_re = re.compile( r""" (?P.+?)-(?P.+?) (-(?Ppy\d\.\d+) (-(?P.+?))? )?.egg$""", re.VERBOSE, ) egg_info_re = re.compile( r""" ^(?P.+?)-(?P.+?) (-(?Ppy\d\.\d+) )?.egg-info/""", re.VERBOSE, ) wininst_re = re.compile( r"\.(?Pwin32|win-amd64)(?:-(?Ppy\d\.\d))?\.exe$" ) pyd_re = re.compile(r"\.(?P[a-z0-9]+)-(?Pwin32|win_amd64)\.pyd$") serialization_policy = EmailPolicy( utf8=True, mangle_from_=False, max_line_length=0, ) GENERATOR = f"wheel {__version__}" def convert_requires(requires: str, metadata: Message) -> None: extra: str | None = None requirements: dict[str | None, list[str]] = defaultdict(list) for line in requires.splitlines(): line = line.strip() if not line: continue if line.startswith("[") and line.endswith("]"): extra = line[1:-1] continue requirements[extra].append(line) for key, value in generate_requirements(requirements): metadata.add_header(key, value) def convert_pkg_info(pkginfo: str, metadata: Message) -> None: parsed_message = Parser().parsestr(pkginfo) for key, value in parsed_message.items(): key_lower = key.lower() if value == "UNKNOWN": continue if key_lower == "description": description_lines = value.splitlines() if description_lines: value = "\n".join( ( description_lines[0].lstrip(), dedent("\n".join(description_lines[1:])), "\n", ) ) else: value = "\n" metadata.set_payload(value) elif key_lower == "home-page": metadata.add_header("Project-URL", f"Homepage, {value}") elif key_lower == "download-url": metadata.add_header("Project-URL", f"Download, {value}") else: metadata.add_header(key, value) metadata.replace_header("Metadata-Version", "2.4") def normalize(name: str) -> str: return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_") class ConvertSource(metaclass=ABCMeta): name: str version: str pyver: str = "py2.py3" abi: str = "none" platform: str = "any" metadata: Message @property def dist_info_dir(self) -> str: return f"{self.name}-{self.version}.dist-info" @abstractmethod def generate_contents(self) -> Iterator[tuple[str, bytes]]: pass class EggFileSource(ConvertSource): def __init__(self, path: Path): if not (match := egg_filename_re.match(path.name)): raise ValueError(f"Invalid egg file name: {path.name}") # Binary wheels are assumed to be for CPython self.path = path self.name = normalize(match.group("name")) self.version = match.group("ver") if pyver := match.group("pyver"): self.pyver = pyver.replace(".", "") if arch := match.group("arch"): self.abi = self.pyver.replace("py", "cp") self.platform = normalize(arch) self.metadata = Message() def generate_contents(self) -> Iterator[tuple[str, bytes]]: with ZipFile(self.path, "r") as zip_file: for filename in sorted(zip_file.namelist()): # Skip pure directory entries if filename.endswith("/"): continue # Handle files in the egg-info directory specially, selectively moving # them to the dist-info directory while converting as needed if filename.startswith("EGG-INFO/"): if filename == "EGG-INFO/requires.txt": requires = zip_file.read(filename).decode("utf-8") convert_requires(requires, self.metadata) elif filename == "EGG-INFO/PKG-INFO": pkginfo = zip_file.read(filename).decode("utf-8") convert_pkg_info(pkginfo, self.metadata) elif filename == "EGG-INFO/entry_points.txt": yield ( f"{self.dist_info_dir}/entry_points.txt", zip_file.read(filename), ) continue # For any other file, just pass it through yield filename, zip_file.read(filename) class EggDirectorySource(EggFileSource): def generate_contents(self) -> Iterator[tuple[str, bytes]]: for dirpath, _, filenames in os.walk(self.path): for filename in sorted(filenames): path = Path(dirpath, filename) if path.parent.name == "EGG-INFO": if path.name == "requires.txt": requires = path.read_text("utf-8") convert_requires(requires, self.metadata) elif path.name == "PKG-INFO": pkginfo = path.read_text("utf-8") convert_pkg_info(pkginfo, self.metadata) if name := self.metadata.get("Name"): self.name = normalize(name) if version := self.metadata.get("Version"): self.version = version elif path.name == "entry_points.txt": yield ( f"{self.dist_info_dir}/entry_points.txt", path.read_bytes(), ) continue # For any other file, just pass it through yield str(path.relative_to(self.path)), path.read_bytes() class WininstFileSource(ConvertSource): """ Handles distributions created with ``bdist_wininst``. The egginfo filename has the format:: name-ver(-pyver)(-arch).egg-info The installer filename has the format:: name-ver.arch(-pyver).exe Some things to note: 1. The installer filename is not definitive. An installer can be renamed and work perfectly well as an installer. So more reliable data should be used whenever possible. 2. The egg-info data should be preferred for the name and version, because these come straight from the distutils metadata, and are mandatory. 3. The pyver from the egg-info data should be ignored, as it is constructed from the version of Python used to build the installer, which is irrelevant - the installer filename is correct here (even to the point that when it's not there, any version is implied). 4. The architecture must be taken from the installer filename, as it is not included in the egg-info data. 5. Architecture-neutral installers still have an architecture because the installer format itself (being executable) is architecture-specific. We should therefore ignore the architecture if the content is pure-python. """ def __init__(self, path: Path): self.path = path self.metadata = Message() # Determine the initial architecture and Python version from the file name # (if possible) if match := wininst_re.search(path.name): self.platform = normalize(match.group("platform")) if pyver := match.group("pyver"): self.pyver = pyver.replace(".", "") # Look for an .egg-info directory and any .pyd files for more precise info egg_info_found = pyd_found = False with ZipFile(self.path) as zip_file: for filename in zip_file.namelist(): prefix, filename = filename.split("/", 1) if not egg_info_found and (match := egg_info_re.match(filename)): egg_info_found = True self.name = normalize(match.group("name")) self.version = match.group("ver") if pyver := match.group("pyver"): self.pyver = pyver.replace(".", "") elif not pyd_found and (match := pyd_re.search(filename)): pyd_found = True self.abi = match.group("abi") self.platform = match.group("platform") if egg_info_found and pyd_found: break def generate_contents(self) -> Iterator[tuple[str, bytes]]: dist_info_dir = f"{self.name}-{self.version}.dist-info" data_dir = f"{self.name}-{self.version}.data" with ZipFile(self.path, "r") as zip_file: for filename in sorted(zip_file.namelist()): # Skip pure directory entries if filename.endswith("/"): continue # Handle files in the egg-info directory specially, selectively moving # them to the dist-info directory while converting as needed prefix, target_filename = filename.split("/", 1) if egg_info_re.search(target_filename): basename = target_filename.rsplit("/", 1)[-1] if basename == "requires.txt": requires = zip_file.read(filename).decode("utf-8") convert_requires(requires, self.metadata) elif basename == "PKG-INFO": pkginfo = zip_file.read(filename).decode("utf-8") convert_pkg_info(pkginfo, self.metadata) elif basename == "entry_points.txt": yield ( f"{dist_info_dir}/entry_points.txt", zip_file.read(filename), ) continue elif prefix == "SCRIPTS": target_filename = f"{data_dir}/scripts/{target_filename}" # For any other file, just pass it through yield target_filename, zip_file.read(filename) def convert(files: list[str], dest_dir: str, verbose: bool) -> None: for pat in files: for archive in iglob(pat): path = Path(archive) if path.suffix == ".egg": if path.is_dir(): source: ConvertSource = EggDirectorySource(path) else: source = EggFileSource(path) else: source = WininstFileSource(path) if verbose: print(f"{archive}...", flush=True, end="") dest_path = Path(dest_dir) / ( f"{source.name}-{source.version}-{source.pyver}-{source.abi}" f"-{source.platform}.whl" ) with WheelFile(dest_path, "w") as wheelfile: for name_or_zinfo, contents in source.generate_contents(): wheelfile.writestr(name_or_zinfo, contents) # Write the METADATA file wheelfile.writestr( f"{source.dist_info_dir}/METADATA", source.metadata.as_string(policy=serialization_policy).encode( "utf-8" ), ) # Write the WHEEL file wheel_message = Message() wheel_message.add_header("Wheel-Version", "1.0") wheel_message.add_header("Generator", GENERATOR) wheel_message.add_header( "Root-Is-Purelib", str(source.platform == "any").lower() ) tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}") for tag in sorted(tags, key=lambda tag: tag.interpreter): wheel_message.add_header("Tag", str(tag)) wheelfile.writestr( f"{source.dist_info_dir}/WHEEL", wheel_message.as_string(policy=serialization_policy).encode( "utf-8" ), ) if verbose: print("OK") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/_commands/pack.py0000644000000000000000000000600614775306464015001 0ustar00from __future__ import annotations import email.policy import os.path import re from email.generator import BytesGenerator from email.parser import BytesParser from ..wheelfile import WheelError, WheelFile DIST_INFO_RE = re.compile(r"^(?P(?P.+?)-(?P\d.*?))\.dist-info$") def pack(directory: str, dest_dir: str, build_number: str | None) -> 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 wheel file name can be determined. :param directory: The unpacked wheel directory :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) ] if len(dist_info_dirs) > 1: raise WheelError(f"Multiple .dist-info directories found in {directory}") elif not dist_info_dirs: 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") # Read the tags and the existing build number from .dist-info/WHEEL wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") with open(wheel_file_path, "rb") as f: info = BytesParser(policy=email.policy.compat32).parse(f) tags: list[str] = info.get_all("Tag", []) existing_build_number = info.get("Build") if not tags: raise WheelError( f"No tags present in {dist_info_dir}/WHEEL; cannot determine target " f"wheel filename" ) # 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: del info["Build"] if build_number: info["Build"] = build_number name_version += "-" + build_number if build_number != existing_build_number: with open(wheel_file_path, "wb") as f: BytesGenerator(f, maxheaderlen=0).flatten(info) # Reassemble the tags for the wheel file tagline = compute_tagline(tags) # Repack the wheel 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") def compute_tagline(tags: list[str]) -> str: """Compute a tagline from a list of tags. :param tags: A list of tags :return: A tagline """ 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}) return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/_commands/tags.py0000644000000000000000000001124714775306464015024 0ustar00from __future__ import annotations import email.policy import itertools import os from collections.abc import Iterable from email.parser import BytesParser from ..wheelfile import WheelFile def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]: """Add or replace tags. Supports dot-separated tags""" if new_tags is None: return set(original_tags) if new_tags.startswith("+"): return {*original_tags, *new_tags[1:].split(".")} if new_tags.startswith("-"): return set(original_tags) - set(new_tags[1:].split(".")) return set(new_tags.split(".")) def tags( wheel: str, python_tags: str | None = None, abi_tags: str | None = None, platform_tags: str | None = None, build_tag: str | None = None, remove: bool = False, ) -> str: """Change the tags on a wheel file. The tags are left unchanged if they are not specified. To specify "none", use ["none"]. To append to the previous tags, a tag should start with a "+". If a tag starts with "-", it will be removed from existing tags. Processing is done left to right. :param wheel: The paths to the wheels :param python_tags: The Python tags to set :param abi_tags: The ABI tags to set :param platform_tags: The platform tags to set :param build_tag: The build tag to set :param remove: Remove the original wheel """ with WheelFile(wheel, "r") as f: assert f.filename, f"{f.filename} must be available" wheel_info = f.read(f.dist_info_path + "/WHEEL") info = BytesParser(policy=email.policy.compat32).parsebytes(wheel_info) original_wheel_name = os.path.basename(f.filename) namever = f.parsed_filename.group("namever") build = f.parsed_filename.group("build") original_python_tags = f.parsed_filename.group("pyver").split(".") original_abi_tags = f.parsed_filename.group("abi").split(".") original_plat_tags = f.parsed_filename.group("plat").split(".") tags: list[str] = info.get_all("Tag", []) existing_build_tag = info.get("Build") impls = {tag.split("-")[0] for tag in tags} abivers = {tag.split("-")[1] for tag in tags} platforms = {tag.split("-")[2] for tag in tags} if impls != set(original_python_tags): msg = f"Wheel internal tags {impls!r} != filename tags {original_python_tags!r}" raise AssertionError(msg) if abivers != set(original_abi_tags): msg = f"Wheel internal tags {abivers!r} != filename tags {original_abi_tags!r}" raise AssertionError(msg) if platforms != set(original_plat_tags): msg = ( f"Wheel internal tags {platforms!r} != filename tags {original_plat_tags!r}" ) raise AssertionError(msg) if existing_build_tag != build: msg = ( f"Incorrect filename '{build}' " f"& *.dist-info/WHEEL '{existing_build_tag}' build numbers" ) raise AssertionError(msg) # Start changing as needed if build_tag is not None: build = build_tag final_python_tags = sorted(_compute_tags(original_python_tags, python_tags)) final_abi_tags = sorted(_compute_tags(original_abi_tags, abi_tags)) final_plat_tags = sorted(_compute_tags(original_plat_tags, platform_tags)) final_tags = [ namever, ".".join(final_python_tags), ".".join(final_abi_tags), ".".join(final_plat_tags), ] if build: final_tags.insert(1, build) final_wheel_name = "-".join(final_tags) + ".whl" if original_wheel_name != final_wheel_name: del info["Tag"], info["Build"] for a, b, c in itertools.product( final_python_tags, final_abi_tags, final_plat_tags ): info["Tag"] = f"{a}-{b}-{c}" if build: info["Build"] = build original_wheel_path = os.path.join( os.path.dirname(f.filename), original_wheel_name ) final_wheel_path = os.path.join(os.path.dirname(f.filename), final_wheel_name) with ( WheelFile(original_wheel_path, "r") as fin, WheelFile(final_wheel_path, "w") as fout, ): fout.comment = fin.comment # preserve the comment for item in fin.infolist(): if item.is_dir(): continue if item.filename == f.dist_info_path + "/RECORD": continue if item.filename == f.dist_info_path + "/WHEEL": fout.writestr(item, info.as_bytes()) else: fout.writestr(item, fin.read(item)) if remove: os.remove(original_wheel_path) return final_wheel_name ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7311742 wheel-0.46.1/src/wheel/_commands/unpack.py0000644000000000000000000000177514775306464015354 0ustar00from __future__ import annotations from pathlib import Path from ..wheelfile import WheelFile def unpack(path: str, dest: str = ".") -> None: """Unpack a wheel. Wheel content will be unpacked to {dest}/{name}-{ver}, where {name} is the package name and {ver} its version. :param path: The path to the wheel. :param dest: Destination directory (default to current directory). """ with WheelFile(path) as wf: namever = wf.parsed_filename.group("namever") destination = Path(dest) / namever print(f"Unpacking to: {destination}...", end="", flush=True) for zinfo in wf.filelist: wf.extract(zinfo, destination) # Set permissions to the same values as they were set in the archive # We have to do this manually due to # https://github.com/python/cpython/issues/59999 permissions = zinfo.external_attr >> 16 & 0o777 destination.joinpath(zinfo.filename).chmod(permissions) print("OK") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/src/wheel/_metadata.py0000644000000000000000000001405414775306464014044 0ustar00""" Tools for converting old- to new-style metadata. """ from __future__ import annotations import functools import itertools import os.path import re import textwrap from collections.abc import Generator, Iterable, Iterator from email.message import Message from email.parser import Parser from typing import Literal from packaging.requirements import Requirement def _nonblank(str: str) -> bool | Literal[""]: return str and not str.startswith("#") @functools.singledispatch def yield_lines(iterable: Iterable[str]) -> Iterator[str]: r""" Yield valid lines of a string or iterable. >>> list(yield_lines('')) [] >>> list(yield_lines(['foo', 'bar'])) ['foo', 'bar'] >>> list(yield_lines('foo\nbar')) ['foo', 'bar'] >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) ['foo', 'baz #comment'] >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) ['foo', 'bar', 'baz', 'bing'] """ return itertools.chain.from_iterable(map(yield_lines, iterable)) @yield_lines.register(str) def _(text: str) -> Iterator[str]: return filter(_nonblank, map(str.strip, text.splitlines())) def split_sections( s: str | Iterator[str], ) -> Generator[tuple[str | None, list[str]], None, None]: """Split a string or iterable thereof into (section, content) pairs Each ``section`` is a stripped version of the section header ("[section]") and each ``content`` is a list of stripped lines excluding blank lines and comment-only lines. If there are any such lines before the first section header, they're returned in a first ``section`` of ``None``. """ section = None content: list[str] = [] for line in yield_lines(s): if line.startswith("["): if line.endswith("]"): if section or content: yield section, content section = line[1:-1].strip() content = [] else: raise ValueError("Invalid section heading", line) else: content.append(line) # wrap up last segment yield section, content def safe_extra(extra: str) -> str: """Convert an arbitrary string to a standard 'extra' name Any runs of non-alphanumeric characters are replaced with a single '_', and the result is always lowercased. """ return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower() def safe_name(name: str) -> str: """Convert an arbitrary string to a standard distribution name Any runs of non-alphanumeric/. characters are replaced with a single '-'. """ return re.sub("[^A-Za-z0-9.]+", "-", name) def requires_to_requires_dist(requirement: Requirement) -> str: """Return the version specifier for a requirement in PEP 345/566 fashion.""" if requirement.url: return " @ " + requirement.url requires_dist: list[str] = [] for spec in requirement.specifier: requires_dist.append(spec.operator + spec.version) if requires_dist: return " " + ",".join(sorted(requires_dist)) else: return "" def convert_requirements(requirements: list[str]) -> Iterator[str]: """Yield Requires-Dist: strings for parsed requirements strings.""" for req in requirements: parsed_requirement = Requirement(req) spec = requires_to_requires_dist(parsed_requirement) extras = ",".join(sorted(safe_extra(e) for e in parsed_requirement.extras)) if extras: extras = f"[{extras}]" yield safe_name(parsed_requirement.name) + extras + spec def generate_requirements( extras_require: dict[str | None, list[str]], ) -> Iterator[tuple[str, str]]: """ 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) extra = safe_extra(extra) if extra: yield "Provides-Extra", extra if condition: condition = "(" + condition + ") and " condition += f"extra == '{extra}'" if condition: condition = " ; " + condition for new_req in convert_requirements(depends): canonical_req = str(Requirement(new_req + condition)) yield "Requires-Dist", canonical_req 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 """ 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") if os.path.exists(requires_path): with open(requires_path, encoding="utf-8") as requires_file: requires = requires_file.read() 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"] if 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/src/wheel/bdist_wheel.py0000644000000000000000000000073414775306464014416 0ustar00from warnings import warn ERROR = """\ The 'wheel.bdist_wheel' module has been removed. Please update your setuptools to v70.1 or later. If you're explicitly importing 'wheel.bdist_wheel', please update your import to point \ to 'setuptools.command.bdist_wheel' instead. """ try: from setuptools.command.bdist_wheel import bdist_wheel as bdist_wheel except ModuleNotFoundError as exc: raise ImportError(ERROR) from exc warn(ERROR, DeprecationWarning, stacklevel=2) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/src/wheel/macosx_libfile.py0000644000000000000000000004051014775306464015101 0ustar00""" IMPORTANT: DO NOT IMPORT THIS MODULE DIRECTLY. THIS IS ONLY KEPT IN PLACE FOR BACKWARDS COMPATIBILITY WITH setuptools.command.bdist_wheel. This module contains function to analyse dynamic library headers to extract system information Currently only for MacOSX Library file on macosx system starts with Mach-O or Fat field. This can be distinguish by first 32 bites and it is called magic number. Proper value of magic number is with suffix _MAGIC. Suffix _CIGAM means reversed bytes order. Both fields can occur in two types: 32 and 64 bytes. FAT field inform that this library contains few version of library (typically for different types version). It contains information where Mach-O headers starts. Each section started with Mach-O header contains one library (So if file starts with this field it contains only one version). After filed Mach-O there are section fields. Each of them starts with two fields: cmd - magic number for this command cmdsize - total size occupied by this section information. In this case only sections LC_VERSION_MIN_MACOSX (for macosx 10.13 and earlier) and LC_BUILD_VERSION (for macosx 10.14 and newer) are interesting, because them contains information about minimal system version. Important remarks: - For fat files this implementation looks for maximum number version. It not check if it is 32 or 64 and do not compare it with currently built package. So it is possible to false report higher version that needed. - All structures signatures are taken form macosx header files. - I think that binary format will be more stable than `otool` output. and if apple introduce some changes both implementation will need to be updated. - The system compile will set the deployment target no lower than 11.0 for arm64 builds. For "Universal 2" builds use the x86_64 deployment target when the arm64 target is 11.0. """ from __future__ import annotations import ctypes import os import sys from io import BufferedIOBase from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union StrPath = Union[str, os.PathLike[str]] """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 LC_VERSION_MIN_MACOSX = 0x24 LC_BUILD_VERSION = 0x32 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), ] """ struct mach_header { uint32_t magic; /* mach magic number identifier */ cpu_type_t cputype; /* cpu specifier */ cpu_subtype_t cpusubtype; /* machine specifier */ uint32_t filetype; /* type of file */ uint32_t ncmds; /* number of load commands */ uint32_t sizeofcmds; /* the size of all the load commands */ uint32_t flags; /* flags */ }; typedef integer_t cpu_type_t; typedef integer_t cpu_subtype_t; """ mach_header_fields_64 = mach_header_fields + [("reserved", ctypes.c_uint32)] """ struct mach_header_64 { uint32_t magic; /* mach magic number identifier */ cpu_type_t cputype; /* cpu specifier */ cpu_subtype_t cpusubtype; /* machine specifier */ uint32_t filetype; /* type of file */ uint32_t ncmds; /* number of load commands */ uint32_t sizeofcmds; /* the size of all the load commands */ uint32_t flags; /* flags */ uint32_t reserved; /* reserved */ }; """ fat_header_fields = [("magic", ctypes.c_uint32), ("nfat_arch", ctypes.c_uint32)] """ struct fat_header { uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */ uint32_t nfat_arch; /* number of structs that follow */ }; """ fat_arch_fields = [ ("cputype", ctypes.c_int), ("cpusubtype", ctypes.c_int), ("offset", ctypes.c_uint32), ("size", ctypes.c_uint32), ("align", ctypes.c_uint32), ] """ struct fat_arch { cpu_type_t cputype; /* cpu specifier (int) */ cpu_subtype_t cpusubtype; /* machine specifier (int) */ uint32_t offset; /* file offset to this object file */ uint32_t size; /* size of this object file */ uint32_t align; /* alignment as a power of 2 */ }; """ 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), ] """ struct fat_arch_64 { cpu_type_t cputype; /* cpu specifier (int) */ cpu_subtype_t cpusubtype; /* machine specifier (int) */ uint64_t offset; /* file offset to this object file */ uint64_t size; /* size of this object file */ uint32_t align; /* alignment as a power of 2 */ uint32_t reserved; /* reserved */ }; """ segment_base_fields = [("cmd", ctypes.c_uint32), ("cmdsize", ctypes.c_uint32)] """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), ("flags", ctypes.c_uint32), ] """ struct segment_command { /* for 32-bit architectures */ uint32_t cmd; /* LC_SEGMENT */ uint32_t cmdsize; /* includes sizeof section structs */ char segname[16]; /* segment name */ uint32_t vmaddr; /* memory address of this segment */ uint32_t vmsize; /* memory size of this segment */ uint32_t fileoff; /* file offset of this segment */ uint32_t filesize; /* amount to map from the file */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ }; typedef int vm_prot_t; """ 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), ("flags", ctypes.c_uint32), ] """ struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ uint32_t cmdsize; /* includes sizeof section_64 structs */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment */ uint64_t vmsize; /* memory size of this segment */ uint64_t fileoff; /* file offset of this segment */ uint64_t filesize; /* amount to map from the file */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ }; """ 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 LC_VERSION_MIN_IPHONEOS or LC_VERSION_MIN_WATCHOS or LC_VERSION_MIN_TVOS */ uint32_t cmdsize; /* sizeof(struct min_version_command) */ uint32_t version; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ uint32_t sdk; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ }; """ 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 */ uint32_t cmdsize; /* sizeof(struct build_version_command) plus */ /* ntools * sizeof(struct build_tool_version) */ uint32_t platform; /* platform */ uint32_t minos; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ uint32_t sdk; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ uint32_t ntools; /* number of tool entries following this */ }; """ def swap32(x: int) -> int: return ( ((x << 24) & 0xFF000000) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) | ((x >> 24) & 0x000000FF) ) def get_base_class_and_magic_number( lib_file: BufferedIOBase, seek: int | None = None, ) -> tuple[type[ctypes.Structure], int]: if seek is None: seek = lib_file.tell() else: lib_file.seek(seek) magic_number = ctypes.c_uint32.from_buffer_copy( 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]: if sys.byteorder == "little": BaseClass = ctypes.BigEndianStructure else: BaseClass = ctypes.LittleEndianStructure magic_number = swap32(magic_number) else: BaseClass = ctypes.Structure lib_file.seek(seek) return BaseClass, magic_number def read_data(struct_class: type[ctypes.Structure], lib_file: BufferedIOBase): return struct_class.from_buffer_copy(lib_file.read(ctypes.sizeof(struct_class))) def extract_macosx_min_system_version(path_to_lib: str): with open(path_to_lib, "rb") as lib_file: BaseClass, magic_number = get_base_class_and_magic_number(lib_file, 0) if magic_number not in [FAT_MAGIC, FAT_MAGIC_64, MH_MAGIC, MH_MAGIC_64]: return if magic_number in [FAT_MAGIC, FAT_CIGAM_64]: class FatHeader(BaseClass): _fields_ = fat_header_fields fat_header = read_data(FatHeader, lib_file) if magic_number == FAT_MAGIC: 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) ] versions_list: list[tuple[int, int, int]] = [] for el in fat_arch_list: try: version = read_mach_header(lib_file, el.offset) if version is not None: if el.cputype == CPU_TYPE_ARM64 and len(fat_arch_list) != 1: # Xcode will not set the deployment target below 11.0.0 # for the arm64 architecture. Ignore the arm64 deployment # in fat binaries when the target is 11.0.0, that way # the other architectures can select a lower deployment # target. # This is safe because there is no arm64 variant for # macOS 10.15 or earlier. if version == (11, 0, 0): continue versions_list.append(version) except ValueError: pass if len(versions_list) > 0: return max(versions_list) else: return None else: try: return read_mach_header(lib_file, 0) except ValueError: """when some error during read library files""" return None def read_mach_header( lib_file: BufferedIOBase, seek: int | None = None, ) -> tuple[int, int, int] | None: """ This function parses a Mach-O header and extracts information about the minimal macOS version. :param lib_file: reference to opened library file with pointer """ base_class, magic_number = get_base_class_and_magic_number(lib_file, seek) arch = "32" if magic_number == MH_MAGIC else "64" class SegmentBase(base_class): _fields_ = segment_base_fields if arch == "32": class MachHeader(base_class): _fields_ = mach_header_fields else: class MachHeader(base_class): _fields_ = mach_header_fields_64 mach_header = read_data(MachHeader, lib_file) for _i in range(mach_header.ncmds): pos = lib_file.tell() 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 version_info = read_data(VersionBuild, lib_file) return parse_version(version_info.minos) else: lib_file.seek(pos + segment_base.cmdsize) continue def parse_version(version: int) -> tuple[int, int, int]: x = (version & 0xFFFF0000) >> 16 y = (version & 0x0000FF00) >> 8 z = version & 0x000000FF return x, y, z def calculate_macosx_platform_tag(archive_root: StrPath, platform_tag: str) -> str: """ Calculate proper macosx platform tag basing on files which are included to wheel 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(".")) 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 = 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), ) ) else: base_version = deploy_target assert len(base_version) == 2 start_version = base_version versions_dict: dict[str, tuple[int, int]] = {} for dirpath, _dirnames, filenames in os.walk(archive_root): for filename in filenames: 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: min_ver = min_ver[0:2] if min_ver[0] > 10: min_ver = (min_ver[0], 0) versions_dict[lib_path] = min_ver if len(versions_dict) > 0: base_version = max(base_version, max(versions_dict.values())) # macosx platform tag do not support minor bugfix release fin_base_version = "_".join([str(x) for x in base_version]) if start_version < base_version: problematic_files = [k for k, v in versions_dict.items() if v > start_version] problematic_files = "\n".join(problematic_files) if len(problematic_files) == 1: 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 " "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." ) else: error_message = error_message.format( "the version your Python interpreter is compiled against." ) sys.stderr.write(error_message) platform_tag = prefix + "_" + fin_base_version + "_" + suffix return platform_tag ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/src/wheel/metadata.py0000644000000000000000000000136514775306464013706 0ustar00from warnings import warn from ._metadata import convert_requirements as convert_requirements from ._metadata import generate_requirements as generate_requirements from ._metadata import pkginfo_to_metadata as pkginfo_to_metadata from ._metadata import requires_to_requires_dist as requires_to_requires_dist from ._metadata import safe_extra as safe_extra from ._metadata import safe_name as safe_name from ._metadata import split_sections as split_sections warn( f"The {__name__!r} package has been made private and should no longer be imported. " f"Please either copy the code or find an alternative library to import it from, as " f"this warning will be removed in a future version of 'wheel'.", DeprecationWarning, stacklevel=2, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/src/wheel/wheelfile.py0000644000000000000000000002102014775306464014060 0ustar00from __future__ import annotations __all__ = ["WHEEL_INFO_RE", "WheelFile", "WheelError"] import base64 import csv import hashlib import logging import os.path import re import stat import time from io import StringIO, TextIOWrapper from typing import IO, TYPE_CHECKING, Literal from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo if TYPE_CHECKING: from _typeshed import SizedBuffer, StrPath # 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[^\s-]+?)-(?P[^\s-]+?))(-(?P\d[^\s-]*))? -(?P[^\s-]+?)-(?P[^\s-]+?)-(?P\S+)\.whl$""", re.VERBOSE, ) MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC log = logging.getLogger("wheel") class WheelError(Exception): pass def urlsafe_b64encode(data: bytes) -> bytes: """urlsafe_b64encode without padding""" return base64.urlsafe_b64encode(data).rstrip(b"=") def urlsafe_b64decode(data: bytes) -> bytes: """urlsafe_b64decode without padding""" pad = b"=" * (4 - (len(data) & 3)) return base64.urlsafe_b64decode(data + pad) def get_zipinfo_datetime( timestamp: float | None = None, ) -> tuple[int, int, int, int, int]: # 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] class WheelFile(ZipFile): """A ZipFile derivative class that also reads SHA-256 hashes from .dist-info/RECORD and checks any read files against those. """ _default_algorithm = hashlib.sha256 def __init__( self, file: StrPath, mode: Literal["r", "w", "x", "a"] = "r", compression: int = 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(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._file_hashes: dict[str, tuple[None, None] | tuple[int, bytes]] = {} self._file_sizes = {} 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 # Fill in the expected hashes by reading them from RECORD try: record = self.open(self.record_path) except KeyError: raise WheelError(f"Missing {self.record_path} file") from None with 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("=") try: hashlib.new(algorithm) except ValueError: raise WheelError( f"Unsupported hash algorithm: {algorithm}" ) from None if algorithm.lower() in {"md5", "sha1"}: raise WheelError( f"Weak hash algorithm ({algorithm}) is not permitted by " f"PEP 427" ) self._file_hashes[path] = ( algorithm, urlsafe_b64decode(hash_sum.encode("ascii")), ) def open( self, name_or_info: str | ZipInfo, mode: Literal["r", "w"] = "r", pwd: bytes | None = None, ) -> IO[bytes]: def _update_crc(newdata: bytes) -> None: eof = ef._eof update_crc_orig(newdata) running_hash.update(newdata) if eof and running_hash.digest() != expected_hash: 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("/"): 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 running_hash = hashlib.new(algorithm) update_crc_orig, ef._update_crc = ef._update_crc, _update_crc return ef def write_files(self, base_dir: str) -> None: log.info("creating %r and adding %r to it", self.filename, base_dir) deferred: list[tuple[str, str]] = [] for root, dirnames, filenames in os.walk(base_dir): # Sort the directory names so that `os.walk` will walk them in a # defined order on the next iteration. dirnames.sort() 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, "/") if arcname == self.record_path: pass elif root.endswith(".dist-info"): deferred.append((path, arcname)) else: self.write(path, arcname) deferred.sort() for path, arcname in deferred: self.write(path, arcname) def write( self, filename: str, arcname: str | None = None, compress_type: int | None = None, ) -> None: 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.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: str | ZipInfo, data: SizedBuffer | str, compress_type: int | None = None, ) -> None: if isinstance(zinfo_or_arcname, str): zinfo_or_arcname = ZipInfo( zinfo_or_arcname, date_time=get_zipinfo_datetime() ) zinfo_or_arcname.compress_type = self.compression zinfo_or_arcname.external_attr = (0o664 | stat.S_IFREG) << 16 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("adding %r", fname) if fname != self.record_path: 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) -> None: # Write RECORD 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( ( (fname, algorithm + "=" + hash_, self._file_sizes[fname]) for fname, (algorithm, hash_) in self._file_hashes.items() ) ) writer.writerow((format(self.record_path), "", "")) self.writestr(self.record_path, data.getvalue()) ZipFile.close(self) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/__init__.py0000644000000000000000000000000014775306464014716 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/test_convert.py0000644000000000000000000002170314775306464015713 0ustar00from __future__ import annotations import zipfile from email.message import Message from pathlib import Path from textwrap import dedent import pytest from _pytest.fixtures import SubRequest from pytest import TempPathFactory import wheel from commands.util import run_command from wheel._commands.convert import convert_pkg_info, egg_filename_re from wheel.wheelfile import WheelFile PKG_INFO = """\ Metadata-Version: 2.1 Name: Sampledist Version: 1.0.0 Author: Alex Grönholm Author-email: alex.gronholm@example.com Home-page: https://example.com Download-URL: https://example.com/sampledist License: Sample license text second row third row fourth row Description: Sample Distribution =================== Test description """.encode() REQUIRES_TXT = b"""\ somepackage>=1.5 otherpackage>=1.7 [:python_version < '3'] six """ EXPECTED_METADATA = """\ Metadata-Version: 2.4 Name: Sampledist Version: 1.0.0 Author: Alex Grönholm Author-email: alex.gronholm@example.com Project-URL: Homepage, https://example.com Project-URL: Download, https://example.com/sampledist License: Sample license text second row third row fourth row Requires-Dist: somepackage>=1.5 Requires-Dist: otherpackage>=1.7 Requires-Dist: six; python_version < "3" Sample Distribution =================== Test description """.encode() @pytest.fixture( params=[ pytest.param(("py3.7", "win32"), id="win32"), pytest.param(("py3.7", "win_amd64"), id="amd64"), pytest.param((None, "any"), id="pure"), ] ) def pyver_arch(request: SubRequest) -> tuple[str | None, str]: return request.param @pytest.fixture def pyver(pyver_arch: tuple[str | None, str]) -> str | None: return pyver_arch[0] @pytest.fixture def arch(pyver_arch: tuple[str | None, str]) -> str: return pyver_arch[1] @pytest.fixture def expected_wheelfile(arch: str) -> bytes: root_is_purelib = str(arch == "any").lower() text = dedent( f"""\ Wheel-Version: 1.0 Generator: wheel {wheel.__version__} Root-Is-Purelib: {root_is_purelib} """ ) if arch == "any": text += "Tag: py2-none-any\nTag: py3-none-any\n\n" else: text += f"Tag: py37-cp37-{arch}\n\n" return text.encode("utf-8") @pytest.fixture def bdist_wininst_path(arch: str, pyver: str | None, tmp_path: Path) -> str: # As bdist_wininst is no longer present in Python, and carrying .exe files in the # tarball is risky, we have to fake this a bit if pyver: filename = f"Sampledist-1.0.0-{arch.replace('_', '-')}-{pyver}.exe" pyver_suffix = f"-{pyver}" else: filename = f"Sampledist-1.0.0-{arch.replace('_', '-')}.exe" pyver_suffix = "" bdist_path = tmp_path / filename prefix = "PURELIB" if arch == "any" else "PLATLIB" with zipfile.ZipFile(bdist_path, "w") as zip: zip.writestr(f"{prefix}/", b"") zip.writestr(f"{prefix}/sampledist/", b"") zip.writestr(f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/", b"") zip.writestr(f"{prefix}/sampledist/__init__.py", b"") if arch != "any": zip.writestr(f"{prefix}/sampledist/_extmodule.cp37-{arch}.pyd", b"") zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/dependency_links.txt", b"", ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/PKG-INFO", PKG_INFO, ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/SOURCES.txt", b"" ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/top_level.txt", b"" ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/entry_points.txt", b"" ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/requires.txt", REQUIRES_TXT, ) zip.writestr(f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/zip-safe", b"") zip.writestr("SCRIPTS/somecommand", b"#!python\nprint('hello')") return str(bdist_path) @pytest.fixture def egg_path(arch: str, pyver: str | None, tmp_path: Path) -> Path: if pyver: filename = f"Sampledist-1.0.0-{pyver}-{arch}.egg" else: filename = "Sampledist-1.0.0.egg" bdist_path = tmp_path / filename with zipfile.ZipFile(bdist_path, "w") as zip: zip.writestr("sampledist/", b"") zip.writestr("EGG-INFO/", b"") zip.writestr("sampledist/__init__.py", b"") zip.writestr(f"sampledist/_extmodule.cp37-{arch}.pyd", b"") zip.writestr("EGG-INFO/dependency_links.txt", b"") zip.writestr("EGG-INFO/PKG-INFO", PKG_INFO) zip.writestr("EGG-INFO/SOURCES.txt", b"") zip.writestr("EGG-INFO/top_level.txt", b"") zip.writestr("EGG-INFO/entry_points.txt", b"") zip.writestr("EGG-INFO/requires.txt", REQUIRES_TXT) zip.writestr("EGG-INFO/zip-safe", b"") return bdist_path @pytest.fixture def expected_wheel_filename(pyver: str | None, arch: str) -> str: if arch != "any": pyver = pyver.replace(".", "") if pyver else "py2.py3" abiver = pyver.replace("py", "cp") return f"sampledist-1.0.0-{pyver}-{abiver}-{arch}.whl" else: return "sampledist-1.0.0-py2.py3-none-any.whl" def test_egg_re() -> None: """Make sure egg_info_re matches.""" egg_names_path = Path(__file__).parent.parent / "testdata" / "eggnames.txt" with egg_names_path.open(encoding="utf-8") as egg_names: for line in egg_names: line = line.strip() if line: assert egg_filename_re.match(line), line def test_convert_egg_file( egg_path: Path, tmp_path: Path, arch: str, expected_wheelfile: bytes, expected_wheel_filename: str, ) -> None: output = run_command("convert", "-v", "--dest", tmp_path, egg_path) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") assert wheel_path.name == expected_wheel_filename with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" assert output == f"{egg_path}...OK\n" def test_convert_egg_directory( egg_path: Path, tmp_path: Path, tmp_path_factory: TempPathFactory, pyver: str | None, arch: str, expected_wheelfile: bytes, expected_wheel_filename: str, ) -> None: with zipfile.ZipFile(egg_path) as egg_file: egg_dir_path = tmp_path_factory.mktemp("eggdir") / Path(egg_path).name egg_dir_path.mkdir() egg_file.extractall(egg_dir_path) output = run_command("convert", "-v", "--dest", tmp_path, egg_dir_path) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") assert wheel_path.name == expected_wheel_filename with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" assert output == f"{egg_dir_path}...OK\n" def test_convert_bdist_wininst( bdist_wininst_path: str, tmp_path: Path, arch: str, expected_wheelfile: bytes, expected_wheel_filename: str, ) -> None: output = run_command("convert", "-v", "--dest", tmp_path, bdist_wininst_path) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") assert wheel_path.name == expected_wheel_filename with WheelFile(wheel_path) as wf: assert ( wf.read("sampledist-1.0.0.data/scripts/somecommand") == b"#!python\nprint('hello')" ) assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" assert output == f"{bdist_wininst_path}...OK\n" def test_convert_pkg_info_with_empty_description() -> None: # Regression test for https://github.com/pypa/wheel/issues/645 pkginfo = """\ Metadata-Version: 2.1 Name: Sampledist Version: 1.0.0 Home-page: https://example.com Download-URL: https://example.com/sampledist Description:""" message = Message() convert_pkg_info(pkginfo, message) assert message.get_all("Name") == ["Sampledist"] assert message.get_payload() == "\n" def test_convert_pkg_info_with_one_line_description() -> None: pkginfo = """\ Metadata-Version: 2.1 Name: Sampledist Version: 1.0.0 Home-page: https://example.com Download-URL: https://example.com/sampledist Description: My cool package""" message = Message() convert_pkg_info(pkginfo, message) assert message.get_all("Name") == ["Sampledist"] assert message.get_payload() == "My cool package\n\n\n" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/test_pack.py0000644000000000000000000000641014775306464015147 0ustar00from __future__ import annotations import email.policy import os from email.message import Message from email.parser import BytesParser from zipfile import Path, ZipFile import pytest from pytest import TempPathFactory from .util import run_command THISDIR = os.path.dirname(__file__) 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", [ pytest.param(None, None, "test-1.0-py2.py3-none-any.whl", id="nobuildnum"), pytest.param("2b", None, "test-1.0-2b-py2.py3-none-any.whl", id="newbuildarg"), pytest.param(None, "3", "test-1.0-3-py2.py3-none-any.whl", id="oldbuildnum"), pytest.param("", "3", "test-1.0-py2.py3-none-any.whl", id="erasebuildnum"), ], ) def test_pack( tmp_path_factory: TempPathFactory, tmp_path: Path, build_tag_arg: str | None, existing_build_tag: str | None, filename: str, ) -> None: unpack_dir = tmp_path_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,") ) zf.extractall(unpack_dir) if existing_build_tag: # Add the build number to WHEEL wheel_file_path = unpack_dir.joinpath("test-1.0.dist-info").joinpath("WHEEL") wheel_file_content = wheel_file_path.read_bytes() assert b"Build" not in wheel_file_content wheel_file_content += b"Build: 3\r\n" wheel_file_path.write_bytes(wheel_file_content) args = ["--dest", tmp_path, unpack_dir] if build_tag_arg is not None: (args.insert(3, "--build"),) args.insert(4, build_tag_arg) run_command("pack", *args) new_wheel_path = tmp_path.joinpath(filename) assert new_wheel_path.is_file() with ZipFile(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,") ) parser = BytesParser(policy=email.policy.compat32) new_wheel_file_content = parser.parsebytes(zf.read("test-1.0.dist-info/WHEEL")) assert new_record_lines == old_record_lines # Line endings and trailing blank line will depend on whether WHEEL # was modified. Circumvent this by comparing parsed key/value pairs. expected_wheel_content = Message() expected_wheel_content["Wheel-Version"] = "1.0" expected_wheel_content["Generator"] = "bdist_wheel (0.30.0)" expected_wheel_content["Root-Is-Purelib"] = "false" expected_wheel_content["Tag"] = "py2-none-any" expected_wheel_content["Tag"] = "py3-none-any" expected_build_num = ( build_tag_arg if build_tag_arg is not None else existing_build_tag ) if expected_build_num: expected_wheel_content["Build"] = expected_build_num assert sorted(new_wheel_file_content.items()) == sorted( expected_wheel_content.items() ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/test_tags.py0000644000000000000000000001635414775306464015177 0ustar00from __future__ import annotations import shutil from pathlib import Path from subprocess import CalledProcessError from zipfile import ZipFile import pytest from wheel.wheelfile import WheelFile from .util import run_command TESTWHEEL_NAME = "test-1.0-py2.py3-none-any.whl" TESTWHEEL_PATH = Path(__file__).parent.parent / "testdata" / TESTWHEEL_NAME @pytest.fixture def wheelpath(tmp_path: Path) -> Path: wheels_dir = tmp_path / "wheels" wheels_dir.mkdir() fn = wheels_dir / TESTWHEEL_NAME shutil.copy(TESTWHEEL_PATH, fn) return fn def test_tags_no_args(wheelpath: Path) -> None: newname = run_command("tags", wheelpath).strip() assert newname == TESTWHEEL_NAME assert wheelpath.exists() def test_python_tags(wheelpath: Path) -> None: newname = run_command("tags", "--python-tag", "py3", wheelpath).strip() assert newname == TESTWHEEL_NAME.replace("py2.py3", "py3") output_file = wheelpath.parent / newname with WheelFile(output_file) as f: output = f.read(f.dist_info_path + "/WHEEL") assert ( output == b"Wheel-Version: 1.0\nGenerator: bdist_wheel (0.30.0)" b"\nRoot-Is-Purelib: false\nTag: py3-none-any\n\n" ) output_file.unlink() newname = run_command("tags", wheelpath, "--python-tag", "py2.py3").strip() assert newname == TESTWHEEL_NAME newname = run_command("tags", "--remove", "--python-tag", "+py4", wheelpath).strip() assert not wheelpath.exists() assert newname == TESTWHEEL_NAME.replace("py2.py3", "py2.py3.py4") output_file = wheelpath.parent / newname output_file.unlink() def test_abi_tags(wheelpath: Path) -> None: newname = run_command("tags", wheelpath, "--abi-tag", "cp33m").strip() assert newname == TESTWHEEL_NAME.replace("none", "cp33m") output_file = wheelpath.parent / newname output_file.unlink() newname = run_command("tags", wheelpath, "--abi-tag", "cp33m.abi3").strip() assert newname == TESTWHEEL_NAME.replace("none", "abi3.cp33m") output_file = wheelpath.parent / newname output_file.unlink() newname = run_command("tags", wheelpath, "--abi-tag", "none").strip() assert newname == TESTWHEEL_NAME newname = run_command( "tags", wheelpath, "--remove", "--abi-tag", "+abi3.cp33m" ).strip() assert not wheelpath.exists() assert newname == TESTWHEEL_NAME.replace("none", "abi3.cp33m.none") output_file = wheelpath.parent / newname output_file.unlink() def test_plat_tags(wheelpath: Path) -> None: newname = run_command("tags", "--platform-tag", "linux_x86_64", wheelpath).strip() assert newname == TESTWHEEL_NAME.replace("any", "linux_x86_64") output_file = wheelpath.parent / newname assert output_file.exists() output_file.unlink() newname = run_command( "tags", "--platform-tag", "linux_x86_64.win32", wheelpath ).strip() assert newname == TESTWHEEL_NAME.replace("any", "linux_x86_64.win32") output_file = wheelpath.parent / newname assert output_file.exists() output_file.unlink() newname = run_command( "tags", "--platform-tag", "+linux_x86_64.win32", wheelpath ).strip() assert newname == TESTWHEEL_NAME.replace("any", "any.linux_x86_64.win32") output_file = wheelpath.parent / newname assert output_file.exists() output_file.unlink() newname = run_command( "tags", "--platform-tag", "+linux_x86_64.win32", wheelpath ).strip() assert newname == TESTWHEEL_NAME.replace("any", "any.linux_x86_64.win32") output_file = wheelpath.parent / newname assert output_file.exists() newname2 = run_command("tags", "--platform-tag=-any", output_file).strip() output_file.unlink() assert newname2 == TESTWHEEL_NAME.replace("any", "linux_x86_64.win32") output_file2 = wheelpath.parent / newname2 assert output_file2.exists() output_file2.unlink() newname = run_command("tags", "--platform-tag", "any", wheelpath).strip() assert newname == TESTWHEEL_NAME def test_build_tag(wheelpath: Path) -> None: newname = run_command("tags", "--build", "1bah", wheelpath).strip() assert newname == TESTWHEEL_NAME.replace("-py2", "-1bah-py2") output_file = wheelpath.parent / newname assert output_file.exists() newname = run_command("tags", "--build", "", wheelpath).strip() assert newname == TESTWHEEL_NAME output_file.unlink() @pytest.mark.parametrize( "build_tag, error", [ pytest.param("foo", "build tag must begin with a digit", id="digitstart"), pytest.param("1-f", "invalid character ('-') in build tag", id="hyphen"), ], ) def test_invalid_build_tag(wheelpath: Path, build_tag: str, error: str) -> None: with pytest.raises(CalledProcessError) as exc_info: run_command("tags", "--build", build_tag, wheelpath, catch_systemexit=False) exc = exc_info.value assert exc.returncode == 2 assert f"error: argument --build: {error}" in exc.stderr def test_multi_tags(wheelpath: Path) -> None: newname = run_command( "tags", "--platform-tag", "linux_x86_64", "--python-tag", "+py4", "--build", "1", wheelpath, ).strip() assert newname == "test-1.0-1-py2.py3.py4-none-linux_x86_64.whl" output_file = wheelpath.parent / newname assert output_file.exists() with WheelFile(output_file) as f: output = f.read(f.dist_info_path + "/WHEEL") assert output == ( b"Wheel-Version: 1.0\n" b"Generator: bdist_wheel (0.30.0)\nRoot-Is-Purelib: false\n" b"Tag: py2-none-linux_x86_64\n" b"Tag: py3-none-linux_x86_64\n" b"Tag: py4-none-linux_x86_64\n" b"Build: 1\n\n" ) output_file.unlink() def test_tags_command(wheelpath: Path) -> None: newname = run_command( "tags", "--python-tag", "py3", "--abi-tag", "cp33m", "--platform-tag", "linux_x86_64", "--build", "7", wheelpath, ).strip() assert "test-1.0-7-py3-cp33m-linux_x86_64.whl" == newname output_file = wheelpath.parent / newname output_file.unlink() def test_tags_command_del(wheelpath: Path) -> None: newname = run_command( "tags", "--python-tag", "+py4", "--abi-tag", "cp33m", "--platform-tag", "linux_x86_64", "--remove", wheelpath, ).strip() assert "test-1.0-py2.py3.py4-cp33m-linux_x86_64.whl" == newname output_file = wheelpath.parent / newname output_file.unlink() def test_permission_bits(wheelpath: Path) -> None: newname = run_command("tags", "--python-tag", "+py4", wheelpath).strip() assert "test-1.0-py2.py3.py4-none-any.whl" == newname output_file = wheelpath.parent / newname with ZipFile(output_file, "r") as outf, ZipFile(wheelpath, "r") as inf: for member in inf.namelist(): member_info = inf.getinfo(member) if member_info.is_dir(): continue if member_info.filename.endswith("/RECORD"): continue out_attr = outf.getinfo(member).external_attr inf_attr = member_info.external_attr assert out_attr == inf_attr, ( f"{member} 0x{out_attr:012o} != 0x{inf_attr:012o}" ) output_file.unlink() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/test_unpack.py0000644000000000000000000000355214775306464015516 0ustar00from __future__ import annotations import platform import stat from pathlib import Path import pytest from pytest import TempPathFactory from wheel.wheelfile import WheelFile from .util import run_command def test_unpack(tmp_path_factory: TempPathFactory) -> None: wheel_path = tmp_path_factory.mktemp("build") / "test-1.0-py3-none-any.whl" with WheelFile(wheel_path, "w") as wf: wf.writestr( "test-1.0.dist-info/METADATA", "Metadata-Version: 2.4\nName: test\nVersion: 1.0\n", ) wf.writestr("test-1.0/package/__init__.py", "") wf.writestr("test-1.0/package/module.py", "print('hello world')\n") extract_path = tmp_path_factory.mktemp("extract") run_command("unpack", "--dest", extract_path, wheel_path) extract_path /= "test-1.0" assert extract_path.joinpath("test-1.0.dist-info", "METADATA").read_text( "utf-8" ) == ("Metadata-Version: 2.4\nName: test\nVersion: 1.0\n") assert ( extract_path.joinpath("test-1.0", "package", "__init__.py").read_text("utf-8") == "" ) assert extract_path.joinpath("test-1.0", "package", "module.py").read_text( "utf-8" ) == ("print('hello world')\n") @pytest.mark.skipif( platform.system() == "Windows", reason="Windows does not support the executable bit" ) def test_unpack_executable_bit(tmp_path: Path) -> None: wheel_path = tmp_path / "test-1.0-py3-none-any.whl" script_path = tmp_path / "script" script_path.write_bytes(b"test script") script_path.chmod(0o755) with WheelFile(wheel_path, "w") as wf: wf.write(str(script_path), "nested/script") script_path.unlink() script_path = tmp_path / "test-1.0" / "nested" / "script" run_command("unpack", "--dest", tmp_path, wheel_path) assert not script_path.is_dir() assert stat.S_IMODE(script_path.stat().st_mode) == 0o755 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/commands/util.py0000644000000000000000000000213414775306464014146 0ustar00from __future__ import annotations import sys from io import StringIO from os import PathLike from subprocess import CalledProcessError from unittest.mock import patch import pytest from wheel._commands import main def run_command( command: str, *args: str | PathLike, catch_systemexit: bool = True ) -> str: returncode = 0 stdout = StringIO() stderr = StringIO() args = ("wheel", command) + tuple(str(arg) for arg in args) with ( patch.object(sys, "argv", args), patch.object(sys, "stdout", stdout), patch.object(sys, "stderr", stderr), ): try: main() except SystemExit as exc: if not catch_systemexit: raise CalledProcessError( exc.code, args, stdout.getvalue(), stderr.getvalue() ) from exc returncode = exc.code if returncode: pytest.fail( f"'wheel {command}' exited with return code {returncode}\n" f"arguments: {args}\n" f"error output:\n{stderr.getvalue()}" ) return stdout.getvalue() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7321742 wheel-0.46.1/tests/test_bdist_wheel.py0000644000000000000000000000030314775306464014714 0ustar00import pytest def test_import_bdist_wheel() -> None: with pytest.warns(DeprecationWarning, match="module has been removed"): from wheel.bdist_wheel import bdist_wheel # noqa: F401 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7331743 wheel-0.46.1/tests/test_metadata.py0000644000000000000000000000477114775306464014220 0ustar00from __future__ import annotations from pathlib import Path import pytest from wheel._metadata import pkginfo_to_metadata def test_pkginfo_to_metadata(tmp_path: Path) -> None: 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"'), ] pkg_info = tmp_path.joinpath("PKG-INFO") pkg_info.write_text( """\ Metadata-Version: 0.0 Name: spam Version: 0.1 Provides-Extra: empty+extra Provides-Extra: test Provides-Extra: reST Provides-Extra: signatures Provides-Extra: Signatures Provides-Extra: faster-signatures""", encoding="utf-8", ) egg_info_dir = tmp_path.joinpath("test.egg-info") egg_info_dir.mkdir(exist_ok=True) egg_info_dir.joinpath("requires.txt").write_text( """\ pip@https://github.com/pypa/pip/archive/1.3.1.zip [extra] bar @ http://host/bar.zip [empty+extra] [:sys_platform=="win32"] pywin32 foo @http://host/foo.zip [faster-signatures] ed25519ll [reST] docutils>=0.8 [signatures] keyring keyrings.alt [Signatures:sys_platform!="win32"] pyxdg [test] pytest>=3.0.0 pytest-cov""", encoding="utf-8", ) message = pkginfo_to_metadata( egg_info_path=str(egg_info_dir), pkginfo_path=str(pkg_info) ) assert message.items() == expected_metadata def test_metadata_deprecated() -> None: with pytest.warns(DeprecationWarning, match="has been made private"): from wheel import metadata assert hasattr(metadata, "pkginfo_to_metadata") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7331743 wheel-0.46.1/tests/test_sdist.py0000644000000000000000000000242614775306464013561 0ustar00import subprocess import sys import tarfile from pathlib import Path import pytest from pytest import MonkeyPatch pytest.importorskip("flit") pytest.importorskip("build") # This test must be run from the source directory - okay to skip if not DIR = Path(__file__).parent.resolve() MAIN_DIR = DIR.parent def test_compare_sdists(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: monkeypatch.chdir(MAIN_DIR) sdist_build_dir = tmp_path / "bdir" subprocess.run( [ sys.executable, "-m", "build", "--sdist", "--no-isolation", f"--outdir={sdist_build_dir}", ], check=True, ) (sdist_build,) = sdist_build_dir.glob("*.tar.gz") # Flit doesn't allow targeting directories, as far as I can tell process = subprocess.run( [sys.executable, "-m", "flit", "build", "--format=sdist"], stderr=subprocess.PIPE, ) if process.returncode != 0: pytest.fail(process.stderr.decode("utf-8")) (sdist_flit,) = Path("dist").glob("*.tar.gz") out = [set(), set()] for i, sdist in enumerate([sdist_build, sdist_flit]): with tarfile.open(sdist, "r:gz") as tar: out[i] = set(tar.getnames()) assert out[0] == (out[1] - {"setup.py"}) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7331743 wheel-0.46.1/tests/test_wheelfile.py0000644000000000000000000001564214775306464014403 0ustar00from __future__ import annotations import stat import sys from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile import pytest from pytest import MonkeyPatch, TempPathFactory from wheel.wheelfile import WheelError, WheelFile @pytest.fixture def wheel_path(tmp_path: Path) -> Path: return tmp_path.joinpath("test-1.0-py2.py3-none-any.whl") @pytest.mark.parametrize( "filename", [ "foo-2-py3-none-any.whl", "foo-2-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", ], ) def test_wheelfile_re(filename: str, tmp_path: Path) -> None: # Regression test for #208 and #485 path = tmp_path / filename with WheelFile(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", "test-1.0-py 2-none-any.whl", ], ) def test_bad_wheel_filename(filename: str) -> None: exc = pytest.raises(WheelError, WheelFile, filename) exc.match(f"^Bad wheel filename {filename!r}$") def test_missing_record(wheel_path: Path) -> None: 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: Path) -> None: 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", "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", [ pytest.param("md5", "4J-scNa2qvSgy07rS4at-Q", id="md5"), pytest.param("sha1", "QjCnGu5Qucb6-vir1a6BVptvOA4", id="sha1"), ], ) def test_weak_hash_algorithm(wheel_path: Path, algorithm: str, digest: str) -> None: 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(rf"^Weak hash algorithm \({algorithm}\) is not permitted by PEP 427$") @pytest.mark.parametrize( "algorithm, digest", [ pytest.param( "sha256", "bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo", id="sha256" ), pytest.param( "sha384", "cDXriAy_7i02kBeDkN0m2RIDz85w6pwuHkt2PZ4VmT2PQc1TZs8Ebvf6eKDFcD_S", id="sha384", ), pytest.param( "sha512", "kdX9CQlwNt4FfOpOKO_X0pn_v1opQuksE40SrWtMyP1NqooWVWpzCE3myZTfpy8g2azZON_" "iLNpWVxTwuDWqBQ", id="sha512", ), ], ) def test_testzip(wheel_path: Path, algorithm: str, digest: str) -> None: 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: Path) -> None: 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("^No hash found for file 'hello/héllö.py'$") def test_testzip_bad_hash(wheel_path: Path) -> None: 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", "hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25", ) with WheelFile(wheel_path) as wf: exc = pytest.raises(WheelError, wf.testzip) exc.match("^Hash mismatch for file 'hello/héllö.py'$") def test_write_str(wheel_path: Path) -> None: 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: infolist = zf.infolist() assert len(infolist) == 3 assert infolist[0].filename == "hello/héllö.py" assert infolist[0].file_size == 25 assert infolist[1].filename == "hello/h,ll,.py" assert infolist[1].file_size == 25 assert infolist[2].filename == "test-1.0.dist-info/RECORD" 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" ) def test_timestamp( tmp_path_factory: TempPathFactory, wheel_path: Path, monkeypatch: MonkeyPatch ) -> None: # An environment variable can be used to influence the timestamp on # TarInfo objects inside the zip. See issue #143. build_dir = tmp_path_factory.mktemp("build") for filename in ("one", "two", "three"): build_dir.joinpath(filename).write_text(filename + "\n", encoding="utf-8") # The earliest date representable in TarInfos, 1980-01-01 monkeypatch.setenv("SOURCE_DATE_EPOCH", "315576060") with WheelFile(wheel_path, "w") as wf: wf.write_files(str(build_dir)) 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" ) def test_attributes(tmp_path_factory: TempPathFactory, wheel_path: Path) -> None: # With the change from ZipFile.write() to .writestr(), we need to manually # set member attributes. build_dir = tmp_path_factory.mktemp("build") files = (("foo", 0o644), ("bar", 0o755)) for filename, mode in files: path = build_dir.joinpath(filename) path.write_text(filename + "\n", encoding="utf-8") path.chmod(mode) with WheelFile(wheel_path, "w") as wf: wf.write_files(str(build_dir)) with ZipFile(wheel_path, "r") as zf: for filename, mode in files: info = zf.getinfo(filename) assert info.external_attr == (mode | stat.S_IFREG) << 16 assert info.compress_type == ZIP_DEFLATED info = zf.getinfo("test-1.0.dist-info/RECORD") assert info.external_attr == (0o664 | stat.S_IFREG) << 16 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7331743 wheel-0.46.1/tests/testdata/eggnames.txt0000644000000000000000000000467214775306464015167 0ustar00vcard-0.7.8-py2.7.egg qtalchemy-0.7.1-py2.7.egg AMQPDeliver-0.1-py2.7.egg infi.registry-0.1.1-py2.7.egg infi.instruct-0.5.5-py2.7.egg infi.devicemanager-0.1.2-py2.7.egg TracTixSummary-1.0-py2.7.egg ToscaWidgets-0.9.12-py2.7.egg archipel_agent_iphone_notification-0.5.0beta-py2.7.egg archipel_agent_action_scheduler-0.5.0beta-py2.7.egg ao.social-1.0.2-py2.7.egg apgl-0.7-py2.7.egg satchmo_payment_payworld-0.1.1-py2.7.egg snmpsim-0.1.3-py2.7.egg sshim-0.2-py2.7.egg shove-0.3.4-py2.7.egg simpleavro-0.3.0-py2.7.egg wkhtmltopdf-0.2-py2.7.egg wokkel-0.7.0-py2.7.egg jmbo_social-0.0.6-py2.7.egg jmbo_post-0.0.6-py2.7.egg jcrack-0.0.2-py2.7.egg riak-1.4.0-py2.7.egg restclient-0.10.2-py2.7.egg Sutekh-0.8.1-py2.7.egg trayify-0.0.1-py2.7.egg tweepy-1.9-py2.7.egg topzootools-0.2.1-py2.7.egg haystack-0.16-py2.7.egg zope.interface-4.0.1-py2.7-win32.egg neuroshare-0.8.5-py2.7-macosx-10.7-intel.egg ndg_httpsclient-0.2.0-py2.7.egg libtele-0.3-py2.7.egg litex.cxpool-1.0.2-py2.7.egg obspy.iris-0.5.1-py2.7.egg obspy.mseed-0.6.1-py2.7-win32.egg obspy.core-0.6.2-py2.7.egg CorePost-0.0.3-py2.7.egg fnordstalk-0.0.3-py2.7.egg Persistence-2.13.2-py2.7-win32.egg Pydap-3.1.RC1-py2.7.egg PyExecJS-1.0.4-py2.7.egg Wally-0.7.2-py2.7.egg ExtensionClass-4.0a1-py2.7-win32.egg Feedjack-0.9.16-py2.7.egg Mars24-0.3.9-py2.7.egg HalWeb-0.6.0-py2.7.egg DARE-0.7.140-py2.7.egg macholib-1.3-py2.7.egg marrow.wsgi.egress.compression-1.1-py2.7.egg mcs-0.3.7-py2.7.egg Kook-0.6.0-py2.7.egg er-0.1-py2.7.egg evasion_director-1.1.4-py2.7.egg djquery-0.1a-py2.7.egg django_factory-0.7-py2.7.egg django_gizmo-0.0.3-py2.7.egg django_category-0.1-py2.7.egg dbwrap-0.3.2-py2.7.egg django_supergeneric-1.0-py2.7.egg django_dynamo-0.25-py2.7.egg django_acollabauth-0.1-py2.7.egg django_qrlink-0.1.0-py2.7.egg django_addons-0.6.6-py2.7.egg cover_grabber-1.1.2-py2.7.egg chem-1.1-py2.7.egg crud-0.1-py2.7.egg bongo-0.1-py2.7.egg bytecodehacks-April2000-py2.7.egg greenlet-0.3.4-py2.7-win32.egg ginvoke-0.3.1-py2.7.egg pyobjc_framework_ScriptingBridge-2.3-py2.7.egg pecan-0.2.0a-py2.7.egg pyress-0.2.0-py2.7.egg pyobjc_framework_PubSub-2.3-py2.7.egg pyobjc_framework_ExceptionHandling-2.3-py2.7.egg pywps-trunk-py2.7.egg pyobjc_framework_CFNetwork-2.3-py2.7-macosx-10.6-fat.egg py.saunter-0.40-py2.7.egg pyfnordmetric-0.0.1-py2.7.egg pyws-1.1.1-py2.7.egg prestapyt-0.4.0-py2.7.egg passlib-1.5.3-py2.7.egg pyga-2.1-py2.7.egg pygithub3-0.3-py2.7.egg pyobjc_framework_OpenDirectory-2.3-py2.7.egg yaposib-0.2.75-py2.7-linux-x86_64.egg ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744145715.7331743 wheel-0.46.1/tests/testdata/test-1.0-py2.py3-none-any.whl0000644000000000000000000001457114775306464017572 0ustar00PK XfLhello/UT xI[xI[ux PKٝM?7(*hello/hello.pyUT NxI[ux KIMSHдR̼ %By~QN&/PK ʰT@hello/__init__.pyUT BOmI[ux PKT@K  hello.pydUT ,BOxI[ux X P\%Yª5!ZC>\$ #,e{a-M}bLNǎFѶijRSu0$ *̔PݔTQ-%$ig=sϹ&@iDH.)+^\f_UZ~c Țh' |H$ʙbGLzc)Ņ·:^|$v ڷĎHUF$ Iil Km[)s5թ5~c7KTY-^: @&ihjʜt%IZhȇ5(>ZhU/Ŀ7]G0}|1{8h?) Z,2Ōy jeZRح,ah9A!yd12>Z_@CQrVpyp˿PB]_S+VQȰI4>dB/kCAAS(j2EÒHbG!$wIM[4rDctZ[rm0`&++Qj=P`*RIVk m[eN=L; ƾ\.f6][D~!ЫJV ԻA yRd 57c񻓙N=I=:w\V׊T-xa-`jZ[ ʃ |vCZDde,)U[ MjqtdIuE׀R@N:=o=8Ao9Wujf֩b_*!#`8 KzP-DŽi!]_خg##"drE8pNˌDyZV+JlLpMLJe.-s긽 !':azi׋n.)]ı&˜3έrvCzeC ׅ-:&֣ L>7I?_P{A(gףN(eªkwABM5w!^R [pѾsT_E& r/ࢴA'd%\jdN"[[zATD&=8%[9$6 IAجO- u+7PIm0&CҲdg.GVg&; 7\#rꥸ qL\Cv_թT ,gEJ>?yC֚-ٯſLfaq2!jq4>q\`>I(A-12&ֺ;i2o=%*"}H8ete>vMyƑ}rrݷzZw.v]/3YB.m_U1&eMBr_Nu!h!pg윶 1Q 2n .ou&<,qKHBeԊqkʟ&jZaQ!Rj+~KJT#bd@QT+ՍMk >KJBS8|pܰd*QBlKm{ER˥&X6OgtwYh e 1{qRx%'I]qꎇBQюHMY=N LA ̐q(ki3yZ"8C'8YNPT p$LsDꞂhDi@.U_<4),qۨ#x` E db=4$fnS^qڀjh@7;܆4Hcj]Q8Me. ^p7 x#fl_4F]*.Č'nC%u-H4b:140F/ΕVE^ς1Td[C12^ Bˢƌz3ntD7UQ^bN5}$Bϩ`\3 pZ?9\7aF2p1X'&=Z[@(p~4_Z#C8eSQӀ+TײJ fA)˹i.iK[&8{HS @p%g0lrqw#1> ŋ7ag"&.M,6E;LAULpmaO҆`N p8BG?8&!S0#xɖ,knT#*sTC*@ h|9ZWҳ7r/P'[{᠏n5jypq\`%؁Y 3$k* T&S9@Oj`&}<z1p|VcptbvV8W쁩$Mp L!RWxH, 3&>Gnok\aEkl|%k-_3FIKGn?$,3JqIS=r;PK XfLtest-1.0.data/UT xI[xI[ux PK XfLtest-1.0.data/scripts/UT xI[xI[ux PK T@)jtest-1.0.data/scripts/hello.shUT BOxI[ux #!/bin/sh python hello.py PK XfLtest-1.0.data/data/UT xI[xI[ux PK ذT@ljtest-1.0.data/data/hello.datUT اBOxI[ux Hello! PK XfLtest-1.0.data/headers/UT xI[xI[ux PK ذT@ljtest-1.0.data/headers/hello.datUT اBOxI[ux Hello! PKy(Ad;5Utest-1.0.dist-info/METADATAUT 6KPxI[ux MN0>\ b]Ѧia=$4d;ܞqAB|o)b7Fb)J$!RbV5Qž@vP$a)F'< B-sz  zJiqMߐ -0]~e%B{/K{/E0׿'!Ǟ8ךΤd"@Jx *S6I+w&idJ$Ƌ0)&}^js2VnS.`圷g#;6gMA=HʲYK.e0gC߉%MfUc PV$(ZIgnTƨaVfmQ$;mzz4a@^[ @%ѾSj>kabH6Z?}ݎWAKn@'f~njqF 6:{ڠ E_j MwI]4ϱ;Z8%я$R3ddOO:脈apq@dwżi3\*<ʢJdٜqJ&@Mxzɇ+gI,xI=!>F|PK XfLAhello/UTxI[ux PKٝM?7(*@hello/hello.pyUTNux PK ʰT@hello/__init__.pyUTBOux PKT@K  hello.pydUT,BOux PK XfLA test-1.0.data/UTxI[ux PK XfLA+test-1.0.data/scripts/UTxI[ux PK T@)j{test-1.0.data/scripts/hello.shUTBOux PK XfLAtest-1.0.data/data/UTxI[ux PK ذT@lj>test-1.0.data/data/hello.datUTاBOux PK XfLAtest-1.0.data/headers/UTxI[ux PK ذT@ljtest-1.0.data/headers/hello.datUTاBOux PKy(Ad;5UMtest-1.0.dist-info/METADATAUT6KPux PKPhLuucstest-1.0.dist-info/WHEELUT7I[ux PK xL] Gtest-1.0.dist-info/RECORDUTI[ux PKowheel-0.46.1/setup.py0000644000000000000000000000152400000000000011311 0ustar00#!/usr/bin/env python # setup.py generated by flit for tools that don't yet use PEP 517 from distutils.core import setup packages = \ ['wheel', 'wheel._commands'] package_data = \ {'': ['*']} package_dir = \ {'': 'src'} install_requires = \ ['packaging >= 24.0'] extras_require = \ {'test': ['pytest >= 6.0.0', 'setuptools >= 77']} entry_points = \ {'console_scripts': ['wheel = wheel._commands:main']} setup(name='wheel', version='0.46.1', description='Command line tool for manipulating wheel files', author=None, author_email='Daniel Holth ', url=None, packages=packages, package_data=package_data, package_dir=package_dir, install_requires=install_requires, extras_require=extras_require, entry_points=entry_points, python_requires='>=3.9', ) wheel-0.46.1/PKG-INFO0000644000000000000000000000455200000000000010700 0ustar00Metadata-Version: 2.4 Name: wheel Version: 0.46.1 Summary: Command line tool for manipulating wheel files Keywords: wheel,packaging Author-email: Daniel Holth Maintainer-email: Alex Grönholm Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-Expression: MIT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: System :: Archiving :: Packaging Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 License-File: LICENSE.txt Requires-Dist: packaging >= 24.0 Requires-Dist: pytest >= 6.0.0 ; extra == "test" Requires-Dist: setuptools >= 77 ; extra == "test" Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html Project-URL: Documentation, https://wheel.readthedocs.io/ Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues Project-URL: Source, https://github.com/pypa/wheel Provides-Extra: test wheel ===== This is a command line tool for manipulating Python wheel files, as defined in `PEP 427`_. It contains the following functionality: * Convert ``.egg`` archives into ``.whl`` * Unpack wheel archives * Repack wheel archives * Add or remove tags in existing wheel archives .. _PEP 427: https://www.python.org/dev/peps/pep-0427/ Historical note --------------- This project used to contain the implementation of the setuptools_ ``bdist_wheel`` command, but as of setuptools v70.1, it no longer needs ``wheel`` installed for that to work. Thus, you should install this **only** if you intend to use the ``wheel`` command line tool! .. _setuptools: https://pypi.org/project/setuptools/ Documentation ------------- The documentation_ can be found on Read The Docs. .. _documentation: https://wheel.readthedocs.io/ Code of Conduct --------------- 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