diff --git a/fmf/__init__.py b/fmf/__init__.py index 12af8b8..fc36bac 100644 --- a/fmf/__init__.py +++ b/fmf/__init__.py @@ -1,4 +1,6 @@ -""" Flexible Metadata Format """ +""" +Flexible Metadata Format +""" from __future__ import annotations diff --git a/fmf/base.py b/fmf/base.py index 766570d..31959bb 100644 --- a/fmf/base.py +++ b/fmf/base.py @@ -1,4 +1,6 @@ -""" Base Metadata Classes """ +""" +Base Metadata Classes +""" import copy import os @@ -67,7 +69,9 @@ def __call__( class Tree: - """ Metadata Tree """ + """ + Metadata Tree + """ def __init__(self, data, name=None, parent=None): """ @@ -160,11 +164,16 @@ def commit(self): return self._commit def __str__(self): - """ Use tree name as identifier """ + """ + Use tree name as identifier + """ + return self.name def _initialize(self, path): - """ Find metadata tree root, detect format version, check for config """ + """ + Find metadata tree root, detect format version, check for config + """ # Find the tree root root = os.path.abspath(path) @@ -202,7 +211,9 @@ def _initialize(self, path): raise utils.FileError(f"Failed to parse '{config_file_path}'.\n{error}") def _merge_plus(self, data, key, value, prepend=False): - """ Handle extending attributes using the '+' suffix """ + """ + Handle extending attributes using the '+' suffix + """ # Set the value if key is not present if key not in data: @@ -252,7 +263,10 @@ def _merge_plus(self, data, key, value, prepend=False): key, self.name, str(error))) def _merge_regexp(self, data, key, value): - """ Handle substitution of current values """ + """ + Handle substitution of current values + """ + # Nothing to substitute if the key is not present in parent if key not in data: return @@ -274,7 +288,10 @@ def _merge_regexp(self, data, key, value): key, self.name)) def _merge_minus_regexp(self, data, key, value): - """ Handle removing current values if they match regexp """ + """ + Handle removing current values if they match regexp + """ + # A bit faster but essentially `any` def lazy_any_search(item, patterns): for p in patterns: @@ -301,7 +318,10 @@ def lazy_any_search(item, patterns): key, self.name)) def _merge_minus(self, data, key, value): - """ Handle reducing attributes using the '-' suffix """ + """ + Handle reducing attributes using the '-' suffix + """ + # Cannot reduce attribute if key is not present in parent if key not in data: return @@ -324,7 +344,10 @@ def _merge_minus(self, data, key, value): key, self.name)) def _merge_special(self, data, source): - """ Merge source dict into data, handle special suffixes """ + """ + Merge source dict into data, handle special suffixes + """ + for key, value in source.items(): # Handle special attribute merging if key.endswith('+'): @@ -342,10 +365,15 @@ def _merge_special(self, data, source): data[key] = value def _process_directives(self, directives): - """ Check and process special fmf directives """ + """ + Check and process special fmf directives + """ def check(value, type_, name=None): - """ Check for correct type """ + """ + Check for correct type + """ + if not isinstance(value, type_): name = f" '{name}'" if name else "" raise fmf.utils.FormatError( @@ -372,7 +400,10 @@ def check(value, type_, name=None): @staticmethod def init(path): - """ Create metadata tree root under given path """ + """ + Create metadata tree root under given path + """ + root = os.path.abspath(os.path.join(path, ".fmf")) if os.path.exists(root): raise utils.FileError("{0} '{1}' already exists.".format( @@ -387,7 +418,10 @@ def init(path): return root def merge(self, parent=None): - """ Merge parent data """ + """ + Merge parent data + """ + # Check parent if parent is None: parent = self.parent @@ -401,7 +435,10 @@ def merge(self, parent=None): self.data = data def inherit(self): - """ Apply inheritance """ + """ + Apply inheritance + """ + # Preserve original data and merge parent # (original data needed for custom inheritance extensions) self.original_data = self.data @@ -413,7 +450,10 @@ def inherit(self): child.inherit() def update(self, data): - """ Update metadata, handle virtual hierarchy """ + """ + Update metadata, handle virtual hierarchy + """ + # Make a note that the data dictionary has been updated # None is handled in the same way as an empty dictionary self._updated = True @@ -605,6 +645,7 @@ def get(self, name=None, default=None): default value when any of the dictionary keys does not exist. """ + # Return the whole dictionary if no attribute specified if name is None: return self.data @@ -619,7 +660,10 @@ def get(self, name=None, default=None): return data def child(self, name, data, source=None): - """ Create or update child with given data """ + """ + Create or update child with given data + """ + try: # Update data from a dictionary (handle empty nodes) if isinstance(data, dict) or data is None: @@ -636,7 +680,10 @@ def child(self, name, data, source=None): @property def explore_include(self): - """ Additional filenames to be explored """ + """ + Additional filenames to be explored + """ + try: explore_include = self.config["explore"]["include"] if not isinstance(explore_include, list): @@ -657,6 +704,7 @@ def grow(self, path): from the same path multiple times with attribute adding using the "+" sign leads to adding the value more than once! """ + if path != '/': path = path.rstrip("/") if path in IGNORED_DIRECTORIES: # pragma: no cover @@ -764,14 +812,20 @@ def climb(self, whole: bool = False, sort: bool = True): @property def select(self): - """ Respect directive, otherwise by being leaf/branch node""" + """ + Respect directive, otherwise by being leaf/branch node + """ + try: return self._directives["select"] except KeyError: return not self.children def find(self, name): - """ Find node with given name """ + """ + Find node with given name + """ + for node in self.climb(whole=True): if node.name == name: return node @@ -807,6 +861,7 @@ def prune( default. Set to ``False`` if you prefer to keep the order in which the child nodes were inserted into the tree. """ + keys = keys or [] names = names or [] filters = filters or [] @@ -842,7 +897,10 @@ def prune( yield node def show(self, brief=False, formatting=None, values=None): - """ Show metadata """ + """ + Show metadata + """ + values = values or [] # Custom formatting @@ -919,6 +977,7 @@ def copy(self): node and the rest of the tree attached to it is not copied in order to save memory. """ + original_parent = self.parent self.parent = None duplicate = copy.deepcopy(self) @@ -939,6 +998,7 @@ def validate(self, schema, schema_store=None): Raises utils.JsonSchemaError if the supplied schema was invalid. """ + return utils.validate_data(self.data, schema, schema_store=schema_store) def _locate_raw_data(self): @@ -954,8 +1014,8 @@ def _locate_raw_data(self): node_data ... dictionary containing raw data for the current node full_data ... full raw data from the closest parent node source ... file system path where the full raw data are stored - """ + # List of node names in the virtual hierarchy hierarchy = list() @@ -1011,10 +1071,14 @@ def __enter__(self): export to yaml does not preserve this information. The feature is experimental and can be later modified, use at your own risk. """ + return self._locate_raw_data()[0] def __exit__(self, exc_type, exc_val, exc_tb): - """ Experimental: Store modified metadata to disk """ + """ + Experimental: Store modified metadata to disk + """ + _, full_data, source = self._locate_raw_data() with open(source, "w", encoding='utf-8') as file: file.write(dict_to_yaml(full_data)) @@ -1026,6 +1090,7 @@ def __getitem__(self, key): To get a child the key has to start with a '/'. as identification of child item string """ + if key.startswith("/"): return self.children[key[1:]] else: diff --git a/fmf/cli.py b/fmf/cli.py index 6b28f2d..5e1acf4 100644 --- a/fmf/cli.py +++ b/fmf/cli.py @@ -31,10 +31,15 @@ class Parser: - """ Command line options parser """ + """ + Command line options parser + """ def __init__(self, arguments=None, path=None): - """ Prepare the parser. """ + """ + Prepare the parser. + """ + # Change current working directory (used for testing) if path is not None: os.chdir(path) @@ -70,7 +75,10 @@ def __init__(self, arguments=None, path=None): getattr(self, "command_" + self.command)() def options_select(self): - """ Select by name, filter """ + """ + Select by name, filter + """ + group = self.parser.add_argument_group("Select") group.add_argument( "--key", dest="keys", action="append", default=[], @@ -93,7 +101,10 @@ def options_select(self): help="Consider the whole tree (leaves only by default)") def options_formatting(self): - """ Formating options """ + """ + Formating options + """ + group = self.parser.add_argument_group("Format") group.add_argument( "--format", dest="formatting", default=None, @@ -103,7 +114,10 @@ def options_formatting(self): help="Values for the custom formatting string") def options_utils(self): - """ Utilities """ + """ + Utilities + """ + group = self.parser.add_argument_group("Utils") group.add_argument( "--path", action="append", dest="paths", @@ -116,7 +130,10 @@ def options_utils(self): help="Turn on debugging output, do not catch exceptions") def command_ls(self): - """ List names """ + """ + List names + """ + self.parser = argparse.ArgumentParser( description="List names of available objects") self.options_select() @@ -125,13 +142,19 @@ def command_ls(self): self.show(brief=True) def command_clean(self): - """ Clean cache """ + """ + Clean cache + """ + self.parser = argparse.ArgumentParser( description="Remove cache directory and its content") self.clean() def command_show(self): - """ Show metadata """ + """ + Show metadata + """ + self.parser = argparse.ArgumentParser( description="Show metadata of available objects") self.options_select() @@ -141,7 +164,10 @@ def command_show(self): self.show(brief=False) def command_init(self): - """ Initialize tree """ + """ + Initialize tree + """ + self.parser = argparse.ArgumentParser( description="Initialize a new metadata tree") self.options_utils() @@ -152,7 +178,10 @@ def command_init(self): print("Metadata tree '{0}' successfully initialized.".format(root)) def show(self, brief=False): - """ Show metadata for each path given """ + """ + Show metadata for each path given + """ + output = [] for path in self.options.paths or ["."]: if self.options.verbose: @@ -191,7 +220,10 @@ def show(self, brief=False): self.output = joined def clean(self): - """ Remove cache directory """ + """ + Remove cache directory + """ + try: cache = utils.get_cache_directory(create=False) utils.clean_cache_directory() @@ -206,7 +238,10 @@ def clean(self): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def main(arguments=None, path=None): - """ Parse options, do what is requested """ + """ + Parse options, do what is requested + """ + parser = Parser(arguments, path) return parser.output diff --git a/fmf/context.py b/fmf/context.py index d6806a3..29f86c8 100644 --- a/fmf/context.py +++ b/fmf/context.py @@ -32,7 +32,9 @@ class InvalidContext(Exception): class ContextValue: - """ Value for dimension """ + """ + Value for dimension + """ def __init__(self, raw): """ @@ -148,7 +150,9 @@ def version_cmp(self, other, minor_mode=False, ordered=True, case_sensitive=True @staticmethod def compare(first, second, case_sensitive=True): - """ compare two version parts """ + """ + Compare two version parts + """ # Ideally use `from packaging import version` but we need older # python support too so very rough try: @@ -212,19 +216,27 @@ def __hash__(self): class Context: - """ Represents https://fmf.readthedocs.io/en/latest/context.html """ + """ + Represents https://fmf.readthedocs.io/en/latest/context.html + """ # Operators' definitions def _op_defined(self, dimension_name, values): - """ 'is defined' operator """ + """ + 'is defined' operator + """ return dimension_name in self._dimensions def _op_not_defined(self, dimension_name, values): - """ 'is not defined' operator """ + """ + 'is not defined' operator + """ return dimension_name not in self._dimensions def _op_eq(self, dimension_name, values): - """ '=' operator """ + """ + '=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -233,7 +245,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_not_eq(self, dimension_name, values): - """ '!=' operator """ + """ + '!=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -242,7 +256,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_match(self, dimension_name, values): - """ '~' operator, regular expression matches """ + """ + '~' operator, regular expression matches + """ def comparator(dimension_value, it_val): return re.search(it_val.raw, dimension_value.raw) is not None @@ -250,7 +266,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_not_match(self, dimension_name, values): - """ '~' operator, regular expression does not match """ + """ + '~' operator, regular expression does not match + """ def comparator(dimension_value, it_val): return re.search(it_val.raw, dimension_value.raw) is None @@ -258,7 +276,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_eq(self, dimension_name, values): - """ '~=' operator """ + """ + '~=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -267,7 +287,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_not_eq(self, dimension_name, values): - """ '~!=' operator """ + """ + '~!=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -276,7 +298,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_less_or_eq(self, dimension_name, values): - """ '~<=' operator """ + """ + '~<=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -285,7 +309,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_less(self, dimension_name, values): - """ '~<' operator """ + """ + '~<' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -294,7 +320,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_less(self, dimension_name, values): - """ '<' operator """ + """ + '<' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -303,7 +331,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_less_or_equal(self, dimension_name, values): - """ '<=' operator """ + """ + '<=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -312,7 +342,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_greater_or_equal(self, dimension_name, values): - """ '>=' operator """ + """ + '>=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -321,7 +353,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_greater_or_equal(self, dimension_name, values): - """ '~>=' operator """ + """ + '~>=' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -330,7 +364,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_greater(self, dimension_name, values): - """ '>' operator """ + """ + '>' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -339,7 +375,9 @@ def comparator(dimension_value, it_val): return self._op_core(dimension_name, values, comparator) def _op_minor_greater(self, dimension_name, values): - """ '~>' operator """ + """ + '~>' operator + """ def comparator(dimension_value, it_val): return dimension_value.version_cmp( @@ -501,7 +539,9 @@ def parse_rule(rule): @staticmethod def parse_value(value): - """ Single place to convert to ContextValue """ + """ + Single place to convert to ContextValue + """ return ContextValue(str(value)) @staticmethod diff --git a/fmf/utils.py b/fmf/utils.py index 0c92148..4d405e0 100644 --- a/fmf/utils.py +++ b/fmf/utils.py @@ -1,4 +1,6 @@ -""" Logging, config, constants & utilities """ +""" +Logging, config, constants & utilities +""" import copy import logging @@ -62,43 +64,61 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class GeneralError(Exception): - """ General error """ + """ + General error + """ class FormatError(GeneralError): - """ Metadata format error """ + """ + Metadata format error + """ class FileError(GeneralError): - """ File reading error """ + """ + File reading error + """ class RootError(FileError): - """ Metadata tree root missing """ + """ + Metadata tree root missing + """ class FilterError(GeneralError): - """ Missing data when filtering """ + """ + Missing data when filtering + """ class MergeError(GeneralError): - """ Unable to merge data between parent and child """ + """ + Unable to merge data between parent and child + """ class ReferenceError(GeneralError): - """ Referenced tree node cannot be found """ + """ + Referenced tree node cannot be found + """ class FetchError(GeneralError): - """ Fatal error in helper command while fetching """ - # Keep previously used format of the message + """ + Fatal error in helper command while fetching + """ + # Keep previously used format of the message def __str__(self): return self.args[0] if self.args else '' class JsonSchemaError(GeneralError): - """ Invalid JSON Schema """ + """ + Invalid JSON Schema + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -106,7 +126,10 @@ class JsonSchemaError(GeneralError): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def pluralize(singular=None): - """ Naively pluralize words """ + """ + Naively pluralize words + """ + if singular.endswith("y") and not singular.endswith("ay"): plural = singular[:-1] + "ies" elif singular.endswith("s"): @@ -182,13 +205,17 @@ def split(values, separator=re.compile("[ ,]+")): Accepts both string and list. By default space and comma are used as value separators. Use any regular expression for custom separator. """ + if not isinstance(values, list): values = [values] return sum([separator.split(value) for value in values], []) def info(message, newline=True): - """ Log provided info message to the standard error output """ + """ + Log provided info message to the standard error output + """ + sys.stderr.write(message + ("\n" if newline else "")) @@ -203,6 +230,7 @@ def evaluate(expression, data, _node=None): Expects data dictionary which will be used to populate local namespace. Used to provide flexible conditions for filtering. """ + namespace = dict(data) try: return eval(expression, namespace) @@ -271,18 +299,23 @@ def filter(filter, data, sensitive=True, regexp=False, name=None): :returns: True if the filter matches given dictionary of values and the node name (if provided). - """ def match_value(pattern, text): - """ Match value against data (simple or regexp) """ + """ + Match value against data (simple or regexp) + """ + if regexp: return re.match("^{0}$".format(pattern), text) else: return pattern == text def check_value(dimension, value): - """ Check whether the value matches data """ + """ + Check whether the value matches data + """ + # E.g. value = 'A, B' or value = "C" or value = "-D" # If there are multiple values, at least one must match for atom in re.split(r"\s*,\s*", value): @@ -307,7 +340,10 @@ def check_value(dimension, value): return False def check_dimension(dimension, values): - """ Check whether all values for given dimension match data """ + """ + Check whether all values for given dimension match data + """ + # E.g. dimension = 'tag', values = ['A, B', 'C', '-D'] # Raise exception upon unknown dimension if dimension not in data: @@ -322,6 +358,7 @@ def check_name(pattern: str) -> bool: Search for regular expression pattern if `regexp` is turned on, simply compare strings otherwise. """ + # Node name has to be provided if name search requested if name is None: raise FilterError( @@ -334,7 +371,10 @@ def check_name(pattern: str) -> bool: return pattern == name def check_clause(clause): - """ Split into literals and check whether all match """ + """ + Split into literals and check whether all match + """ + # E.g. clause = 'tag: A, B & tag: C & tag: -D' # Split into individual literals by dimension literals = dict() @@ -393,7 +433,9 @@ def check_clause(clause): class Logging: - """ Logging Configuration """ + """ + Logging Configuration + """ # Color mapping COLORS = { @@ -433,7 +475,9 @@ def __init__(self, name='fmf'): self.set() class ColoredFormatter(logging.Formatter): - """ Custom color formatter for logging """ + """ + Custom color formatter for logging + """ def format(self, record): # Handle custom log level names @@ -459,7 +503,10 @@ def format(self, record): @staticmethod def _create_logger(name='fmf', level=None): - """ Create fmf logger """ + """ + Create fmf logger + """ + # Create logger, handler and formatter logger = logging.getLogger(name) handler = logging.StreamHandler() @@ -491,6 +538,7 @@ def set(self, level=None): DEBUG=4 ... LOG_DATA DEBUG=5 ... LOG_ALL (log all messages) """ + # If level specified, use given if level is not None: Logging._level = level @@ -503,7 +551,10 @@ def set(self, level=None): self.logger.setLevel(Logging._level) def get(self): - """ Get the current log level """ + """ + Get the current log level + """ + return self.logger.level @@ -518,6 +569,7 @@ def color(text, color=None, background=None, light=False, enabled="auto"): Available colors: black red green yellow blue magenta cyan white. Alternatively color can be prefixed with "light", e.g. lightgreen. """ + colors = {"black": 30, "red": 31, "green": 32, "yellow": 33, "blue": 34, "magenta": 35, "cyan": 36, "white": 37} # Nothing do do if coloring disabled @@ -539,7 +591,9 @@ def color(text, color=None, background=None, light=False, enabled="auto"): class Coloring: - """ Coloring configuration """ + """ + Coloring configuration + """ # Default color mode is auto-detected from the terminal presence _mode = None @@ -548,13 +602,19 @@ class Coloring: _instance = None def __new__(cls, *args, **kwargs): - """ Make sure we create a single instance only """ + """ + Make sure we create a single instance only + """ + if not cls._instance: cls._instance = super(Coloring, cls).__new__(cls, *args, **kwargs) return cls._instance def __init__(self, mode=None): - """ Initialize the coloring mode """ + """ + Initialize the coloring mode + """ + # Nothing to do if already initialized if self._mode is not None: return @@ -576,6 +636,7 @@ def set(self, mode=None): Environment variable COLOR can be used to set up the coloring to the desired mode without modifying code. """ + # Detect from the environment if no mode given (only once) if mode is None: # Nothing to do if already detected @@ -595,11 +656,17 @@ def set(self, mode=None): self.MODES[self._mode])) def get(self): - """ Get the current color mode """ + """ + Get the current color mode + """ + return self._mode def enabled(self): - """ True if coloring is currently enabled """ + """ + True if coloring is currently enabled + """ + # In auto-detection mode color enabled when terminal attached if self._mode == COLOR_AUTO: return sys.stdout.isatty() @@ -622,6 +689,7 @@ def get_cache_directory(create=True): Raise GeneralError if it is not possible to create it. """ + cache = ( os.environ.get('FMF_CACHE_DIRECTORY', _CACHE_DIRECTORY) or os.path.join( @@ -639,26 +707,38 @@ def get_cache_directory(create=True): def set_cache_directory(cache_directory): - """ Set preferred cache directory """ + """ + Set preferred cache directory + """ + global _CACHE_DIRECTORY _CACHE_DIRECTORY = cache_directory def set_cache_expiration(seconds): - """ Seconds until cache expires """ + """ + Seconds until cache expires + """ + global CACHE_EXPIRATION CACHE_EXPIRATION = int(seconds) def clean_cache_directory(): - """ Delete used cache directory if it exists """ + """ + Delete used cache directory if it exists + """ + cache = get_cache_directory(create=False) if os.path.isdir(cache): shutil.rmtree(cache) def invalidate_cache(): - """ Force fetch next time cache is used regardless its age """ + """ + Force fetch next time cache is used regardless its age + """ + # Missing FETCH_HEAD means `git fetch` will happen cache = get_cache_directory(create=False) # Cache not exists, nothing to do @@ -704,6 +784,7 @@ def fetch_tree(url, ref=None, path='.'): Raises GeneralError when lock couldn't be acquired. """ + # Create lock path to fetch/read git from URL to the cache cache_dir = get_cache_directory() # Use LOCK_SUFFIX_READ suffix (different from the inner fetch lock) @@ -728,7 +809,10 @@ def fetch_tree(url, ref=None, path='.'): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def fetch(url, ref=None, destination=None, env=None): - """ Deprecated: Use :func:`fetch_repo` instead """ + """ + Deprecated: Use :func:`fetch_repo` instead + """ + # DeprecationWarning is hidden by default (unless -Wall or -Wonce option) # so using FutureWarning to have this visible by default warnings.warn( @@ -743,7 +827,10 @@ def fetch(url, ref=None, destination=None, env=None): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def default_branch(repository, remote="origin"): - """ Detect default branch from given local git repository """ + """ + Detect default branch from given local git repository + """ + head = os.path.join(repository, f".git/refs/remotes/{remote}/HEAD") # Make sure the HEAD reference is available if not os.path.exists(head): @@ -853,6 +940,7 @@ def run(command, cwd=None, check_exit_code=True, env=None): :check_exit_code raise CalledProcessError if exit code is non-zero :env dictionary of the environment variables for the command """ + log.debug("Running command: '{0}'.".format(' '.join(command))) process = subprocess.Popen( @@ -882,7 +970,10 @@ def run(command, cwd=None, check_exit_code=True, env=None): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def dict_to_yaml(data, width=None, sort=False): - """ Convert dictionary into yaml """ + """ + Convert dictionary into yaml + """ + output = StringIO() # Set formatting options @@ -913,7 +1004,10 @@ def dict_to_yaml(data, width=None, sort=False): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class JsonSchemaValidationResult(NamedTuple): - """ Represents JSON Schema validation result """ + """ + Represents JSON Schema validation result + """ + result: bool errors: list[Any] @@ -923,11 +1017,13 @@ def validate_data( schema: Any, schema_store: Optional[dict[str, Any]] = None ) -> JsonSchemaValidationResult: - """Validate data against a JSON Schema. + """ + Validate data against a JSON Schema. Validates the given data using the specified JSON Schema and optional schema references. """ + validator = get_validator(schema, schema_store) try: diff --git a/tests/unit/test_adjust.py b/tests/unit/test_adjust.py index 2b0b489..d7b4b3d 100644 --- a/tests/unit/test_adjust.py +++ b/tests/unit/test_adjust.py @@ -11,19 +11,28 @@ @pytest.fixture def fedora(): - """ Fedora 33 on x86_64 and ppc64 """ + """ + Fedora 33 on x86_64 and ppc64 + """ + return Context(distro='fedora-33', arch=['x86_64', 'ppc64']) @pytest.fixture def centos(): - """ CentOS 8.4 """ + """ + CentOS 8.4 + """ + return Context(distro='centos-8.4') @pytest.fixture def mini(): - """ Simple tree node with minimum set of attributes """ + """ + Simple tree node with minimum set of attributes + """ + data = """ enabled: true adjust: @@ -36,7 +45,10 @@ def mini(): @pytest.fixture def full(): - """ More complex metadata structure with inheritance """ + """ + More complex metadata structure with inheritance + """ + data = """ duration: 5m enabled: true @@ -73,7 +85,9 @@ def full(): class TestInvalid: - """ Ensure that invalid input is correctly handled """ + """ + Ensure that invalid input is correctly handled + """ def test_invalid_context(self, mini): with pytest.raises(GeneralError, match='Invalid adjust context'): @@ -101,7 +115,9 @@ def test_undecided_invalid(self, mini, fedora): class TestSpecial: - """ Check various special cases """ + """ + Check various special cases + """ def test_single_rule(self, mini, fedora): mini.adjust(fedora) @@ -118,7 +134,9 @@ def test_missing_when(self, mini, fedora): class TestAdjust: - """ Verify adjusting works as expected """ + """ + Verify adjusting works as expected + """ def test_original(self, mini): assert mini.get('enabled') is True @@ -128,12 +146,18 @@ def test_adjusted(self, mini, centos): assert mini.get('enabled') is False def test_adjusted_additional(self, mini, centos): - """ Additional rule is evaluated even if 'main' rule matched """ + """ + Additional rule is evaluated even if 'main' rule matched + """ + mini.adjust(centos, additional_rules={'enabled': True}) assert mini.get('enabled') is True def test_additional_rules_callback(self, fedora): - """ Additional rules might be ignored """ + """ + Additional rules might be ignored + """ + data = """ /is_test: test: echo @@ -159,7 +183,10 @@ def additional_rules_callback(tree_node): assert tree.find('/is_plan').data == {'execute': {'how': 'tmt'}} def test_adjusted_additional_after_continue(self, full, centos): - """ Additional rule is evaluated even if 'node' rule has continue:false """ + """ + Additional rule is evaluated even if 'node' rule has continue:false + """ + full.adjust(centos, additional_rules=[{'tag': 'foo'}, {'require': 'bar', @@ -245,7 +272,10 @@ def test_extend_centos(self, full, centos): assert 'recommend' not in extend.get() def test_continue_default(self, full, fedora): - """ The continue key should default to True """ + """ + The continue key should default to True + """ + full.adjust(fedora) extend = full.find('/extend') assert extend.get('require') == ['one', 'two', 'three'] @@ -275,13 +305,19 @@ def test_adjust_callback(self, mini, fedora, centos): assert mock_callback.call_count == 2 def test_case_sensitive(self, mini, centos): - """ Make sure the adjust rules are case-sensitive by default """ + """ + Make sure the adjust rules are case-sensitive by default + """ + mini.data['adjust'] = dict(when='distro = CentOS', enabled=False) mini.adjust(centos) assert mini.get('enabled') is True def test_case_insensitive(self, mini, centos): - """ Make sure the adjust rules are case-insensitive when requested """ + """ + Make sure the adjust rules are case-insensitive when requested + """ + mini.data['adjust'] = dict(when='distro = CentOS', enabled=False) mini.adjust(centos, case_sensitive=False) assert mini.get('enabled') is False diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index 54e494e..f913b6e 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -30,15 +30,23 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class TestTree: - """ Tree class """ + """ + Tree class + """ def setup_method(self, method): - """ Load examples """ + """ + Load examples + """ + self.wget = Tree(EXAMPLES + "wget") self.merge = Tree(EXAMPLES + "merge") def test_basic(self): - """ No directory path given """ + """ + No directory path given + """ + with pytest.raises(utils.GeneralError): Tree("") with pytest.raises(utils.GeneralError): @@ -47,21 +55,30 @@ def test_basic(self): Tree("/") def test_hidden(self): - """ Hidden files and directories """ + """ + Hidden files and directories + """ + assert ".hidden" not in self.wget.children hidden = Tree(EXAMPLES + "hidden") plan = hidden.find("/.plans/basic") assert plan.get("discover") == {"how": "fmf"} def test_inheritance(self): - """ Inheritance and data types """ + """ + Inheritance and data types + """ + deep = self.wget.find('/recursion/deep') assert deep.data['depth'] == 1000 assert deep.data['description'] == 'Check recursive download options' assert deep.data['tags'] == ['Tier2'] def test_scatter(self): - """ Scattered files """ + """ + Scattered files + """ + scatter = Tree(EXAMPLES + "scatter").find("/object") assert len(list(scatter.climb())) == 1 assert scatter.data['one'] == 1 @@ -69,7 +86,10 @@ def test_scatter(self): assert scatter.data['three'] == 3 def test_scattered_inheritance(self): - """ Inheritance of scattered files """ + """ + Inheritance of scattered files + """ + grandson = Tree(EXAMPLES + "child").find("/son/grandson") assert grandson.data['name'] == 'Hugo' assert grandson.data['eyes'] == 'blue' @@ -77,12 +97,17 @@ def test_scattered_inheritance(self): assert grandson.data['hair'] == 'fair' def test_subtrees(self): - """ Subtrees should be ignored """ + """ + Subtrees should be ignored + """ + child = Tree(EXAMPLES + "child") assert child.find("/nobody") is None def test_insert_child(self): - """ Manual child creation """ + """ + Manual child creation + """ # Prepare a simple tree by manually inserting child nodes tree = Tree(data={"key": "value"}) @@ -103,7 +128,10 @@ def test_insert_child(self): assert [node.name for node in tree.prune(sort=False)] == expected def test_prune_sources(self): - """ Pruning by sources """ + """ + Pruning by sources + """ + original_directory = os.getcwd() # Change directory to make relative paths work os.chdir(SELECT_SOURCE) @@ -123,18 +151,27 @@ def test_prune_sources(self): os.chdir(original_directory) def test_empty(self): - """ Empty structures should be ignored """ + """ + Empty structures should be ignored + """ + child = Tree(EXAMPLES + "empty") assert child.find("/nothing") is None assert child.find("/zero") is not None def test_none_key(self): - """ Handle None keys """ + """ + Handle None keys + """ + with pytest.raises(utils.FormatError): Tree({None: "weird key"}) def test_control_keys(self): - """ No special handling outside adjust """ + """ + No special handling outside adjust + """ + child_data = {k: str(v) for v, k in enumerate(ADJUST_CONTROL_KEYS)} tree = Tree({ 'key': 'value', @@ -145,7 +182,10 @@ def test_control_keys(self): assert tree.find('/child').data == expected def test_adjust_strips_control_keys(self): - """ They are not merged during adjust """ + """ + They are not merged during adjust + """ + tree = Tree({'adjust': [ { 'because': 'reasons', @@ -160,7 +200,10 @@ def test_adjust_strips_control_keys(self): assert 'foo' in child.data def test_adjust_can_extend(self): - """ It is possible to extend dictionary with adjust """ + """ + It is possible to extend dictionary with adjust + """ + tree = Tree(YAML(typ='safe').load(dedent(""" /a: environment+: @@ -175,7 +218,10 @@ def test_adjust_can_extend(self): assert child.data['environment']['BAR'] == "baz" def test_node_without_parent_strips_merge_suffix(self): - """ Merge suffix is stripped in the top node as well """ + """ + Merge suffix is stripped in the top node as well + """ + tree = Tree(EXAMPLES + 'merge') stray_child = tree.find('/stray') assert 'environment' in stray_child.data @@ -183,12 +229,18 @@ def test_node_without_parent_strips_merge_suffix(self): assert 'environment' in child.data def test_deep_hierarchy(self): - """ Deep hierarchy on one line """ + """ + Deep hierarchy on one line + """ + deep = Tree(EXAMPLES + "deep") assert len(deep.children) == 3 def test_deep_dictionary(self): - """ Get value from a deep dictionary """ + """ + Get value from a deep dictionary + """ + deep = Tree(EXAMPLES + "deep") assert deep.data['hardware']['memory']['size'] == 8 assert deep.get(['hardware', 'memory', 'size']) == 8 @@ -196,7 +248,10 @@ def test_deep_dictionary(self): assert deep.get('nonexistent', default=3) == 3 def test_deep_dictionary_undefined_keys(self): - """ Extending undefined keys using '+' should work """ + """ + Extending undefined keys using '+' should work + """ + deep = Tree(EXAMPLES + "deep") single = deep.find("/single") assert single.get(["undefined", "deeper+", "key"]) == "value" @@ -207,7 +262,10 @@ def test_deep_dictionary_undefined_keys(self): assert child.get(["undefined", "deeper+", "key"]) == "value" def test_merge_plus(self): - """ Extending attributes using the '+' suffix """ + """ + Extending attributes using the '+' suffix + """ + child = self.merge.find('/parent/extended') assert 'General' in child.data['description'] assert 'Specific' in child.data['description'] @@ -221,7 +279,10 @@ def test_merge_plus(self): child.inherit() def test_merge_plus_parent_dict(self): - """ Merging parent dict with child list """ + """ + Merging parent dict with child list + """ + child = self.merge.find('/parent-dict/path') assert len(child.data['discover']) == 2 assert child.data['discover'][0]['how'] == 'fmf' @@ -234,7 +295,10 @@ def test_merge_plus_parent_dict(self): assert child.data['discover'][1]['summary'] == 'test.downstream' def test_merge_plus_parent_list(self): - """ Merging parent list with child dict """ + """ + Merging parent list with child dict + """ + for i in [1, 2]: child = self.merge.find(f'/parent-list/tier{i}') assert child.data['summary'] == 'basic tests' if i == 1 else 'detailed tests' @@ -247,7 +311,10 @@ def test_merge_plus_parent_list(self): assert child.data['discover'][1]['summary'] == f'project2.tier{i}' def test_merge_minus(self): - """ Reducing attributes using the '-' suffix """ + """ + Reducing attributes using the '-' suffix + """ + child = self.merge.find('/parent/reduced') assert 'General' in child.data['description'] assert 'description' not in child.data['description'] @@ -264,7 +331,10 @@ def test_merge_minus(self): child.inherit() def test_merge_regexp(self): - """ Do re.sub during the merge """ + """ + Do re.sub during the merge + """ + child = self.merge.find('/parent/regexp') assert 'general' == child.data['description'] # First rule changes the Tier2 into t2, @@ -272,31 +342,46 @@ def test_merge_regexp(self): assert ['t1', 't2'] == child.data['tags'] def test_merge_minus_regexp(self): - """ Merging with '-~' operation """ + """ + Merging with '-~' operation + """ + child = self.merge.find('/parent/minus-regexp') assert '' == child.data['description'] assert ['Tier2'] == child.data['tags'] assert {'x': 1} == child.data['vars'] def test_merge_deep(self): - """ Merging a deeply nested dictionary """ + """ + Merging a deeply nested dictionary + """ + child = self.merge.find('/parent/buried') assert child.data['very']['deep']['dict'] == dict(x=2, y=1, z=0) def test_merge_order(self): - """ Inheritance should be applied in the given order """ + """ + Inheritance should be applied in the given order + """ + child = self.merge.find('/parent/order/add-first') assert child.data['tag'] == ['one', 'four'] child = self.merge.find('/parent/order/remove-first') assert child.data['tag'] == ['one', 'three', 'four'] def test_get(self): - """ Get attributes """ + """ + Get attributes + """ + assert isinstance(self.wget.get(), dict) assert 'Petr' in self.wget.get('tester') def test_show(self): - """ Show metadata """ + """ + Show metadata + """ + assert isinstance(self.wget.show(brief=True), str) assert self.wget.show(brief=True).endswith("\n") assert isinstance(self.wget.show(), str) @@ -304,24 +389,36 @@ def test_show(self): assert 'tester' in self.wget.show() def test_update(self): - """ Update data """ + """ + Update data + """ + data = self.wget.get() self.wget.update(None) assert self.wget.data == data def test_find_node(self): - """ Find node by name """ + """ + Find node by name + """ + assert self.wget.find("non-existent") is None protocols = self.wget.find('/protocols') assert isinstance(protocols, Tree) def test_find_root(self): - """ Find metadata tree root """ + """ + Find metadata tree root + """ + tree = Tree(os.path.join(EXAMPLES, "wget", "protocols")) assert tree.find("/download/test") def test_yaml_syntax_errors(self): - """ Handle YAML syntax errors """ + """ + Handle YAML syntax errors + """ + path = tempfile.mkdtemp() fmf.cli.main("fmf init", path) with open(os.path.join(path, "main.fmf"), "w") as main: @@ -331,7 +428,10 @@ def test_yaml_syntax_errors(self): rmtree(path) def test_yaml_duplicate_keys(self): - """ Handle YAML duplicate keys """ + """ + Handle YAML duplicate keys + """ + path = tempfile.mkdtemp() fmf.cli.main("fmf init", path) @@ -358,7 +458,10 @@ def test_yaml_duplicate_keys(self): rmtree(path) def test_inaccessible_directories(self): - """ Inaccessible directories should be silently ignored """ + """ + Inaccessible directories should be silently ignored + """ + directory = tempfile.mkdtemp() accessible = os.path.join(directory, 'accessible') inaccessible = os.path.join(directory, 'inaccessible') @@ -374,7 +477,10 @@ def test_inaccessible_directories(self): rmtree(directory) def test_node_copy_complete(self): - """ Create deep copy of the whole tree """ + """ + Create deep copy of the whole tree + """ + original = self.merge duplicate = original.copy() duplicate.data['x'] = 1 @@ -384,7 +490,10 @@ def test_node_copy_complete(self): assert duplicate.get('x') == 1 def test_node_copy_child(self): - """ Duplicate child changes do not affect original """ + """ + Duplicate child changes do not affect original + """ + original = self.merge duplicate = original.copy() original_child = original.find('/parent/extended') @@ -397,7 +506,10 @@ def test_node_copy_child(self): assert duplicate_child.get('duplicate') is True def test_node_copy_subtree(self): - """ Create deep copy of a subtree """ + """ + Create deep copy of a subtree + """ + original = self.merge.find('/parent/extended') duplicate = original.copy() duplicate.data['x'] = 1 @@ -407,7 +519,10 @@ def test_node_copy_subtree(self): assert original.get('x') is None def test_validation(self): - """ Test JSON Schema validation """ + """ + Test JSON Schema validation + """ + test_schema_path = os.path.join(PATH, 'assets', 'schema_test.yaml') plan_schema_path = os.path.join(PATH, 'assets', 'schema_plan.yaml') @@ -427,7 +542,9 @@ def test_validation(self): assert not test.validate(plan_schema).result def test_validation_with_store(self): - """ Test JSON Schema validation with schema store """ + """ + Test JSON Schema validation with schema store + """ base_schema_path = os.path.join(PATH, 'assets', 'schema_base.yaml') test_schema_ref_path = os.path.join( @@ -451,13 +568,18 @@ def test_validation_with_store(self): schema_store=schema_store) == expected def test_validation_invalid_schema(self): - """ Test invalid JSON Schema handling """ + """ + Test invalid JSON Schema handling + """ + with pytest.raises(fmf.utils.JsonSchemaError): self.wget.find('/recursion/deep').validate('invalid') class TestRemote: - """ Get tree node data using remote reference """ + """ + Get tree node data using remote reference + """ @pytest.mark.web def test_tree_node_remote(self): diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 96a6d2e..2ed2bd2 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -13,47 +13,73 @@ class TestCommandLine: - """ Command Line """ + """ + Command Line + """ def test_smoke(self): - """ Smoke test """ + """ + Smoke test + """ + fmf.cli.main("fmf show", WGET) fmf.cli.main("fmf show --debug", WGET) fmf.cli.main("fmf show --verbose", WGET) fmf.cli.main("fmf --version") def test_missing_root(self): - """ Missing root """ + """ + Missing root + """ + with pytest.raises(utils.FileError): fmf.cli.main("fmf show", "/") def test_invalid_path(self): - """ Missing root """ + """ + Missing root + """ + with pytest.raises(utils.FileError): fmf.cli.main("fmf show --path /some-non-existent-path") def test_wrong_command(self): - """ Wrong command """ + """ + Wrong command + """ + with pytest.raises(utils.GeneralError): fmf.cli.main("fmf wrongcommand") def test_output(self): - """ There is some output """ + """ + There is some output + """ + output = fmf.cli.main("fmf show", WGET) assert "download" in output def test_recursion(self): - """ Recursion """ + """ + Recursion + """ + output = fmf.cli.main("fmf show --name recursion/deep", WGET) assert "1000" in output def test_inheritance(self): - """ Inheritance """ + """ + Inheritance + """ + output = fmf.cli.main("fmf show --name protocols/https", WGET) assert "psplicha" in output def test_sys_argv(self): - """ Parsing sys.argv """ + """ + Parsing sys.argv + """ + backup = sys.argv sys.argv = ['fmf', 'show', '--path', WGET, '--name', 'recursion/deep'] output = fmf.cli.main() @@ -61,17 +87,26 @@ def test_sys_argv(self): sys.argv = backup def test_missing_attribute(self): - """ Missing attribute """ + """ + Missing attribute + """ + output = fmf.cli.main("fmf show --filter x:y", WGET) assert "wget" not in output def test_filtering_by_source(self): - """ By source """ + """ + By source + """ + output = fmf.cli.main("fmf show --source protocols/ftp/main.fmf", WGET) assert "/protocols/ftp" in output def test_filtering(self): - """ Filtering """ + """ + Filtering + """ + output = fmf.cli.main( "fmf show --filter tags:Tier1 --filter tags:TierSecurity", WGET) assert "/download/test" in output @@ -84,25 +119,37 @@ def test_filtering(self): assert "/recursion" not in output def test_key_content(self): - """ Key content """ + """ + Key content + """ + output = fmf.cli.main("fmf show --key depth") assert "/recursion/deep" in output assert "/download/test" not in output def test_format_basic(self): - """ Custom format (basic) """ + """ + Custom format (basic) + """ + output = fmf.cli.main(WGET + "fmf show --format foo") assert "wget" not in output assert "foo" in output def test_format_key(self): - """ Custom format (find by key, check the name) """ + """ + Custom format (find by key, check the name) + """ + output = fmf.cli.main( "fmf show --key depth --format {0} --value name", WGET) assert "/recursion/deep" in output def test_format_functions(self): - """ Custom format (using python functions) """ + """ + Custom format (using python functions) + """ + output = fmf.cli.main( "fmf show --key depth --format {0} --value os.path.basename(name)", WGET) @@ -111,7 +158,10 @@ def test_format_functions(self): @pytest.mark.skipif(os.geteuid() == 0, reason="Running as root") def test_init(self): - """ Initialize metadata tree """ + """ + Initialize metadata tree + """ + path = tempfile.mkdtemp() fmf.cli.main("fmf init", path) fmf.cli.main("fmf show", path) @@ -139,7 +189,10 @@ def test_init(self): fmf.cli.main("fmf ls", path) def test_conditions(self): - """ Advanced filters via conditions """ + """ + Advanced filters via conditions + """ + path = PATH + "/../../examples/conditions" # Compare numbers output = fmf.cli.main("fmf ls --condition 'float(release) >= 7'", path) @@ -156,7 +209,10 @@ def test_conditions(self): assert output == '' def test_clean(self, tmpdir, monkeypatch): - """ Cache cleanup """ + """ + Cache cleanup + """ + # Do not manipulate with real, user's cache monkeypatch.setattr('fmf.utils._CACHE_DIRECTORY', str(tmpdir)) testing_file = tmpdir.join("something") diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index a7bfd55..b26e375 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -14,10 +14,14 @@ def env_centos(): class TestExample: - """ Examples of possible conditions """ + """ + Examples of possible conditions + """ def test_simple_conditions(self, env_centos): - """ Rules with single condition """ + """ + Rules with single condition + """ # "Version" comparison if possible assert env_centos.matches("distro == centos") @@ -49,7 +53,10 @@ def test_simple_conditions(self, env_centos): assert not env_centos.matches("component = csh, ksh") def test_multi_condition(self, env_centos): - """ Rules with multiple conditions """ + """ + Rules with multiple conditions + """ + assert env_centos.matches( "arch=x86_64 and distro != fedora") assert env_centos.matches( @@ -60,7 +67,10 @@ def test_multi_condition(self, env_centos): "distro = rhel and component >= bash-4.9") def test_minor_comparison_mode(self): - """ How it minor comparison should work """ + """ + How it minor comparison should work + """ + centos = Context(distro="centos-7.3.0") centos6 = Context(distro="centos-6.9.0") @@ -110,7 +120,10 @@ def test_minor_comparison_mode(self): ) def test_true_false(self): - """ true/false can be used in rule """ + """ + true/false can be used in rule + """ + empty = Context() assert empty.matches('true') assert empty.matches(True) @@ -125,7 +138,10 @@ def test_true_false(self): assert fedora.matches('true or distro == centos-stream') def test_right_side_defines_precision(self): - """ Right side defines how many version parts need to match """ + """ + Right side defines how many version parts need to match + """ + bar_830 = Context(dimension="bar-8.3.0") bar_ = Context(dimension="bar") # so essentially bar-0.0.0 @@ -171,7 +187,10 @@ def test_right_side_defines_precision(self): bar_.matches("dimension {0} {1}".format(op, value)) def test_right_side_defines_precision_tilda(self): - """ Right side defines how many version parts need to match (~ operations) """ + """ + Right side defines how many version parts need to match (~ operations) + """ + bar_830 = Context(dimension="bar-8.3.0") bar_ = Context(dimension="bar") # missing major bar_8 = Context(dimension="bar-8") # so essentially bar-8.0.0 @@ -223,7 +242,10 @@ def test_right_side_defines_precision_tilda(self): bar_8.matches("dimension {0} {1}".format(op, value)) def test_module_streams(self): - """ How you can use Context for modules """ + """ + How you can use Context for modules + """ + perl = Context("module = perl:5.28") assert perl.matches("module >= perl:5") @@ -243,7 +265,10 @@ def test_module_streams(self): Context("module = perl:6.28").matches("module ~>= perl:5.28") def test_comma(self): - """ Comma is sugar for OR """ + """ + Comma is sugar for OR + """ + con = Context(single="foo", multi=["first", "second"]) # First as longer line, then using comma assert con.matches("single == foo or single == bar") @@ -270,7 +295,10 @@ def test_comma(self): assert not distro.matches("distro < fedora-34, centos-stream-8") def test_case_insensitive(self): - """ Test for case-insensitive matching """ + """ + Test for case-insensitive matching + """ + python = Context(component="python3-3.8.5-5.fc32") python.case_sensitive = False @@ -283,7 +311,9 @@ def test_case_insensitive(self): assert python.matches("component < PYTHON3-3.9") def test_regular_expression_matching(self): - """ Matching regular expressions """ + """ + Matching regular expressions + """ assert Context(distro="fedora-42").matches("distro ~ ^fedora-42$") assert Context(distro="fedora-42").matches("distro ~ fedora") @@ -316,14 +346,20 @@ class TestContextValue: ] def test_simple_names(self): - """ Values with simple name """ + """ + Values with simple name + """ + for name in self.impossible_split: assert ContextValue(name)._to_compare == tuple([name]) for name, _ in self.splittable: assert ContextValue([name])._to_compare == tuple([name]) def test_split_to_version(self): - """ Possible/impossible splitting to version""" + """ + Possible/impossible splitting to version + """ + for name in self.impossible_split: assert ContextValue._split_to_version(name) == tuple([name]) for name, expected in self.splittable: @@ -429,7 +465,10 @@ def test_version_cmp(self): assert sixth != Context() def test_version_cmp_fedora(self): - """ Fedora comparison """ + """ + Fedora comparison + """ + f33 = ContextValue("fedora-33") frawhide = ContextValue("fedora-rawhide") @@ -488,7 +527,10 @@ class TestParser: ] def test_split_rule_to_groups(self): - """ Split to lists """ + """ + Split to lists + """ + for invalid_rule in self.rule_groups_invalid: with pytest.raises(InvalidRule): Context.split_rule_to_groups(invalid_rule) @@ -516,7 +558,10 @@ def test_split_rule_to_groups(self): ] def test_split_expression(self): - """ Split to dimension/operator/value tuple """ + """ + Split to dimension/operator/value tuple + """ + for invalid in self.invalid_expressions: with pytest.raises(InvalidRule): Context.split_expression(invalid) @@ -541,7 +586,10 @@ def test_split_expression(self): "provision-method", "is defined", None) def test_parse_rule(self): - """ Rule parsing """ + """ + Rule parsing + """ + for invalid in self.rule_groups_invalid + self.invalid_expressions: with pytest.raises(InvalidRule): Context.parse_rule(invalid) @@ -597,7 +645,10 @@ def test_prints(self): ) def test_matches_groups(self): - """ and/or in rules with yes/no/cannotdecide outcome """ + """ + and/or in rules with yes/no/cannotdecide outcome + """ + context = Context(distro="centos-8.2.0") # Clear outcome @@ -623,7 +674,9 @@ def test_matches_groups(self): context.matches(undecidable) def test_matches(self): - """ yes/no/skip test per operator for matches """ + """ + yes/no/skip test per operator for matches + """ context = Context( distro="fedora-32", @@ -742,7 +795,9 @@ def test_matches(self): assert not context.matches("distro ~> fedora") def test_known_troublemakers(self): - """ Do not regress on these expressions """ + """ + Do not regress on these expressions + """ # From fmf/issues/89: # following is true (missing left values are treated as lower) @@ -823,7 +878,9 @@ def test_cannotdecides(self): class TestOperators: - """ more thorough testing for operations """ + """ + more thorough testing for operations + """ context = Context( # nvr like single diff --git a/tests/unit/test_items.py b/tests/unit/test_items.py index 08ab393..0d67b91 100644 --- a/tests/unit/test_items.py +++ b/tests/unit/test_items.py @@ -9,7 +9,9 @@ class TestGetItems(unittest.TestCase): - """ Verify getter of items """ + """ + Verify getter of items + """ def setUp(self): self.wget_path = EXAMPLES + "wget" diff --git a/tests/unit/test_modify.py b/tests/unit/test_modify.py index 0795e06..ec38bdf 100644 --- a/tests/unit/test_modify.py +++ b/tests/unit/test_modify.py @@ -17,7 +17,9 @@ class TestModify(unittest.TestCase): - """ Verify storing modifed data to disk """ + """ + Verify storing modifed data to disk + """ def setUp(self): self.wget_path = EXAMPLES + "wget" @@ -29,7 +31,10 @@ def tearDown(self): rmtree(self.tempdir) def test_inheritance(self): - """ Inheritance and data types """ + """ + Inheritance and data types + """ + item = self.wget.find('/recursion/deep') # Modify data and store to disk with item as data: @@ -50,7 +55,10 @@ def test_inheritance(self): self.assertTrue(re.search('two\n +lines', file.read())) def test_deep_modify(self): - """ Deep structures """ + """ + Deep structures + """ + requirements = self.wget.find('/requirements') protocols = self.wget.find('/requirements/protocols') ftp = self.wget.find('/requirements/protocols/ftp') @@ -80,7 +88,10 @@ def test_deep_modify(self): self.assertEqual(ftp.data['adjust'][0]['when'], "arch != x86_64") def test_deep_hierarchy(self): - """ Multiple virtual hierarchy levels shortcut """ + """ + Multiple virtual hierarchy levels shortcut + """ + with open(os.path.join(self.tempdir, 'deep.fmf'), 'w') as file: file.write('/one/two/three:\n x: 1\n') deep = Tree(self.tempdir).find('/deep/one/two/three') @@ -91,7 +102,10 @@ def test_deep_hierarchy(self): self.assertEqual(deep.get('y'), 2) def test_modify_empty(self): - """ Nodes with no content should be handled as an empty dict """ + """ + Nodes with no content should be handled as an empty dict + """ + with self.wget.find('/download/requirements/spider') as data: data['x'] = 1 self.wget = Tree(self.tempdir) @@ -99,7 +113,10 @@ def test_modify_empty(self): self.assertEqual(node.data['x'], 1) def test_modify_pop(self): - """ Pop elements from node data """ + """ + Pop elements from node data + """ + item = '/requirements/protocols/ftp' with self.wget.find(item) as data: data.pop('coverage') @@ -111,7 +128,10 @@ def test_modify_pop(self): self.assertIn('requirement', node.data) def test_modify_clear(self): - """ Clear node data """ + """ + Clear node data + """ + item = '/requirements/protocols/ftp' with self.wget.find(item) as data: data.clear() @@ -122,13 +142,19 @@ def test_modify_clear(self): self.assertNotIn('requirement', node.data) def test_modify_unsupported_method(self): - """ Raise error for trees initialized from a dict """ + """ + Raise error for trees initialized from a dict + """ + with pytest.raises(GeneralError, match='No raw data'): with Tree(dict(x=1)) as data: data['y'] = 2 def test_context_manager(self): - """ Use context manager to save node data """ + """ + Use context manager to save node data + """ + item = '/requirements/protocols/ftp' with self.wget.find(item) as data: data.pop("coverage") @@ -142,7 +168,10 @@ def test_context_manager(self): self.assertIn("server", node.data) def test_modify_unicode(self): - """ Ensure that unicode characters are properly handled """ + """ + Ensure that unicode characters are properly handled + """ + path = os.path.join(self.tempdir, 'unicode.fmf') with io.open(path, 'w', encoding='utf-8') as file: file.write('jméno: Leoš') @@ -153,7 +182,10 @@ def test_modify_unicode(self): assert reloaded.get('příjmení') == 'Janáček' def test_modify_after_adjust(self): - """ Preserve original data even when adjust is used """ + """ + Preserve original data even when adjust is used + """ + item = '/requirements/protocols/ftp' wget = Tree(self.tempdir) # Expects new attribute + original data diff --git a/tests/unit/test_smoke.py b/tests/unit/test_smoke.py index c41ce6a..22e2dc1 100644 --- a/tests/unit/test_smoke.py +++ b/tests/unit/test_smoke.py @@ -8,13 +8,21 @@ class TestSmoke: - """ Smoke Test """ + """ + Smoke Test + """ def test_smoke(self): - """ Smoke test """ + """ + Smoke test + """ + fmf.cli.main("fmf ls", WGET) def test_output(self): - """ There is some output """ + """ + There is some output + """ + output = fmf.cli.main("fmf ls", WGET) assert "download" in output diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index cb9a363..7f3005d 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -16,7 +16,9 @@ class TestFilter: - """ Function filter() """ + """ + Function filter() + """ def setup_method(self, method): self.data = { @@ -25,26 +27,38 @@ def setup_method(self, method): } def test_invalid(self): - """ Invalid filter format """ + """ + Invalid filter format + """ + with pytest.raises(utils.FilterError): filter("status:proposed", self.data) with pytest.raises(utils.FilterError): filter("x: 1", None) def test_empty_filter(self): - """ Empty filter should return True """ + """ + Empty filter should return True + """ + assert filter(None, self.data) is True assert filter("", self.data) is True def test_name_missing(self): - """ Node name has to be provided when searching for names """ + """ + Node name has to be provided when searching for names + """ + with pytest.raises(utils.FilterError): filter("/tests/core", self.data) with pytest.raises(utils.FilterError): filter("/tests/one | /tests/two", self.data) def test_name_provided(self): - """ Searching by node names """ + """ + Searching by node names + """ + # Basic assert filter("/tests/one", self.data, name="/tests/one") is True assert filter("/tests/two", self.data, name="/tests/one") is False @@ -61,7 +75,10 @@ def test_name_provided(self): assert filter("/.*/one", self.data, name="/tests/one", regexp=True) is True def test_basic(self): - """ Basic stuff and negation """ + """ + Basic stuff and negation + """ + assert filter("tag: Tier1", self.data) is True assert filter("tag: mod:S", self.data) is True assert filter("tag: -Tier2", self.data) is True @@ -74,7 +91,10 @@ def test_basic(self): assert filter("category: -Sanity", self.data) is False def test_operators(self): - """ Operators """ + """ + Operators + """ + assert filter("tag: Tier1 | tag: Tier2", self.data) is True assert filter("tag: Tier1 | tag: mod:S", self.data) is True assert filter("tag: mod:X | tag: mod:S", self.data) is True @@ -90,7 +110,10 @@ def test_operators(self): assert filter("tag: Tier2 | category: Regression", self.data) is False def test_sugar(self): - """ Syntactic sugar """ + """ + Syntactic sugar + """ + assert filter("tag: Tier1, Tier2", self.data) is True assert filter("tag: Tier2, mod:S", self.data) is True assert filter("tag: Tier1, TIPpass", self.data) is True @@ -100,7 +123,10 @@ def test_sugar(self): assert filter("tag: Tier2, Tier3", self.data) is False def test_regexp(self): - """ Regular expressions """ + """ + Regular expressions + """ + assert filter("tag: Tier.*", self.data, regexp=True) is True assert filter("tag: Tier[123]", self.data, regexp=True) is True assert filter("tag: mod:[XS]", self.data, regexp=True) is True @@ -108,19 +134,27 @@ def test_regexp(self): assert filter("tag: -Tier.*", self.data, regexp=True) is False def test_case(self): - """ Case insensitive """ + """ + Case insensitive + """ + assert filter("tag: tier1", self.data, sensitive=False) is True assert filter("tag: tippass", self.data, sensitive=False) is True def test_unicode(self): - """ Unicode support """ + """ + Unicode support + """ + assert filter("tag: -ťip", self.data) is True assert filter("tag: ťip", self.data) is False assert filter("tag: ťip", {"tag": ["ťip"]}) is True assert filter("tag: -ťop", {"tag": ["ťip"]}) is True def test_escape_or(self): - """ Escaping the | operator """ + """ + Escaping the | operator + """ # Escaped assert filter(r"category: (Sanity\|Security)", self.data, regexp=True) is True @@ -133,7 +167,10 @@ def test_escape_or(self): assert filter(r"tag: Tier(1|2)", self.data, regexp=True) is True def test_escape_and(self): - """ Escaping the & operator """ + """ + Escaping the & operator + """ + self.data["text"] = "Q&A" # Escaped @@ -147,7 +184,9 @@ def test_escape_and(self): class TestPluralize: - """ Function pluralize() """ + """ + Function pluralize() + """ def test_basic(self): assert utils.pluralize("cloud") == "clouds" @@ -156,7 +195,9 @@ def test_basic(self): class TestListed: - """ Function listed() """ + """ + Function listed() + """ def test_basic(self): assert listed(range(1)) == '0' @@ -176,7 +217,9 @@ def test_text(self): class TestSplit: - """ Function split() """ + """ + Function split() + """ def test_basic(self): assert utils.split('a b c') == ['a', 'b', 'c'] @@ -185,7 +228,9 @@ def test_basic(self): class TestLogging: - """ Logging """ + """ + Logging + """ def test_level(self): for level in [1, 4, 7, 10, 20, 30, 40]: @@ -203,7 +248,9 @@ def test_smoke(self): class TestColoring: - """ Coloring """ + """ + Coloring + """ def test_invalid(self): with pytest.raises(RuntimeError): @@ -220,7 +267,9 @@ def test_color(self): class TestCache: - """ Local cache manipulation """ + """ + Local cache manipulation + """ def test_clean_cache_directory(self, tmpdir): utils.set_cache_directory(str(tmpdir)) @@ -238,7 +287,9 @@ def test_set_cache_expiration(self): @pytest.mark.web class TestFetch: - """ Remote reference from fmf github """ + """ + Remote reference from fmf github + """ def test_fetch_default_branch(self): # On GitHub 'main' is the default @@ -360,7 +411,10 @@ def test_env(self): @pytest.mark.parametrize("ref", ["main", "0.10", "8566a39"]) def test_out_of_sync_ref(self, ref): - """ Solve Your branch is behind ... """ + """ + Solve Your branch is behind ... + """ + repo = utils.fetch_repo(GIT_REPO, ref) out, err = run(["git", "rev-parse", "HEAD"], repo) old_ref = out @@ -476,10 +530,15 @@ def test_force_cache_fetch(self, monkeypatch, tmpdir): class TestDictToYaml: - """ Verify dictionary to yaml format conversion """ + """ + Verify dictionary to yaml format conversion + """ def test_sort(self): - """ Verify key sorting """ + """ + Verify key sorting + """ + data = dict(y=2, x=1) assert fmf.utils.dict_to_yaml(data) == "y: 2\nx: 1\n" assert fmf.utils.dict_to_yaml(data, sort=True) == "x: 1\ny: 2\n"