Previously, these were used when the user did not provide an explicit
ABI. Now this is handled internally in `packaging.tags` (by
`_cpython_abi` and `_generic_abi`).
Since we only call this function when platform is not None, we can drop
one of the branches. This is the only place using our manylinux
auto-deduction functions, so those can be removed next.
Now that we're fully using packaging.tags, everything in the supported
list is already Tag.
Further, since each function is responsible for its own set of
non-overlapping tags, we can also remove the de-duplication.
As with cpython_tags and compatible_tags, we assume this function covers
our use cases in the no-argument case and will customize our arguments
for it in a few steps.
We assume this function improves the status quo over the current
`_cpython_tags`, so we use it when no arguments need to be customized.
Since `packaging.tags` has its own tests and derives defaults
differently than pep425tags, we also remove the applicable tests that
were testing against the result of a plain call to
`pep425tags.get_supported()`.
We only call this function when the user or platform-provided
implementation is "cp", so we can inline the literal in place of the
parameter. This will make refactoring easier.
We assume this function improves on our existing behavior, so use it
as-is. We will customize the arguments over the next few commits.
Since packaging.tags internally calculates platforms when not provided,
we skip the tests which patch functions assuming that manylinux
compatibility determination depends on them.
Since `_compatible_tags` is the function that will be responsible for
generating the non-interpreter-specific tags, we remove the
corresponding sections from `_cpython_tags` and `_generic_tags`. The
resulting tags in `get_supported` are equivalent because these were
the last tags to be computed in those functions, and `_compatible_tags`
is executed after them (so any non-duplicate tags it produces will be
last).
To reinforce the reponsibility of `_compatible_tags` we also remove the
abi-related tag generation, which is already handled in `_cpython_tags`
and `_generic_tags`.
Since these functions are copies of the existing code, there is no
behavior change except each tag will now be present 3 times. To
accommodate this we remove all but the first duplicate tag from the
set of all tags.
We put `_compatible_tags` last because it will provide the lowest
priority tags. The order of `_cpython_tags` and `_generic_tags`
here is not significant - when we start customizing them we will
introduce a condition so that they are mutually exclusive.
packaging.tags provides a simple `sys_tags` function for getting
applicable tags for the running interpreter, but it does not allow
customization of arguments.
packaging.tags provides three functions for getting custom tags:
1. `cpython_tags` - for CPython only
2. `generic_tags` - for any non-CPython Python implementation
3. `compatible_tags` - tags that are not specific to an interpreter
implementation
Since pip allows users to provide explicit impl, platform, abi, and
version, we have to use these functions.
`cpython_tags` and `generic_tags` are mutually exclusive, and return tags
that are the highest priority. These capture the most specific tags.
`compatible_tags` are always applicable, and a lower priority since they
may just be compatible with e.g. the Python language version, but lack
any optimizations available in the interpreter-specific tags.
To be able to do a meaningful comparison between our current
implementation and the above functions, we need to segment the pip code
into pieces that look like them.
To that end, we now have copies of the current `get_supported` function,
one for each of the functions provided by packaging.tags, and will walk
through converting them to rely on packaging.tags. For each
simplification step, if desired, we can compare the implementation in
the packaging.tags function with what we're replacing in pip.
Specifically, for each function in turn, we will:
1. Refactor it locally, taking into account its new, more limited,
responsibilities
2. Introduce the packaging.tags function for the case where there are no
custom arguments provided
3. Customize arguments one-by-one and delegate to the packaging.tags
function
4. When there is no pip-specific logic left, remove the intermediate
function and use the packaging.tags function directly in
`get_supported`
In the end all these functions will be gone again and we'll be left with
an implementation that relies solely on the tag generation in
packaging.tags.
This is the standard type used by packaging.tags. Making this change
throughout the code lets us start switching over to using its
tag-generating functions in get_supported().
We also get rid of a test, since it was superseded by `__str__` in
packaging.tags.Tag.
This reduces the amount of code we have to manage.
interpreter_name is calculated differently, defaulting to the
long name of the interpreter rather than "cp", but that is more
conformant.
Since the new packaging has types, it includes a py.typed. No harm in
including this in our package, and it may facilitate debug tool usage on
an installed pip by signaling that pip._vendor.packaging is
type-annotated.
The only purpose of _collect_buildset is now
to compute the cache directory to use
for a given requirements. This is better
computed one by one in the build loop.
Actual installation has been using the wheel file directly for some
time. The last piece that required an unpacked wheel was metadata. Now
that it uses the wheel file directly, we can remove the unpacking after
build.
We now extract all metadata files from the wheel directly into memory
and make them available to the wrapping pkg_resources.Distribution via
the DictMetadata introduced earlier.
pkg_resources.Distribution classes delegate to the IMetadataProvider
implementation provided at construction. This is the one we'll use for
adapting a ZipFile to pkg_resources.DistInfoDistribution.
In order to parse metadata from wheel files directly we want to reuse
parse_wheel. Moving it out helps avoid creating an unnecessary
dependence on operations.install.wheel.
This functions as a guard for the rest of our wheel-handling code,
ensuring that we will only get past this point if we have a wheel that
we should be able to handle version-wise.
We return a tuple instead of bundling up the result in a dedicated type
because it's the simplest option. The interface will be easy to update
later if the need arises.
First example of transitioning a directory-aware function to using a
zipfile directly. Since we will not need to maintain the unpacked dir
going forward, we don't need to worry about making wheel_dist_info_dir
"generic", just that the same tests pass for both cases at each commit.
To do this neatly we use pytest.fixture(params=[...]), which
generates a test for each param. Once we've transitioned the
necessary functions we only need to replace the fixture name and remove
the dead code.
Since
- download_dir is only set by the download command
- download_dir is
normalized at the beginning of the download command
- path normalization includes expanduser
Therefore expanduser in the preparer is redundant
Since retrieval of the .dist-info dir already ensures that a
distribution is found, this reduces responsibility on wheel_metadata and
lets us remove a few tests already covered by the tests for
test_wheel_dist_info_dir_*.
This will make it easier to transition to the already-determined
dist-info directory and reduces some of our dependence on pkg_resources.
Despite the name, the `egg_info` member is also populated for
.dist-info dirs.
ensure_str uses encoding='utf-8' and errors='strict' for Python 3
by default, which matches the behavior in
`pkg_resources.NullProvider.get_metadata`.
This will let us re-use the wheel_metadata for other parts of
processing, and by parameterizing checks in terms of metadata we will be
able to substitute in metadata derived directly from the zip.
* Raise exception on exception in finding wheel dist
We plan to replace this code with direct extraction from a zip, so no
point catching anything more precise.
* Raise exception if no dist is found in wheel_version
* Catch file read errors when reading WHEEL
get_metadata delegates to the underlying implementation which tries
to locate and read the file, throwing an IOError (Python 2) or OSError
subclass on any errors.
Since the new explicit test checks the same case as brokenwheel in
test_wheel_version we remove the redundant test.
* Check for WHEEL decoding errors explicitly
This was the last error that could be thrown by get_metadata, so we can
also remove the catch-all except block.
* Move WHEEL parsing outside try...except
This API does not raise an exception, but returns any errors on the
message object itself. We are preserving the original behavior, and can
decide later whether to start warning or raising our own exception.
* Raise explicit error if Wheel-Version is missing
`email.message.Message.__getitem__` returns None on missing values, so
we have to check for ourselves explicitly.
* Raise explicit exception on failure to parse Wheel-Version
This is also the last exception that can be raised, so we remove
`except Exception`.
* Remove dead code
Since wheel_version never returns None, this exception will never be
raised.
* Edit subdirs of top-level instead of checking in each directory
Previously, we were checking whether the top of the relative path ended
with .data. Now, we do not recurse into those directories, so there's no
need to check every time.
* Store info_dir in separate variable
Instead of working with a list everywhere, we use the single info_dir.
* Separate variables for info_dir and the destination path
* Use destination .dist-info dir only when needed
By initially storing just the name of the folder we ensure our code is
agnostic to the destination, so it'll be easier to install from a zip
later.
* Use os.listdir instead of os.walk for wheel dir population
Since we only execute any code when basedir == '', we only need the
top-level directories.
* Inline data_dirs calculation
* Inline info_dirs calculation
This is ensured at the beginning of the wheel
command, which is the only command that
sets wheel_download_dir.
This is also similar to what is done for download_dir
in the download command.
Previously we were making unguarded calls to non-Windows-only APIs. Mypy
only automatically excludes these from platform-specific checks when
inside conditions.
Previously we were restricting to a single .dist-info directory anywhere
in the unpacked wheel directory. That was incorrect since only a
top-level .dist-info directory indicates a contained "package". Now we
limit our restriction to top-level .dist-info directories.
We unconditionally update the requirement
link with the build wheel (in cache), so
when build() will return build success as
well as build failure, the caller can obtain
the built wheel by looking at req.local_file_path
install_given_reqs is only called from InstallCommand.run, which calls
RequirementSet.cleanup_files, which calls
InstallRequirement.remove_temporary_source for each InstallRequirement,
so the call here was not necessary.
We have test coverage affirming this still works as expected in
tests/functional/test_install_cleanup.py.
This aligns with the previous behavior that would have enforced the
found .dist-info directory starting with the name of the package.
We raise UnsupportedWheel because it looks better in output than the
AssertionError (which includes traceback).