diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..342b4c18d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: Documentation + +on: + pull_request: + paths: + - 'docs/**' + push: + paths: + - 'docs/**' + +jobs: + docs: + name: nox -s docs + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade nox + + - name: Build documentation + run: python -m nox -s docs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..6c1715815 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,59 @@ +name: Linting + +on: + pull_request: + paths: + - '**.py' + push: + paths: + - '**.py' + +jobs: + lint: + name: nox -s lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade nox + + - name: Run `nox -s lint` + run: python -m nox -s lint + + build: + name: Build sdist and wheel + runs-on: ubuntu-latest + # Linting verifies that the project is in an acceptable state to create files + # for releasing. + # And this action should be run whenever a release is ready to go public as + # the version number will be changed by editing __about__.py. + needs: lint + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.8' + + - name: Install dependencies + run: python -m pip install --upgrade setuptools wheel + + - name: Build + run: python setup.py sdist bdist_wheel + + - name: Archive files + uses: actions/upload-artifact@v1 + with: + name: dist + path: dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..229fd51f9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +name: Test + +on: + pull_request: + paths: + - '.github/workflows/test.yml' + - '**.py' + push: + paths: + - '.github/workflows/test.yml' + - '**.py' + +jobs: + test: + name: ${{ matrix.os }} / ${{ matrix.python_version }} + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: [Ubuntu, Windows, macOS] + # Python 3.4 is not available from actions/setup-python@v1. + python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3'] + exclude: + # This is failing due to pip not being in the virtual environment. + # https://github.com/pypa/packaging/runs/424785871#step:7:9 + - os: windows + python_version: pypy3 + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python ${{ matrix.python_version }} + with: + python-version: ${{ matrix.python_version }} + + # Set `python` to a recent 3.x version if we're not testing Python 3.6+. + # Why? Nox needs Python 3.5+ and everyone likes f-strings. + - uses: actions/setup-python@v1 + name: Install Python 3.x + with: + python-version: 3.x + if: > + ( + matrix.python_version == '2.7' || + matrix.python_version == 'pypy2' || + matrix.python_version == '3.5' + ) + + # Workaround https://github.com/theacodes/nox/issues/250 + - name: Workaround for Windows Python 2.7 + # This is in PATH, so nox resolves to it - but then subsequent steps fail. + run: rm C:/ProgramData/Chocolatey/bin/python2.7.exe + shell: bash + if: runner.os == 'Windows' && matrix.python_version == '2.7' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install nox + shell: bash + + - name: Run nox + run: | + python -m nox -s tests-${{ matrix.python_version }} + shell: bash diff --git a/.gitignore b/.gitignore index 498bce1a4..05e554a64 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,14 @@ *.egg *.py[co] -.tox/ +.[nt]ox/ .cache/ .coverage .idea .venv* +.vscode/ +.mypy_cache/ .pytest_cache/ __pycache__/ _build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..50d1f6c80 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.770 + hooks: + - id: mypy + exclude: '^(docs|tasks|tests)|setup\.py' + args: [] + - id: mypy + name: mypy for Python 2 + exclude: '^(docs|tasks|tests)|setup\.py|noxfile\.py' + args: ['--py2'] + +- repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black + +- repo: https://gitlab.com/PyCQA/flake8 + rev: '3.7.8' + hooks: + - id: flake8 + additional_dependencies: ['pep8-naming'] + # Ignore all format-related checks as Black takes care of those. + args: ['--ignore', 'E2,W5', '--select', 'E,W,F,N'] + +- repo: https://github.com/mgedmin/check-manifest + rev: '0.39' + hooks: + - id: check-manifest diff --git a/.travis.yml b/.travis.yml index c7ab82239..b60b6f12a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,43 +1,42 @@ language: python cache: pip -python: 3.6 +python: 3.8 matrix: include: - python: 2.7 - env: TOXENV=py27 + env: NOXSESSION=tests-2.7 - python: pypy - env: TOXENV=pypy + env: NOXSESSION=tests-pypy2 - python: pypy3 - env: TOXENV=pypy3 + env: NOXSESSION=tests-pypy3 - python: 3.4 - env: TOXENV=py34 + env: NOXSESSION=tests-3.4 - python: 3.5 - env: TOXENV=py35 + env: NOXSESSION=tests-3.5 - python: 3.6 - env: TOXENV=py36 + env: NOXSESSION=tests-3.6 - python: 3.7 - env: TOXENV=py37 - dist: xenial - - python: 3.8-dev - env: TOXENV=py - dist: xenial - - env: TOXENV=lint - - env: TOXENV=docs - - env: TOXENV=packaging - - allow_failures: - - python: 3.8-dev + env: NOXSESSION=tests-3.7 + - python: 3.8 + env: NOXSESSION=tests-3.8 + - env: NOXSESSION=lint + - env: NOXSESSION=docs install: - - pip install tox + - pyenv global 3.7.1 + - python3.7 -m pip install nox script: - - tox + - python3.7 -m nox notifications: irc: channels: - - "irc.freenode.org#pypa-dev" + # This is set to a secure variable to prevent forks from notifying the + # IRC channel whenever they fail a build. This can be removed when travis + # implements https://github.com/travis-ci/travis-ci/issues/1094. + # The actual value here is: irc.freenode.org#pypa-dev + - secure: "Br6aYBYkjL17fBZ6+AczCkaBMWQAY6To8IH4zqhHrORXAWq4zeuC4VyCZ4MXmDzk0WbA3h3Ea7u9kodUf1sVK1h0q7HX66p8qmFyTncQoLFgo2LF/x1aU1FGWKDDSX5K6qKOzUKrHUhQyVq+uAuRVUm7bJhJL0/viPwEoh+bONo=" use_notice: true skip_join: true diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 24a97655e..b38a7494d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,50 @@ Changelog --------- +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ + +* Canonicalize version before comparing specifiers. (:issue:`282`) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. + +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. + +20.1 - 2020-01-24 +~~~~~~~~~~~~~~~~~~~ + +* Fix a bug caused by reuse of an exhausted iterator. (:issue:`257`) + +20.0 - 2020-01-06 +~~~~~~~~~~~~~~~~~ + +* Add type hints (:issue:`191`) + +* Add proper trove classifiers for PyPy support (:issue:`198`) + +* Scale back depending on ``ctypes`` for manylinux support detection (:issue:`171`) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (:issue:`193`) + +* Expand upon the API provded by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (:issue:`187`) + +* Officially support Python 3.8 (:issue:`232`) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (:issue:`226`) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (:issue:`226`) + 19.2 - 2019-09-18 ~~~~~~~~~~~~~~~~~ diff --git a/LICENSE.APACHE b/LICENSE.APACHE index 4947287f7..f433b1a53 100644 --- a/LICENSE.APACHE +++ b/LICENSE.APACHE @@ -174,4 +174,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS \ No newline at end of file + END OF TERMS AND CONDITIONS diff --git a/MANIFEST.in b/MANIFEST.in index 36fcbd8e6..468f5b7e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,13 +3,17 @@ include LICENSE LICENSE.APACHE LICENSE.BSD include .coveragerc include .flake8 -include tox.ini +include .pre-commit-config.yaml recursive-include docs * recursive-include tests *.py +recursive-include tests hello-world-* +exclude noxfile.py exclude .travis.yml exclude dev-requirements.txt +exclude tests/build-hello-world.sh +exclude tests/hello-world.c prune docs/_build prune tasks diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 0b0442882..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Install our development requirements -black; python_version >= '3.6' -coverage -flake8 -pep8-naming -pretend -pytest -tox - -# Install packaging itself --e . diff --git a/docs/Makefile b/docs/Makefile index ec2771be2..9d683b402 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -150,4 +150,4 @@ linkcheck: doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." \ No newline at end of file + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst index aa47fe684..c8e948782 100644 --- a/docs/development/getting-started.rst +++ b/docs/development/getting-started.rst @@ -2,22 +2,13 @@ Getting started =============== Working on packaging requires the installation of a small number of -development dependencies. These are listed in ``dev-requirements.txt`` and they -can be installed in a `virtualenv`_ using `pip`_. Once you've installed the -dependencies, install packaging in ``editable`` mode. For example: - -.. code-block:: console - - $ # Create a virtualenv and activate it - $ python -m pip install --requirement dev-requirements.txt - $ python -m pip install --editable . - -You are now ready to run the tests and build the documentation. +development dependencies. To see what dependencies are required to +run the tests manually, please look at the ``noxfile.py`` file. Running tests ~~~~~~~~~~~~~ -packaging unit tests are found in the ``tests/`` directory and are +The packaging unit tests are found in the ``tests/`` directory and are designed to be run using `pytest`_. `pytest`_ will discover the tests automatically, so all you have to do is: @@ -25,30 +16,43 @@ automatically, so all you have to do is: $ python -m pytest ... - 62746 passed in 220.43 seconds + 29204 passed, 4 skipped, 1 xfailed in 83.98 seconds -This runs the tests with the default Python interpreter. +This runs the tests with the default Python interpreter. This also allows +you to run select tests instead of the entire test suite. You can also verify that the tests pass on other supported Python interpreters. -For this we use `tox`_, which will automatically create a `virtualenv`_ for +For this we use `nox`_, which will automatically create a `virtualenv`_ for each supported Python version and run the tests. For example: .. code-block:: console - $ tox + $ nox -s tests ... - py27: commands succeeded - ERROR: pypy: InterpreterNotFound: pypy - ERROR: py34: InterpreterNotFound: python3.4 - ERROR: py35: InterpreterNotFound: python3.5 - py36: commands succeeded - ERROR: py37: InterpreterNotFound: python3.7 - docs: commands succeeded - pep8: commands succeeded + nox > Ran multiple sessions: + nox > * tests-2.7: success + nox > * tests-3.4: skipped + nox > * tests-3.5: success + nox > * tests-3.6: success + nox > * tests-3.7: success + nox > * tests-3.8: success + nox > * tests-pypy: skipped + nox > * tests-pypy3: skipped You may not have all the required Python versions installed, in which case you will see one or more ``InterpreterNotFound`` errors. +Running linters +~~~~~~~~~~~~~~~ + +If you wish to run the linting rules, you may use `pre-commit`_ or run +``nox -s lint``. + +.. code-block:: console + + $ nox -s lint + ... + nox > Session lint was successful. Building documentation ~~~~~~~~~~~~~~~~~~~~~~ @@ -56,21 +60,21 @@ Building documentation packaging documentation is stored in the ``docs/`` directory. It is written in `reStructured Text`_ and rendered using `Sphinx`_. -Use `tox`_ to build the documentation. For example: +Use `nox`_ to build the documentation. For example: .. code-block:: console - $ tox -e docs + $ nox -s docs ... - docs: commands succeeded - congratulations :) + nox > Session docs was successful. The HTML documentation index can now be found at ``docs/_build/html/index.html``. .. _`pytest`: https://pypi.org/project/pytest/ -.. _`tox`: https://pypi.org/project/tox/ +.. _`nox`: https://pypi.org/project/nox/ .. _`virtualenv`: https://pypi.org/project/virtualenv/ .. _`pip`: https://pypi.org/project/pip/ .. _`sphinx`: https://pypi.org/project/Sphinx/ .. _`reStructured Text`: http://sphinx-doc.org/rest.html +.. _`pre-commit`: https://pre-commit.com diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst index 11c61aabd..f634ac40b 100644 --- a/docs/development/release-process.rst +++ b/docs/development/release-process.rst @@ -1,35 +1,21 @@ Release Process =============== -#. Checkout the current ``master`` branch, with a clean working directory. -#. Modify the ``CHANGELOG.rst`` to include changes made since the last release - and update the section header for the new release. -#. Bump the version in ``packaging/__about__.py`` +#. Checkout the current ``master`` branch. +#. Install the latest ``nox``:: -#. Install the latest ``setuptools``, ``wheel`` and ``twine`` packages - from PyPI:: + $ pip install nox - $ pip install --upgrade setuptools wheel twine +#. Run the release automation with the required version number (YY.N):: -#. Ensure no ``dist/`` folder exists and then create the distribution files:: + $ nox -s release -- YY.N - $ python setup.py sdist bdist_wheel - -#. Check the built distribution files with ``twine``:: - - $ twine check dist/* - -#. Commit the changes to ``master``. - -#. If all goes well, upload the build distribution files:: - - $ twine upload dist/* +#. Notify the other project owners of the release. -#. Create a - `release on GitHub `_ and - include the artifacts uploaded to PyPI. +.. note:: -#. Bump the version for development in ``packaging/__about__.py`` and - ``CHANGELOG.rst``. + Access needed for making the release are: -#. Notify the other project owners of the release. + - PyPI maintainer (or owner) access to `packaging` + - push directly to the `master` branch on the source repository + - push tags directly to the source repository diff --git a/docs/development/reviewing-patches.rst b/docs/development/reviewing-patches.rst index 4f1810c66..c476c7512 100644 --- a/docs/development/reviewing-patches.rst +++ b/docs/development/reviewing-patches.rst @@ -34,4 +34,4 @@ These are small things that are not caught by the automated style checkers. * Does a variable need a better name? * Should this be a keyword argument? -.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review \ No newline at end of file +.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index 875b79030..cf433c45f 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -19,8 +19,8 @@ Code ---- This project's source is auto-formatted with |black|. You can check if your -code meets our requirements by running our linters against it with ``tox -e -lint``. +code meets our requirements by running our linters against it with ``nox -s +lint`` or ``pre-commit run --all-files``. `Write comments as complete sentences.`_ diff --git a/docs/tags.rst b/docs/tags.rst index c6f70360f..0851d7893 100644 --- a/docs/tags.rst +++ b/docs/tags.rst @@ -38,6 +38,7 @@ Reference A dictionary mapping interpreter names to their `abbreviation codes`_ (e.g. ``"cpython"`` is ``"cp"``). All interpreter names are lower-case. + .. class:: Tag(interpreter, abi, platform) A representation of the tag triple for a wheel. Instances are considered @@ -65,17 +66,18 @@ Reference .. function:: parse_tag(tag) - Parse the provided *tag* into a set of :class:`Tag` instances. + Parses the provided ``tag`` into a set of :class:`Tag` instances. - The returning of a set is required due to the possibility that the tag is a - `compressed tag set`_, e.g. ``"py2.py3-none-any"``. + Returning a set is required due to the possibility that the tag is a + `compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both + Python 2 and Python 3. :param str tag: The tag to parse, e.g. ``"py3-none-any"``. -.. function:: sys_tags() +.. function:: sys_tags(*, warn=False) - Create an iterable of tags that the running interpreter supports. + Yields the tags that the running interpreter supports. The iterable is ordered so that the best-matching tag is first in the sequence. The exact preferential order to tags is interpreter-specific, but @@ -92,8 +94,107 @@ Reference The function returns an iterable in order to allow for the possible short-circuiting of tag generation if the entire sequence is not necessary - and calculating some tags happens to be expensive. + and tag calculation happens to be expensive. + + :param bool warn: Whether warnings should be logged. Defaults to ``False``. + + +.. function:: interpreter_name() + + Returns the running interpreter's name. + + This typically acts as the prefix to the :attr:`~Tag.interpreter` tag. + + +.. function:: interpreter_version(*, warn=False) + + Returns the running interpreter's version. + + This typically acts as the suffix to the :attr:`~Tag.interpreter` tag. + + +.. function:: mac_platforms(version=None, arch=None) + + Yields the :attr:`~Tag.platform` tags for macOS. + + :param tuple version: A two-item tuple presenting the version of macOS. + Defaults to the current system's version. + :param str arch: The CPU architecture. Defaults to the architecture of the + current system, e.g. ``"x86_64"``. + + .. note:: + Equivalent support for the other major platforms is purposefully not + provided: + + - On Windows, platform compatibility is statically specified + - On Linux, code must be run on the system itself to determine + compatibility + + +.. function:: compatible_tags(python_version=None, interpreter=None, platforms=None) + + Yields the tags for an interpreter compatible with the Python version + specified by ``python_version``. + + The specific tags generated are: + + - ``py*-none-`` + - ``-none-any`` if ``interpreter`` is provided + - ``py*-none-any`` + + :param Sequence python_version: A one- or two-item sequence representing the + compatible version of Python. Defaults to + ``sys.version_info[:2]``. + :param str interpreter: The name of the interpreter (if known), e.g. + ``"cp38"``. Defaults to the current interpreter. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + +.. function:: cpython_tags(python_version=None, abis=None, platforms=None, *, warn=False) + + Yields the tags for the CPython interpreter. + + The specific tags generated are: + + - ``cp--`` + - ``cp-abi3-`` + - ``cp-none-`` + - ``cp-abi3-`` where "older version" is all older + minor versions down to Python 3.2 (when ``abi3`` was introduced) + + If ``python_version`` only provides a major-only version then only + user-provided ABIs via ``abis`` and the ``none`` ABI will be used. + + :param Sequence python_version: A one- or two-item sequence representing the + targetted Python version. Defaults to + ``sys.version_info[:2]``. + :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs + compatible with the current system. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + :param bool warn: Whether warnings should be logged. Defaults to ``False``. + +.. function:: generic_tags(interpreter=None, abis=None, platforms=None, *, warn=False) + + Yields the tags for an interpreter which requires no specialization. + + This function should be used if one of the other interpreter-specific + functions provided by this module is not appropriate (i.e. not calculating + tags for a CPython interpreter). + + The specific tags generated are: + + - ``--`` + + The ``"none"`` ABI will be added if it was not explicitly provided. + :param str interpreter: The name of the interpreter. Defaults to being + calculated. + :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs + compatible with the current system. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + :param bool warn: Whether warnings should be logged. Defaults to ``False``. .. _abbreviation codes: https://www.python.org/dev/peps/pep-0425/#python-tag .. _compressed tag set: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets diff --git a/docs/version.rst b/docs/version.rst index 8eed7357d..ff58c51d7 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -86,6 +86,18 @@ Reference version number, including trailing zeroes but not including the epoch or any prerelease/development/postrelease suffixes + .. attribute:: major + + An integer representing the first item of :attr:`release` or ``0`` if unavailable. + + .. attribute:: minor + + An integer representing the second item of :attr:`release` or ``0`` if unavailable. + + .. attribute:: micro + + An integer representing the third item of :attr:`release` or ``0`` if unavailable. + .. attribute:: local A string representing the local version portion of this ``Version()`` @@ -139,6 +151,52 @@ Reference :param str version: The string representation of a version which will be used as is. + .. note:: + + :class:`LegacyVersion` instances are always ordered lower than :class:`Version` instances. + + >>> from packaging.version import Version, LegacyVersion + >>> v1 = Version("1.0") + >>> v2 = LegacyVersion("1.0") + >>> v1 > v2 + True + >>> v3 = LegacyVersion("1.3") + >>> v1 > v3 + True + + Also note that some strings are still valid PEP 440 strings (:class:`Version`), even if they look very similar to + other versions that are not (:class:`LegacyVersion`). Examples include versions with `Pre-release spelling`_ and + `Post-release spelling`_. + + >>> from packaging.version import parse + >>> v1 = parse('0.9.8a') + >>> v2 = parse('0.9.8beta') + >>> v3 = parse('0.9.8r') + >>> v4 = parse('0.9.8rev') + >>> v5 = parse('0.9.8t') + >>> v1 + + >>> v1.is_prerelease + True + >>> v2 + + >>> v2.is_prerelease + True + >>> v3 + + >>> v3.is_postrelease + True + >>> v4 + + >>> v4.is_postrelease + True + >>> v5 + + >>> v5.is_prerelease + False + >>> v5.is_postrelease + False + .. attribute:: public A string representing the public version portion of this @@ -225,4 +283,6 @@ Reference ``re.VERBOSE`` and ``re.IGNORECASE`` flags set. -.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/ +.. _PEP 440: https://www.python.org/dev/peps/pep-0440/ +.. _Pre-release spelling : https://www.python.org/dev/peps/pep-0440/#pre-release-spelling +.. _Post-release spelling : https://www.python.org/dev/peps/pep-0440/#post-release-spelling diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..925215cfe --- /dev/null +++ b/noxfile.py @@ -0,0 +1,311 @@ +# mypy: disallow-untyped-defs=False, disallow-untyped-calls=False + +import time +import re +import os +import sys +import glob +import shutil +import difflib +import tempfile +import textwrap +import datetime +import contextlib +import subprocess +from pathlib import Path + +import nox + +nox.options.sessions = ["lint"] +nox.options.reuse_existing_virtualenvs = True + + +@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8", "pypy2", "pypy3"]) +def tests(session): + def coverage(*args): + session.run("python", "-m", "coverage", *args) + + session.install("coverage<5.0.0", "pretend", "pytest", "pip>=9.0.2") + + if "pypy" not in session.python: + coverage( + "run", + "--source", + "packaging/", + "-m", + "pytest", + "--strict", + *session.posargs, + ) + coverage("report", "-m", "--fail-under", "100") + else: + # Don't do coverage tracking for PyPy, since it's SLOW. + session.run( + "python", "-m", "pytest", "--capture=no", "--strict", *session.posargs + ) + + +@nox.session(python="3.8") +def lint(session): + # Run the linters (via pre-commit) + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files") + + # Check the distribution + session.install("setuptools", "twine", "wheel") + session.run("python", "setup.py", "--quiet", "sdist", "bdist_wheel") + session.run("twine", "check", *glob.glob("dist/*")) + + +@nox.session(python="3.8") +def docs(session): + shutil.rmtree("docs/_build", ignore_errors=True) + session.install("sphinx", "sphinx-rtd-theme") + + variants = [ + # (builder, dest) + ("html", "html"), + ("latex", "latex"), + ("doctest", "html"), + ] + + for builder, dest in variants: + session.run( + "sphinx-build", + "-W", + "-b", + builder, + "-d", + "docs/_build/doctrees/" + dest, + "docs", # source directory + "docs/_build/" + dest, # output directory + ) + + +@nox.session +def release(session): + package_name = "packaging" + version_file = Path(f"{package_name}/__about__.py") + changelog_file = Path(f"CHANGELOG.rst") + + try: + release_version = _get_version_from_arguments(session.posargs) + except ValueError as e: + session.error(f"Invalid arguments: {e}") + + # Check state of working directory and git. + _check_working_directory_state(session) + _check_git_state(session, release_version) + + # Prepare for release. + _changelog_update_unreleased_title(release_version, file=changelog_file) + session.run("git", "add", str(changelog_file), external=True) + _bump(session, version=release_version, file=version_file, kind="release") + + # Tag the release commit. + # fmt: off + session.run( + "git", "tag", + "-s", release_version, + "-m", f"Release {release_version}", + external=True, + ) + # fmt: on + + # Prepare for development. + _changelog_add_unreleased_title(file=changelog_file) + session.run("git", "add", str(changelog_file), external=True) + + major, minor = map(int, release_version.split(".")) + next_version = f"{major}.{minor + 1}.dev0" + _bump(session, version=next_version, file=version_file, kind="development") + + # Checkout the git tag. + session.run("git", "checkout", "-q", release_version, external=True) + + session.install("twine", "setuptools", "wheel") + + # Build the distribution. + session.run("python", "setup.py", "sdist", "bdist_wheel") + + # Check what files are in dist/ for upload. + files = sorted(glob.glob(f"dist/*")) + expected = [ + f"dist/{package_name}-{release_version}-py2.py3-none-any.whl", + f"dist/{package_name}-{release_version}.tar.gz", + ] + if files != expected: + diff_generator = difflib.context_diff( + expected, files, fromfile="expected", tofile="got", lineterm="" + ) + diff = "\n".join(diff_generator) + session.error(f"Got the wrong files:\n{diff}") + + # Get back out into master. + session.run("git", "checkout", "-q", "master", external=True) + + # Check and upload distribution files. + session.run("twine", "check", *files) + + # Upload the distribution. + session.run("twine", "upload", *files) + + # Push the commits and tag. + # NOTE: The following fails if pushing to the branch is not allowed. This can + # happen on GitHub, if the master branch is protected, there are required + # CI checks and "Include administrators" is enabled on the protection. + session.run("git", "push", "upstream", "master", release_version, external=True) + + +# ----------------------------------------------------------------------------- +# Helpers +# ----------------------------------------------------------------------------- +def _get_version_from_arguments(arguments): + """Checks the arguments passed to `nox -s release`. + + Only 1 argument that looks like a version? Return the argument. + Otherwise, raise a ValueError describing what's wrong. + """ + if len(arguments) != 1: + raise ValueError("Expected exactly 1 argument") + + version = arguments[0] + parts = version.split(".") + + if len(parts) != 2: + # Not of the form: YY.N + raise ValueError("not of the form: YY.N") + + if not all(part.isdigit() for part in parts): + # Not all segments are integers. + raise ValueError("non-integer segments") + + # All is good. + return version + + +def _check_working_directory_state(session): + """Check state of the working directory, prior to making the release. + """ + should_not_exist = ["build/", "dist/"] + + bad_existing_paths = list(filter(os.path.exists, should_not_exist)) + if bad_existing_paths: + session.error(f"Remove {', '.join(bad_existing_paths)} and try again") + + +def _check_git_state(session, version_tag): + """Check state of the git repository, prior to making the release. + """ + # Ensure the upstream remote pushes to the correct URL. + allowed_upstreams = [ + "git@github.com:pypa/packaging.git", + "https://github.com/pypa/packaging.git", + ] + result = subprocess.run( + ["git", "remote", "get-url", "--push", "upstream"], + capture_output=True, + encoding="utf-8", + ) + if result.stdout.rstrip() not in allowed_upstreams: + session.error(f"git remote `upstream` is not one of {allowed_upstreams}") + # Ensure we're on master branch for cutting a release. + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + encoding="utf-8", + ) + if result.stdout != "master\n": + session.error(f"Not on master branch: {result.stdout!r}") + + # Ensure there are no uncommitted changes. + result = subprocess.run( + ["git", "status", "--porcelain"], capture_output=True, encoding="utf-8" + ) + if result.stdout: + print(result.stdout, end="", file=sys.stderr) + session.error(f"The working tree has uncommitted changes") + + # Ensure this tag doesn't exist already. + result = subprocess.run( + ["git", "rev-parse", version_tag], capture_output=True, encoding="utf-8" + ) + if not result.returncode: + session.error(f"Tag already exists! {version_tag} -- {result.stdout!r}") + + # Back up the current git reference, in a tag that's easy to clean up. + _release_backup_tag = "auto/release-start-" + str(int(time.time())) + session.run("git", "tag", _release_backup_tag, external=True) + + +def _bump(session, *, version, file, kind): + session.log(f"Bump version to {version!r}") + contents = file.read_text() + new_contents = re.sub( + '__version__ = "(.+)"', f'__version__ = "{version}"', contents + ) + file.write_text(new_contents) + + session.log(f"git commit") + subprocess.run(["git", "add", str(file)]) + subprocess.run(["git", "commit", "-m", f"Bump for {kind}"]) + + +@contextlib.contextmanager +def _replace_file(original_path): + # Create a temporary file. + fh, replacement_path = tempfile.mkstemp() + + try: + with os.fdopen(fh, "w") as replacement: + with open(original_path) as original: + yield original, replacement + except Exception: + raise + else: + shutil.copymode(original_path, replacement_path) + os.remove(original_path) + shutil.move(replacement_path, original_path) + + +def _changelog_update_unreleased_title(version, *, file): + """Update an "*unreleased*" heading to "{version} - {date}" + """ + yyyy_mm_dd = datetime.datetime.today().strftime("%Y-%m-%d") + title = f"{version} - {yyyy_mm_dd}" + + with _replace_file(file) as (original, replacement): + for line in original: + if line == "*unreleased*\n": + replacement.write(f"{title}\n") + replacement.write(len(title) * "~" + "\n") + # Skip processing the next line (the heading underline for *unreleased*) + # since we already wrote the heading underline. + next(original) + else: + replacement.write(line) + + +def _changelog_add_unreleased_title(*, file): + with _replace_file(file) as (original, replacement): + # Duplicate first 3 lines from the original file. + for _ in range(3): + line = next(original) + replacement.write(line) + + # Write the heading. + replacement.write( + textwrap.dedent( + """\ + *unreleased* + ~~~~~~~~~~~~ + + No unreleased changes. + + """ + ) + ) + + # Duplicate all the remaining lines. + for line in original: + replacement.write(line) diff --git a/packaging/__about__.py b/packaging/__about__.py index dc95138d0..4d998578d 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,10 +18,10 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "19.2" +__version__ = "20.4" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" -__license__ = "BSD or Apache License, Version 2.0" +__license__ = "BSD-2-Clause or Apache-2.0" __copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/packaging/_compat.py b/packaging/_compat.py index 25da473c1..e54bd4ede 100644 --- a/packaging/_compat.py +++ b/packaging/_compat.py @@ -5,6 +5,11 @@ import sys +from ._typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from typing import Any, Dict, Tuple, Type + PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 @@ -18,14 +23,16 @@ def with_metaclass(meta, *bases): + # type: (Type[Any], Tuple[Type[Any], ...]) -> Any """ Create a base class with a metaclass. """ # This requires a bit of explanation: the basic idea is to make a dummy # metaclass for one level of class instantiation that replaces itself with # the actual metaclass. - class metaclass(meta): + class metaclass(meta): # type: ignore def __new__(cls, name, this_bases, d): + # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any return meta(name, bases, d) return type.__new__(metaclass, "temporary_class", (), {}) diff --git a/packaging/_structures.py b/packaging/_structures.py index 68dcca634..800d5c558 100644 --- a/packaging/_structures.py +++ b/packaging/_structures.py @@ -4,65 +4,83 @@ from __future__ import absolute_import, division, print_function -class Infinity(object): +class InfinityType(object): def __repr__(self): + # type: () -> str return "Infinity" def __hash__(self): + # type: () -> int return hash(repr(self)) def __lt__(self, other): + # type: (object) -> bool return False def __le__(self, other): + # type: (object) -> bool return False def __eq__(self, other): + # type: (object) -> bool return isinstance(other, self.__class__) def __ne__(self, other): + # type: (object) -> bool return not isinstance(other, self.__class__) def __gt__(self, other): + # type: (object) -> bool return True def __ge__(self, other): + # type: (object) -> bool return True def __neg__(self): + # type: (object) -> NegativeInfinityType return NegativeInfinity -Infinity = Infinity() +Infinity = InfinityType() -class NegativeInfinity(object): +class NegativeInfinityType(object): def __repr__(self): + # type: () -> str return "-Infinity" def __hash__(self): + # type: () -> int return hash(repr(self)) def __lt__(self, other): + # type: (object) -> bool return True def __le__(self, other): + # type: (object) -> bool return True def __eq__(self, other): + # type: (object) -> bool return isinstance(other, self.__class__) def __ne__(self, other): + # type: (object) -> bool return not isinstance(other, self.__class__) def __gt__(self, other): + # type: (object) -> bool return False def __ge__(self, other): + # type: (object) -> bool return False def __neg__(self): + # type: (object) -> InfinityType return Infinity -NegativeInfinity = NegativeInfinity() +NegativeInfinity = NegativeInfinityType() diff --git a/packaging/_typing.py b/packaging/_typing.py new file mode 100644 index 000000000..77a8b9185 --- /dev/null +++ b/packaging/_typing.py @@ -0,0 +1,48 @@ +"""For neatly implementing static typing in packaging. + +`mypy` - the static type analysis tool we use - uses the `typing` module, which +provides core functionality fundamental to mypy's functioning. + +Generally, `typing` would be imported at runtime and used in that fashion - +it acts as a no-op at runtime and does not have any run-time overhead by +design. + +As it turns out, `typing` is not vendorable - it uses separate sources for +Python 2/Python 3. Thus, this codebase can not expect it to be present. +To work around this, mypy allows the typing import to be behind a False-y +optional to prevent it from running at runtime and type-comments can be used +to remove the need for the types to be accessible directly during runtime. + +This module provides the False-y guard in a nicely named fashion so that a +curious maintainer can reach here to read this. + +In packaging, all static-typing related imports should be guarded as follows: + + from packaging._typing import TYPE_CHECKING + + if TYPE_CHECKING: + from typing import ... + +Ref: https://github.com/python/mypy/issues/3216 +""" + +__all__ = ["TYPE_CHECKING", "cast"] + +# The TYPE_CHECKING constant defined by the typing module is False at runtime +# but True while type checking. +if False: # pragma: no cover + from typing import TYPE_CHECKING +else: + TYPE_CHECKING = False + +# typing's cast syntax requires calling typing.cast at runtime, but we don't +# want to import typing at runtime. Here, we inform the type checkers that +# we're importing `typing.cast` as `cast` and re-implement typing.cast's +# runtime behavior in a block that is ignored by type checkers. +if TYPE_CHECKING: # pragma: no cover + # not executed at runtime + from typing import cast +else: + # executed at runtime + def cast(type_, value): # noqa + return value diff --git a/packaging/markers.py b/packaging/markers.py index 3b8af3242..87cd3f958 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -13,8 +13,14 @@ from pyparsing import Literal as L # noqa from ._compat import string_types +from ._typing import TYPE_CHECKING from .specifiers import Specifier, InvalidSpecifier +if TYPE_CHECKING: # pragma: no cover + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + Operator = Callable[[str, str], bool] + __all__ = [ "InvalidMarker", @@ -46,30 +52,37 @@ class UndefinedEnvironmentName(ValueError): class Node(object): def __init__(self, value): + # type: (Any) -> None self.value = value def __str__(self): + # type: () -> str return str(self.value) def __repr__(self): + # type: () -> str return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) def serialize(self): + # type: () -> str raise NotImplementedError class Variable(Node): def serialize(self): + # type: () -> str return str(self) class Value(Node): def serialize(self): + # type: () -> str return '"{0}"'.format(self) class Op(Node): def serialize(self): + # type: () -> str return str(self) @@ -85,13 +98,13 @@ def serialize(self): | L("python_version") | L("sys_platform") | L("os_name") - | L("os.name") + | L("os.name") # PEP-345 | L("sys.platform") # PEP-345 | L("platform.version") # PEP-345 | L("platform.machine") # PEP-345 | L("platform.python_implementation") # PEP-345 - | L("python_implementation") # PEP-345 - | L("extra") # undocumented setuptools legacy + | L("python_implementation") # undocumented setuptools legacy + | L("extra") # PEP-508 ) ALIASES = { "os.name": "os_name", @@ -131,6 +144,7 @@ def serialize(self): def _coerce_parse_result(results): + # type: (Union[ParseResults, List[Any]]) -> List[Any] if isinstance(results, ParseResults): return [_coerce_parse_result(i) for i in results] else: @@ -138,6 +152,8 @@ def _coerce_parse_result(results): def _format_marker(marker, first=True): + # type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str + assert isinstance(marker, (list, tuple, string_types)) # Sometimes we have a structure like [[...]] which is a single item list @@ -172,10 +188,11 @@ def _format_marker(marker, first=True): "!=": operator.ne, ">=": operator.ge, ">": operator.gt, -} +} # type: Dict[str, Operator] def _eval_op(lhs, op, rhs): + # type: (str, Op, str) -> bool try: spec = Specifier("".join([op.serialize(), rhs])) except InvalidSpecifier: @@ -183,7 +200,7 @@ def _eval_op(lhs, op, rhs): else: return spec.contains(lhs) - oper = _operators.get(op.serialize()) + oper = _operators.get(op.serialize()) # type: Optional[Operator] if oper is None: raise UndefinedComparison( "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) @@ -192,13 +209,18 @@ def _eval_op(lhs, op, rhs): return oper(lhs, rhs) -_undefined = object() +class Undefined(object): + pass + + +_undefined = Undefined() def _get_env(environment, name): - value = environment.get(name, _undefined) + # type: (Dict[str, str], str) -> str + value = environment.get(name, _undefined) # type: Union[str, Undefined] - if value is _undefined: + if isinstance(value, Undefined): raise UndefinedEnvironmentName( "{0!r} does not exist in evaluation environment.".format(name) ) @@ -207,7 +229,8 @@ def _get_env(environment, name): def _evaluate_markers(markers, environment): - groups = [[]] + # type: (List[Any], Dict[str, str]) -> bool + groups = [[]] # type: List[List[bool]] for marker in markers: assert isinstance(marker, (list, tuple, string_types)) @@ -234,6 +257,7 @@ def _evaluate_markers(markers, environment): def format_full_version(info): + # type: (sys._version_info) -> str version = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel if kind != "final": @@ -242,9 +266,13 @@ def format_full_version(info): def default_environment(): + # type: () -> Dict[str, str] if hasattr(sys, "implementation"): - iver = format_full_version(sys.implementation.version) - implementation_name = sys.implementation.name + # Ignoring the `sys.implementation` reference for type checking due to + # mypy not liking that the attribute doesn't exist in Python 2.7 when + # run with the `--py27` flag. + iver = format_full_version(sys.implementation.version) # type: ignore + implementation_name = sys.implementation.name # type: ignore else: iver = "0" implementation_name = "" @@ -266,6 +294,7 @@ def default_environment(): class Marker(object): def __init__(self, marker): + # type: (str) -> None try: self._markers = _coerce_parse_result(MARKER.parseString(marker)) except ParseException as e: @@ -275,12 +304,15 @@ def __init__(self, marker): raise InvalidMarker(err_str) def __str__(self): + # type: () -> str return _format_marker(self._markers) def __repr__(self): + # type: () -> str return "".format(str(self)) def evaluate(self, environment=None): + # type: (Optional[Dict[str, str]]) -> bool """Evaluate a marker. Return the boolean from evaluating the given marker against the diff --git a/packaging/py.typed b/packaging/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packaging/requirements.py b/packaging/requirements.py index 4d9688b93..91f81ede0 100644 --- a/packaging/requirements.py +++ b/packaging/requirements.py @@ -11,9 +11,13 @@ from pyparsing import Literal as L # noqa from six.moves.urllib import parse as urlparse +from ._typing import TYPE_CHECKING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet +if TYPE_CHECKING: # pragma: no cover + from typing import List + class InvalidRequirement(ValueError): """ @@ -89,6 +93,7 @@ class Requirement(object): # TODO: Can we normalize the name and extra name? def __init__(self, requirement_string): + # type: (str) -> None try: req = REQUIREMENT.parseString(requirement_string) except ParseException as e: @@ -116,7 +121,8 @@ def __init__(self, requirement_string): self.marker = req.marker if req.marker else None def __str__(self): - parts = [self.name] + # type: () -> str + parts = [self.name] # type: List[str] if self.extras: parts.append("[{0}]".format(",".join(sorted(self.extras)))) @@ -135,4 +141,5 @@ def __str__(self): return "".join(parts) def __repr__(self): + # type: () -> str return "".format(str(self)) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 743576a08..fe09bb1db 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -9,8 +9,27 @@ import re from ._compat import string_types, with_metaclass +from ._typing import TYPE_CHECKING +from .utils import canonicalize_version from .version import Version, LegacyVersion, parse +if TYPE_CHECKING: # pragma: no cover + from typing import ( + List, + Dict, + Union, + Iterable, + Iterator, + Optional, + Callable, + Tuple, + FrozenSet, + ) + + ParsedVersion = Union[Version, LegacyVersion] + UnparsedVersion = Union[Version, LegacyVersion, str] + CallableOperator = Callable[[ParsedVersion, str], bool] + class InvalidSpecifier(ValueError): """ @@ -18,9 +37,10 @@ class InvalidSpecifier(ValueError): """ -class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): +class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): # type: ignore @abc.abstractmethod def __str__(self): + # type: () -> str """ Returns the str representation of this Specifier like object. This should be representative of the Specifier itself. @@ -28,12 +48,14 @@ def __str__(self): @abc.abstractmethod def __hash__(self): + # type: () -> int """ Returns a hash value for this Specifier like object. """ @abc.abstractmethod def __eq__(self, other): + # type: (object) -> bool """ Returns a boolean representing whether or not the two Specifier like objects are equal. @@ -41,6 +63,7 @@ def __eq__(self, other): @abc.abstractmethod def __ne__(self, other): + # type: (object) -> bool """ Returns a boolean representing whether or not the two Specifier like objects are not equal. @@ -48,6 +71,7 @@ def __ne__(self, other): @abc.abstractproperty def prereleases(self): + # type: () -> Optional[bool] """ Returns whether or not pre-releases as a whole are allowed by this specifier. @@ -55,6 +79,7 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None """ Sets whether or not pre-releases as a whole are allowed by this specifier. @@ -62,12 +87,14 @@ def prereleases(self, value): @abc.abstractmethod def contains(self, item, prereleases=None): + # type: (str, Optional[bool]) -> bool """ Determines if the given item is contained within this specifier. """ @abc.abstractmethod def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] """ Takes an iterable of items and filters them so that only items which are contained within this specifier are allowed in it. @@ -76,19 +103,24 @@ def filter(self, iterable, prereleases=None): class _IndividualSpecifier(BaseSpecifier): - _operators = {} + _operators = {} # type: Dict[str, str] def __init__(self, spec="", prereleases=None): + # type: (str, Optional[bool]) -> None match = self._regex.search(spec) if not match: raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) - self._spec = (match.group("operator").strip(), match.group("version").strip()) + self._spec = ( + match.group("operator").strip(), + match.group("version").strip(), + ) # type: Tuple[str, str] # Store whether or not this Specifier should accept prereleases self._prereleases = prereleases def __repr__(self): + # type: () -> str pre = ( ", prereleases={0!r}".format(self.prereleases) if self._prereleases is not None @@ -98,26 +130,35 @@ def __repr__(self): return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) def __str__(self): + # type: () -> str return "{0}{1}".format(*self._spec) + @property + def _canonical_spec(self): + # type: () -> Tuple[str, Union[Version, str]] + return self._spec[0], canonicalize_version(self._spec[1]) + def __hash__(self): - return hash(self._spec) + # type: () -> int + return hash(self._canonical_spec) def __eq__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: - other = self.__class__(other) + other = self.__class__(str(other)) except InvalidSpecifier: return NotImplemented elif not isinstance(other, self.__class__): return NotImplemented - return self._spec == other._spec + return self._canonical_spec == other._canonical_spec def __ne__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: - other = self.__class__(other) + other = self.__class__(str(other)) except InvalidSpecifier: return NotImplemented elif not isinstance(other, self.__class__): @@ -126,52 +167,67 @@ def __ne__(self, other): return self._spec != other._spec def _get_operator(self, op): - return getattr(self, "_compare_{0}".format(self._operators[op])) + # type: (str) -> CallableOperator + operator_callable = getattr( + self, "_compare_{0}".format(self._operators[op]) + ) # type: CallableOperator + return operator_callable def _coerce_version(self, version): + # type: (UnparsedVersion) -> ParsedVersion if not isinstance(version, (LegacyVersion, Version)): version = parse(version) return version @property def operator(self): + # type: () -> str return self._spec[0] @property def version(self): + # type: () -> str return self._spec[1] @property def prereleases(self): + # type: () -> Optional[bool] return self._prereleases @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value def __contains__(self, item): + # type: (str) -> bool return self.contains(item) def contains(self, item, prereleases=None): + # type: (UnparsedVersion, Optional[bool]) -> bool + # Determine if prereleases are to be allowed or not. if prereleases is None: prereleases = self.prereleases # Normalize item to a Version or LegacyVersion, this allows us to have # a shortcut for ``"2.0" in Specifier(">=2") - item = self._coerce_version(item) + normalized_item = self._coerce_version(item) # Determine if we should be supporting prereleases in this specifier # or not, if we do not support prereleases than we can short circuit # logic if this version is a prereleases. - if item.is_prerelease and not prereleases: + if normalized_item.is_prerelease and not prereleases: return False # Actually do the comparison to determine if this item is contained # within this Specifier or not. - return self._get_operator(self.operator)(item, self.version) + operator_callable = self._get_operator(self.operator) # type: CallableOperator + return operator_callable(normalized_item, self.version) def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] + yielded = False found_prereleases = [] @@ -230,32 +286,43 @@ class LegacySpecifier(_IndividualSpecifier): } def _coerce_version(self, version): + # type: (Union[ParsedVersion, str]) -> LegacyVersion if not isinstance(version, LegacyVersion): version = LegacyVersion(str(version)) return version def _compare_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective == self._coerce_version(spec) def _compare_not_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective != self._coerce_version(spec) def _compare_less_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective <= self._coerce_version(spec) def _compare_greater_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective >= self._coerce_version(spec) def _compare_less_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective < self._coerce_version(spec) def _compare_greater_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective > self._coerce_version(spec) -def _require_version_compare(fn): +def _require_version_compare( + fn # type: (Callable[[Specifier, ParsedVersion, str], bool]) +): + # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool] @functools.wraps(fn) def wrapped(self, prospective, spec): + # type: (Specifier, ParsedVersion, str) -> bool if not isinstance(prospective, Version): return False return fn(self, prospective, spec) @@ -373,6 +440,8 @@ class Specifier(_IndividualSpecifier): @_require_version_compare def _compare_compatible(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + # Compatible releases have an equivalent combination of >= and ==. That # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to # implement this in terms of the other specifiers instead of @@ -400,56 +469,75 @@ def _compare_compatible(self, prospective, spec): @_require_version_compare def _compare_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + # We need special logic to handle prefix matching if spec.endswith(".*"): # In the case of prefix matching we want to ignore local segment. prospective = Version(prospective.public) # Split the spec out by dots, and pretend that there is an implicit # dot in between a release segment and a pre-release segment. - spec = _version_split(spec[:-2]) # Remove the trailing .* + split_spec = _version_split(spec[:-2]) # Remove the trailing .* # Split the prospective version out by dots, and pretend that there # is an implicit dot in between a release segment and a pre-release # segment. - prospective = _version_split(str(prospective)) + split_prospective = _version_split(str(prospective)) # Shorten the prospective version to be the same length as the spec # so that we can determine if the specifier is a prefix of the # prospective version or not. - prospective = prospective[: len(spec)] + shortened_prospective = split_prospective[: len(split_spec)] # Pad out our two sides with zeros so that they both equal the same # length. - spec, prospective = _pad_version(spec, prospective) + padded_spec, padded_prospective = _pad_version( + split_spec, shortened_prospective + ) + + return padded_prospective == padded_spec else: # Convert our spec string into a Version - spec = Version(spec) + spec_version = Version(spec) # If the specifier does not have a local segment, then we want to # act as if the prospective version also does not have a local # segment. - if not spec.local: + if not spec_version.local: prospective = Version(prospective.public) - return prospective == spec + return prospective == spec_version @_require_version_compare def _compare_not_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool return not self._compare_equal(prospective, spec) @_require_version_compare def _compare_less_than_equal(self, prospective, spec): - return prospective <= Version(spec) + # type: (ParsedVersion, str) -> bool + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) @_require_version_compare def _compare_greater_than_equal(self, prospective, spec): - return prospective >= Version(spec) + # type: (ParsedVersion, str) -> bool + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) @_require_version_compare - def _compare_less_than(self, prospective, spec): + def _compare_less_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is less than the spec # version. If it's not we can short circuit and just return False now @@ -471,10 +559,12 @@ def _compare_less_than(self, prospective, spec): return True @_require_version_compare - def _compare_greater_than(self, prospective, spec): + def _compare_greater_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is greater than the spec # version. If it's not we can short circuit and just return False now @@ -502,10 +592,13 @@ def _compare_greater_than(self, prospective, spec): return True def _compare_arbitrary(self, prospective, spec): + # type: (Version, str) -> bool return str(prospective).lower() == str(spec).lower() @property def prereleases(self): + # type: () -> bool + # If there is an explicit prereleases set for this, then we'll just # blindly use that. if self._prereleases is not None: @@ -530,6 +623,7 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value @@ -537,7 +631,8 @@ def prereleases(self, value): def _version_split(version): - result = [] + # type: (str) -> List[str] + result = [] # type: List[str] for item in version.split("."): match = _prefix_regex.search(item) if match: @@ -548,6 +643,7 @@ def _version_split(version): def _pad_version(left, right): + # type: (List[str], List[str]) -> Tuple[List[str], List[str]] left_split, right_split = [], [] # Get the release segment of our versions @@ -567,14 +663,16 @@ def _pad_version(left, right): class SpecifierSet(BaseSpecifier): def __init__(self, specifiers="", prereleases=None): - # Split on , to break each indidivual specifier into it's own item, and + # type: (str, Optional[bool]) -> None + + # Split on , to break each individual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. - specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] # Parsed each individual specifier, attempting first to make it a # Specifier and falling back to a LegacySpecifier. parsed = set() - for specifier in specifiers: + for specifier in split_specifiers: try: parsed.add(Specifier(specifier)) except InvalidSpecifier: @@ -588,6 +686,7 @@ def __init__(self, specifiers="", prereleases=None): self._prereleases = prereleases def __repr__(self): + # type: () -> str pre = ( ", prereleases={0!r}".format(self.prereleases) if self._prereleases is not None @@ -597,12 +696,15 @@ def __repr__(self): return "".format(str(self), pre) def __str__(self): + # type: () -> str return ",".join(sorted(str(s) for s in self._specs)) def __hash__(self): + # type: () -> int return hash(self._specs) def __and__(self, other): + # type: (Union[SpecifierSet, str]) -> SpecifierSet if isinstance(other, string_types): other = SpecifierSet(other) elif not isinstance(other, SpecifierSet): @@ -626,9 +728,8 @@ def __and__(self, other): return specifier def __eq__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -636,9 +737,8 @@ def __eq__(self, other): return self._specs == other._specs def __ne__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -646,13 +746,17 @@ def __ne__(self, other): return self._specs != other._specs def __len__(self): + # type: () -> int return len(self._specs) def __iter__(self): + # type: () -> Iterator[FrozenSet[_IndividualSpecifier]] return iter(self._specs) @property def prereleases(self): + # type: () -> Optional[bool] + # If we have been given an explicit prerelease modifier, then we'll # pass that through here. if self._prereleases is not None: @@ -670,12 +774,16 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value def __contains__(self, item): + # type: (Union[ParsedVersion, str]) -> bool return self.contains(item) def contains(self, item, prereleases=None): + # type: (Union[ParsedVersion, str], Optional[bool]) -> bool + # Ensure that our item is a Version or LegacyVersion instance. if not isinstance(item, (LegacyVersion, Version)): item = parse(item) @@ -701,7 +809,13 @@ def contains(self, item, prereleases=None): # will always return True, this is an explicit design decision. return all(s.contains(item, prereleases=prereleases) for s in self._specs) - def filter(self, iterable, prereleases=None): + def filter( + self, + iterable, # type: Iterable[Union[ParsedVersion, str]] + prereleases=None, # type: Optional[bool] + ): + # type: (...) -> Iterable[Union[ParsedVersion, str]] + # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the # SpecifierSet thinks for whether or not we should support prereleases. @@ -719,8 +833,8 @@ def filter(self, iterable, prereleases=None): # which will filter out any pre-releases, unless there are no final # releases, and which will filter out LegacyVersion in general. else: - filtered = [] - found_prereleases = [] + filtered = [] # type: List[Union[ParsedVersion, str]] + found_prereleases = [] # type: List[Union[ParsedVersion, str]] for item in iterable: # Ensure that we some kind of Version class for this item. diff --git a/packaging/tags.py b/packaging/tags.py index ec9942f0f..9064910b8 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -13,12 +13,37 @@ EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] del imp +import logging +import os import platform import re +import struct import sys import sysconfig import warnings +from ._typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: # pragma: no cover + from typing import ( + Dict, + FrozenSet, + IO, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + ) + + PythonVersion = Sequence[int] + MacVersion = Tuple[int, int] + GlibcVersion = Tuple[int, int] + + +logger = logging.getLogger(__name__) INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. @@ -26,34 +51,48 @@ "pypy": "pp", "ironpython": "ip", "jython": "jy", -} +} # type: Dict[str, str] _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 class Tag(object): + """ + A representation of the tag triple for a wheel. + + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ __slots__ = ["_interpreter", "_abi", "_platform"] def __init__(self, interpreter, abi, platform): + # type: (str, str, str) -> None self._interpreter = interpreter.lower() self._abi = abi.lower() self._platform = platform.lower() @property def interpreter(self): + # type: () -> str return self._interpreter @property def abi(self): + # type: () -> str return self._abi @property def platform(self): + # type: () -> str return self._platform def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, Tag): + return NotImplemented + return ( (self.platform == other.platform) and (self.abi == other.abi) @@ -61,16 +100,26 @@ def __eq__(self, other): ) def __hash__(self): + # type: () -> int return hash((self._interpreter, self._abi, self._platform)) def __str__(self): + # type: () -> str return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) def __repr__(self): + # type: () -> str return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) def parse_tag(tag): + # type: (str) -> FrozenSet[Tag] + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ tags = set() interpreters, abis, platforms = tag.split("-") for interpreter in interpreters.split("."): @@ -80,20 +129,54 @@ def parse_tag(tag): return frozenset(tags) +def _warn_keyword_parameter(func_name, kwargs): + # type: (str, Dict[str, bool]) -> bool + """ + Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only. + """ + if not kwargs: + return False + elif len(kwargs) > 1 or "warn" not in kwargs: + kwargs.pop("warn", None) + arg = next(iter(kwargs.keys())) + raise TypeError( + "{}() got an unexpected keyword argument {!r}".format(func_name, arg) + ) + return kwargs["warn"] + + +def _get_config_var(name, warn=False): + # type: (str, bool) -> Union[int, str, None] + value = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + def _normalize_string(string): + # type: (str) -> str return string.replace(".", "_").replace("-", "_") -def _cpython_interpreter(py_version): - # TODO: Is using py_version_nodot for interpreter version critical? - return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) +def _abi3_applies(python_version): + # type: (PythonVersion) -> bool + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) -def _cpython_abis(py_version): +def _cpython_abis(py_version, warn=False): + # type: (PythonVersion, bool) -> List[str] + py_version = tuple(py_version) # To allow for version comparison. abis = [] - version = "{}{}".format(*py_version[:2]) + version = _version_nodot(py_version[:2]) debug = pymalloc = ucs4 = "" - with_debug = sysconfig.get_config_var("Py_DEBUG") + with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled # extension modules is the best option. @@ -102,11 +185,11 @@ def _cpython_abis(py_version): if with_debug or (with_debug is None and (has_refcount or has_ext)): debug = "d" if py_version < (3, 8): - with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) if with_pymalloc or with_pymalloc is None: pymalloc = "m" if py_version < (3, 3): - unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) if unicode_size == 4 or ( unicode_size is None and sys.maxunicode == 0x10FFFF ): @@ -124,86 +207,148 @@ def _cpython_abis(py_version): return abis -def _cpython_tags(py_version, interpreter, abis, platforms): +def cpython_tags( + python_version=None, # type: Optional[PythonVersion] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + warn = _warn_keyword_parameter("cpython_tags", kwargs) + if not python_version: + python_version = sys.version_info[:2] + + interpreter = "cp{}".format(_version_nodot(python_version[:2])) + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or _platform_tags()) for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): - yield tag + if _abi3_applies(python_version): + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): yield tag - # PEP 384 was first implemented in Python 3.2. - for minor_version in range(py_version[1] - 1, 1, -1): - for platform_ in platforms: - interpreter = "cp{major}{minor}".format( - major=py_version[0], minor=minor_version - ) - yield Tag(interpreter, "abi3", platform_) - -def _pypy_interpreter(): - return "pp{py_major}{pypy_major}{pypy_minor}".format( - py_major=sys.version_info[0], - pypy_major=sys.pypy_version_info.major, - pypy_minor=sys.pypy_version_info.minor, - ) + if _abi3_applies(python_version): + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) + ) + yield Tag(interpreter, "abi3", platform_) def _generic_abi(): + # type: () -> Iterator[str] abi = sysconfig.get_config_var("SOABI") if abi: - return _normalize_string(abi) - else: - return "none" + yield _normalize_string(abi) -def _pypy_tags(py_version, interpreter, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - for tag in (Tag(interpreter, "none", platform) for platform in platforms): - yield tag +def generic_tags( + interpreter=None, # type: Optional[str] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a generic interpreter. + The tags consist of: + - -- -def _generic_tags(interpreter, py_version, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - if abi != "none": - tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) - for tag in tags: - yield tag + The "none" ABI will be added if it was not explicitly provided. + """ + warn = _warn_keyword_parameter("generic_tags", kwargs) + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + platforms = list(platforms or _platform_tags()) + abis = list(abis) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) def _py_interpreter_range(py_version): + # type: (PythonVersion) -> Iterator[str] """ - Yield Python versions in descending order. + Yields Python versions in descending order. After the latest version, the major-only version will be yielded, and then - all following versions up to 'end'. + all previous versions of that major version. """ - yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + if len(py_version) > 1: + yield "py{version}".format(version=_version_nodot(py_version[:2])) yield "py{major}".format(major=py_version[0]) - for minor in range(py_version[1] - 1, -1, -1): - yield "py{major}{minor}".format(major=py_version[0], minor=minor) + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield "py{version}".format(version=_version_nodot((py_version[0], minor))) -def _independent_tags(interpreter, py_version, platforms): +def compatible_tags( + python_version=None, # type: Optional[PythonVersion] + interpreter=None, # type: Optional[str] + platforms=None, # type: Optional[Iterable[str]] +): + # type: (...) -> Iterator[Tag] """ - Return the sequence of tags that are consistent across implementations. + Yields the sequence of tags that are compatible with a specific version of Python. The tags consist of: - py*-none- - - -none-any + - -none-any # ... if `interpreter` is provided. - py*-none-any """ - for version in _py_interpreter_range(py_version): + if not python_version: + python_version = sys.version_info[:2] + platforms = list(platforms or _platform_tags()) + for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) - yield Tag(interpreter, "none", "any") - for version in _py_interpreter_range(py_version): + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): yield Tag(version, "none", "any") def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + # type: (str, bool) -> str if not is_32bit: return arch @@ -214,6 +359,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): def _mac_binary_formats(version, cpu_arch): + # type: (MacVersion, str) -> List[str] formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -240,32 +386,42 @@ def _mac_binary_formats(version, cpu_arch): return formats -def _mac_platforms(version=None, arch=None): - version_str, _, cpu_arch = platform.mac_ver() +def mac_platforms(version=None, arch=None): + # type: (Optional[MacVersion], Optional[str]) -> Iterator[str] + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() # type: ignore if version is None: - version = tuple(map(int, version_str.split(".")[:2])) + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version if arch is None: arch = _mac_arch(cpu_arch) - platforms = [] + else: + arch = arch for minor_version in range(version[1], -1, -1): compat_version = version[0], minor_version binary_formats = _mac_binary_formats(compat_version, arch) for binary_format in binary_formats: - platforms.append( - "macosx_{major}_{minor}_{binary_format}".format( - major=compat_version[0], - minor=compat_version[1], - binary_format=binary_format, - ) + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, ) - return platforms # From PEP 513. def _is_manylinux_compatible(name, glibc_version): + # type: (str, GlibcVersion) -> bool # Check for presence of _manylinux module. try: - import _manylinux + import _manylinux # noqa return bool(getattr(_manylinux, name + "_compatible")) except (ImportError, AttributeError): @@ -276,14 +432,50 @@ def _is_manylinux_compatible(name, glibc_version): def _glibc_version_string(): + # type: () -> Optional[str] # Returns glibc version string, or None if not using glibc. - import ctypes + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _glibc_version_string_confstr(): + # type: () -> Optional[str] + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 + try: + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". + version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 + "CS_GNU_LIBC_VERSION" + ) + assert version_string is not None + _, version = version_string.split() # type: Tuple[str, str] + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes(): + # type: () -> Optional[str] + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen # manpage says, "If filename is NULL, then the returned handle is for the # main program". This way we can let the linker do the work to figure out # which libc our process is actually using. - process_namespace = ctypes.CDLL(None) + # + # Note: typeshed is wrong here so we are ignoring this line. + process_namespace = ctypes.CDLL(None) # type: ignore try: gnu_get_libc_version = process_namespace.gnu_get_libc_version except AttributeError: @@ -293,7 +485,7 @@ def _glibc_version_string(): # Call gnu_get_libc_version, which returns a string like "2.5" gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() + version_str = gnu_get_libc_version() # type: str # py2 / py3 compatibility: if not isinstance(version_str, str): version_str = version_str.decode("ascii") @@ -303,6 +495,7 @@ def _glibc_version_string(): # Separated out from have_compatible_glibc for easier unit testing. def _check_glibc_version(version_str, required_major, minimum_minor): + # type: (str, int, int) -> bool # Parse string and check against requested version. # # We use a regexp instead of str.split because we want to discard any @@ -324,81 +517,235 @@ def _check_glibc_version(version_str, required_major, minimum_minor): def _have_compatible_glibc(required_major, minimum_minor): + # type: (int, int) -> bool version_str = _glibc_version_string() if version_str is None: return False return _check_glibc_version(version_str, required_major, minimum_minor) +# Python does not provide platform information at sufficient granularity to +# identify the architecture of the running executable in some cases, so we +# determine it dynamically by reading the information from the running +# process. This only applies on Linux, which uses the ELF format. +class _ELFFileHeader(object): + # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + class _InvalidELFFileHeader(ValueError): + """ + An invalid ELF file header was found. + """ + + ELF_MAGIC_NUMBER = 0x7F454C46 + ELFCLASS32 = 1 + ELFCLASS64 = 2 + ELFDATA2LSB = 1 + ELFDATA2MSB = 2 + EM_386 = 3 + EM_S390 = 22 + EM_ARM = 40 + EM_X86_64 = 62 + EF_ARM_ABIMASK = 0xFF000000 + EF_ARM_ABI_VER5 = 0x05000000 + EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + def __init__(self, file): + # type: (IO[bytes]) -> None + def unpack(fmt): + # type: (str) -> int + try: + (result,) = struct.unpack( + fmt, file.read(struct.calcsize(fmt)) + ) # type: (int, ) + except struct.error: + raise _ELFFileHeader._InvalidELFFileHeader() + return result + + self.e_ident_magic = unpack(">I") + if self.e_ident_magic != self.ELF_MAGIC_NUMBER: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_class = unpack("B") + if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_data = unpack("B") + if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_version = unpack("B") + self.e_ident_osabi = unpack("B") + self.e_ident_abiversion = unpack("B") + self.e_ident_pad = file.read(7) + format_h = "H" + format_i = "I" + format_q = "Q" + format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q + self.e_type = unpack(format_h) + self.e_machine = unpack(format_h) + self.e_version = unpack(format_i) + self.e_entry = unpack(format_p) + self.e_phoff = unpack(format_p) + self.e_shoff = unpack(format_p) + self.e_flags = unpack(format_i) + self.e_ehsize = unpack(format_h) + self.e_phentsize = unpack(format_h) + self.e_phnum = unpack(format_h) + self.e_shentsize = unpack(format_h) + self.e_shnum = unpack(format_h) + self.e_shstrndx = unpack(format_h) + + +def _get_elf_header(): + # type: () -> Optional[_ELFFileHeader] + try: + with open(sys.executable, "rb") as f: + elf_header = _ELFFileHeader(f) + except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): + return None + return elf_header + + +def _is_linux_armhf(): + # type: () -> bool + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_ARM + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABIMASK + ) == elf_header.EF_ARM_ABI_VER5 + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD + ) == elf_header.EF_ARM_ABI_FLOAT_HARD + return result + + +def _is_linux_i686(): + # type: () -> bool + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_386 + return result + + +def _have_compatible_manylinux_abi(arch): + # type: (str) -> bool + if arch == "armv7l": + return _is_linux_armhf() + if arch == "i686": + return _is_linux_i686() + return True + + def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + # type: (bool) -> Iterator[str] linux = _normalize_string(distutils.util.get_platform()) - if linux == "linux_x86_64" and is_32bit: - linux = "linux_i686" - manylinux_support = ( - ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) - ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) - ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) - ) + if is_32bit: + if linux == "linux_x86_64": + linux = "linux_i686" + elif linux == "linux_aarch64": + linux = "linux_armv7l" + manylinux_support = [] + _, arch = linux.split("_", 1) + if _have_compatible_manylinux_abi(arch): + if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}: + manylinux_support.append( + ("manylinux2014", (2, 17)) + ) # CentOS 7 w/ glibc 2.17 (PEP 599) + if arch in {"x86_64", "i686"}: + manylinux_support.append( + ("manylinux2010", (2, 12)) + ) # CentOS 6 w/ glibc 2.12 (PEP 571) + manylinux_support.append( + ("manylinux1", (2, 5)) + ) # CentOS 5 w/ glibc 2.5 (PEP 513) manylinux_support_iter = iter(manylinux_support) for name, glibc_version in manylinux_support_iter: if _is_manylinux_compatible(name, glibc_version): - platforms = [linux.replace("linux", name)] + yield linux.replace("linux", name) break - else: - platforms = [] # Support for a later manylinux implies support for an earlier version. - platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] - platforms.append(linux) - return platforms + for name, _ in manylinux_support_iter: + yield linux.replace("linux", name) + yield linux def _generic_platforms(): - platform = _normalize_string(distutils.util.get_platform()) - return [platform] + # type: () -> Iterator[str] + yield _normalize_string(distutils.util.get_platform()) -def _interpreter_name(): - name = platform.python_implementation().lower() +def _platform_tags(): + # type: () -> Iterator[str] + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() + + +def interpreter_name(): + # type: () -> str + """ + Returns the name of the running interpreter. + """ + try: + name = sys.implementation.name # type: ignore + except AttributeError: # pragma: no cover + # Python 2.7 compatibility. + name = platform.python_implementation().lower() return INTERPRETER_SHORT_NAMES.get(name) or name -def _generic_interpreter(name, py_version): - version = sysconfig.get_config_var("py_version_nodot") - if not version: - version = "".join(map(str, py_version[:2])) - return "{name}{version}".format(name=name, version=version) +def interpreter_version(**kwargs): + # type: (bool) -> str + """ + Returns the version of the running interpreter. + """ + warn = _warn_keyword_parameter("interpreter_version", kwargs) + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + return version + + +def _version_nodot(version): + # type: (PythonVersion) -> str + if any(v >= 10 for v in version): + sep = "_" + else: + sep = "" + return sep.join(map(str, version)) -def sys_tags(): +def sys_tags(**kwargs): + # type: (bool) -> Iterator[Tag] """ Returns the sequence of tag triples for the running interpreter. The order of the sequence corresponds to priority order for the interpreter, from most to least important. """ - py_version = sys.version_info[:2] - interpreter_name = _interpreter_name() - if platform.system() == "Darwin": - platforms = _mac_platforms() - elif platform.system() == "Linux": - platforms = _linux_platforms() - else: - platforms = _generic_platforms() + warn = _warn_keyword_parameter("sys_tags", kwargs) - if interpreter_name == "cp": - interpreter = _cpython_interpreter(py_version) - abis = _cpython_abis(py_version) - for tag in _cpython_tags(py_version, interpreter, abis, platforms): - yield tag - elif interpreter_name == "pp": - interpreter = _pypy_interpreter() - abi = _generic_abi() - for tag in _pypy_tags(py_version, interpreter, abi, platforms): + interp_name = interpreter_name() + if interp_name == "cp": + for tag in cpython_tags(warn=warn): yield tag else: - interpreter = _generic_interpreter(interpreter_name, py_version) - abi = _generic_abi() - for tag in _generic_tags(interpreter, py_version, abi, platforms): + for tag in generic_tags(): yield tag - for tag in _independent_tags(interpreter, py_version, platforms): + + for tag in compatible_tags(): yield tag diff --git a/packaging/utils.py b/packaging/utils.py index 884187869..19579c1a0 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -5,28 +5,36 @@ import re +from ._typing import TYPE_CHECKING, cast from .version import InvalidVersion, Version +if TYPE_CHECKING: # pragma: no cover + from typing import NewType, Union + + NormalizedName = NewType("NormalizedName", str) _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): + # type: (str) -> NormalizedName # This is taken from PEP 503. - return _canonicalize_regex.sub("-", name).lower() + value = _canonicalize_regex.sub("-", name).lower() + return cast("NormalizedName", value) -def canonicalize_version(version): +def canonicalize_version(_version): + # type: (str) -> Union[Version, str] """ - This is very similar to Version.__str__, but has one subtle differences + This is very similar to Version.__str__, but has one subtle difference with the way it handles the release segment. """ try: - version = Version(version) + version = Version(_version) except InvalidVersion: # Legacy versions cannot be normalized - return version + return _version parts = [] diff --git a/packaging/version.py b/packaging/version.py index 95157a1f7..00371e86a 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -7,8 +7,35 @@ import itertools import re -from ._structures import Infinity - +from ._structures import Infinity, NegativeInfinity +from ._typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union + + from ._structures import InfinityType, NegativeInfinityType + + InfiniteTypes = Union[InfinityType, NegativeInfinityType] + PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] + SubLocalType = Union[InfiniteTypes, int, str] + LocalType = Union[ + NegativeInfinityType, + Tuple[ + Union[ + SubLocalType, + Tuple[SubLocalType, str], + Tuple[NegativeInfinityType, SubLocalType], + ], + ..., + ], + ] + CmpKey = Tuple[ + int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType + ] + LegacyCmpKey = Tuple[int, Tuple[str, ...]] + VersionComparisonMethod = Callable[ + [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool + ] __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] @@ -19,6 +46,7 @@ def parse(version): + # type: (str) -> Union[LegacyVersion, Version] """ Parse the given version string and return either a :class:`Version` object or a :class:`LegacyVersion` object depending on if the given version is @@ -37,28 +65,38 @@ class InvalidVersion(ValueError): class _BaseVersion(object): + _key = None # type: Union[CmpKey, LegacyCmpKey] + def __hash__(self): + # type: () -> int return hash(self._key) def __lt__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s < o) def __le__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s <= o) def __eq__(self, other): + # type: (object) -> bool return self._compare(other, lambda s, o: s == o) def __ge__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s >= o) def __gt__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s > o) def __ne__(self, other): + # type: (object) -> bool return self._compare(other, lambda s, o: s != o) def _compare(self, other, method): + # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented] if not isinstance(other, _BaseVersion): return NotImplemented @@ -67,57 +105,71 @@ def _compare(self, other, method): class LegacyVersion(_BaseVersion): def __init__(self, version): + # type: (str) -> None self._version = str(version) self._key = _legacy_cmpkey(self._version) def __str__(self): + # type: () -> str return self._version def __repr__(self): + # type: () -> str return "".format(repr(str(self))) @property def public(self): + # type: () -> str return self._version @property def base_version(self): + # type: () -> str return self._version @property def epoch(self): + # type: () -> int return -1 @property def release(self): + # type: () -> None return None @property def pre(self): + # type: () -> None return None @property def post(self): + # type: () -> None return None @property def dev(self): + # type: () -> None return None @property def local(self): + # type: () -> None return None @property def is_prerelease(self): + # type: () -> bool return False @property def is_postrelease(self): + # type: () -> bool return False @property def is_devrelease(self): + # type: () -> bool return False @@ -133,6 +185,7 @@ def is_devrelease(self): def _parse_version_parts(s): + # type: (str) -> Iterator[str] for part in _legacy_version_component_re.split(s): part = _legacy_version_replacement_map.get(part, part) @@ -150,6 +203,8 @@ def _parse_version_parts(s): def _legacy_cmpkey(version): + # type: (str) -> LegacyCmpKey + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch # greater than or equal to 0. This will effectively put the LegacyVersion, # which uses the defacto standard originally implemented by setuptools, @@ -158,7 +213,7 @@ def _legacy_cmpkey(version): # This scheme is taken from pkg_resources.parse_version setuptools prior to # it's adoption of the packaging library. - parts = [] + parts = [] # type: List[str] for part in _parse_version_parts(version.lower()): if part.startswith("*"): # remove "-" before a prerelease tag @@ -171,9 +226,8 @@ def _legacy_cmpkey(version): parts.pop() parts.append(part) - parts = tuple(parts) - return epoch, parts + return epoch, tuple(parts) # Deliberately not anchored to the start and end of the string, to make it @@ -215,6 +269,8 @@ class Version(_BaseVersion): _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) def __init__(self, version): + # type: (str) -> None + # Validate the version and parse it into pieces match = self._regex.search(version) if not match: @@ -243,9 +299,11 @@ def __init__(self, version): ) def __repr__(self): + # type: () -> str return "".format(repr(str(self))) def __str__(self): + # type: () -> str parts = [] # Epoch @@ -275,26 +333,35 @@ def __str__(self): @property def epoch(self): - return self._version.epoch + # type: () -> int + _epoch = self._version.epoch # type: int + return _epoch @property def release(self): - return self._version.release + # type: () -> Tuple[int, ...] + _release = self._version.release # type: Tuple[int, ...] + return _release @property def pre(self): - return self._version.pre + # type: () -> Optional[Tuple[str, int]] + _pre = self._version.pre # type: Optional[Tuple[str, int]] + return _pre @property def post(self): + # type: () -> Optional[Tuple[str, int]] return self._version.post[1] if self._version.post else None @property def dev(self): + # type: () -> Optional[Tuple[str, int]] return self._version.dev[1] if self._version.dev else None @property def local(self): + # type: () -> Optional[str] if self._version.local: return ".".join(str(x) for x in self._version.local) else: @@ -302,10 +369,12 @@ def local(self): @property def public(self): + # type: () -> str return str(self).split("+", 1)[0] @property def base_version(self): + # type: () -> str parts = [] # Epoch @@ -319,18 +388,41 @@ def base_version(self): @property def is_prerelease(self): + # type: () -> bool return self.dev is not None or self.pre is not None @property def is_postrelease(self): + # type: () -> bool return self.post is not None @property def is_devrelease(self): + # type: () -> bool return self.dev is not None + @property + def major(self): + # type: () -> int + return self.release[0] if len(self.release) >= 1 else 0 + + @property + def minor(self): + # type: () -> int + return self.release[1] if len(self.release) >= 2 else 0 + + @property + def micro(self): + # type: () -> int + return self.release[2] if len(self.release) >= 3 else 0 + + +def _parse_letter_version( + letter, # type: str + number, # type: Union[str, bytes, SupportsInt] +): + # type: (...) -> Optional[Tuple[str, int]] -def _parse_letter_version(letter, number): if letter: # We consider there to be an implicit 0 in a pre-release if there is # not a numeral associated with it. @@ -360,11 +452,14 @@ def _parse_letter_version(letter, number): return letter, int(number) + return None + _local_version_separators = re.compile(r"[\._-]") def _parse_local_version(local): + # type: (str) -> Optional[LocalType] """ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). """ @@ -373,15 +468,25 @@ def _parse_local_version(local): part.lower() if not part.isdigit() else int(part) for part in _local_version_separators.split(local) ) + return None + +def _cmpkey( + epoch, # type: int + release, # type: Tuple[int, ...] + pre, # type: Optional[Tuple[str, int]] + post, # type: Optional[Tuple[str, int]] + dev, # type: Optional[Tuple[str, int]] + local, # type: Optional[Tuple[SubLocalType]] +): + # type: (...) -> CmpKey -def _cmpkey(epoch, release, pre, post, dev, local): # When we compare a release version, we want to compare it with all of the # trailing zeros removed. So we'll use a reverse the list, drop all the now # leading zeros until we come to something non zero, then take the rest # re-reverse it back into the correct order and make it a tuple and use # that for our sorting key. - release = tuple( + _release = tuple( reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) ) @@ -390,23 +495,31 @@ def _cmpkey(epoch, release, pre, post, dev, local): # if there is not a pre or a post segment. If we have one of those then # the normal sorting rules will handle this case correctly. if pre is None and post is None and dev is not None: - pre = -Infinity + _pre = NegativeInfinity # type: PrePostDevType # Versions without a pre-release (except as noted above) should sort after # those with one. elif pre is None: - pre = Infinity + _pre = Infinity + else: + _pre = pre # Versions without a post segment should sort before those with one. if post is None: - post = -Infinity + _post = NegativeInfinity # type: PrePostDevType + + else: + _post = post # Versions without a development segment should sort after those with one. if dev is None: - dev = Infinity + _dev = Infinity # type: PrePostDevType + + else: + _dev = dev if local is None: # Versions without a local segment should sort before those with one. - local = -Infinity + _local = NegativeInfinity # type: LocalType else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -415,6 +528,8 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Numeric segments sort numerically # - Shorter versions sort before longer versions when the prefixes # match exactly - local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local) + _local = tuple( + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local + ) - return epoch, release, pre, post, dev, local + return epoch, _release, _pre, _post, _dev, _local diff --git a/setup.cfg b/setup.cfg index 3c6e79cf3..c67cd2261 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,23 @@ [bdist_wheel] universal=1 + +[mypy] +ignore_missing_imports = True + +# The following are the flags enabled by --strict +# +# Note: warn_unused_ignores is False due to incorrect typeshed annotations for +# platform.mac_ver() +warn_unused_configs = True +disallow_subclassing_any = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = False +warn_return_any = True +no_implicit_reexport = True diff --git a/setup.py b/setup.py index b38efb829..f533a7d4a 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,10 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ], packages=["packaging"], + package_data={"packaging": ["py.typed"]}, ) diff --git a/tests/build-hello-world.sh b/tests/build-hello-world.sh new file mode 100755 index 000000000..9c3e1e181 --- /dev/null +++ b/tests/build-hello-world.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -x +set -e + +if [ $# -eq 0 ]; then + docker run --rm -v $(pwd):/home/hello-world arm32v5/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world arm32v7/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world i386/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world s390x/debian /home/hello-world/build-hello-world.sh incontainer 64 + docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh incontainer 64 + docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh x32 52 + cp -f hello-world-x86_64-i386 hello-world-invalid-magic + printf "\x00" | dd of=hello-world-invalid-magic bs=1 seek=0x00 count=1 conv=notrunc + cp -f hello-world-x86_64-i386 hello-world-invalid-class + printf "\x00" | dd of=hello-world-invalid-class bs=1 seek=0x04 count=1 conv=notrunc + cp -f hello-world-x86_64-i386 hello-world-invalid-data + printf "\x00" | dd of=hello-world-invalid-data bs=1 seek=0x05 count=1 conv=notrunc + head -c 40 hello-world-x86_64-i386 > hello-world-too-short + exit 0 +fi + +export DEBIAN_FRONTEND=noninteractive +cd /home/hello-world/ +apt-get update +apt-get install -y --no-install-recommends gcc libc6-dev +if [ "$1" == "incontainer" ]; then + ARCH=$(dpkg --print-architecture) + CFLAGS="" +else + ARCH=$1 + dpkg --add-architecture ${ARCH} + apt-get install -y --no-install-recommends gcc-multilib libc6-dev-${ARCH} + CFLAGS="-mx32" +fi +NAME=hello-world-$(uname -m)-${ARCH} +gcc -Os -s ${CFLAGS} -o ${NAME}-full hello-world.c +head -c $2 ${NAME}-full > ${NAME} +rm -f ${NAME}-full diff --git a/tests/hello-world-armv7l-armel b/tests/hello-world-armv7l-armel new file mode 100755 index 000000000..1dfd23fa3 Binary files /dev/null and b/tests/hello-world-armv7l-armel differ diff --git a/tests/hello-world-armv7l-armhf b/tests/hello-world-armv7l-armhf new file mode 100755 index 000000000..965ab3003 Binary files /dev/null and b/tests/hello-world-armv7l-armhf differ diff --git a/tests/hello-world-invalid-class b/tests/hello-world-invalid-class new file mode 100755 index 000000000..5e9899fc0 Binary files /dev/null and b/tests/hello-world-invalid-class differ diff --git a/tests/hello-world-invalid-data b/tests/hello-world-invalid-data new file mode 100755 index 000000000..2659b8ee2 Binary files /dev/null and b/tests/hello-world-invalid-data differ diff --git a/tests/hello-world-invalid-magic b/tests/hello-world-invalid-magic new file mode 100755 index 000000000..46066ad2d Binary files /dev/null and b/tests/hello-world-invalid-magic differ diff --git a/tests/hello-world-s390x-s390x b/tests/hello-world-s390x-s390x new file mode 100644 index 000000000..c4e957888 Binary files /dev/null and b/tests/hello-world-s390x-s390x differ diff --git a/tests/hello-world-too-short b/tests/hello-world-too-short new file mode 100644 index 000000000..4e5c0396b Binary files /dev/null and b/tests/hello-world-too-short differ diff --git a/tests/hello-world-x86_64-amd64 b/tests/hello-world-x86_64-amd64 new file mode 100644 index 000000000..c7f5b0b5e Binary files /dev/null and b/tests/hello-world-x86_64-amd64 differ diff --git a/tests/hello-world-x86_64-i386 b/tests/hello-world-x86_64-i386 new file mode 100755 index 000000000..ff1d540a3 Binary files /dev/null and b/tests/hello-world-x86_64-i386 differ diff --git a/tests/hello-world-x86_64-x32 b/tests/hello-world-x86_64-x32 new file mode 100755 index 000000000..daf85d347 Binary files /dev/null and b/tests/hello-world-x86_64-x32 differ diff --git a/tests/hello-world.c b/tests/hello-world.c new file mode 100644 index 000000000..5e591c3ec --- /dev/null +++ b/tests/hello-world.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char* argv[]) +{ + printf("Hello world"); + return 0; +} diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index a3feecc23..6126f3657 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -253,6 +253,12 @@ def test_comparison_true(self, left, right, op): assert op(left, Specifier(right)) assert op(Specifier(left), right) + @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")]) + def test_comparison_canonicalizes(self, left, right): + assert Specifier(left) == Specifier(right) + assert left == Specifier(right) + assert Specifier(left) == right + @pytest.mark.parametrize( ("left", "right", "op"), itertools.chain( @@ -961,6 +967,26 @@ def test_comparison_false(self, left, right, op): assert not op(left, SpecifierSet(right)) assert not op(SpecifierSet(left), right) + @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")]) + def test_comparison_canonicalizes(self, left, right): + assert SpecifierSet(left) == SpecifierSet(right) + assert left == SpecifierSet(right) + assert SpecifierSet(left) == right + def test_comparison_non_specifier(self): assert SpecifierSet("==1.0") != 12 assert not SpecifierSet("==1.0") == 12 + + @pytest.mark.parametrize( + ("version", "specifier", "expected"), + [ + ("1.0.0+local", "==1.0.0", True), + ("1.0.0+local", "!=1.0.0", False), + ("1.0.0+local", "<=1.0.0", True), + ("1.0.0+local", ">=1.0.0", True), + ("1.0.0+local", "<1.0.0", False), + ("1.0.0+local", ">1.0.0", False), + ], + ) + def test_comparison_ignores_local(self, version, specifier, expected): + assert (Version(version) in SpecifierSet(specifier)) == expected diff --git a/tests/test_tags.py b/tests/test_tags.py index 39927e684..4840e196d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -2,7 +2,10 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -import collections +try: + import collections.abc as collections_abc +except ImportError: + import collections as collections_abc try: import ctypes @@ -10,6 +13,7 @@ ctypes = None import distutils.util +import os import platform import re import sys @@ -17,6 +21,7 @@ import types import warnings +import pretend import pytest from packaging import tags @@ -33,597 +38,1114 @@ def is_x86(): @pytest.fixture -def is_64bit_os(): - return platform.architecture()[0] == "64bit" - - -def test_tag_lowercasing(): - tag = tags.Tag("PY3", "None", "ANY") - assert tag.interpreter == "py3" - assert tag.abi == "none" - assert tag.platform == "any" - - -def test_tag_equality(): - args = "py3", "none", "any" - assert tags.Tag(*args) == tags.Tag(*args) - - -def test_tag_hashing(example_tag): - tags = {example_tag} # Should not raise TypeError. - assert example_tag in tags - - -def test_tag_hash_equality(example_tag): - equal_tag = tags.Tag("py3", "none", "any") - assert example_tag == equal_tag - assert example_tag.__hash__() == equal_tag.__hash__() - - -def test_tag_str(example_tag): - assert str(example_tag) == "py3-none-any" - - -def test_tag_repr(example_tag): - assert repr(example_tag) == "".format( - tag_id=id(example_tag) - ) - - -def test_tag_attribute_access(example_tag): - assert example_tag.interpreter == "py3" - assert example_tag.abi == "none" - assert example_tag.platform == "any" - - -def test_parse_tag_simple(example_tag): - parsed_tags = tags.parse_tag(str(example_tag)) - assert parsed_tags == {example_tag} - - -def test_parse_tag_multi_interpreter(example_tag): - expected = {example_tag, tags.Tag("py2", "none", "any")} - given = tags.parse_tag("py2.py3-none-any") - assert given == expected +def manylinux_module(monkeypatch): + monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) + module_name = "_manylinux" + module = types.ModuleType(module_name) + monkeypatch.setitem(sys.modules, module_name, module) + return module -def test_parse_tag_multi_platform(): - expected = { - tags.Tag("cp37", "cp37m", platform) - for platform in ( - "macosx_10_6_intel", - "macosx_10_9_intel", - "macosx_10_9_x86_64", - "macosx_10_10_intel", - "macosx_10_10_x86_64", +@pytest.fixture +def mock_interpreter_name(monkeypatch): + def mock(name): + if hasattr(sys, "implementation") and sys.implementation.name != name.lower(): + monkeypatch.setattr(sys.implementation, "name", name.lower()) + return True + elif platform.python_implementation() != name: + monkeypatch.setattr(platform, "python_implementation", lambda: name) + return True + return False + + return mock + + +class TestTag: + def test_lowercasing(self): + tag = tags.Tag("PY3", "None", "ANY") + assert tag.interpreter == "py3" + assert tag.abi == "none" + assert tag.platform == "any" + + def test_equality(self): + args = "py3", "none", "any" + assert tags.Tag(*args) == tags.Tag(*args) + + def test_equality_fails_with_non_tag(self): + assert not tags.Tag("py3", "none", "any") == "non-tag" + + def test_hashing(self, example_tag): + tags = {example_tag} # Should not raise TypeError. + assert example_tag in tags + + def test_hash_equality(self, example_tag): + equal_tag = tags.Tag("py3", "none", "any") + assert example_tag == equal_tag # Sanity check. + assert example_tag.__hash__() == equal_tag.__hash__() + + def test_str(self, example_tag): + assert str(example_tag) == "py3-none-any" + + def test_repr(self, example_tag): + assert repr(example_tag) == "".format( + tag_id=id(example_tag) ) - } - given = tags.parse_tag( - "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64." - "macosx_10_10_intel.macosx_10_10_x86_64" - ) - assert given == expected - - -@pytest.mark.parametrize( - "name,expected", - [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], -) -def test__interpreter_name_cpython(name, expected, monkeypatch): - if platform.python_implementation().lower() != name: - monkeypatch.setattr(platform, "python_implementation", lambda: name) - assert tags._interpreter_name() == expected - - -@pytest.mark.parametrize( - "arch, is_32bit, expected", - [ - ("i386", True, "i386"), - ("ppc", True, "ppc"), - ("x86_64", False, "x86_64"), - ("x86_64", True, "i386"), - ("ppc64", False, "ppc64"), - ("ppc64", True, "ppc"), - ], -) -def test_macos_architectures(arch, is_32bit, expected): - assert tags._mac_arch(arch, is_32bit=is_32bit) == expected - - -@pytest.mark.parametrize( - "version,arch,expected", - [ - ((10, 17), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), - ((10, 4), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), - ((10, 3), "x86_64", []), - ((10, 17), "i386", ["i386", "intel", "fat32", "fat", "universal"]), - ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]), - ((10, 3), "i386", []), - ((10, 17), "ppc64", []), - ((10, 6), "ppc64", []), - ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]), - ((10, 3), "ppc64", []), - ((10, 17), "ppc", []), - ((10, 7), "ppc", []), - ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]), - ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]), - ((11, 0), "riscv", ["riscv", "universal"]), - ], -) -def test_macos_binary_formats(version, arch, expected): - assert tags._mac_binary_formats(version, arch) == expected - -def test_mac_platforms(): - platforms = tags._mac_platforms((10, 5), "x86_64") - assert platforms == [ - "macosx_10_5_x86_64", - "macosx_10_5_intel", - "macosx_10_5_fat64", - "macosx_10_5_fat32", - "macosx_10_5_universal", - "macosx_10_4_x86_64", - "macosx_10_4_intel", - "macosx_10_4_fat64", - "macosx_10_4_fat32", - "macosx_10_4_universal", - ] + def test_attribute_access(self, example_tag): + assert example_tag.interpreter == "py3" + assert example_tag.abi == "none" + assert example_tag.platform == "any" - assert len(tags._mac_platforms((10, 17), "x86_64")) == 14 * 5 - assert not tags._mac_platforms((10, 0), "x86_64") +class TestWarnKeywordOnlyParameter: + def test_no_argument(self): + assert not tags._warn_keyword_parameter("test_warn_keyword_parameters", {}) - -def test_macos_version_detection(monkeypatch): - if platform.system() != "Darwin": - monkeypatch.setattr( - platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64") + def test_false(self): + assert not tags._warn_keyword_parameter( + "test_warn_keyword_parameters", {"warn": False} ) - version = platform.mac_ver()[0].split(".") - expected = "macosx_{major}_{minor}".format(major=version[0], minor=version[1]) - platforms = tags._mac_platforms(arch="x86_64") - assert platforms[0].startswith(expected) - - -@pytest.mark.parametrize("arch", ["x86_64", "i386"]) -def test_macos_arch_detection(arch, monkeypatch): - if platform.system() != "Darwin" or platform.mac_ver()[2] != arch: - monkeypatch.setattr(platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch)) - monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch) - assert tags._mac_platforms((10, 14))[0].endswith(arch) - - -@pytest.mark.parametrize( - "py_debug,gettotalrefcount,result", - [(1, False, True), (0, False, False), (None, True, True)], -) -def test_cpython_abis_debug(py_debug, gettotalrefcount, result, monkeypatch): - config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - if gettotalrefcount: - monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False) - expected = ["cp37d" if result else "cp37"] - assert tags._cpython_abis((3, 7)) == expected - - -def test_cpython_abis_debug_file_extension(monkeypatch): - config = {"Py_DEBUG": None} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - monkeypatch.delattr(sys, "gettotalrefcount", raising=False) - monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"}) - assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"] + def test_true(self): + assert tags._warn_keyword_parameter( + "test_warn_keyword_parameters", {"warn": True} + ) -@pytest.mark.parametrize( - "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])] -) -def test_cpython_abis_debug_38(debug, expected, monkeypatch): - config = {"Py_DEBUG": debug} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - assert tags._cpython_abis((3, 8)) == expected + def test_too_many_arguments(self): + message_re = re.compile(r"too_many.+{!r}".format("whatever")) + with pytest.raises(TypeError, match=message_re): + tags._warn_keyword_parameter("too_many", {"warn": True, "whatever": True}) + + def test_wrong_argument(self): + message_re = re.compile(r"missing.+{!r}".format("unexpected")) + with pytest.raises(TypeError, match=message_re): + tags._warn_keyword_parameter("missing", {"unexpected": True}) + + +class TestParseTag: + def test_simple(self, example_tag): + parsed_tags = tags.parse_tag(str(example_tag)) + assert parsed_tags == {example_tag} + + def test_multi_interpreter(self, example_tag): + expected = {example_tag, tags.Tag("py2", "none", "any")} + given = tags.parse_tag("py2.py3-none-any") + assert given == expected + + def test_multi_platform(self): + expected = { + tags.Tag("cp37", "cp37m", platform) + for platform in ( + "macosx_10_6_intel", + "macosx_10_9_intel", + "macosx_10_9_x86_64", + "macosx_10_10_intel", + "macosx_10_10_x86_64", + ) + } + given = tags.parse_tag( + "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64." + "macosx_10_10_intel.macosx_10_10_x86_64" + ) + assert given == expected -@pytest.mark.parametrize( - "pymalloc,version,result", - [(1, (3, 7), True), (0, (3, 7), False), (None, (3, 7), True), (1, (3, 8), False)], -) -def test_cpython_abis_pymalloc(pymalloc, version, result, monkeypatch): - config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - base_abi = "cp{}{}".format(version[0], version[1]) - expected = [base_abi + "m" if result else base_abi] - assert tags._cpython_abis(version) == expected +class TestInterpreterName: + def test_sys_implementation_name(self, monkeypatch): + class MockImplementation(object): + pass + mock_implementation = MockImplementation() + mock_implementation.name = "sillywalk" + monkeypatch.setattr(sys, "implementation", mock_implementation, raising=False) + assert tags.interpreter_name() == "sillywalk" -@pytest.mark.parametrize( - "unicode_size,maxunicode,version,result", - [ - (4, 0x10FFFF, (3, 2), True), - (2, 0xFFFF, (3, 2), False), - (None, 0x10FFFF, (3, 2), True), - (None, 0xFFFF, (3, 2), False), - (4, 0x10FFFF, (3, 3), False), - ], -) -def test_cpython_abis_wide_unicode( - unicode_size, maxunicode, version, result, monkeypatch -): - config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - monkeypatch.setattr(sys, "maxunicode", maxunicode) - base_abi = "cp{}{}".format(version[0], version[1]) - expected = [base_abi + "u" if result else base_abi] - assert tags._cpython_abis(version) == expected - - -def test_independent_tags(): - result = list(tags._independent_tags("cp33", (3, 3), ["plat1", "plat2"])) - assert result == [ - tags.Tag("py33", "none", "plat1"), - tags.Tag("py33", "none", "plat2"), - tags.Tag("py3", "none", "plat1"), - tags.Tag("py3", "none", "plat2"), - tags.Tag("py32", "none", "plat1"), - tags.Tag("py32", "none", "plat2"), - tags.Tag("py31", "none", "plat1"), - tags.Tag("py31", "none", "plat2"), - tags.Tag("py30", "none", "plat1"), - tags.Tag("py30", "none", "plat2"), - tags.Tag("cp33", "none", "any"), - tags.Tag("py33", "none", "any"), - tags.Tag("py3", "none", "any"), - tags.Tag("py32", "none", "any"), - tags.Tag("py31", "none", "any"), - tags.Tag("py30", "none", "any"), - ] - - -def test_cpython_tags(): - result = list( - tags._cpython_tags((3, 8), "cp38", ["cp38d", "cp38"], ["plat1", "plat2"]) + def test_platform(self, monkeypatch): + monkeypatch.delattr(sys, "implementation", raising=False) + name = "SillyWalk" + monkeypatch.setattr(platform, "python_implementation", lambda: name) + assert tags.interpreter_name() == name.lower() + + def test_interpreter_short_names(self, mock_interpreter_name, monkeypatch): + mock_interpreter_name("cpython") + assert tags.interpreter_name() == "cp" + + +class TestInterpreterVersion: + def test_warn(self, monkeypatch): + class MockConfigVar(object): + def __init__(self, return_): + self.warn = None + self._return = return_ + + def __call__(self, name, warn): + self.warn = warn + return self._return + + mock_config_var = MockConfigVar("38") + monkeypatch.setattr(tags, "_get_config_var", mock_config_var) + tags.interpreter_version(warn=True) + assert mock_config_var.warn + + def test_python_version_nodot(self, monkeypatch): + monkeypatch.setattr(tags, "_get_config_var", lambda var, warn: "NN") + assert tags.interpreter_version() == "NN" + + @pytest.mark.parametrize( + "version_info,version_str", + [ + ((1, 2, 3), "12"), + ((1, 12, 3), "1_12"), + ((11, 2, 3), "11_2"), + ((11, 12, 3), "11_12"), + ((1, 2, 13), "12"), + ], ) - assert result == [ - tags.Tag("cp38", "cp38d", "plat1"), - tags.Tag("cp38", "cp38d", "plat2"), - tags.Tag("cp38", "cp38", "plat1"), - tags.Tag("cp38", "cp38", "plat2"), - tags.Tag("cp38", "abi3", "plat1"), - tags.Tag("cp38", "abi3", "plat2"), - tags.Tag("cp38", "none", "plat1"), - tags.Tag("cp38", "none", "plat2"), - tags.Tag("cp37", "abi3", "plat1"), - tags.Tag("cp37", "abi3", "plat2"), - tags.Tag("cp36", "abi3", "plat1"), - tags.Tag("cp36", "abi3", "plat2"), - tags.Tag("cp35", "abi3", "plat1"), - tags.Tag("cp35", "abi3", "plat2"), - tags.Tag("cp34", "abi3", "plat1"), - tags.Tag("cp34", "abi3", "plat2"), - tags.Tag("cp33", "abi3", "plat1"), - tags.Tag("cp33", "abi3", "plat2"), - tags.Tag("cp32", "abi3", "plat1"), - tags.Tag("cp32", "abi3", "plat2"), - ] - result = list(tags._cpython_tags((3, 3), "cp33", ["cp33m"], ["plat1", "plat2"])) - assert result == [ - tags.Tag("cp33", "cp33m", "plat1"), - tags.Tag("cp33", "cp33m", "plat2"), - tags.Tag("cp33", "abi3", "plat1"), - tags.Tag("cp33", "abi3", "plat2"), - tags.Tag("cp33", "none", "plat1"), - tags.Tag("cp33", "none", "plat2"), - tags.Tag("cp32", "abi3", "plat1"), - tags.Tag("cp32", "abi3", "plat2"), - ] - - -def test_sys_tags_on_mac_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) - if platform.system() != "Darwin": - monkeypatch.setattr(platform, "system", lambda: "Darwin") - monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._mac_platforms() - result = list(tags.sys_tags()) - assert len(abis) == 1 - assert result[0] == tags.Tag( - "cp{major}{minor}".format(major=sys.version_info[0], minor=sys.version_info[1]), - abis[0], - platforms[0], + def test_sys_version_info(self, version_info, version_str, monkeypatch): + monkeypatch.setattr(tags, "_get_config_var", lambda *args, **kwargs: None) + monkeypatch.setattr(sys, "version_info", version_info) + assert tags.interpreter_version() == version_str + + +class TestMacOSPlatforms: + @pytest.mark.parametrize( + "arch, is_32bit, expected", + [ + ("i386", True, "i386"), + ("ppc", True, "ppc"), + ("x86_64", False, "x86_64"), + ("x86_64", True, "i386"), + ("ppc64", False, "ppc64"), + ("ppc64", True, "ppc"), + ], ) - assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - - -def test_generic_abi(monkeypatch): - abi = sysconfig.get_config_var("SOABI") - if abi: - abi = abi.replace(".", "_").replace("-", "_") - else: - abi = "none" - assert abi == tags._generic_abi() - - monkeypatch.setattr(sysconfig, "get_config_var", lambda key: "cpython-37m-darwin") - assert tags._generic_abi() == "cpython_37m_darwin" - - monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None) - assert tags._generic_abi() == "none" - - -def test_pypy_interpreter(monkeypatch): - if hasattr(sys, "pypy_version_info"): - major, minor = sys.pypy_version_info[:2] - else: - attributes = ["major", "minor", "micro", "releaselevel", "serial"] - PyPyVersion = collections.namedtuple("version_info", attributes) - major, minor = 6, 0 - pypy_version = PyPyVersion( - major=major, minor=minor, micro=1, releaselevel="final", serial=0 + def test_architectures(self, arch, is_32bit, expected): + assert tags._mac_arch(arch, is_32bit=is_32bit) == expected + + @pytest.mark.parametrize( + "version,arch,expected", + [ + ((10, 17), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 4), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 3), "x86_64", []), + ((10, 17), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 3), "i386", []), + ((10, 17), "ppc64", []), + ((10, 6), "ppc64", []), + ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]), + ((10, 3), "ppc64", []), + ((10, 17), "ppc", []), + ((10, 7), "ppc", []), + ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((11, 0), "riscv", ["riscv", "universal"]), + ], + ) + def test_binary_formats(self, version, arch, expected): + assert tags._mac_binary_formats(version, arch) == expected + + def test_version_detection(self, monkeypatch): + if platform.system() != "Darwin": + monkeypatch.setattr( + platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64") + ) + version = platform.mac_ver()[0].split(".") + expected = "macosx_{major}_{minor}".format(major=version[0], minor=version[1]) + platforms = list(tags.mac_platforms(arch="x86_64")) + assert platforms[0].startswith(expected) + + @pytest.mark.parametrize("arch", ["x86_64", "i386"]) + def test_arch_detection(self, arch, monkeypatch): + if platform.system() != "Darwin" or platform.mac_ver()[2] != arch: + monkeypatch.setattr( + platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch) + ) + monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch) + assert next(tags.mac_platforms((10, 14))).endswith(arch) + + def test_mac_platforms(self): + platforms = list(tags.mac_platforms((10, 5), "x86_64")) + assert platforms == [ + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal", + ] + + assert len(list(tags.mac_platforms((10, 17), "x86_64"))) == 14 * 5 + + assert not list(tags.mac_platforms((10, 0), "x86_64")) + + +class TestManylinuxPlatform: + def test_module_declaration_true(self, manylinux_module): + manylinux_module.manylinux1_compatible = True + assert tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_module_declaration_false(self, manylinux_module): + manylinux_module.manylinux1_compatible = False + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_module_declaration_missing_attribute(self, manylinux_module): + try: + del manylinux_module.manylinux1_compatible + except AttributeError: + pass + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_is_manylinux_compatible_module_support( + self, manylinux_module, monkeypatch + ): + monkeypatch.setitem(sys.modules, manylinux_module.__name__, None) + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + @pytest.mark.parametrize( + "version,compatible", (((2, 0), True), ((2, 5), True), ((2, 10), False)) + ) + def test_is_manylinux_compatible_glibc_support( + self, version, compatible, monkeypatch + ): + monkeypatch.setitem(sys.modules, "_manylinux", None) + monkeypatch.setattr( + tags, + "_have_compatible_glibc", + lambda major, minor: (major, minor) <= (2, 5), ) - monkeypatch.setattr(sys, "pypy_version_info", pypy_version, raising=False) - expected = "pp{}{}{}".format(sys.version_info[0], major, minor) - assert expected == tags._pypy_interpreter() - - -def test_pypy_tags(monkeypatch): - if platform.python_implementation() != "PyPy": - monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") - monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") - interpreter = tags._pypy_interpreter() - result = list(tags._pypy_tags((3, 3), interpreter, "pypy3_60", ["plat1", "plat2"])) - assert result == [ - tags.Tag(interpreter, "pypy3_60", "plat1"), - tags.Tag(interpreter, "pypy3_60", "plat2"), - tags.Tag(interpreter, "none", "plat1"), - tags.Tag(interpreter, "none", "plat2"), - ] - - -def test_sys_tags_on_mac_pypy(monkeypatch): - if platform.python_implementation() != "PyPy": - monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") - monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") - if platform.system() != "Darwin": - monkeypatch.setattr(platform, "system", lambda: "Darwin") - monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) - interpreter = tags._pypy_interpreter() - abi = tags._generic_abi() - platforms = tags._mac_platforms() - result = list(tags.sys_tags()) - assert result[0] == tags.Tag(interpreter, abi, platforms[0]) - assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - - -def test_generic_interpreter(): - version = sysconfig.get_config_var("py_version_nodot") - if not version: - version = "".join(sys.version_info[:2]) - result = tags._generic_interpreter("sillywalk", sys.version_info[:2]) - assert result == "sillywalk{version}".format(version=version) - - -def test_generic_interpreter_no_config_var(monkeypatch): - monkeypatch.setattr(sysconfig, "get_config_var", lambda _: None) - assert tags._generic_interpreter("sillywalk", (3, 6)) == "sillywalk36" - - -def test_generic_platforms(): - platform = distutils.util.get_platform().replace("-", "_") - platform = platform.replace(".", "_") - assert tags._generic_platforms() == [platform] - - -def test_generic_tags(): - result = list(tags._generic_tags("sillywalk33", (3, 3), "abi", ["plat1", "plat2"])) - assert result == [ - tags.Tag("sillywalk33", "abi", "plat1"), - tags.Tag("sillywalk33", "abi", "plat2"), - tags.Tag("sillywalk33", "none", "plat1"), - tags.Tag("sillywalk33", "none", "plat2"), - ] - - no_abi = tags._generic_tags("sillywalk34", (3, 4), "none", ["plat1", "plat2"]) - assert list(no_abi) == [ - tags.Tag("sillywalk34", "none", "plat1"), - tags.Tag("sillywalk34", "none", "plat2"), - ] - - -def test_sys_tags_on_windows_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) - if platform.system() != "Windows": - monkeypatch.setattr(platform, "system", lambda: "Windows") - monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._generic_platforms() - result = list(tags.sys_tags()) - interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] + assert bool(tags._is_manylinux_compatible("manylinux1", version)) == compatible + + @pytest.mark.parametrize( + "version_str,major,minor,expected", + [ + ("2.4", 2, 4, True), + ("2.4", 2, 5, False), + ("2.4", 2, 3, True), + ("3.4", 2, 4, False), + ], ) - assert len(abis) == 1 - expected = tags.Tag(interpreter, abis[0], platforms[0]) - assert result[0] == expected - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected - - -def test_is_manylinux_compatible_module_support(monkeypatch): - monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) - module_name = "_manylinux" - module = types.ModuleType(module_name) - module.manylinux1_compatible = True - monkeypatch.setitem(sys.modules, module_name, module) - assert tags._is_manylinux_compatible("manylinux1", (2, 5)) - module.manylinux1_compatible = False - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - del module.manylinux1_compatible - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - monkeypatch.setitem(sys.modules, module_name, None) - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - - -def test_is_manylinux_compatible_glibc_support(monkeypatch): - monkeypatch.setitem(sys.modules, "_manylinux", None) - monkeypatch.setattr( - tags, "_have_compatible_glibc", lambda major, minor: (major, minor) <= (2, 5) + def test_check_glibc_version(self, version_str, major, minor, expected): + assert expected == tags._check_glibc_version(version_str, major, minor) + + @pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"]) + def test_check_glibc_version_warning(self, version_str): + with warnings.catch_warnings(record=True) as w: + tags._check_glibc_version(version_str, 2, 4) + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + + @pytest.mark.skipif(not ctypes, reason="requires ctypes") + @pytest.mark.parametrize( + "version_str,expected", + [ + # Be very explicit about bytes and Unicode for Python 2 testing. + (b"2.4", "2.4"), + (u"2.4", "2.4"), + ], ) - assert tags._is_manylinux_compatible("manylinux1", (2, 0)) - assert tags._is_manylinux_compatible("manylinux1", (2, 5)) - assert not tags._is_manylinux_compatible("manylinux1", (2, 10)) + def test_glibc_version_string(self, version_str, expected, monkeypatch): + class LibcVersion: + def __init__(self, version_str): + self.version_str = version_str + def __call__(self): + return version_str -@pytest.mark.parametrize( - "version_str,major,minor,expected", - [ - ("2.4", 2, 4, True), - ("2.4", 2, 5, False), - ("2.4", 2, 3, True), - ("3.4", 2, 4, False), - ], -) -def test_check_glibc_version(version_str, major, minor, expected): - assert expected == tags._check_glibc_version(version_str, major, minor) - - -@pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"]) -def test_check_glibc_version_warning(version_str): - with warnings.catch_warnings(record=True) as w: - tags._check_glibc_version(version_str, 2, 4) - assert len(w) == 1 - assert issubclass(w[0].category, RuntimeWarning) - - -@pytest.mark.skipif(not ctypes, reason="requires ctypes") -@pytest.mark.parametrize( - "version_str,expected", - [ - # Be very explicit about bytes and Unicode for Python 2 testing. - (b"2.4", "2.4"), - (u"2.4", "2.4"), - ], -) -def test_glibc_version_string(version_str, expected, monkeypatch): - class LibcVersion: - def __init__(self, version_str): - self.version_str = version_str + class ProcessNamespace: + def __init__(self, libc_version): + self.gnu_get_libc_version = libc_version - def __call__(self): - return version_str + process_namespace = ProcessNamespace(LibcVersion(version_str)) + monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) + monkeypatch.setattr(tags, "_glibc_version_string_confstr", lambda: False) - class ProcessNamespace: - def __init__(self, libc_version): - self.gnu_get_libc_version = libc_version + assert tags._glibc_version_string() == expected - process_namespace = ProcessNamespace(LibcVersion(version_str)) - monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) + del process_namespace.gnu_get_libc_version + assert tags._glibc_version_string() is None - assert tags._glibc_version_string() == expected + def test_glibc_version_string_confstr(self, monkeypatch): + monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False) + assert tags._glibc_version_string_confstr() == "2.20" - del process_namespace.gnu_get_libc_version - assert tags._glibc_version_string() is None - - -def test_have_compatible_glibc(monkeypatch): - if platform.system() == "Linux": + @pytest.mark.parametrize( + "failure", + [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"], + ) + def test_glibc_version_string_confstr_fail(self, monkeypatch, failure): + monkeypatch.setattr(os, "confstr", failure, raising=False) + assert tags._glibc_version_string_confstr() is None + + def test_glibc_version_string_confstr_missing(self, monkeypatch): + monkeypatch.delattr(os, "confstr", raising=False) + assert tags._glibc_version_string_confstr() is None + + def test_glibc_version_string_ctypes_missing(self, monkeypatch): + monkeypatch.setitem(sys.modules, "ctypes", None) + assert tags._glibc_version_string_ctypes() is None + + def test_get_config_var_does_not_log(self, monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing") + assert debug.calls == [] + + def test_get_config_var_does_log(self, monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing", warn=True) + assert debug.calls == [ + pretend.call( + "Config variable '%s' is unset, Python ABI tag may be incorrect", + "missing", + ) + ] + + @pytest.mark.skipif(platform.system() != "Linux", reason="requires Linux") + def test_have_compatible_glibc_linux(self): # Assuming no one is running this test with a version of glibc released in # 1997. assert tags._have_compatible_glibc(2, 0) - else: + + def test_have_compatible_glibc(self, monkeypatch): monkeypatch.setattr(tags, "_glibc_version_string", lambda: "2.4") assert tags._have_compatible_glibc(2, 4) - monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) - assert not tags._have_compatible_glibc(2, 4) + def test_glibc_version_string_none(self, monkeypatch): + monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) + assert not tags._have_compatible_glibc(2, 4) + + @pytest.mark.parametrize( + "arch,is_32bit,expected", + [ + ("linux-x86_64", False, "linux_x86_64"), + ("linux-x86_64", True, "linux_i686"), + ("linux-aarch64", False, "linux_aarch64"), + ("linux-aarch64", True, "linux_armv7l"), + ], + ) + def test_linux_platforms_32_64bit_on_64bit_os( + self, arch, is_32bit, expected, monkeypatch + ): + monkeypatch.setattr(distutils.util, "get_platform", lambda: arch) + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[-1] + assert linux_platform == expected -def test_linux_platforms_64bit_on_64bit_os(is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: + def test_linux_platforms_manylinux_unsupported(self, monkeypatch): monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=False)[-1] - assert linux_platform == "linux_x86_64" + linux_platform = list(tags._linux_platforms(is_32bit=False)) + assert linux_platform == ["linux_x86_64"] + def test_linux_platforms_manylinux1(self, is_x86, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" + ) + if platform.system() != "Linux" or not is_x86: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + arch = platform.machine() + assert platforms == ["manylinux1_" + arch, "linux_" + arch] + + def test_linux_platforms_manylinux2010(self, is_x86, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" + ) + if platform.system() != "Linux" or not is_x86: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + arch = platform.machine() + expected = ["manylinux2010_" + arch, "manylinux1_" + arch, "linux_" + arch] + assert platforms == expected + + def test_linux_platforms_manylinux2014(self, is_x86, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + if platform.system() != "Linux" or not is_x86: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + arch = platform.machine() + expected = [ + "manylinux2014_" + arch, + "manylinux2010_" + arch, + "manylinux1_" + arch, + "linux_" + arch, + ] + assert platforms == expected + + def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv7l") + monkeypatch.setattr( + sys, + "executable", + os.path.join(os.path.dirname(__file__), "hello-world-armv7l-armhf"), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["manylinux2014_armv7l", "linux_armv7l"] + assert platforms == expected -def test_linux_platforms_32bit_on_64bit_os(is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: + def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=True)[-1] - assert linux_platform == "linux_i686" + monkeypatch.setattr( + sys, + "executable", + os.path.join(os.path.dirname(__file__), "hello-world-x86_64-i386"), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = [ + "manylinux2014_i686", + "manylinux2010_i686", + "manylinux1_i686", + "linux_i686", + ] + assert platforms == expected + + def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv6l") + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["linux_armv6l"] + assert platforms == expected + + @pytest.mark.parametrize( + "machine, abi, alt_machine", + [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")], + ) + def test_linux_platforms_not_manylinux_abi( + self, monkeypatch, machine, abi, alt_machine + ): + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda name, _: True) + monkeypatch.setattr( + distutils.util, "get_platform", lambda: "linux_{}".format(machine) + ) + monkeypatch.setattr( + sys, + "executable", + os.path.join( + os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi) + ), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["linux_{}".format(alt_machine)] + assert platforms == expected + + @pytest.mark.parametrize( + "machine, abi, elf_class, elf_data, elf_machine", + [ + ( + "x86_64", + "x32", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_X86_64, + ), + ( + "x86_64", + "i386", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_386, + ), + ( + "x86_64", + "amd64", + tags._ELFFileHeader.ELFCLASS64, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_X86_64, + ), + ( + "armv7l", + "armel", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_ARM, + ), + ( + "armv7l", + "armhf", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_ARM, + ), + ( + "s390x", + "s390x", + tags._ELFFileHeader.ELFCLASS64, + tags._ELFFileHeader.ELFDATA2MSB, + tags._ELFFileHeader.EM_S390, + ), + ], + ) + def test_get_elf_header( + self, monkeypatch, machine, abi, elf_class, elf_data, elf_machine + ): + path = os.path.join( + os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi) + ) + monkeypatch.setattr(sys, "executable", path) + elf_header = tags._get_elf_header() + assert elf_header.e_ident_class == elf_class + assert elf_header.e_ident_data == elf_data + assert elf_header.e_machine == elf_machine + + @pytest.mark.parametrize( + "content", [None, "invalid-magic", "invalid-class", "invalid-data", "too-short"] + ) + def test_get_elf_header_bad_excutable(self, monkeypatch, content): + if content: + path = os.path.join( + os.path.dirname(__file__), "hello-world-{}".format(content) + ) + else: + path = None + monkeypatch.setattr(sys, "executable", path) + assert tags._get_elf_header() is None + def test_is_linux_armhf_not_elf(self, monkeypatch): + monkeypatch.setattr(tags, "_get_elf_header", lambda: None) + assert not tags._is_linux_armhf() -def test_linux_platforms_manylinux_unsupported(monkeypatch): - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=False) - assert linux_platform == ["linux_x86_64"] + def test_is_linux_i686_not_elf(self, monkeypatch): + monkeypatch.setattr(tags, "_get_elf_header", lambda: None) + assert not tags._is_linux_i686() -def test_linux_platforms_manylinux1(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" - ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() - assert platforms == ["manylinux1_" + arch, "linux_" + arch] +@pytest.mark.parametrize( + "platform_name,dispatch_func", + [ + ("Darwin", "mac_platforms"), + ("Linux", "_linux_platforms"), + ("Generic", "_generic_platforms"), + ], +) +def test__platform_tags(platform_name, dispatch_func, monkeypatch): + expected = ["sillywalk"] + monkeypatch.setattr(platform, "system", lambda: platform_name) + monkeypatch.setattr(tags, dispatch_func, lambda: expected) + assert tags._platform_tags() == expected -def test_linux_platforms_manylinux2010(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" +class TestCPythonABI: + @pytest.mark.parametrize( + "py_debug,gettotalrefcount,result", + [(1, False, True), (0, False, False), (None, True, True)], ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() - expected = ["manylinux2010_" + arch, "manylinux1_" + arch, "linux_" + arch] - assert platforms == expected - + def test_debug(self, py_debug, gettotalrefcount, result, monkeypatch): + config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + if gettotalrefcount: + monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False) + expected = ["cp37d" if result else "cp37"] + assert tags._cpython_abis((3, 7)) == expected + + def test_debug_file_extension(self, monkeypatch): + config = {"Py_DEBUG": None} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + monkeypatch.delattr(sys, "gettotalrefcount", raising=False) + monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"}) + assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"] + + @pytest.mark.parametrize( + "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])] + ) + def test__debug_cp38(self, debug, expected, monkeypatch): + config = {"Py_DEBUG": debug} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + assert tags._cpython_abis((3, 8)) == expected + + @pytest.mark.parametrize( + "pymalloc,version,result", + [ + (1, (3, 7), True), + (0, (3, 7), False), + (None, (3, 7), True), + (1, (3, 8), False), + ], + ) + def test_pymalloc(self, pymalloc, version, result, monkeypatch): + config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + base_abi = "cp{}{}".format(version[0], version[1]) + expected = [base_abi + "m" if result else base_abi] + assert tags._cpython_abis(version) == expected + + @pytest.mark.parametrize( + "unicode_size,maxunicode,version,result", + [ + (4, 0x10FFFF, (3, 2), True), + (2, 0xFFFF, (3, 2), False), + (None, 0x10FFFF, (3, 2), True), + (None, 0xFFFF, (3, 2), False), + (4, 0x10FFFF, (3, 3), False), + ], + ) + def test_wide_unicode(self, unicode_size, maxunicode, version, result, monkeypatch): + config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + monkeypatch.setattr(sys, "maxunicode", maxunicode) + base_abi = "cp" + tags._version_nodot(version) + expected = [base_abi + "u" if result else base_abi] + assert tags._cpython_abis(version) == expected + + +class TestCPythonTags: + def test_iterator_returned(self): + result_iterator = tags.cpython_tags( + (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] + ) + assert isinstance(result_iterator, collections_abc.Iterator) -def test_linux_platforms_manylinux2014(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + def test_all_args(self): + result_iterator = tags.cpython_tags( + (3, 11), ["cp3_11d", "cp3_11"], ["plat1", "plat2"] + ) + result = list(result_iterator) + assert result == [ + tags.Tag("cp3_11", "cp3_11d", "plat1"), + tags.Tag("cp3_11", "cp3_11d", "plat2"), + tags.Tag("cp3_11", "cp3_11", "plat1"), + tags.Tag("cp3_11", "cp3_11", "plat2"), + tags.Tag("cp3_11", "abi3", "plat1"), + tags.Tag("cp3_11", "abi3", "plat2"), + tags.Tag("cp3_11", "none", "plat1"), + tags.Tag("cp3_11", "none", "plat2"), + tags.Tag("cp3_10", "abi3", "plat1"), + tags.Tag("cp3_10", "abi3", "plat2"), + tags.Tag("cp39", "abi3", "plat1"), + tags.Tag("cp39", "abi3", "plat2"), + tags.Tag("cp38", "abi3", "plat1"), + tags.Tag("cp38", "abi3", "plat2"), + tags.Tag("cp37", "abi3", "plat1"), + tags.Tag("cp37", "abi3", "plat2"), + tags.Tag("cp36", "abi3", "plat1"), + tags.Tag("cp36", "abi3", "plat2"), + tags.Tag("cp35", "abi3", "plat1"), + tags.Tag("cp35", "abi3", "plat2"), + tags.Tag("cp34", "abi3", "plat1"), + tags.Tag("cp34", "abi3", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + result_iterator = tags.cpython_tags( + (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] + ) + result = list(result_iterator) + assert result == [ + tags.Tag("cp38", "cp38d", "plat1"), + tags.Tag("cp38", "cp38d", "plat2"), + tags.Tag("cp38", "cp38", "plat1"), + tags.Tag("cp38", "cp38", "plat2"), + tags.Tag("cp38", "abi3", "plat1"), + tags.Tag("cp38", "abi3", "plat2"), + tags.Tag("cp38", "none", "plat1"), + tags.Tag("cp38", "none", "plat2"), + tags.Tag("cp37", "abi3", "plat1"), + tags.Tag("cp37", "abi3", "plat2"), + tags.Tag("cp36", "abi3", "plat1"), + tags.Tag("cp36", "abi3", "plat2"), + tags.Tag("cp35", "abi3", "plat1"), + tags.Tag("cp35", "abi3", "plat2"), + tags.Tag("cp34", "abi3", "plat1"), + tags.Tag("cp34", "abi3", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + + result = list(tags.cpython_tags((3, 3), ["cp33m"], ["plat1", "plat2"])) + assert result == [ + tags.Tag("cp33", "cp33m", "plat1"), + tags.Tag("cp33", "cp33m", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp33", "none", "plat1"), + tags.Tag("cp33", "none", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + + def test_python_version_defaults(self): + tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"])) + interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) + assert interpreter == tag.interpreter + + def test_abi_defaults(self, monkeypatch): + monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp38"]) + result = list(tags.cpython_tags((3, 8), platforms=["any"])) + assert tags.Tag("cp38", "cp38", "any") in result + assert tags.Tag("cp38", "abi3", "any") in result + assert tags.Tag("cp38", "none", "any") in result + + def test_abi_defaults_needs_underscore(self, monkeypatch): + monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp3_11"]) + result = list(tags.cpython_tags((3, 11), platforms=["any"])) + assert tags.Tag("cp3_11", "cp3_11", "any") in result + assert tags.Tag("cp3_11", "abi3", "any") in result + assert tags.Tag("cp3_11", "none", "any") in result + + def test_platforms_defaults(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat1"]) + result = list(tags.cpython_tags((3, 8), abis=["whatever"])) + assert tags.Tag("cp38", "whatever", "plat1") in result + + def test_platforms_defaults_needs_underscore(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat1"]) + result = list(tags.cpython_tags((3, 11), abis=["whatever"])) + assert tags.Tag("cp3_11", "whatever", "plat1") in result + + def test_major_only_python_version(self): + result = list(tags.cpython_tags((3,), ["abi"], ["plat"])) + assert result == [ + tags.Tag("cp3", "abi", "plat"), + tags.Tag("cp3", "none", "plat"), + ] + + def test_major_only_python_version_with_default_abis(self): + result = list(tags.cpython_tags((3,), platforms=["plat"])) + assert result == [tags.Tag("cp3", "none", "plat")] + + @pytest.mark.parametrize("abis", [[], ["abi3"], ["none"]]) + def test_skip_redundant_abis(self, abis): + results = list(tags.cpython_tags((3, 0), abis=abis, platforms=["any"])) + assert results == [tags.Tag("cp30", "none", "any")] + + def test_abi3_python33(self): + results = list(tags.cpython_tags((3, 3), abis=["cp33"], platforms=["plat"])) + assert results == [ + tags.Tag("cp33", "cp33", "plat"), + tags.Tag("cp33", "abi3", "plat"), + tags.Tag("cp33", "none", "plat"), + tags.Tag("cp32", "abi3", "plat"), + ] + + def test_no_excess_abi3_python32(self): + results = list(tags.cpython_tags((3, 2), abis=["cp32"], platforms=["plat"])) + assert results == [ + tags.Tag("cp32", "cp32", "plat"), + tags.Tag("cp32", "abi3", "plat"), + tags.Tag("cp32", "none", "plat"), + ] + + def test_no_abi3_python31(self): + results = list(tags.cpython_tags((3, 1), abis=["cp31"], platforms=["plat"])) + assert results == [ + tags.Tag("cp31", "cp31", "plat"), + tags.Tag("cp31", "none", "plat"), + ] + + def test_no_abi3_python27(self): + results = list(tags.cpython_tags((2, 7), abis=["cp27"], platforms=["plat"])) + assert results == [ + tags.Tag("cp27", "cp27", "plat"), + tags.Tag("cp27", "none", "plat"), + ] + + +class TestGenericTags: + @pytest.mark.skipif( + not sysconfig.get_config_var("SOABI"), reason="SOABI not defined" ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() - expected = [ - "manylinux2014_" + arch, - "manylinux2010_" + arch, - "manylinux1_" + arch, - "linux_" + arch, - ] - assert platforms == expected - - -def test_sys_tags_linux_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) - if platform.system() != "Linux": - monkeypatch.setattr(platform, "system", lambda: "Linux") - monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._linux_platforms() - result = list(tags.sys_tags()) - expected_interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] + def test__generic_abi_soabi_provided(self): + abi = sysconfig.get_config_var("SOABI").replace(".", "_").replace("-", "_") + assert [abi] == list(tags._generic_abi()) + + def test__generic_abi(self, monkeypatch): + monkeypatch.setattr( + sysconfig, "get_config_var", lambda key: "cpython-37m-darwin" + ) + assert list(tags._generic_abi()) == ["cpython_37m_darwin"] + + def test__generic_abi_no_soabi(self, monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None) + assert not list(tags._generic_abi()) + + def test_generic_platforms(self): + platform = distutils.util.get_platform().replace("-", "_") + platform = platform.replace(".", "_") + assert list(tags._generic_platforms()) == [platform] + + def test_iterator_returned(self): + result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"]) + assert isinstance(result_iterator, collections_abc.Iterator) + + def test_all_args(self): + result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"]) + result = list(result_iterator) + assert result == [ + tags.Tag("sillywalk33", "abi", "plat1"), + tags.Tag("sillywalk33", "abi", "plat2"), + tags.Tag("sillywalk33", "none", "plat1"), + tags.Tag("sillywalk33", "none", "plat2"), + ] + + @pytest.mark.parametrize("abi", [[], ["none"]]) + def test_abi_unspecified(self, abi): + no_abi = list(tags.generic_tags("sillywalk34", abi, ["plat1", "plat2"])) + assert no_abi == [ + tags.Tag("sillywalk34", "none", "plat1"), + tags.Tag("sillywalk34", "none", "plat2"), + ] + + def test_interpreter_default(self, monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "sillywalk") + monkeypatch.setattr(tags, "interpreter_version", lambda warn: "NN") + result = list(tags.generic_tags(abis=["none"], platforms=["any"])) + assert result == [tags.Tag("sillywalkNN", "none", "any")] + + def test_abis_default(self, monkeypatch): + monkeypatch.setattr(tags, "_generic_abi", lambda: iter(["abi"])) + result = list(tags.generic_tags(interpreter="sillywalk", platforms=["any"])) + assert result == [ + tags.Tag("sillywalk", "abi", "any"), + tags.Tag("sillywalk", "none", "any"), + ] + + def test_platforms_default(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat"]) + result = list(tags.generic_tags(interpreter="sillywalk", abis=["none"])) + assert result == [tags.Tag("sillywalk", "none", "plat")] + + +class TestCompatibleTags: + def test_all_args(self): + result = list(tags.compatible_tags((3, 3), "cp33", ["plat1", "plat2"])) + assert result == [ + tags.Tag("py33", "none", "plat1"), + tags.Tag("py33", "none", "plat2"), + tags.Tag("py3", "none", "plat1"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py32", "none", "plat1"), + tags.Tag("py32", "none", "plat2"), + tags.Tag("py31", "none", "plat1"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py30", "none", "plat1"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp33", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_all_args_needs_underscore(self): + result = list(tags.compatible_tags((3, 11), "cp3_11", ["plat1", "plat2"])) + assert result == [ + tags.Tag("py3_11", "none", "plat1"), + tags.Tag("py3_11", "none", "plat2"), + tags.Tag("py3", "none", "plat1"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py3_10", "none", "plat1"), + tags.Tag("py3_10", "none", "plat2"), + tags.Tag("py39", "none", "plat1"), + tags.Tag("py39", "none", "plat2"), + tags.Tag("py38", "none", "plat1"), + tags.Tag("py38", "none", "plat2"), + tags.Tag("py37", "none", "plat1"), + tags.Tag("py37", "none", "plat2"), + tags.Tag("py36", "none", "plat1"), + tags.Tag("py36", "none", "plat2"), + tags.Tag("py35", "none", "plat1"), + tags.Tag("py35", "none", "plat2"), + tags.Tag("py34", "none", "plat1"), + tags.Tag("py34", "none", "plat2"), + tags.Tag("py33", "none", "plat1"), + tags.Tag("py33", "none", "plat2"), + tags.Tag("py32", "none", "plat1"), + tags.Tag("py32", "none", "plat2"), + tags.Tag("py31", "none", "plat1"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py30", "none", "plat1"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp3_11", "none", "any"), + tags.Tag("py3_11", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py3_10", "none", "any"), + tags.Tag("py39", "none", "any"), + tags.Tag("py38", "none", "any"), + tags.Tag("py37", "none", "any"), + tags.Tag("py36", "none", "any"), + tags.Tag("py35", "none", "any"), + tags.Tag("py34", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_major_only_python_version(self): + result = list(tags.compatible_tags((3,), "cp33", ["plat"])) + assert result == [ + tags.Tag("py3", "none", "plat"), + tags.Tag("cp33", "none", "any"), + tags.Tag("py3", "none", "any"), + ] + + def test_default_python_version(self, monkeypatch): + monkeypatch.setattr(sys, "version_info", (3, 1)) + result = list(tags.compatible_tags(interpreter="cp31", platforms=["plat"])) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("cp31", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_default_python_version_needs_underscore(self, monkeypatch): + monkeypatch.setattr(sys, "version_info", (3, 11)) + result = list(tags.compatible_tags(interpreter="cp3_11", platforms=["plat"])) + assert result == [ + tags.Tag("py3_11", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py3_10", "none", "plat"), + tags.Tag("py39", "none", "plat"), + tags.Tag("py38", "none", "plat"), + tags.Tag("py37", "none", "plat"), + tags.Tag("py36", "none", "plat"), + tags.Tag("py35", "none", "plat"), + tags.Tag("py34", "none", "plat"), + tags.Tag("py33", "none", "plat"), + tags.Tag("py32", "none", "plat"), + tags.Tag("py31", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("cp3_11", "none", "any"), + tags.Tag("py3_11", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py3_10", "none", "any"), + tags.Tag("py39", "none", "any"), + tags.Tag("py38", "none", "any"), + tags.Tag("py37", "none", "any"), + tags.Tag("py36", "none", "any"), + tags.Tag("py35", "none", "any"), + tags.Tag("py34", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_default_interpreter(self): + result = list(tags.compatible_tags((3, 1), platforms=["plat"])) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_default_platforms(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: iter(["plat", "plat2"])) + result = list(tags.compatible_tags((3, 1), "cp31")) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py30", "none", "plat"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp31", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + +class TestSysTags: + @pytest.mark.parametrize( + "name,expected", + [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], ) - assert len(abis) == 1 - assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0]) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected + def test_interpreter_name(self, name, expected, mock_interpreter_name): + mock_interpreter_name(name) + assert tags.interpreter_name() == expected + + def test_iterator(self): + assert isinstance(tags.sys_tags(), collections_abc.Iterator) + + def test_mac_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Darwin": + monkeypatch.setattr(platform, "system", lambda: "Darwin") + monkeypatch.setattr(tags, "mac_platforms", lambda: ["macosx_10_5_x86_64"]) + abis = tags._cpython_abis(sys.version_info[:2]) + platforms = list(tags.mac_platforms()) + result = list(tags.sys_tags()) + assert len(abis) == 1 + assert result[0] == tags.Tag( + "cp" + tags._version_nodot(sys.version_info[:2]), abis[0], platforms[0] + ) + assert result[-1] == tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) + def test_windows_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Windows": + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) + abis = list(tags._cpython_abis(sys.version_info[:2])) + platforms = list(tags._generic_platforms()) + result = list(tags.sys_tags()) + interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) + assert len(abis) == 1 + expected = tags.Tag(interpreter, abis[0], platforms[0]) + assert result[0] == expected + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) + assert result[-1] == expected + + def test_linux_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Linux": + monkeypatch.setattr(platform, "system", lambda: "Linux") + monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) + abis = list(tags._cpython_abis(sys.version_info[:2])) + platforms = list(tags._linux_platforms()) + result = list(tags.sys_tags()) + expected_interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) + assert len(abis) == 1 + assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0]) + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) + assert result[-1] == expected -def test_generic_sys_tags(monkeypatch): - monkeypatch.setattr(platform, "system", lambda: "Generic") - monkeypatch.setattr(tags, "_interpreter_name", lambda: "generic") + def test_generic(self, monkeypatch): + monkeypatch.setattr(platform, "system", lambda: "Generic") + monkeypatch.setattr(tags, "interpreter_name", lambda: "generic") - result = list(tags.sys_tags()) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected + result = list(tags.sys_tags()) + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) + assert result[-1] == expected diff --git a/tests/test_version.py b/tests/test_version.py index 4e8079dce..67e18e9fe 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -758,6 +758,18 @@ def test_compare_legacyversion_version(self): result = sorted([Version("0"), LegacyVersion("1")]) assert result == [LegacyVersion("1"), Version("0")] + def test_major_version(self): + assert Version("2.1.0").major == 2 + + def test_minor_version(self): + assert Version("2.1.0").minor == 1 + assert Version("2").minor == 0 + + def test_micro_version(self): + assert Version("2.1.3").micro == 3 + assert Version("2.1").micro == 0 + assert Version("2").micro == 0 + LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"] diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e5fc0d4c9..000000000 --- a/tox.ini +++ /dev/null @@ -1,53 +0,0 @@ -[tox] -envlist = py27,pypy,pypy3,py34,py35,py36,py37,docs,lint - -[testenv] -deps = - coverage - pretend - pytest - pip>=9.0.2 -commands = - python -m coverage run --source packaging/ -m pytest --strict {posargs} - python -m coverage report -m --fail-under 100 - -[testenv:pypy] -commands = - py.test --capture=no --strict {posargs} - -[testenv:pypy3] -commands = - py.test --capture=no --strict {posargs} - -[testenv:docs] -basepython = python3 -deps = - sphinx - sphinx_rtd_theme -commands = - sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex - sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html - -[testenv:lint] -basepython = python3 -deps = - flake8 - pep8-naming - black -commands = - flake8 . - black --check . - -[testenv:packaging] -deps = - check-manifest - readme_renderer -commands = - check-manifest - python setup.py check --metadata --restructuredtext --strict - -[flake8] -exclude = .tox,*.egg -select = E,W,F,N -ignore = W504