Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support license expression #26

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
949f198
added support for license_file defined in PEP639
mboisson Aug 17, 2021
546a693
adding support for Requires keyword (multi-valued)
mboisson Oct 26, 2021
74a1751
ensure that file permissions are kept for distinfo and data directories
mboisson Nov 29, 2021
24861ab
appease flak8
mboisson Dec 9, 2021
86b61a3
ensure ZipInfo is propagated for data and distinfo directories, unmar…
mboisson Dec 9, 2021
d65ee4f
Merge branch 'accept_requires'
mboisson Feb 7, 2022
aaea216
Merge branch 'keep_permissions'
mboisson Feb 7, 2022
eb76fc0
Merge branch 'master' of github.com:mboisson/wheelfile
mboisson Feb 7, 2022
6d16e28
supports requires and provides in metadata
mboisson Feb 7, 2022
bb44384
raise exceptions when caught
mboisson Feb 7, 2022
0b665c1
fixed self by cls for static method
mboisson Feb 7, 2022
a5aa0c1
Merge branch 'keep_permissions'
mboisson Feb 7, 2022
3fab6b6
Allow RECORD in subfolders, such as what is provided in 'py', which c…
mboisson Feb 7, 2022
2236e92
added missing comma
mboisson Feb 7, 2022
766c4fa
This fixes https://github.com/ComputeCanada/wheels_builder/issues/57 …
mboisson Jun 7, 2022
1233880
Merge branch 'no_canonicalize_when_building_from_wheel'
mboisson Jun 7, 2022
6ce25bd
added support for License field
mboisson Aug 11, 2022
8ea62a1
when WHEEL file is corrupted, set wheeldata to None instead of metadata
mboisson Sep 12, 2022
a5e9995
Merge branch 'invalid_WHEEL'
mboisson Sep 12, 2022
a2c6d11
add support for PEP 0639's License-Expression field
mboisson Feb 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions tests/test_wheelfile_cloning.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ def test_zip_attributes_are_preserved_writestr(self, wf, buf, attr):

assert getattr(czf, attr) == getattr(zf, attr)

@pytest.mark.xfail(reason="writestr_data does not propagate zinfo yet")
@pytest.mark.parametrize("attr", PRESERVED_ZIPINFO_ATTRS)
def test_zip_attributes_are_preserved_writestr_data(self, wf, buf, attr):
zf = self.custom_zipinfo()
Expand All @@ -255,9 +254,6 @@ def test_zip_attributes_are_preserved_writestr_data(self, wf, buf, attr):

assert getattr(czf, attr) == getattr(zf, attr)

# writestr_data does not propagate zinfo yet
# skipped because it generates lots of warnings
@pytest.mark.xfail(reason="writestr_distinfo does not propagate zinfo yet")
@pytest.mark.parametrize("attr", PRESERVED_ZIPINFO_ATTRS)
def test_zip_attributes_are_preserved_writestr_distinfo(self, wf, buf,
attr):
Expand Down
82 changes: 70 additions & 12 deletions wheelfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ class MetaData:
classifier is used, this parameter may be omitted or used to specify the
particular version of the intended legal text.

license_file
The License-File is a string that is a path, relative to``.dist-info``,
to a license file. The license file content MUST be UTF-8 encoded text.

license_expression
The License-Expression optional field is specified to contain a text
string that is a valid SPDX license expression, as defined herein.

home_page
URL of the home page for this distribution (project).

Expand Down Expand Up @@ -273,6 +281,12 @@ class MetaData:
Each item may end with a semicolon followed by a PEP-496 environment
markers.

requires
Package requirements

provides
The name of the provided package

