commit 65bb4bee40ca56ff4483b0e7df13e06fe715a69e Author: Karolina Surma Date: Mon Aug 16 12:39:44 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c110466 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +.pytest_cache/ +python-*.spec diff --git a/README.md b/README.md new file mode 100644 index 0000000..eae1751 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# pyp2spec + +This project is a thought descendant of [specfile_generator](https://github.com/frenzymadness/specfile_generator). + +It aims to generate working Fedora RPM spec file for Python projects. +The produced spec files must be compliant with the current [Python Packaging Guidelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/) (in effect since 2021). +It utilizes the benefits of [pyproject-rpm-macros](https://src.fedoraproject.org/rpms/pyproject-rpm-macros). + +It is under development. + +## What it will do + +The project will consist of two parts: +- *pyp2conf*: gathers of all the necessary information to produce a spec file and stores it in a configuration file - **not available yet** +- *conf2spec*: produces working spec file using all the information from configuration file - **a limited set of functionalities is available** + +## Configuration file specification + +Configuration data is stored in a TOML file. + +TBD + +## How to run + +To run whatever this project offers at this point, install to your virtual environment the dependencies from `requirements.txt`: + +``` +python -m pip install -r requirements.txt +``` + +Until the configuration file specification is set, you can use the test files to run the script: +``` +python conf2spec.py -f tests/pyp2spec_click/pyp2spec_click.conf +``` + +### Tests + +To run the tests, install pytest & run it: + +``` +python -m pip install pytest +python -m pytest +``` diff --git a/conf2spec.py b/conf2spec.py new file mode 100644 index 0000000..0233f06 --- /dev/null +++ b/conf2spec.py @@ -0,0 +1,112 @@ +from pathlib import Path + +import click +import tomli + +from jinja2 import Template + + +TEMPLATE_PATH = Path().resolve(__file__) / 'template.spec' + + +def load_configuration(filename): + """Load TOML configuration file. + Raise exception if file's contents is not a valid TOML.""" + + with open(filename, "rb") as configuration_file: + try: + return tomli.load(configuration_file) + except tomli.TOMLDecodeError as err: + print("That's not a valid TOML file.") + raise err + + +def generate_extra_build_requires(config): + """If defined in config file, return extra build requires. + If none were defined, return an empty string.""" + + # TODO: This doesn't handle doubles like -xr + options = { + "test": "-t", + "runtime": "-r", + } + return options.get(config["extra_build_requires"], "") + + +def generate_check(config): + """ If defined in config file, return the applicable macro invocation. + If none was defined, return %py3_check_import with module name.""" + + # TODO: This list is far from complete + options = { + "tox": "%tox", + "pytest": "%pytest", + } + fallback = f"%py3_check_import {config['module_name']}" + return options.get(config["test"], fallback) + + +def generate_manual_build_requires(config): + """If defined in config file, return manual build requires. + If none were defined, return an empty string.""" + + return config.get("manual_build_requires", "") + + +def fill_in_template(config): + """Return template rendered with data from config file.""" + + with open(TEMPLATE_PATH) as template_file: + spec_template = Template(template_file.read()) + + result = spec_template.render( + name=config["name"], + python_name=config["python_name"], + version=config["version"], + release=config["release"], + summary=config["summary"], + license=config["license"], + url=config["url"], + source=config["source"], + description=config["description"], + module_name=config["module_name"], + manual_build_requires=generate_manual_build_requires(config), + extra_build_requires=generate_extra_build_requires(config), + test=generate_check(config), + license_files=" ".join(config["files"]["license_files"]), + doc_files=" ".join(config["files"]["doc_files"]), + + changelog_head=config["changelog"]["changelog_head"], + changelog_msg=config["changelog"]["changelog_msg"], + ) + + return result + + +def write_spec_file(config): + """Save the spec file in the current directory. + Return the saved file name""" + + # TODO: make it possible to write the file to a given directory + result = fill_in_template(config) + spec_file_name = config["python_name"] + ".spec" + with open(spec_file_name, "w") as spec_file: + spec_file.write(result) + print(f"Spec file {spec_file_name} was saved.") + return spec_file_name + + +@click.command() +@click.option( + "--filename", "-f", + required=True, + help="Provide configuration file", +) +def main(filename): + config = load_configuration(filename) + print(f"{filename} loaded successful") + write_spec_file(config) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..50380b2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +click +jinja2 +tomli diff --git a/template.spec b/template.spec new file mode 100644 index 0000000..25760b5 --- /dev/null +++ b/template.spec @@ -0,0 +1,54 @@ +Name: {{python_name}} +Version: {{version}} +Release: {{release}}%{?dist} +Summary: {{summary}} + +License: {{license}} +URL: {{url}} +Source0: {{source}} + +BuildArch: noarch +BuildRequires: python3-devel + +%global _description %{expand: +{{description}}} + +{% for br in manual_build_requires %}BuildRequires: {{br}} +{% endfor %} +%description %_description + +%package -n python3-{{name}} +Summary: %{summary} + +%description -n python3-{{name}} %_description + + +%prep +%autosetup -p1 -n {{name}}-%{version} + + +%generate_buildrequires +%pyproject_buildrequires {{extra_build_requires}} + + +%build +%pyproject_wheel + + +%install +%pyproject_install +%pyproject_save_files {{module_name}} + + +%check +{{test}} + + +%files -n python3-{{name}} -f %{pyproject_files} +%doc {{doc_files}} +%license {{license_files}} + + +%changelog +* {{changelog_head}} - {{version}}-{{release}} +- {{changelog_msg}} \ No newline at end of file diff --git a/tests/pyp2spec_click/pyp2spec_click.conf b/tests/pyp2spec_click/pyp2spec_click.conf new file mode 100644 index 0000000..ccbfe15 --- /dev/null +++ b/tests/pyp2spec_click/pyp2spec_click.conf @@ -0,0 +1,26 @@ +name = "click" +python_name = "python-click" +module_name = "click" +version = "7.1.2" +release = "6" +summary = "Simple wrapper around optparse for powerful command line utilities" +license = "BSD" +url = "https://github.com/mitsuhiko/click" +source = "%{url}/archive/%{version}/%{pypi_name}-%{version}.tar.gz" +description = ''' +click is a Python package for creating beautiful command line\ +interfaces in a composable way with as little amount of code as necessary.\ +It's the "Command Line Interface Creation Kit". It's highly configurable but\ +comes with good defaults out of the box. +''' + +extra_build_requires = "test" +test = "tox" + +[files] +license_files = ["LICENSE.rst"] +doc_files = ["README.rst", "CHANGES.rst"] + +[changelog] +changelog_head = "Wed Jun 02 2021 Package Maintainer " +changelog_msg = "Rebuilt for Python 3.10" \ No newline at end of file diff --git a/tests/pyp2spec_click/pyp2spec_click.spec b/tests/pyp2spec_click/pyp2spec_click.spec new file mode 100644 index 0000000..36a7616 --- /dev/null +++ b/tests/pyp2spec_click/pyp2spec_click.spec @@ -0,0 +1,57 @@ +Name: python-click +Version: 7.1.2 +Release: 6%{?dist} +Summary: Simple wrapper around optparse for powerful command line utilities + +License: BSD +URL: https://github.com/mitsuhiko/click +Source0: %{url}/archive/%{version}/%{pypi_name}-%{version}.tar.gz + +BuildArch: noarch +BuildRequires: python3-devel + +%global _description %{expand: +click is a Python package for creating beautiful command line\ +interfaces in a composable way with as little amount of code as necessary.\ +It's the "Command Line Interface Creation Kit". It's highly configurable but\ +comes with good defaults out of the box. +} + + +%description %_description + +%package -n python3-click +Summary: %{summary} + +%description -n python3-click %_description + + +%prep +%autosetup -p1 -n click-%{version} + + +%generate_buildrequires +%pyproject_buildrequires -t + + +%build +%pyproject_wheel + + +%install +%pyproject_install +%pyproject_save_files click + + +%check +%tox + + +%files -n python3-click -f %{pyproject_files} +%doc README.rst CHANGES.rst +%license LICENSE.rst + + +%changelog +* Wed Jun 02 2021 Package Maintainer - 7.1.2-6 +- Rebuilt for Python 3.10 \ No newline at end of file diff --git a/tests/pyp2spec_tomli/pyp2spec_tomli.conf b/tests/pyp2spec_tomli/pyp2spec_tomli.conf new file mode 100644 index 0000000..9cee52d --- /dev/null +++ b/tests/pyp2spec_tomli/pyp2spec_tomli.conf @@ -0,0 +1,32 @@ +name = "tomli" +python_name = "python-tomli" +module_name = "tomli" +version = "1.1.0" +release = "1" +summary = "A little TOML parser for Python" +license = "MIT" +url = "https://pypi.org/project/tomli/" +source = "https://github.com/hukkin/tomli/archive/refs/tags/%{version}.tar.gz" +description = ''' +Tomli is a Python library for parsing TOML. +Tomli is fully compatible with TOML v1.0.0. +''' + +manual_build_requires = [ + "python3-pytest", + "python3-dateutil", +] + +extra_build_requires = "runtime" +test = "pytest" + +[files] +license_files = ["LICENSE"] +doc_files = ["README.md", "CHANGELOG.md"] + +[changelog] +changelog_head = "Thu Jul 29 2021 Package Maintainer " +changelog_msg = ''' +Update to version 1.1.0 + - `load` can now take a binary file object +''' \ No newline at end of file diff --git a/tests/pyp2spec_tomli/pyp2spec_tomli.spec b/tests/pyp2spec_tomli/pyp2spec_tomli.spec new file mode 100644 index 0000000..e339428 --- /dev/null +++ b/tests/pyp2spec_tomli/pyp2spec_tomli.spec @@ -0,0 +1,58 @@ +Name: python-tomli +Version: 1.1.0 +Release: 1%{?dist} +Summary: A little TOML parser for Python + +License: MIT +URL: https://pypi.org/project/tomli/ +Source0: https://github.com/hukkin/tomli/archive/refs/tags/%{version}.tar.gz + +BuildArch: noarch +BuildRequires: python3-devel + +%global _description %{expand: +Tomli is a Python library for parsing TOML. +Tomli is fully compatible with TOML v1.0.0. +} + +BuildRequires: python3-pytest +BuildRequires: python3-dateutil + +%description %_description + +%package -n python3-tomli +Summary: %{summary} + +%description -n python3-tomli %_description + + +%prep +%autosetup -p1 -n tomli-%{version} + + +%generate_buildrequires +%pyproject_buildrequires -r + + +%build +%pyproject_wheel + + +%install +%pyproject_install +%pyproject_save_files tomli + + +%check +%pytest + + +%files -n python3-tomli -f %{pyproject_files} +%doc README.md CHANGELOG.md +%license LICENSE + + +%changelog +* Thu Jul 29 2021 Package Maintainer - 1.1.0-1 +- Update to version 1.1.0 + - `load` can now take a binary file object diff --git a/tests/test_conf2spec.py b/tests/test_conf2spec.py new file mode 100644 index 0000000..5154964 --- /dev/null +++ b/tests/test_conf2spec.py @@ -0,0 +1,37 @@ +from pathlib import Path +import pytest + +import conf2spec + + +def get_test_cases(): + """Go through all 'tests/' folders and get their names. + Yield the next folder name.""" + + source_path = Path("tests/") + for path in source_path.glob("pyp2spec_*"): + yield path.name + + +@pytest.mark.parametrize( + ("test_case"), + get_test_cases(), + ) +def test_generated_specfile(test_case): + # Get config and expected resulting file + config_file = Path("tests/") / test_case / f"{test_case}.conf" + expected_file = Path("tests/") / test_case / f"{test_case}.spec" + + # Run the conf2spec converter + conf = conf2spec.load_configuration(config_file) + rendered_file = conf2spec.write_spec_file(conf) + + # Compare the results + with open(rendered_file, "r") as rendered_f: + rendered = rendered_f.read() + with open(expected_file, "r") as expected_f: + expected = expected_f.read() + assert rendered == expected + + # Delete rendered file + Path(rendered_file).unlink() \ No newline at end of file