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

Add ability to generate .spec files from local PKG-INFO file #189

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 78 additions & 11 deletions py2pack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
from py2pack.utils import (_get_archive_filelist, get_pyproject_table,
parse_pyproject, get_setuptools_scripts)

from email import parser


def replace_string(output_string, replaces):
for name, replacement in replaces.items():
pattern = r'(?<!%)%{' + name + '}' # Negative lookbehind to exclude "%%{name}"
output_string = re.sub(pattern, replacement.replace(r'%', r'%%'), output_string)
return output_string.replace(r'%%', r'%')


warnings.simplefilter('always', DeprecationWarning)

Expand All @@ -56,6 +65,33 @@ def pypi_json(project, release=None):
return pypimeta


def pypi_text_file(pkg_info_path):
with open(pkg_info_path, 'r') as pkg_info_file:
pkg_info_lines = parser.Parser().parse(pkg_info_file)
pkg_info_dict = {}
for key, value in pkg_info_lines.items():
key = key.lower().replace('-', '_')
if key in {'classifiers', 'requires_dist', 'provides_extra'}:
val = pkg_info_dict.get(key)
if val is None:
val = []
pkg_info_dict[key] = val
val.append(value)
else:
pkg_info_dict[key] = value
return {'info': pkg_info_dict, 'urls': []}


def pypi_json_file(file_path):
with open(file_path, 'r') as json_file:
js = json.load(json_file)
if 'info' not in js:
js = {'info': js}
if 'urls' not in js:
js['urls'] = []
return js


def _get_template_dirs():
"""existing directories where to search for jinja2 templates. The order
is important. The first found template from the first found dir wins!"""
Expand Down Expand Up @@ -311,29 +347,41 @@ def generate(args):
print('generating spec file for {0}...'.format(args.name))
data = args.fetched_data['info']
durl = newest_download_url(args)
data['source_url'] = (args.source_url or
(durl and durl['url']) or
args.name + '-' + args.version + '.zip')
source_url = data['source_url'] = (args.source_url or (durl and durl['url']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is just a movement of code and adds the lines 382 and 383. I think it's not needed, just keep it as it is.

Copy link
Contributor Author

@huakim huakim Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes we place our generated source files archive under different file paths, provided by --source-glob parameter. it would be nice to take this into account in our code, and set source_url as the basename of our generated source files archive

data['year'] = datetime.datetime.now().year # set current year
data['user_name'] = pwd.getpwuid(os.getuid())[4] # set system user (packager)
data['summary_no_ending_dot'] = re.sub(r'(.*)\.', r'\g<1>', data.get('summary', ""))

# If package name supplied on command line differs in case from PyPI's one
# then package archive will be fetched but the name will be the one from PyPI.
# Eg. send2trash vs Send2Trash. Check that.
for name in (args.name, data['name']):
tarball_file = glob.glob("{0}-{1}.*".format(name, args.version))
# also check tarball files with underscore. Some packages have a name with
# a '-' or '.' but the tarball name has a '_' . Eg the package os-faults
tr = str.maketrans('-.', '__')
tarball_file += glob.glob("{0}-{1}.*".format(name.translate(tr),
args.version))
tr = str.maketrans('-.', '__')
version = args.version
name = args.name
try:
source_glob = args.source_glob
except AttributeError:
source_glob = '%{name}-%{version}.*'
data_name = data['name'] or name

tarball_file = []
for __name in (name, name.translate(tr), data_name, data_name.translate(tr)):
tarball_file.extend(glob.glob(replace_string(source_glob, {'name': __name, 'version': version})))
if tarball_file:
break
Comment on lines +364 to +371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use this new function replace_string instead of just keeping the python format for this simple replacement?

Suggested change
source_glob = '%{name}-%{version}.*'
data_name = data['name'] or name
tarball_file = []
for __name in (name, name.translate(tr), data_name, data_name.translate(tr)):
tarball_file.extend(glob.glob(replace_string(source_glob, {'name': __name, 'version': version})))
if tarball_file:
break
source_glob = '{name}-{version}.*'
data_name = data['name'] or name
tarball_file = []
for n in (name, name.translate(tr), data_name, data_name.translate(tr)):
tarball_file.extend(glob.glob(source_glob.format(name=n, version=version)))
if tarball_file:
break

Copy link
Contributor Author

@huakim huakim Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Python's 'format' method for replacement explicitly replaces all elements with {name}, but sometimes we need to leave {name} unchanged, also Python's 'format' method does look for neither % symbol, nor %% symbol.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why it's required to be able to look for {name} or something like that in the glob search for filenames, but if that is your user case, it's okay, not a big deal.


if tarball_file: # get some more info from that
_augment_data_from_tarball(args, tarball_file[0], data)
tarball_file = tarball_file[0]
_augment_data_from_tarball(args, tarball_file, data)

else:
warnings.warn("No tarball for {} in version {} found. Valuable "
"information for the generation might be missing."
"".format(args.name, args.version))
tarball_file = args.name + '-' + args.version + '.zip'

if not source_url:
data['source_url'] = os.path.basename(tarball_file)

_normalize_license(data)

Expand All @@ -348,6 +396,22 @@ def generate(args):


def fetch_data(args):
try:
localfile = args.localfile
local = args.local
except AttributeError:
localfile = local = ''

if not localfile and local:
localfile = f'{args.name}.egg-info/PKG-INFO'
if os.path.isfile(localfile):
try:
data = pypi_json_file(localfile)
except json.decoder.JSONDecodeError:
data = pypi_text_file(localfile)
args.fetched_data = data
args.version = args.fetched_data['info']['version']
return
args.fetched_data = pypi_json(args.name, args.version)
urls = args.fetched_data['urls']
if len(urls) == 0:
Expand Down Expand Up @@ -412,6 +476,9 @@ def main():
parser_generate.add_argument('name', help='package name')
parser_generate.add_argument('version', nargs='?', help='package version (optional)')
parser_generate.add_argument('--source-url', default=None, help='source url')
parser_generate.add_argument('--source-glob', help='source glob template')
parser_generate.add_argument('--local', action='store_true', help='build from local package')
parser_generate.add_argument('--localfile', default='', help='path to the local PKG-INFO or json metadata')
parser_generate.add_argument('-t', '--template', choices=file_template_list(), default='opensuse.spec', help='file template')
parser_generate.add_argument('-f', '--filename', help='spec filename (optional)')
# TODO (toabctl): remove this is a later release
Expand Down
7 changes: 7 additions & 0 deletions test/test_py2pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ddt import ddt, data, unpack

import py2pack
from py2pack import replace_string


@ddt
Expand All @@ -46,6 +47,12 @@ def test__get_source_url(self, pypi_name, extension, expected_url):
self.assertEqual(py2pack._get_source_url(pypi_name, extension),
expected_url)

def test_replace_text(self):
input_string = 'This is %{name} and %%{name} %{what}. Also, replace %% with %.'
output_string = replace_string(input_string, {'name': 'replacement', 'what': r'%placeholders%%'})
expected_output_string = r'This is replacement and %{name} %placeholders%%. Also, replace % with %.'
self.assertEqual(output_string, expected_output_string)

def test_list(self):
py2pack.list_packages(self.args)

Expand Down