provides_extras
List of names of optional features provided by a distribution. Used to
specify which dependencies should be installed depending on which of
Expand Down Expand Up @@ -338,7 +352,11 @@ def __init__(self, *, name: str, version: Union[str, Version],
requires_externals: Optional[List[str]] = None,
provides_extras: Optional[List[str]] = None,
provides_dists: Optional[List[str]] = None,
obsoletes_dists: Optional[List[str]] = None
obsoletes_dists: Optional[List[str]] = None,
license_files: Optional[List[str]] = None,
license_expression: Optional[str] = None,
provides: Optional[str] = None,
requires: Optional[str] = None,
):
# self.metadata_version = '2.1' by property
self.name = name
Expand Down Expand Up @@ -373,6 +391,10 @@ def __init__(self, *, name: str, version: Union[str, Version],
self.provides_extras = provides_extras or []
self.provides_dists = provides_dists or []
self.obsoletes_dists = obsoletes_dists or []
self.license_files = license_files or []
self.license_expression = license_expression or []
self.provides = provides
self.requires = requires

__slots__ = _slots_from_params(__init__)

Expand All @@ -384,6 +406,8 @@ def metadata_version(self):
@classmethod
def field_is_multiple_use(cls, field_name: str) -> bool:
field_name = field_name.lower().replace('-', '_').rstrip('s')
if field_name in ['provide', 'require']:
return False
if field_name in cls.__slots__ or field_name == 'keyword':
return False
if field_name + 's' in cls.__slots__:
Expand Down Expand Up @@ -432,6 +456,8 @@ def __str__(self) -> str:
m.add_header(field_name, value)
elif field_name == 'Description':
m.set_payload(content)
elif field_name == 'License':
m.set_payload(content)
else:
assert isinstance(content, str), (
f"Expected string, got {type(content)} instead: {attr_name}"
Expand Down Expand Up @@ -459,7 +485,6 @@ def __eq__(self, other):
@classmethod
def from_str(cls, s: str) -> 'MetaData':
m = message_from_string(s)

# TODO: validate this when the rest of the versions are implemented
# assert m['Metadata-Version'] == cls._metadata_version

Expand All @@ -468,7 +493,7 @@ def from_str(cls, s: str) -> 'MetaData':
args = {}
for field_name in m.keys():
attr = cls._attr_name(field_name)
if not attr.endswith('s'):
if not attr.endswith('s') or attr in ["provides", "requires"]:
args[attr] = m.get(field_name)
else:
if field_name == "Keywords":
Expand All @@ -477,7 +502,6 @@ def from_str(cls, s: str) -> 'MetaData':
args[attr] = m.get_all(field_name)

args['description'] = m.get_payload()

return cls(**args)


Expand Down Expand Up @@ -699,7 +723,8 @@ def update(self, arcpath: str, buf: IO[bytes]):
assert buf.tell() == 0, (
f"Stale buffer given - current position: {buf.tell()}."
)
assert not arcpath.endswith('.dist-info/RECORD'), (
# if .dist-info/RECORD is not in a subdirectory, it is not allowed
assert "/" in arcpath.replace('.dist-info/RECORD',"") or not arcpath.endswith('.dist-info/RECORD'), (
f"Attempt to add an entry for a RECORD file to the RECORD: "
f"{repr(arcpath)}."
)
Expand Down Expand Up @@ -1398,6 +1423,12 @@ def from_wheelfile(
compresslevel=compresslevel,
strict_timestamps=strict_timestamps,
)
# if we copy a wheel from an existing wheelfile, we should recreate the
# distinfo_prefix with the original distname and the modified version
# if we do not set _distinfo_prefix, the distname will get canonicalized through
# _distinfo_path(), and this is wrong when building a wheel from an existing one
# as it will break package discovery
new_wf._distinfo_prefix = f"{distname}-{version}."

assert new_wf.wheeldata is not None # For MyPy

Expand Down Expand Up @@ -1439,17 +1470,38 @@ def from_wheelfile(
arcname_tail = '/'.join(arcname_tail_parts)
if arcname_head == wf.distinfo_dirname:
new_arcname = new_wf.distinfo_dirname + '/' + arcname_tail
new_wf.writestr(new_arcname, wf.zipfile.read(zinfo))

# create a new ZipInfo
new_zinfo = cls._clone_and_rename_zipinfo(zinfo, new_arcname)

new_wf.writestr(new_zinfo, wf.zipfile.read(zinfo))
continue
if arcname_head == wf.data_dirname:
new_arcname = new_wf.data_dirname + '/' + arcname_tail
new_wf.writestr(new_arcname, wf.zipfile.read(zinfo))

# create a new ZipInfo
new_zinfo = cls._clone_and_rename_zipinfo(zinfo, new_arcname)

new_wf.writestr(new_zinfo, wf.zipfile.read(zinfo))
continue

new_wf.writestr(zinfo, wf.zipfile.read(zinfo))

return new_wf

@staticmethod
def _clone_and_rename_zipinfo(zinfo: zipfile.ZipInfo, new_name: str):
# attributes to preserved
PRESERVED_ZIPINFO_ATTRS = ['date_time', 'compress_type', 'comment',
'extra', 'create_system', 'create_version',
'extract_version', 'flag_bits', 'volume',
'internal_attr', 'external_attr']
new_zinfo = zipfile.ZipInfo(filename=new_name)
for attr in PRESERVED_ZIPINFO_ATTRS:
setattr(new_zinfo, attr, getattr(zinfo, attr))

return new_zinfo

@staticmethod
def _is_unnamed_or_directory(target: Union[Path, BinaryIO]) -> bool:
return (
Expand Down Expand Up @@ -1617,14 +1669,15 @@ def _read_distinfo(self):
try:
metadata = self.zipfile.read(self._distinfo_path('METADATA'))
self.metadata = MetaData.from_str(metadata.decode('utf-8'))
except Exception:
except Exception as e:
self.metadata = None
raise(e)

try:
wheeldata = self.zipfile.read(self._distinfo_path('WHEEL'))
self.wheeldata = WheelData.from_str(wheeldata.decode('utf-8'))
except Exception:
self.metadata = None
self.wheeldata = None

try:
record = self.zipfile.read(self._distinfo_path('RECORD'))
Expand Down Expand Up @@ -1971,7 +2024,6 @@ def _os_walk_path_to_arcpath(prefix: str, directory: str,
path = os.path.join(arcname, directory[len(prefix):], stem)
return path

# TODO: Make sure fields of given ZipInfo objects are propagated
def writestr(self,
zinfo_or_arcname: Union[zipfile.ZipInfo, str],
data: Union[bytes, str],
Expand Down Expand Up @@ -2095,7 +2147,6 @@ def write_data(self, filename: Union[str, Path],

# TODO: drive letter should be stripped from the arcname the same way
# ZipInfo.from_file does it
# TODO: Make sure fields of given ZipInfo objects are propagated
def writestr_data(self, section: str,
zinfo_or_arcname: Union[zipfile.ZipInfo, str],
data: Union[bytes, str],
Expand Down Expand Up @@ -2148,6 +2199,10 @@ def writestr_data(self, section: str,
arcname = self._distinfo_path(section + '/' + arcname.lstrip('/'),
kind='data')

# clone the rest of the attributes from provided zinfo
if isinstance(zinfo_or_arcname, zipfile.ZipInfo):
arcname = self._clone_and_rename_zipinfo(zinfo_or_arcname, arcname)

self.writestr(arcname, data, compress_type, compresslevel)

# TODO: Lazy mode should permit writing meta here
Expand Down Expand Up @@ -2236,7 +2291,6 @@ def write_distinfo(self, filename: Union[str, Path],
self.write(filename, arcname, compress_type, compresslevel,
recursive=recursive, skipdir=skipdir)

# TODO: Make sure fields of given ZipInfo objects are propagated
def writestr_distinfo(self, zinfo_or_arcname: Union[zipfile.ZipInfo, str],
data: Union[bytes, str],
compress_type: Optional[int] = None,
Expand Down Expand Up @@ -2294,6 +2348,10 @@ def writestr_distinfo(self, zinfo_or_arcname: Union[zipfile.ZipInfo, str],
)

arcname = self._distinfo_path(arcname.lstrip('/'))
# clone the rest of the attributes from provided zinfo
if isinstance(zinfo_or_arcname, zipfile.ZipInfo):
arcname = self._clone_and_rename_zipinfo(zinfo_or_arcname, arcname)

self.writestr(arcname, data, compress_type, compresslevel)

@staticmethod
Expand Down