# specfile
Python library for parsing and manipulating RPM spec files. Main focus is on modifying existing spec files, any change should result in a minimal diff.
## Motivation
Originally, [rebase-helper](https://github.com/rebase-helper/rebase-helper/) provided an API for spec file modifications that was also used by [packit](https://github.com/packit/packit). The goal of this project is to make the interface more general and convenient to use by not only packit but also by other Python projects that need to interact with RPM spec files.
## Important terms used in this library
### Section
Section is a spec file section, it has a well-defined name that starts with _%_ character and that can optionally be followed by arguments.
In this library, the starting _%_ of section name is omitted for convenience.
There is a special section internally called `%package`, often also referred to as preamble, and it represents the content of the spec file that precedes the first named section (usually `%description`). This section contains the main package metadata (tags). Metadata of subpackages are defined in subsequent `%package` sections, that are not anonymous and are always followed by arguments specifying the name of the subpackage (e.g. `%package doc` or `%package -n completely-different-subpackage-name`).
### Tag
Tag represents a single item of metadata of a package. It has a well-defined name and a value. Tags are defined in `%package` sections.
For the purposes of this library, a tag can have associated comments. These are consecutive comment lines directly above the tag definition in a spec file.
### Source
Source is a source file or a downstream patch defined by a `Source`/`Patch` tag or by an entry in `%sourcelist`/`%patchlist` section.
Source can be local, specified by a filename, or remote, specified by a URL. Local sources should be located in a directory referred to as `sourcedir`. Remote sources should be downloaded to this directory.
Sources defined by tags can be explicitly numbered, e.g. `Source0` or `Patch999`, otherwise implicit numbering takes place and source numbers are auto-assigned in a sequential manner.
### Prep macros
Prep macros are macros that often appear in (and only in, they don't make sense anywhere else) `%prep` section.
4 such macros are recognized by this library, [`%setup`](https://rpm-packaging-guide.github.io/#setup), [`%patch`](http://ftp.rpm.org/max-rpm/s1-rpm-inside-macros.html#S2-RPM-INSIDE-PATCH-MACRO), [`%autosetup`](https://rpm-software-management.github.io/rpm/manual/autosetup.html#autosetup-description) and [`%autopatch`](https://rpm-software-management.github.io/rpm/manual/autosetup.html#autopatch). A typical spec file uses either `%autosetup` or a combination of `%setup` and `%patch` or `%autopatch`.
## Documentation
[Full documentation generated from code](https://packit.dev/specfile/api/specfile).
## Examples and use cases
The following examples should cover use cases required by [packit](https://github.com/packit/research/blob/main/specfile/README.md).
### Instantiating
```python
from specfile import Specfile
# using an absolute path
specfile = Specfile('/tmp/test.spec')
# using a relative path and a different sourcedir
specfile = Specfile('test.spec', sourcedir='/tmp/sources')
```
### Reloading
```python
# if the spec file happens to be modified externally, it can be reloaded
specfile.reload()
```
### Saving changes
```python
# no autosave
specfile = Specfile('test.spec')
...
# saving explicitly when needed
specfile.save()
# enabling autosave, changes are saved immediately after any modification
specfile = Specfile('test.spec', autosave=True)
# as a context manager, saving is performed at context exit
with Specfile('test.spec') as specfile:
...
```
### Defining and undefining macros
```python
# override macros loaded from system macro files
specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])
# undefine a system macro (in case it's defined)
specfile = Specfile('test.spec', macros=[('rhel', None)])
```
### Low-level manipulation
```python
with specfile.sections() as sections:
# replacing the content of a section
sections.prep = ['%autosetup -p1']
# removing a section
del sections.changelog
# swapping two sections
sections[1], sections[2] = sections[2], sections[1]
# accessing a section with arguments
print(sections.get('package devel'))
# inserting a line into a section
sections.build.insert(0, 'export VERBOSE=1')
# copying a section from one specfile to another
with specfile1.sections() as sections1, with specfile2.sections() as sections2:
sections2.changelog[:] = sections1.changelog
```
### Mid-level manipulation - tags, changelog and prep
```python
# accessing tags in preamble
with specfile.tags() as tags:
# name of the first tag
print(tags[0].name)
# raw value of the first tag
print(tags[0].value)
# expanded value of the first tag
print(tags[0].expanded_value)
# comments associated with the first tag
print(tags[0].comments)
# value of a tag by name
print(tags.url)
tags.url = 'https://example.com'
# accessing tags in subpackages
with specfile.tags('package devel') as tags:
print(tags.requires)
# working with changelog
with specfile.changelog() as changelog:
# most recent changelog entry
print(changelog[-1])
# making changes
changelog[1].content.append('- another line')
# removing the oldest entry
del changelog[0]
# working with macros in %prep section, supports %setup, %patch, %autosetup and %autopatch
from specfile.prep import AutosetupMacro
with specfile.prep() as prep:
# name of the first macro
print(prep.macros[0].name)
# checking if %autosetup is being used
print('%autosetup' in prep)
print(AutosetupMacro in prep)
# changing macro options
prep.autosetup.options.n = '%{srcname}-%{version}'
# adding a new %patch macro
prep.add_patch_macro(28, p=1, b='.test')
# removing an existing %patch macro by name
del prep.patch0
# this works for both '%patch0' and '%patch -P0'
prep.remove_patch_macro(0)
```
### High-level manipulation
#### Version and release
```python
# getting version and release
print(specfile.version)
print(specfile.release)
# setting version and release
specfile.version = '2.1'
specfile.release = '3'
# setting both at the same time (release defaults to 1)
specfile.set_version_and_release('2.1', release='3')
# setting version while trying to preserve macros
specfile.set_version_and_release('2.1', preserve_macros=True)
```
#### Bumping release
To bump release and add a new changelog entry, you could use the following code:
```python
from specfile import Specfile
with Specfile("example.spec") as spec:
spec.release = str(int(spec.expanded_release) + 1)
spec.add_changelog_entry("- Bumped release for test purposes")
```
#### Changelog
```python
# adding a new entry, author is automatically determined
# (using the same heuristics that rpmdev-packager uses) if possible
# this function already honors autochangelog
specfile.add_changelog_entry('- New upstream release 2.1')
# adding a new entry, specifying author and timestamp explicitly
specfile.add_changelog_entry(
'- New upstream release 2.1',
author='Nikola Forró',
email='nforro@redhat.com',
timestamp=datetime.date(2021, 11, 20),
)
if specfile.has_autochangelog:
# do something
```
#### Sources and patches
```python
with specfile.sources() as sources:
# expanded location of the first source
print(sources[0].expanded_location)
# adding a source
sources.append('tests.tar.gz')
with specfile.patches() as patches:
# modifying location of the first patch
patches[0].location = 'downstream.patch'
# removing comments associated with the last patch
patches[-1].comments.clear()
# adding and removing patches
patches.append('another.patch')
del patches[2]
# inserting a patch with a specific number
patches.insert_numbered(999, 'final.patch')
# adding a single patch
specfile.add_patch('necessary.patch', comment='a human-friendly comment to the patch')
```
#### Other attributes
```python
print(specfile.name)
print(specfile.license)
print(specfile.summary)
specfile.url = 'https://example.com'
```
Note that if you want to access multiple tag values, it may be noticeably faster to do it using the `tags` context manager:
```python
# same as above, but roughly 4x times faster (parsing/saving happens only once)
with specfile.tags() as tags:
print(tags.name.value)
print(tags.license.value)
print(tags.summary.value)
tags.url.value = 'https://example.com'
```
### Read-only access
If you don't need write access, you can use the `content` property of context managers and avoid the `with` statement:
```python
# no changes done to the tags object will be saved
tags = specfile.tags().content
print(tags.version.expanded_value)
print(tags.release.expanded_value)
# number of sources
print(len(specfile.sources().content))
```
### Validity
Macro definitions, tags, `%sourcelist`/`%patchlist` entries and sources/patches have a `valid` attribute. An entity is considered valid if it isn't present in a false branch of any condition.
Consider the following in a spec file:
```specfile
%if 0%{?fedora} >= 36
Recommends: %{name}-selinux
%endif
```
Provided there are no other `Recommends` tags, the following would print `True` or `False` depending on the value of the `%fedora` macro:
```python
with specfile.tags() as tags:
print(tags.recommends.valid)
```
You can define macros or redefine/undefine system macros using the `macros` argument of the constructor or by modifying the `macros` attribute of a `Specfile` instance.
The same applies to `%ifarch`/`%ifos` statements:
```specfile
%ifarch %{java_arches}
BuildRequires: java-devel
%endif
```
Provided there are no other `BuildRequires` tags, the following would print `True` in case the current platform was part of `%java_arches`:
```python
with specfile.tags() as tags:
print(tags.buildrequires.valid)
```
To override this, you would have to redefine the `%_target_cpu` system macro (or `%_target_os` in case of `%ifos`).
## Videos
Here is a demo showcasing the `Specfile.update_tag()` method and its use cases:
[![Demo of Specfile.update_tag() functionality](https://img.youtube.com/vi/yzMfBPdFXZY/0.jpg)](https://www.youtube.com/watch?v=yzMfBPdFXZY)
Raw data
{
"_id": null,
"home_page": "https://github.com/packit/specfile",
"name": "specfile",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": "packaging, fedora, rpm, spec",
"author": "Red Hat",
"author_email": "user-cont-team@redhat.com",
"download_url": "https://files.pythonhosted.org/packages/5c/59/a865da63f73dc27fd759566ee08f23348bb7f1671f100b40650939d32e8f/specfile-0.32.6.tar.gz",
"platform": null,
"description": "# specfile\n\nPython library for parsing and manipulating RPM spec files. Main focus is on modifying existing spec files, any change should result in a minimal diff.\n\n## Motivation\n\nOriginally, [rebase-helper](https://github.com/rebase-helper/rebase-helper/) provided an API for spec file modifications that was also used by [packit](https://github.com/packit/packit). The goal of this project is to make the interface more general and convenient to use by not only packit but also by other Python projects that need to interact with RPM spec files.\n\n## Important terms used in this library\n\n### Section\n\nSection is a spec file section, it has a well-defined name that starts with _%_ character and that can optionally be followed by arguments.\n\nIn this library, the starting _%_ of section name is omitted for convenience.\n\nThere is a special section internally called `%package`, often also referred to as preamble, and it represents the content of the spec file that precedes the first named section (usually `%description`). This section contains the main package metadata (tags). Metadata of subpackages are defined in subsequent `%package` sections, that are not anonymous and are always followed by arguments specifying the name of the subpackage (e.g. `%package doc` or `%package -n completely-different-subpackage-name`).\n\n### Tag\n\nTag represents a single item of metadata of a package. It has a well-defined name and a value. Tags are defined in `%package` sections.\n\nFor the purposes of this library, a tag can have associated comments. These are consecutive comment lines directly above the tag definition in a spec file.\n\n### Source\n\nSource is a source file or a downstream patch defined by a `Source`/`Patch` tag or by an entry in `%sourcelist`/`%patchlist` section.\n\nSource can be local, specified by a filename, or remote, specified by a URL. Local sources should be located in a directory referred to as `sourcedir`. Remote sources should be downloaded to this directory.\n\nSources defined by tags can be explicitly numbered, e.g. `Source0` or `Patch999`, otherwise implicit numbering takes place and source numbers are auto-assigned in a sequential manner.\n\n### Prep macros\n\nPrep macros are macros that often appear in (and only in, they don't make sense anywhere else) `%prep` section.\n\n4 such macros are recognized by this library, [`%setup`](https://rpm-packaging-guide.github.io/#setup), [`%patch`](http://ftp.rpm.org/max-rpm/s1-rpm-inside-macros.html#S2-RPM-INSIDE-PATCH-MACRO), [`%autosetup`](https://rpm-software-management.github.io/rpm/manual/autosetup.html#autosetup-description) and [`%autopatch`](https://rpm-software-management.github.io/rpm/manual/autosetup.html#autopatch). A typical spec file uses either `%autosetup` or a combination of `%setup` and `%patch` or `%autopatch`.\n\n## Documentation\n\n[Full documentation generated from code](https://packit.dev/specfile/api/specfile).\n\n## Examples and use cases\n\nThe following examples should cover use cases required by [packit](https://github.com/packit/research/blob/main/specfile/README.md).\n\n### Instantiating\n\n```python\nfrom specfile import Specfile\n\n# using an absolute path\nspecfile = Specfile('/tmp/test.spec')\n\n# using a relative path and a different sourcedir\nspecfile = Specfile('test.spec', sourcedir='/tmp/sources')\n```\n\n### Reloading\n\n```python\n# if the spec file happens to be modified externally, it can be reloaded\nspecfile.reload()\n```\n\n### Saving changes\n\n```python\n# no autosave\nspecfile = Specfile('test.spec')\n...\n# saving explicitly when needed\nspecfile.save()\n\n# enabling autosave, changes are saved immediately after any modification\nspecfile = Specfile('test.spec', autosave=True)\n\n# as a context manager, saving is performed at context exit\nwith Specfile('test.spec') as specfile:\n ...\n```\n\n### Defining and undefining macros\n\n```python\n# override macros loaded from system macro files\nspecfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])\n\n# undefine a system macro (in case it's defined)\nspecfile = Specfile('test.spec', macros=[('rhel', None)])\n```\n\n### Low-level manipulation\n\n```python\nwith specfile.sections() as sections:\n # replacing the content of a section\n sections.prep = ['%autosetup -p1']\n # removing a section\n del sections.changelog\n # swapping two sections\n sections[1], sections[2] = sections[2], sections[1]\n # accessing a section with arguments\n print(sections.get('package devel'))\n # inserting a line into a section\n sections.build.insert(0, 'export VERBOSE=1')\n\n# copying a section from one specfile to another\nwith specfile1.sections() as sections1, with specfile2.sections() as sections2:\n sections2.changelog[:] = sections1.changelog\n```\n\n### Mid-level manipulation - tags, changelog and prep\n\n```python\n# accessing tags in preamble\nwith specfile.tags() as tags:\n # name of the first tag\n print(tags[0].name)\n # raw value of the first tag\n print(tags[0].value)\n # expanded value of the first tag\n print(tags[0].expanded_value)\n # comments associated with the first tag\n print(tags[0].comments)\n # value of a tag by name\n print(tags.url)\n tags.url = 'https://example.com'\n\n# accessing tags in subpackages\nwith specfile.tags('package devel') as tags:\n print(tags.requires)\n\n# working with changelog\nwith specfile.changelog() as changelog:\n # most recent changelog entry\n print(changelog[-1])\n # making changes\n changelog[1].content.append('- another line')\n # removing the oldest entry\n del changelog[0]\n\n# working with macros in %prep section, supports %setup, %patch, %autosetup and %autopatch\nfrom specfile.prep import AutosetupMacro\n\nwith specfile.prep() as prep:\n # name of the first macro\n print(prep.macros[0].name)\n # checking if %autosetup is being used\n print('%autosetup' in prep)\n print(AutosetupMacro in prep)\n # changing macro options\n prep.autosetup.options.n = '%{srcname}-%{version}'\n # adding a new %patch macro\n prep.add_patch_macro(28, p=1, b='.test')\n # removing an existing %patch macro by name\n del prep.patch0\n # this works for both '%patch0' and '%patch -P0'\n prep.remove_patch_macro(0)\n```\n\n### High-level manipulation\n\n#### Version and release\n\n```python\n# getting version and release\nprint(specfile.version)\nprint(specfile.release)\n\n# setting version and release\nspecfile.version = '2.1'\nspecfile.release = '3'\n\n# setting both at the same time (release defaults to 1)\nspecfile.set_version_and_release('2.1', release='3')\n\n# setting version while trying to preserve macros\nspecfile.set_version_and_release('2.1', preserve_macros=True)\n```\n\n#### Bumping release\n\nTo bump release and add a new changelog entry, you could use the following code:\n\n```python\nfrom specfile import Specfile\n\nwith Specfile(\"example.spec\") as spec:\n spec.release = str(int(spec.expanded_release) + 1)\n spec.add_changelog_entry(\"- Bumped release for test purposes\")\n```\n\n#### Changelog\n\n```python\n# adding a new entry, author is automatically determined\n# (using the same heuristics that rpmdev-packager uses) if possible\n# this function already honors autochangelog\nspecfile.add_changelog_entry('- New upstream release 2.1')\n\n# adding a new entry, specifying author and timestamp explicitly\nspecfile.add_changelog_entry(\n '- New upstream release 2.1',\n author='Nikola Forr\u00f3',\n email='nforro@redhat.com',\n timestamp=datetime.date(2021, 11, 20),\n)\n\nif specfile.has_autochangelog:\n # do something\n```\n\n#### Sources and patches\n\n```python\nwith specfile.sources() as sources:\n # expanded location of the first source\n print(sources[0].expanded_location)\n # adding a source\n sources.append('tests.tar.gz')\n\nwith specfile.patches() as patches:\n # modifying location of the first patch\n patches[0].location = 'downstream.patch'\n # removing comments associated with the last patch\n patches[-1].comments.clear()\n # adding and removing patches\n patches.append('another.patch')\n del patches[2]\n # inserting a patch with a specific number\n patches.insert_numbered(999, 'final.patch')\n\n# adding a single patch\nspecfile.add_patch('necessary.patch', comment='a human-friendly comment to the patch')\n```\n\n#### Other attributes\n\n```python\nprint(specfile.name)\nprint(specfile.license)\nprint(specfile.summary)\nspecfile.url = 'https://example.com'\n```\n\nNote that if you want to access multiple tag values, it may be noticeably faster to do it using the `tags` context manager:\n\n```python\n# same as above, but roughly 4x times faster (parsing/saving happens only once)\nwith specfile.tags() as tags:\n print(tags.name.value)\n print(tags.license.value)\n print(tags.summary.value)\n tags.url.value = 'https://example.com'\n```\n\n### Read-only access\n\nIf you don't need write access, you can use the `content` property of context managers and avoid the `with` statement:\n\n```python\n# no changes done to the tags object will be saved\ntags = specfile.tags().content\n\nprint(tags.version.expanded_value)\nprint(tags.release.expanded_value)\n\n# number of sources\nprint(len(specfile.sources().content))\n```\n\n### Validity\n\nMacro definitions, tags, `%sourcelist`/`%patchlist` entries and sources/patches have a `valid` attribute. An entity is considered valid if it isn't present in a false branch of any condition.\n\nConsider the following in a spec file:\n\n```specfile\n%if 0%{?fedora} >= 36\nRecommends: %{name}-selinux\n%endif\n```\n\nProvided there are no other `Recommends` tags, the following would print `True` or `False` depending on the value of the `%fedora` macro:\n\n```python\nwith specfile.tags() as tags:\n print(tags.recommends.valid)\n```\n\nYou can define macros or redefine/undefine system macros using the `macros` argument of the constructor or by modifying the `macros` attribute of a `Specfile` instance.\n\nThe same applies to `%ifarch`/`%ifos` statements:\n\n```specfile\n%ifarch %{java_arches}\nBuildRequires: java-devel\n%endif\n```\n\nProvided there are no other `BuildRequires` tags, the following would print `True` in case the current platform was part of `%java_arches`:\n\n```python\nwith specfile.tags() as tags:\n print(tags.buildrequires.valid)\n```\n\nTo override this, you would have to redefine the `%_target_cpu` system macro (or `%_target_os` in case of `%ifos`).\n\n## Videos\n\nHere is a demo showcasing the `Specfile.update_tag()` method and its use cases:\n\n[![Demo of Specfile.update_tag() functionality](https://img.youtube.com/vi/yzMfBPdFXZY/0.jpg)](https://www.youtube.com/watch?v=yzMfBPdFXZY)\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A library for parsing and manipulating RPM spec files.",
"version": "0.32.6",
"project_urls": {
"Homepage": "https://github.com/packit/specfile"
},
"split_keywords": [
"packaging",
" fedora",
" rpm",
" spec"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "67c5f14b494f46ef72ddd7e0d0ca075300f38bd848576e4f2d989268d3df58b4",
"md5": "e1b92314c32070b325dda0967d586fdb",
"sha256": "4ca38d147882f56c4c611ce825a49dfe5d6936e1d6d80c2cce6e07e16c6956b0"
},
"downloads": -1,
"filename": "specfile-0.32.6-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e1b92314c32070b325dda0967d586fdb",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.6",
"size": 64822,
"upload_time": "2024-11-13T13:13:18",
"upload_time_iso_8601": "2024-11-13T13:13:18.242660Z",
"url": "https://files.pythonhosted.org/packages/67/c5/f14b494f46ef72ddd7e0d0ca075300f38bd848576e4f2d989268d3df58b4/specfile-0.32.6-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "5c59a865da63f73dc27fd759566ee08f23348bb7f1671f100b40650939d32e8f",
"md5": "8abcf5912809aff4cd9a9a219523fb80",
"sha256": "d5e214b38d02b0e38d011a3af5e655b5ea93ee6113570ea0bccb06af314f96d3"
},
"downloads": -1,
"filename": "specfile-0.32.6.tar.gz",
"has_sig": false,
"md5_digest": "8abcf5912809aff4cd9a9a219523fb80",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 107204,
"upload_time": "2024-11-13T13:13:20",
"upload_time_iso_8601": "2024-11-13T13:13:20.301325Z",
"url": "https://files.pythonhosted.org/packages/5c/59/a865da63f73dc27fd759566ee08f23348bb7f1671f100b40650939d32e8f/specfile-0.32.6.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-11-13 13:13:20",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "packit",
"github_project": "specfile",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "specfile"
}