diff --git a/.github/workflows/trigger-packit-dev.yml b/.github/workflows/trigger-packit-dev.yml new file mode 100644 index 0000000..ee49500 --- /dev/null +++ b/.github/workflows/trigger-packit-dev.yml @@ -0,0 +1,17 @@ +name: Rebuild docs on merge + +on: + push: + branches: + - main + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger packit.dev + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.PACKIT_DEV_TOKEN }} + repository: packit/packit.dev + event-type: specfile-docs-updated diff --git a/Makefile b/Makefile index 70825cd..29c79f3 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,6 @@ check-in-container: --env COLOR \ --env COV_REPORT \ $(TEST_IMAGE) make check + +generate-api-docs: + PYTHONPATH=$(CURDIR)/docs/api pydoc-markdown --verbose docs/api/specfile.yml diff --git a/docs/api/processors.py b/docs/api/processors.py new file mode 100644 index 0000000..55af0bb --- /dev/null +++ b/docs/api/processors.py @@ -0,0 +1,28 @@ +import dataclasses +import re +from typing import List, Optional + +import docspec +from pydoc_markdown.interfaces import Processor, Resolver + + +@dataclasses.dataclass +class EscapeBracketsProcessor(Processor): + """ + Processor that escapes curly brackets in Python template placeholders + and RPM macros as they have special meaning in MDX files. + """ + + def process( + self, modules: List[docspec.Module], resolver: Optional[Resolver] + ) -> None: + docspec.visit(modules, self._process) + + def _process(self, obj: docspec.ApiObject) -> None: + if not obj.docstring: + return + obj.docstring.content = re.sub( + r"(%|\$)\{(.+?)\}", + r"\g<1>\{\g<2>\}", + obj.docstring.content, + ) diff --git a/docs/api/specfile.yml b/docs/api/specfile.yml new file mode 100644 index 0000000..c143fba --- /dev/null +++ b/docs/api/specfile.yml @@ -0,0 +1,32 @@ +loaders: + - type: python + modules: + - specfile.specfile + - specfile.changelog + - specfile.conditions + - specfile.context_management + - specfile.exceptions + - specfile.formatter + - specfile.macro_definitions + - specfile.macros + - specfile.options + - specfile.prep + - specfile.sections + - specfile.sourcelist + - specfile.sources + - specfile.spec_parser + - specfile.tags + - specfile.utils + - specfile.value_parser +processors: + - type: filter + - type: google + - type: crossref + - type: processors.EscapeBracketsProcessor +renderer: + type: docusaurus + docs_base_path: docs + relative_output_path: api + markdown: + descriptive_class_title: false + render_typehint_in_data_header: true diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..67faf5e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +# specfile + +specfile is a pure-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. + +## Installation + +The library is packaged for Fedora, EPEL 9 and EPEL 8 and you can simply instal it with dnf: + +```bash +dnf install python3-specfile +``` + +On other systems, you can use pip (just note that it requires RPM Python bindings to be installed): + +```bash +pip install specfile +``` diff --git a/specfile/changelog.py b/specfile/changelog.py index 990374d..6628785 100644 --- a/specfile/changelog.py +++ b/specfile/changelog.py @@ -39,7 +39,9 @@ class ChangelogEntry: """ - Class that represents a changelog entry. + Class that represents a changelog entry. Changelog entry consists of + a header line starting with _*_, followed by timestamp, author and optional + extra text (usually EVR), and one or more content lines. Attributes: header: Header of the entry. @@ -53,15 +55,12 @@ def __init__( following_lines: Optional[List[str]] = None, ) -> None: """ - Constructs a `ChangelogEntry` object. + Initializes a changelog entry object. Args: header: Header of the entry. content: List of lines forming the content of the entry. following_lines: Extra lines that follow the entry. - - Returns: - Constructed instance of `ChangelogEntry` class. """ self.header = header self.content = content.copy() @@ -87,7 +86,7 @@ def __repr__(self) -> str: @property def evr(self) -> Optional[str]: - """EVR (Epoch, Version, Release) of the entry.""" + """EVR (epoch, version, release) of the entry.""" m = re.match( r""" ^.* @@ -170,7 +169,7 @@ def assemble( Args: timestamp: Timestamp of the entry. - Supply `datetime` rather than `date` for extended format. + Supply `datetime` rather than `date` for extended format. author: Author of the entry. content: List of lines forming the content of the entry. evr: EVR (epoch, version, release) of the entry. @@ -178,7 +177,7 @@ def assemble( append_newline: Whether the entry should be followed by an empty line. Returns: - Constructed instance of `ChangelogEntry` class. + New instance of `ChangelogEntry` class. """ weekday = WEEKDAYS[timestamp.weekday()] month = MONTHS[timestamp.month - 1] @@ -200,7 +199,9 @@ def assemble( class Changelog(UserList[ChangelogEntry]): """ - Class that represents a changelog. + Class that represents a changelog. It behaves like a list of changelog entries, + ordered from bottom to top - the top (newest) entry has index _-1_, the bottom + (oldest) one has index _0_. Attributes: data: List of individual entries. @@ -212,15 +213,12 @@ def __init__( predecessor: Optional[List[str]] = None, ) -> None: """ - Constructs a `Changelog` object. + Initializes a changelog object. Args: data: List of individual changelog entries. - predecessor: Lines at the beginning of a section that can't be parsed - into changelog entries. - - Returns: - Constructed instance of `Changelog` class. + predecessor: List of lines at the beginning of a section + that can't be parsed into changelog entries. """ super().__init__() if data is not None: @@ -274,9 +272,9 @@ def filter( Args: since: Optional lower bound. If specified, entries with EVR higher - than or equal to this will be included. + than or equal to this will be included. until: Optional upper bound. If specified, entries with EVR lower - than or equal to this will be included. + than or equal to this will be included. Returns: Filtered changelog. @@ -316,13 +314,13 @@ def parse_evr(s): @classmethod def parse(cls, section: Section) -> "Changelog": """ - Parses a %changelog section. + Parses a `%changelog` section. Args: section: Section to parse. Returns: - Constructed instance of `Changelog` class. + New instance of `Changelog` class. """ def extract_following_lines(content: List[str]) -> List[str]: @@ -378,13 +376,15 @@ def _getent_name() -> str: def guess_packager() -> str: """ - Guess the name and email of a packager to use for changelog entries. - This uses similar logic to rpmdev-packager. + Guesses the name and e-mail of a packager to use for changelog entries. + This function uses logic similar to `rpmdev-packager` utility. + The following places are searched for this value (in this order): - - $RPM_PACKAGER envvar - - %packager macro - - git config - - Unix username + + - `$RPM_PACKAGER` environment variable + - `%packager` macro + - git config + - Unix username Returns: A string to use for the changelog entry author. diff --git a/specfile/conditions.py b/specfile/conditions.py index 1534f08..257ac92 100644 --- a/specfile/conditions.py +++ b/specfile/conditions.py @@ -19,9 +19,9 @@ def resolve_expression( Resolves a RPM expression. Args: - keyword: Condition keyword, e.g. `%if` or `%ifarch`. + keyword: Condition keyword, e.g. _%if_ or _%ifarch_. expression: Expression string or a whitespace-delimited list - of arches/OSes in case keyword is a variant of `%ifarch`/`%ifos`. + of arches/OSes in case keyword is a variant of _%ifarch_/_%ifos_. context: `Specfile` instance that defines the context for macro expansions. Returns: @@ -72,7 +72,7 @@ def process_conditions( Args: lines: List of lines in a spec file. macro_definitions: Parsed macro definitions to be used to prevent parsing conditions - inside their bodies (and most likely failing). + inside their bodies (and most likely failing). context: `Specfile` instance that defines the context for macro expansions. Returns: diff --git a/specfile/context_management.py b/specfile/context_management.py index 1a75b20..0f8a3a4 100644 --- a/specfile/context_management.py +++ b/specfile/context_management.py @@ -16,8 +16,9 @@ @contextlib.contextmanager def capture_stderr() -> Generator[List[bytes], None, None]: """ - Context manager for capturing output to stderr. A stderr output of anything run - in its context will be captured in the target variable of the with statement. + Context manager for capturing output to _stderr_. A _stderr_ output + of anything run in its context will be captured in the target variable + of the __with__ statement. Yields: List of captured lines. @@ -39,7 +40,7 @@ def capture_stderr() -> Generator[List[bytes], None, None]: class GeneratorContextManager(contextlib._GeneratorContextManager): """ - Extended contextlib._GeneratorContextManager that provides content property. + Extended `contextlib._GeneratorContextManager` that provides `content` property. """ def __init__(self, function: Callable) -> None: @@ -56,10 +57,10 @@ def content(self) -> Any: Fully consumes the underlying generator and returns the yielded value. Returns: - Value that would normally be the target variable of an associated with statement. + Value that would normally be the target variable of an associated __with__ statement. Raises: - StopIteration if the underlying generator is already exhausted. + StopIteration: If the underlying generator is already exhausted. """ result = next(self.gen) next(self.gen, None) @@ -70,7 +71,7 @@ class ContextManager: """ Class for decorating generator functions that should act as a context manager. - Just like with contextlib.contextmanager, the generator returned from the decorated function + Just like with `contextlib.contextmanager`, the generator returned from the decorated function must yield exactly one value that will be used as the target variable of the with statement. If the same function with the same arguments is called again from within previously generated context, the generator will be ignored and the target variable will be reused. diff --git a/specfile/exceptions.py b/specfile/exceptions.py index 6dd1565..4915e31 100644 --- a/specfile/exceptions.py +++ b/specfile/exceptions.py @@ -5,11 +5,11 @@ class SpecfileException(Exception): - """Something went wrong during our execution.""" + """Base class for all library exceptions.""" class RPMException(SpecfileException): - """Exception related to RPM.""" + """RPM exception.""" def __init__(self, stderr: List[bytes]) -> None: super().__init__() @@ -24,20 +24,20 @@ def __str__(self) -> str: class MacroRemovalException(SpecfileException): - """Exception related to failed removal of RPM macros.""" + """Impossible to remove a RPM macro.""" class OptionsException(SpecfileException): - """Exception related to processing options.""" + """Unparseable option string.""" class UnterminatedMacroException(SpecfileException): - """Exception related to parsing unterminated macro.""" + """Macro starts but doesn't end.""" class DuplicateSourceException(SpecfileException): - """Exception related to adding a duplicate source.""" + """Source with the same location already exists.""" class SourceNumberException(SpecfileException): - """Exception related to source numbers.""" + """Incorrect numbering of sources.""" diff --git a/specfile/formatter.py b/specfile/formatter.py index 248a6f9..6dd8811 100644 --- a/specfile/formatter.py +++ b/specfile/formatter.py @@ -12,19 +12,19 @@ def format_expression(expression: str, line_length_threshold: int = 80) -> str: """ Formats the specified Python expression. - Only supports a small subset of Python AST that should be sufficient for use in __repr__(). + Only supports a small subset of Python AST that should be sufficient for use in `__repr__()`. Args: expression: Python expression to reformat. line_length_threshold: Threshold for line lengths. It's not a hard limit, - it can be exceeded in some cases. + it can be exceeded in some cases. Returns: Formatted expression. Raises: - SyntaxError if the expression is not parseable. - SpecfileException if there is an unsupported AST node in the expression. + SyntaxError: If the expression is not parseable. + SpecfileException: If there is an unsupported AST node in the expression. """ def fmt(node, indent=0, prefix="", multiline=False): @@ -127,7 +127,7 @@ def find_matching_bracket(value, index): def formatted(function: Callable[..., str]) -> Callable[..., str]: - """Decorator for formatting the output of __repr__().""" + """Decorator for formatting the output of `__repr__()`.""" @functools.wraps(function) def wrapper(*args, **kwargs): diff --git a/specfile/macro_definitions.py b/specfile/macro_definitions.py index 1a0b70a..448c190 100644 --- a/specfile/macro_definitions.py +++ b/specfile/macro_definitions.py @@ -16,12 +16,33 @@ class CommentOutStyle(Enum): + """Style of commenting out a macro definition.""" + DNL = auto() + """Using the _%dnl_ macro.""" + HASH = auto() + """Replacing _%_ in _%global_/_%define_ with _#_.""" + OTHER = auto() + """Prepending the definition with _#_ followed by arbitrary string.""" class MacroDefinition: + """ + Class that represents a macro definition. Macro definition starts with _%global_ + or _%define_ keyword, followed by macro name, optional argument string enclosed + in parentheses and macro body. + + Attributes: + name: Macro name. + body: Macro body. + is_global: Whether the macro is defined using _%global_ rather than _%define_. + commented_out: Whether the definition is commented out. + comment_out_style: Style of commenting out. See `CommentOutStyle`. + valid: Whether the definition is not located in a false branch of a condition. + """ + def __init__( self, name: str, @@ -35,6 +56,24 @@ def __init__( valid: bool = True, preceding_lines: Optional[List[str]] = None, ) -> None: + """ + Initializes a macro definition object. + + Args: + name: Macro name. + body: Macro body. + is_global: Whether the macro is defined using _%global_ rather than _%define_. + commented_out: Whether the definition is commented out. + comment_out_style: Style of commenting out. See `CommentOutStyle`. + whitespace: Tuple of whitespace - (preceding the definition, preceding macro name, + preceding macro body, following the body). + dnl_whitespace: Whitespace between _%dnl_ macro and start of the definition + in case of `CommentOutStyle.DNL`. + comment_prefix: String between _#_ and start of the definition + in case of `CommentOutStyle.OTHER`. + valid: Whether the definition is not located in a false branch of a condition. + preceding_lines: Extra lines that precede the definition. + """ self.name = name self.body = body self.is_global = is_global @@ -129,7 +168,7 @@ def get_raw_data(self) -> List[str]: class MacroDefinitions(UserList[MacroDefinition]): """ - Class that represents all macro definitions. + Class that represents a list of all macro definitions. Attributes: data: List of individual macro definitions. @@ -141,14 +180,11 @@ def __init__( remainder: Optional[List[str]] = None, ) -> None: """ - Constructs a `MacroDefinitions` object. + Initializes a macro definitions object. Args: data: List of individual macro definitions. remainder: Leftover lines that can't be parsed into macro definitions. - - Returns: - Constructed instance of `MacroDefinitions` class. """ super().__init__() if data is not None: @@ -237,7 +273,7 @@ def find(self, name: str, position: Optional[int] = None) -> int: returns the first valid matching macro definiton. If there is no such macro definition, returns the first match, if any. If position is specified and there is a matching macro definition at that position, it is returned, otherwise - ValueError is raised. + `ValueError` is raised. Args: name: Name of the tag to find. @@ -247,7 +283,7 @@ def find(self, name: str, position: Optional[int] = None) -> int: Index of the matching tag. Raises: - ValueError if there is no match. + ValueError: If there is no match. """ first_match = None for i, macro_definition in enumerate(self.data): @@ -274,7 +310,7 @@ def _parse( lines: Lines to parse. Returns: - Constructed instance of `MacroDefinitions` class. + New instance of `MacroDefinitions` class. """ def pop(lines): @@ -389,11 +425,11 @@ def parse( Args: lines: Lines to parse. with_conditions: Whether to process conditions before parsing and populate - the `valid` attribute. + the `valid` attribute. context: `Specfile` instance that defines the context for macro expansions. Returns: - Constructed instance of `MacroDefinitions` class. + New instance of `MacroDefinitions` class. """ result = cls._parse(lines) if not with_conditions: diff --git a/specfile/macros.py b/specfile/macros.py index 7de4977..f3bf952 100644 --- a/specfile/macros.py +++ b/specfile/macros.py @@ -76,10 +76,10 @@ class Macros: @staticmethod def _parse(dump: List[str]) -> List[Macro]: """ - Parses macros in the format of %dump output. + Parses macros in the format of _%dump_ output. Args: - dump: List of lines in the same format as what the %dump macro outputs + dump: List of lines in the same format as what the _%dump_ macro outputs to stderr, including newline characters. Returns: @@ -142,7 +142,7 @@ def expand(expression: str) -> str: Expanded expression. Raises: - RPMException, if expansion error occurs. + RPMException: If expansion error occurs. """ try: with capture_stderr() as stderr: @@ -159,7 +159,7 @@ def remove(cls, macro: str) -> None: macro: Macro name. Raises: - MacroRemovalException, if there were too many unsuccessful + MacroRemovalException: If there were too many unsuccessful retries to remove the macro. """ # Ideally, we would loop until the macro is defined, however in rpm diff --git a/specfile/options.py b/specfile/options.py index 7d09620..d5c33ff 100644 --- a/specfile/options.py +++ b/specfile/options.py @@ -67,13 +67,10 @@ class Positionals(collections.abc.MutableSequence): def __init__(self, options: "Options") -> None: """ - Constructs a `Positionals` object. + Initializes a positionals object. Args: - options: Options instance this object is tied with. - - Returns: - Constructed instance of `Positionals` class. + options: `Options` instance this object is tied with. """ self._options = options @@ -179,7 +176,7 @@ def _get_items(self) -> List[int]: def insert(self, i: int, value: Union[int, str]) -> None: """ - Inserts a new positional argument at a specified index. + Inserts a new positional argument at the specified index. Args: i: Requested index. @@ -214,9 +211,9 @@ class Options(collections.abc.MutableMapping): Attributes: optstring: getopt-like option string containing recognized option characters. - Option characters are ASCII letters, upper or lower-case. - If such a character is followed by a colon, the option - requires an argument. + Option characters are ASCII letters, upper or lower-case. + If such a character is followed by a colon, the option + requires an argument. defaults: Dict specifying default arguments to options. """ @@ -227,18 +224,15 @@ def __init__( defaults: Optional[Dict[str, Union[bool, int, str]]] = None, ) -> None: """ - Constructs a `Options` object. + Initializes an options object. Args: tokens: List of tokens in an option string. optstring: String containing recognized option characters. - Option characters are ASCII letters, upper or lower-case. - If such a character is followed by a colon, the option - requires an argument. + Option characters are ASCII letters, upper or lower-case. + If such a character is followed by a colon, the option + requires an argument. defaults: Dict specifying default arguments to options. - - Returns: - Constructed instance of `Options` class. """ self._tokens = tokens.copy() self.optstring = optstring or "" @@ -259,7 +253,7 @@ def _valid_option(self, name: str) -> bool: name: Name of the option. Returns: - True if the option is recognized, otherwise False. + `True` if the option is recognized, otherwise `False`. """ try: # use parent's __getattribute__() so this method can be called from __getattr__() @@ -276,10 +270,10 @@ def _requires_argument(self, option: str) -> bool: option: Name of the option. Returns: - True if the option requires an argument, otherwise False. + `True` if the option requires an argument, otherwise `False`. Raises: - ValueError if the specified option is not valid. + ValueError: If the specified option is not valid. """ i = self.optstring.index(option) + 1 return i < len(self.optstring) and self.optstring[i] == ":" @@ -294,7 +288,7 @@ def _find_option(self, name: str) -> Tuple[Optional[int], Optional[int]]: Returns: Tuple of indices where the first is the index of a token matching the option and the second is the index of a token matching - its argument, or None if there is no match. + its argument, or `None` if there is no match. """ option = f"-{name}" for i, token in reversed(list(enumerate(self._tokens))): @@ -464,7 +458,7 @@ def tokenize(option_string: str) -> List[Token]: """ Tokenizes an option string. - Follows the same rules as poptParseArgvString() that is used by RPM. + Follows the same rules as `poptParseArgvString()` that is used by RPM. Args: option_string: Option string. @@ -473,7 +467,7 @@ def tokenize(option_string: str) -> List[Token]: List of tokens. Raises: - OptionsException if the option string is untokenizable. + OptionsException: If the option string is untokenizable. """ result: List[Token] = [] token = "" diff --git a/specfile/prep.py b/specfile/prep.py index 2299aee..92609d0 100644 --- a/specfile/prep.py +++ b/specfile/prep.py @@ -20,7 +20,7 @@ def valid_prep_macro(name: str) -> bool: class PrepMacro(ABC): """ - Class that represents a %prep macro. + Class that represents a _%prep_ macro. Attributes: name: Literal name of the macro. @@ -41,7 +41,7 @@ def __init__( preceding_lines: Optional[List[str]] = None, ) -> None: """ - Constructs a `PrepMacro` object. + Initializes a prep macro object. Args: name: Literal name of the macro. @@ -50,9 +50,6 @@ def __init__( prefix: Characters preceding the macro on a line. suffix: Characters following the macro on a line. preceding_lines: Lines of the %prep section preceding the macro. - - Returns: - Constructed instance of `PrepMacro` class. """ self.name = name self.options = copy.deepcopy(options) @@ -96,7 +93,7 @@ def get_raw_data(self) -> List[str]: class SetupMacro(PrepMacro): - """Class that represents a %setup macro.""" + """Class that represents a _%setup_ macro.""" CANONICAL_NAME: str = "%setup" OPTSTRING: str = "a:b:cDn:Tq" @@ -106,7 +103,7 @@ class SetupMacro(PrepMacro): class PatchMacro(PrepMacro): - """Class that represents a %patch macro.""" + """Class that represents a _%patch_ macro.""" CANONICAL_NAME: str = "%patch" OPTSTRING: str = "P:p:REb:z:F:d:o:Z" @@ -131,7 +128,7 @@ def number(self, value: int) -> None: class AutosetupMacro(PrepMacro): - """Class that represents an %autosetup macro.""" + """Class that represents an _%autosetup_ macro.""" CANONICAL_NAME: str = "%autosetup" OPTSTRING: str = "a:b:cDn:TvNS:p:" @@ -142,7 +139,7 @@ class AutosetupMacro(PrepMacro): class AutopatchMacro(PrepMacro): - """Class that represents an %autopatch macro.""" + """Class that represents an _%autopatch_ macro.""" CANONICAL_NAME: str = "%autopatch" OPTSTRING: str = "vp:m:M:" @@ -151,10 +148,10 @@ class AutopatchMacro(PrepMacro): class PrepMacros(UserList[PrepMacro]): """ - Class that represents a list of %prep macros. + Class that represents a list of _%prep_ macros. Attributes: - data: List of individual %prep macros. + data: List of individual _%prep_ macros. """ def __init__( @@ -163,14 +160,11 @@ def __init__( remainder: Optional[List[str]] = None, ) -> None: """ - Constructs a `PrepMacros` object. + Initializes a prep macros object. Args: - data: List of individual %prep macros. + data: List of individual _%prep_ macros. remainder: Leftover lines in the section. - - Returns: - Constructed instance of `PrepMacros` class. """ super().__init__() if data is not None: @@ -255,10 +249,10 @@ def get_raw_data(self) -> List[str]: class Prep(collections.abc.Container): """ - Class that represents a %prep section. + Class that represents a _%prep_ section. Attributes: - macros: List of individual %prep macros. + macros: List of individual _%prep_ macros. """ def __init__(self, macros: PrepMacros) -> None: @@ -288,20 +282,20 @@ def __delattr__(self, name: str) -> None: def add_patch_macro(self, number: int, **kwargs: Any) -> None: """ - Adds a new %patch macro with given number and options. + Adds a new _%patch_ macro with given number and options. Args: number: Macro number. - P: The -P option (patch number). - p: The -p option (strip number). - R: The -R option (reverse). - E: The -E option (remove empty files). - b: The -b option (backup). - z: The -z option (suffix). - F: The -F option (fuzz factor). - d: The -d option (working directory). - o: The -o option (output file). - Z: The -Z option (set UTC times). + P: The _-P_ option (patch number). + p: The _-p_ option (strip number). + R: The _-R_ option (reverse). + E: The _-E_ option (remove empty files). + b: The _-b_ option (backup). + z: The _-z_ option (suffix). + F: The _-F_ option (fuzz factor). + d: The _-d_ option (working directory). + o: The _-o_ option (output file). + Z: The _-Z_ option (set UTC times). """ options = Options([], PatchMacro.OPTSTRING, PatchMacro.DEFAULTS) for k, v in kwargs.items(): @@ -322,7 +316,7 @@ def add_patch_macro(self, number: int, **kwargs: Any) -> None: def remove_patch_macro(self, number: int) -> None: """ - Removes a %patch macro with given number. + Removes a _%patch_ macro with given number. Args: number: Macro number. @@ -344,10 +338,10 @@ def parse(cls, section: Section) -> "Prep": Parses a section into a `Prep` object. Args: - section: %prep section. + section: _%prep_ section. Returns: - Constructed instance of `Prep` class. + New instance of `Prep` class. """ macro_regex = re.compile( r"(?P%(setup|patch\d*|autopatch|autosetup))(?P\s*)(?P.*?)$" diff --git a/specfile/sections.py b/specfile/sections.py index ab40c21..1057e88 100644 --- a/specfile/sections.py +++ b/specfile/sections.py @@ -35,7 +35,7 @@ class Section(collections.UserList): name: Name of the section (without the leading '%'). options: Options of the section. data: List of lines forming the content of the section, - not including newline characters. + not including newline characters. """ def __init__( @@ -47,19 +47,16 @@ def __init__( data: Optional[List[str]] = None, ) -> None: """ - Constructs a `Section` object. + Initializes a section object. Args: name: Name of the section (without the leading '%'). options: Options of the section. delimiter: Delimiter separating name and option string. separator: String separating name and options from section content, - defaults to newline. + defaults to newline. data: List of lines forming the content of the section, - not including newline characters. - - Returns: - Constructed instance of `Section` class. + not including newline characters. """ super().__init__() if name.lower() not in SECTION_NAMES: @@ -229,7 +226,7 @@ def parse( context: `Specfile` instance that defines the context for macro expansions. Returns: - Constructed instance of `Sections` class. + New instance of `Sections` class. """ def expand(s): diff --git a/specfile/sourcelist.py b/specfile/sourcelist.py index 254dbbf..7eea32a 100644 --- a/specfile/sourcelist.py +++ b/specfile/sourcelist.py @@ -19,7 +19,7 @@ class SourcelistEntry: """ - Class that represents a spec file source/patch in a %sourcelist/%patchlist. + Class that represents a spec file source/patch in a _%sourcelist_/_%patchlist_. Attributes: location: Literal location of the source/patch as stored in the spec file. @@ -35,16 +35,13 @@ def __init__( context: Optional["Specfile"] = None, ) -> None: """ - Constructs a `SourceListEntry` object. + Initializes a sourcelist entry object. Args: location: Literal location of the source/patch as stored in the spec file. comments: List of comments associated with the source/patch. valid: Whether the entry is not located in a false branch of a condition. context: `Specfile` instance that defines the context for macro expansions. - - Returns: - Constructed instance of `SourceListEntry` class. """ self.location = location self.comments = comments.copy() @@ -83,7 +80,7 @@ def expanded_location(self) -> str: class Sourcelist(UserList[SourcelistEntry]): """ - Class that represents entries in a %sourcelist/%patchlist section. + Class that represents entries in a _%sourcelist_/_%patchlist_ section. Attributes: data: List of individual sources/patches. @@ -95,14 +92,11 @@ def __init__( remainder: Optional[List[str]] = None, ) -> None: """ - Constructs a `Sourcelist` object. + Initializes a sourcelist object. Args: data: List of individual sources/patches. remainder: Leftover lines in a section that can't be parsed into sources/patches. - - Returns: - Constructed instance of `Sourcelist` class. """ super().__init__() if data is not None: @@ -138,11 +132,11 @@ def parse( Parses a section into sources/patches. Args: - section: %sourcelist/%patchlist section. + section: _%sourcelist_/_%patchlist_ section. context: `Specfile` instance that defines the context for macro expansions. Returns: - Constructed instance of `Sourcelist` class. + New instance of `Sourcelist` class. """ macro_definitions = MacroDefinitions.parse(list(section)) lines = process_conditions(list(section), macro_definitions, context) diff --git a/specfile/sources.py b/specfile/sources.py index 11b5091..0bca3e3 100644 --- a/specfile/sources.py +++ b/specfile/sources.py @@ -78,14 +78,11 @@ class TagSource(Source): def __init__(self, tag: Tag, number: Optional[int] = None) -> None: """ - Constructs a `TagSource` object. + Initializes a tag source object. Args: tag: Tag that this source represents. number: Source number (in the case of implicit numbering). - - Returns: - Constructed instance of `TagSource` class. """ self._tag = tag self._number = number @@ -106,7 +103,7 @@ def _extract_number(self) -> Optional[str]: Extracts source number from tag name. Returns: - Extracted number or None if there isn't one. + Extracted number or `None` if there isn't one. """ tokens = re.split(r"(\d+)", self._tag.name, maxsplit=1) if len(tokens) > 1: @@ -174,18 +171,15 @@ def valid(self) -> bool: class ListSource(Source): - """Class that represents a source backed by a line in a %sourcelist section.""" + """Class that represents a source backed by a line in a _%sourcelist_ section.""" def __init__(self, source: SourcelistEntry, number: int) -> None: """ - Constructs a `ListSource` object. + Initializes a list source object. Args: - source: Sourcelist entry that this source represents. + source: `Sourcelist` entry that this source represents. number: Source number. - - Returns: - Constructed instance of `ListSource` class. """ self._source = source self._number = number @@ -258,7 +252,7 @@ def __init__( context: Optional["Specfile"] = None, ) -> None: """ - Constructs a `Sources` object. + Initializes a sources object. Args: tags: All spec file tags. @@ -267,9 +261,6 @@ def __init__( default_to_implicit_numbering: Use implicit numbering (no source numbers) by default. default_source_number_digits: Default number of digits in a source number. context: `Specfile` instance that defines the context for macro expansions. - - Returns: - Constructed instance of `Sources` class. """ self._tags = tags self._sourcelists = sourcelists @@ -408,7 +399,7 @@ def _detect_implicit_numbering(self) -> bool: tags don't have numbers. Returns: - True if implicit numbering is being/should be used, False otherwise. + `True` if implicit numbering is being/should be used, `False` otherwise. """ tags = self._get_tags() if any(t._number is None for t, _, _ in tags): @@ -428,7 +419,7 @@ def _get_tag_format( a reference tag and the requested source number. The new name has the same number of digits as the reference - (unless number_digits_override is set to a different value) + (unless `number_digits_override` is set to a different value) and the length of the separator is adjusted accordingly. Args: @@ -477,7 +468,7 @@ def _get_initial_tag_setup(self, number: int = 0) -> Tuple[int, str, str]: def _get_tag_validity(self, reference: Optional[TagSource] = None) -> bool: """ Determines validity of a new source tag based on a reference tag, if specified, - or the last tag in the spec file. Defaults to True. + or the last tag in the spec file. Defaults to `True`. Args: reference: Optional reference tag source. @@ -518,8 +509,8 @@ def insert(self, i: int, location: str) -> None: location: Location of the new source. Raises: - DuplicateSourceException if duplicates are disallowed and there - already is a source with the same location. + DuplicateSourceException: If duplicates are disallowed and there + already is a source with the same location. """ if not self._allow_duplicates and location in self: raise DuplicateSourceException(f"{self.prefix} '{location}' already exists") @@ -585,8 +576,8 @@ def insert_numbered(self, number: int, location: str) -> int: Index of the newly inserted source. Raises: - DuplicateSourceException if duplicates are disallowed and there - already is a source with the same location. + DuplicateSourceException: If duplicates are disallowed and there + already is a source with the same location. """ if not self._allow_duplicates and location in self: raise DuplicateSourceException(f"{self.prefix} '{location}' already exists") @@ -663,7 +654,7 @@ class TagPatch(TagSource, Patch): class ListPatch(ListSource, Patch): - """Class that represents a patch backed by a line in a %patchlist section.""" + """Class that represents a patch backed by a line in a _%patchlist_ section.""" class Patches(Sources): diff --git a/specfile/spec_parser.py b/specfile/spec_parser.py index bcbce64..d169194 100644 --- a/specfile/spec_parser.py +++ b/specfile/spec_parser.py @@ -34,13 +34,13 @@ class SpecParser: sourcedir: Path to sources and patches. macros: List of extra macro definitions. force_parse: Whether to attempt to parse the spec file even if one or more - sources required to be present at parsing time are not available. - Such sources include sources referenced from shell expansions - in tag values and sources included using the %include directive. + sources required to be present at parsing time are not available. + Such sources include sources referenced from shell expansions + in tag values and sources included using the _%include_ directive. spec: `rpm.spec` instance representing parsed spec file. tainted: Indication that parsing of the spec file was forced and one or more - sources required to be present at parsing time were not available - and were replaced with dummy files. + sources required to be present at parsing time were not available + and were replaced with dummy files. """ # hash of input parameters to the last parse performed @@ -167,7 +167,7 @@ def _sanitize_environment(self) -> Generator[os._Environ, None, None]: """ Context manager for sanitizing the environment for shell expansions. - Temporarily sets LANG and LC_ALL to C.UTF-8 locale. + Temporarily sets _LANG_ and _LC_ALL_ to _C.UTF-8_ locale. Yields: Sanitized environment. @@ -206,7 +206,7 @@ def _do_parse( at least one file to be included was ignored). Raises: - RPMException, if parsing error occurs. + RPMException: If parsing error occurs. """ def get_rpm_spec(content, flags): @@ -355,7 +355,7 @@ def parse( extra_macros: List of extra macro definitions. Raises: - RPMException, if parsing error occurs. + RPMException: If parsing error occurs. """ # calculate hash of all input parameters payload = ( diff --git a/specfile/specfile.py b/specfile/specfile.py index f93ceb0..70c8b4d 100644 --- a/specfile/specfile.py +++ b/specfile/specfile.py @@ -51,7 +51,7 @@ def __init__( force_parse: bool = False, ) -> None: """ - Constructs a `Specfile` object. + Initializes a specfile object. Args: path: Path to the spec file. @@ -59,12 +59,9 @@ def __init__( autosave: Whether to automatically save any changes made. macros: List of extra macro definitions. force_parse: Whether to attempt to parse the spec file even if one or more - sources required to be present at parsing time are not available. - Such sources include sources referenced from shell expansions - in tag values and sources included using the %include directive. - - Returns: - Constructed instance of `Specfile` class. + sources required to be present at parsing time are not available. + Such sources include sources referenced from shell expansions + in tag values and sources included using the _%include_ directive. """ self.autosave = autosave self._path = Path(path) @@ -161,11 +158,11 @@ def rpm_spec(self) -> rpm.spec: return self._parser.spec def reload(self) -> None: - """Reload the spec file content.""" + """Reloads the spec file content.""" self._lines = self._read_lines(self.path) def save(self) -> None: - """Save the spec file content.""" + """Saves the spec file content.""" self.path.write_text(str(self), encoding="utf8", errors="surrogateescape") def expand( @@ -181,8 +178,8 @@ def expand( expression: Expression to expand. extra_macros: Extra macros to be defined before expansion is performed. skip_parsing: Do not parse the spec file before expansion is performed. - Defaults to False. Mutually exclusive with extra_macros. Set this to True - only if you are certain that the global macro context is up-to-date. + Defaults to `False`. Mutually exclusive with `extra_macros`. Set this to `True` + only if you are certain that the global macro context is up-to-date. Returns: Expanded expression. @@ -264,7 +261,7 @@ def tags( Args: section: Name of the requested section or an existing `Section` instance. - Defaults to preamble. + Defaults to preamble. Yields: Tags in the section as `Tags` object. @@ -287,10 +284,10 @@ def changelog( Args: section: Optional `Section` instance to be processed. If not set, the first - %changelog section (if any) will be processed. + _%changelog_ section (if any) will be processed. Yields: - Spec file changelog as `Changelog` object or None if there is no %changelog section. + Spec file changelog as `Changelog` object or `None` if there is no _%changelog_ section. """ with self.sections() as sections: if section is None: @@ -310,10 +307,10 @@ def changelog( @ContextManager def prep(self) -> Generator[Optional[Prep], None, None]: """ - Context manager for accessing %prep section. + Context manager for accessing _%prep_ section. Yields: - Spec file %prep section as `Prep` object. + Spec file _%prep_ section as `Prep` object. """ with self.sections() as sections: try: @@ -407,7 +404,7 @@ def patches( @property def has_autorelease(self) -> bool: - """Whether the spec file uses %autorelease.""" + """Whether the spec file uses _%autorelease_.""" for node in ValueParser.flatten(ValueParser.parse(self.raw_release)): if ( isinstance(node, (MacroSubstitution, EnclosedMacroSubstitution)) @@ -419,13 +416,13 @@ def has_autorelease(self) -> bool: @staticmethod def contains_autochangelog(section: Section) -> bool: """ - Determines if the specified section contains the %autochangelog macro. + Determines if the specified section contains the _%autochangelog_ macro. Args: section: Section to examine. Returns: - True if the section contains %autochangelog, False otherwise. + `True` if the section contains _%autochangelog_, `False` otherwise. """ for line in section: if line.lstrip().startswith("#"): @@ -441,7 +438,7 @@ def contains_autochangelog(section: Section) -> bool: @property def has_autochangelog(self) -> bool: - """Whether the spec file uses %autochangelog.""" + """Whether the spec file uses _%autochangelog_.""" with self.sections() as sections: # there could be multiple changelog sections, consider all of them for section in sections: @@ -460,8 +457,8 @@ def add_changelog_entry( evr: Optional[str] = None, ) -> None: """ - Adds a new %changelog entry. Does nothing if there is no %changelog section - or if %autochangelog is being used. + Adds a new _%changelog_ entry. Does nothing if there is no _%changelog_ section + or if _%autochangelog_ is being used. If not specified, author and e-mail will be automatically determined, if possible. Timestamp, if not set, will be set to current time (in local timezone). @@ -471,11 +468,11 @@ def add_changelog_entry( author: Author of the entry. email: E-mail of the author. timestamp: Timestamp of the entry. - Supply `datetime` rather than `date` for extended format. + Supply `datetime` rather than `date` for extended format. evr: Override the EVR part of the changelog entry. - Macros will be expanded automatically. By default, the function - determines the appropriate value based on the specfile's current - %{epoch}, %{version}, and %{release} values. + Macros will be expanded automatically. By default, the function + determines the appropriate value based on the spec file current + _%{epoch}_, _%{version}_, and _%{release}_ values. """ with self.sections() as sections: # there could be multiple changelog sections, update all of them @@ -649,7 +646,7 @@ def set_version_and_release(self, version: str, release: str = "1") -> None: Args: version: Version string. - release: Release string, defaults to '1'. + release: Release string, defaults to "1". """ with self.tags() as tags: tags.version.value = version @@ -669,14 +666,14 @@ def add_patch( Args: location: Patch location (filename or URL). number: Patch number. It will be auto-assigned if not specified. - If specified, it must be higher than any existing patch number. + If specified, it must be higher than any existing patch number. comment: Associated comment. initial_number: Auto-assigned number to start with if there are no patches. number_digits: Number of digits in the patch number. Raises: - SourceNumberException when the specified patch number is not higher - than any existing patch number. + SourceNumberException: If the specified patch number is not higher + than any existing patch number. """ with self.patches(default_source_number_digits=number_digits) as patches: highest_number = max((p.number for p in patches), default=-1) @@ -708,7 +705,7 @@ def update_value( requested_value: Requested new value. position: Position (line number) of the value in the spec file. protected_entities: Regular expression specifying protected tags and macro definitions, - ensuring their values won't be updated. + ensuring their values won't be updated. Returns: Updated value. Can be equal to the original value. @@ -858,7 +855,7 @@ def update_tag( name: Tag name. value: Requested new value. protected_entities: Regular expression specifying protected tags and macro definitions, - ensuring their values won't be updated. + ensuring their values won't be updated. """ with self.tags() as tags: tag = getattr(tags, name) @@ -893,17 +890,17 @@ def update_version( Args: version: Version string. prerelease_suffix_pattern: Regular expression specifying recognized - pre-release suffixes. The first capturing group must capture the delimiter - between base version and pre-release suffix and can be empty in case - there is no delimiter. + pre-release suffixes. The first capturing group must capture the delimiter + between base version and pre-release suffix and can be empty in case + there is no delimiter. prerelease_suffix_macro: Macro definition that controls whether spec file - version is a pre-release and contains the pre-release suffix. - To be commented out or uncommented accordingly. - comment_out_style: Whether to use `%dnl` macro or swap the leading '%' - with '#' to comment out `prerelease_suffix_macro`. Defaults to `%dnl`. + version is a pre-release and contains the pre-release suffix. + To be commented out or uncommented accordingly. + comment_out_style: Style of commenting out `prerelease_suffix_macro`. + See `CommentOutStyle`. Defaults to `CommentOutStyle.DNL`. Raises: - SpecfileException if `prerelease_suffix_pattern` is invalid. + SpecfileException: If `prerelease_suffix_pattern` is invalid. """ def update_macro(prerelease_detected): diff --git a/specfile/tags.py b/specfile/tags.py index d0d5aac..bf892cd 100644 --- a/specfile/tags.py +++ b/specfile/tags.py @@ -80,14 +80,11 @@ def __init__( preceding_lines: Optional[List[str]] = None, ) -> None: """ - Constructs a `Comments` object. + Initializes a comments object. Args: data: List of individual comments. preceding_lines: Extra lines that precede comments associated with a tag. - - Returns: - Constructed instance of `Comments` class. """ super().__init__() if data is not None: @@ -180,7 +177,7 @@ def parse(cls, lines: List[str]) -> "Comments": lines: List of lines that precede a tag definition. Returns: - Constructed instance of `Comments` class. + New instance of `Comments` class. """ comment_regex = re.compile(r"^(\s*#\s*)(.*)$") comments: List[Comment] = [] @@ -219,23 +216,18 @@ def __init__( context: Optional["Specfile"] = None, ) -> None: """ - Constructs a `Tag` object. + Initializes a tag object. Args: name: Name of the tag. value: Literal value of the tag as stored in the spec file. - expanded_value: Value of the tag after expansion by RPM. - separator: - Separator between name and literal value (colon usually surrounded by some - amount of whitespace). + separator: Separator between name and literal value (colon usually surrounded by some + amount of whitespace). comments: List of comments associated with the tag. valid: Whether the tag is not located in a false branch of a condition. prefix: Characters preceding the tag on a line. suffix: Characters following the tag on a line. context: `Specfile` instance that defines the context for macro expansions. - - Returns: - Constructed instance of `Tag` class. """ name_regexes = [ re.compile(get_tag_name_regex(t), re.IGNORECASE) for t in TAG_NAMES @@ -335,14 +327,11 @@ def __init__( self, data: Optional[List[Tag]] = None, remainder: Optional[List[str]] = None ) -> None: """ - Constructs a `Tags` object. + Initializes a tags object. Args: data: List of individual tags. remainder: Leftover lines in a section that can't be parsed into tags. - - Returns: - Constructed instance of `Tags` class. """ super().__init__() if data is not None: @@ -445,7 +434,7 @@ def find(self, name: str, position: Optional[int] = None) -> int: Finds a tag with the specified name. If position is not specified, returns the first valid matching tag. If there is no such tag, returns the first match, if any. If position is specified and there is a matching - tag at that position, it is returned, otherwise ValueError is raised. + tag at that position, it is returned, otherwise `ValueError` is raised. Args: name: Name of the tag to find. @@ -455,7 +444,7 @@ def find(self, name: str, position: Optional[int] = None) -> int: Index of the matching tag. Raises: - ValueError if there is no match. + ValueError: If there is no match. """ first_match = None for i, tag in enumerate(self.data): @@ -497,7 +486,7 @@ def parse(cls, section: Section, context: Optional["Specfile"] = None) -> "Tags" context: `Specfile` instance that defines the context for macro expansions. Returns: - Constructed instance of `Tags` class. + New instance of `Tags` class. """ def regex_pattern(tag): diff --git a/specfile/value_parser.py b/specfile/value_parser.py index 40e13c3..d5241be 100644 --- a/specfile/value_parser.py +++ b/specfile/value_parser.py @@ -43,7 +43,7 @@ def __eq__(self, other: object) -> bool: class ShellExpansion(Node): - """Node representing shell expansion, e.g. %(whoami).""" + """Node representing shell expansion, e.g. _%(whoami)_.""" def __init__(self, body: str) -> None: self.body = body @@ -64,14 +64,14 @@ def __eq__(self, other: object) -> bool: class ExpressionExpansion(ShellExpansion): - """Node representing expression expansion, e.g. %[1+1].""" + """Node representing expression expansion, e.g. _%[1+1]_.""" def __str__(self) -> str: return f"%[{self.body}]" class MacroSubstitution(Node): - """Node representing macro substitution, e.g. %version.""" + """Node representing macro substitution, e.g. _%version_.""" def __init__(self, body: str) -> None: tokens = re.split(r"([?!]*)", body, maxsplit=1) @@ -94,7 +94,7 @@ def __eq__(self, other: object) -> bool: class EnclosedMacroSubstitution(Node): - """Node representing macro substitution enclosed in brackets, e.g. %{?dist}.""" + """Node representing macro substitution enclosed in brackets, e.g. _%{?dist}_.""" def __init__(self, body: str) -> None: tokens = re.split(r"([?!]*)", body, maxsplit=1) @@ -126,7 +126,7 @@ def __eq__(self, other: object) -> bool: class ConditionalMacroExpansion(Node): - """Node representing conditional macro expansion, e.g. %{?prerel:0.}.""" + """Node representing conditional macro expansion, e.g. _%{?prerel:0.}_.""" def __init__(self, condition: str, body: List[Node]) -> None: tokens = re.split(r"([?!]*)", condition, maxsplit=1) @@ -157,7 +157,7 @@ def __eq__(self, other: object) -> bool: class BuiltinMacro(Node): - """Node representing built-in macro, e.g. %{quote:Ancient Greek}.""" + """Node representing built-in macro, e.g. _%{quote:Ancient Greek}_.""" def __init__(self, name: str, body: str) -> None: self.name = name @@ -200,7 +200,7 @@ def parse(cls, value: str) -> List[Node]: """ Parses a value into a list of nodes. - Follows the parsing logic of expandMacro() from rpm/rpmio/macro.c in RPM source. + Follows the parsing logic of `expandMacro()` from _rpm/rpmio/macro.c_ in RPM source. Args: value: Value string to parse. @@ -209,7 +209,7 @@ def parse(cls, value: str) -> List[Node]: Parsed value as a list of nodes. Raises: - UnterminatedMacroException if there is a macro that doesn't end. + UnterminatedMacroException: If there is a macro that doesn't end. """ pairs = {"(": ")", "{": "}", "[": "]"} @@ -317,9 +317,9 @@ def construct_regex( Args: value: Value string to parse. modifiable_entities: Names of modifiable entities, i.e. local macro definitions - and tags. + and tags. flippable_entities: Names of entities that can be enabled/disabled, - i.e. macro definitions. Must be a subset of modifiable_entities. + i.e. macro definitions. Must be a subset of modifiable_entities. context: `Specfile` instance that defines the context for macro expansions. Returns: