Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore metadata in hidden files and directories #264

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/concept.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ collisions between similar attributes. For example:
* test_description, requirement_description


.. _trees:

Trees
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -99,6 +101,28 @@ contains at least a ``version`` file with a single integer number
defining version of the format.


.. _config:

Config
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, all hidden files are ignored when exploring metadata
on the disk. If a specific file or directory should be included in
the search, create a simple config file ``.fmf/config`` with the
following format:

.. code-block:: yaml

explore:
include:
- .plans
- .tests

In the example above files or directories named ``.plans`` or
``.tests`` will be included in the discovered metadata. Note that
the ``.fmf`` directory cannot be used for storing metadata.


Names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ directive::
/:
inherit: false


.. _merging:

Merging
Expand Down
11 changes: 10 additions & 1 deletion docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
Releases
======================

fmf-1.4

fmf-1.5.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The fmf :ref:`trees` can now be built from hidden files and
directories as well. Use a simple :ref:`config` file to specify
names which should be included in the search.


fmf-1.4.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

New :ref:`merging<merging>` suffixes ``~`` and ``-~`` can be used
Expand Down
3 changes: 3 additions & 0 deletions examples/hidden/.fmf/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
explore:
include:
- .plans
1 change: 1 addition & 0 deletions examples/hidden/.fmf/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
4 changes: 4 additions & 0 deletions examples/hidden/.plans/basic.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
discover:
how: fmf
execute:
how: tmt
40 changes: 37 additions & 3 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import subprocess
from io import open
from pathlib import Path
from pprint import pformat as pretty
from typing import Any, Dict, Optional, Protocol

Expand Down Expand Up @@ -71,6 +72,7 @@ def __init__(self, data, name=None, parent=None):
self.data = dict()
self.sources = list()
self.root = None
self.config = {}
self.version = utils.VERSION
self.original_data = dict()
self._commit = None
Expand Down Expand Up @@ -98,6 +100,7 @@ def __init__(self, data, name=None, parent=None):
# Handle child node creation
else:
self.root = self.parent.root
self.config = self.parent.config
self.name = os.path.join(self.parent.name, name)

# Update data from a dictionary (handle empty nodes)
Expand Down Expand Up @@ -145,7 +148,8 @@ def __str__(self):
return self.name

def _initialize(self, path):
""" Find metadata tree root, detect format version """
""" Find metadata tree root, detect format version, check for config """

# Find the tree root
root = os.path.abspath(path)
try:
Expand All @@ -159,6 +163,7 @@ def _initialize(self, path):
raise utils.FileError("Invalid directory path: {0}".format(root))
log.info("Root directory found: {0}".format(root))
self.root = root

# Detect format version
try:
with open(os.path.join(self.root, ".fmf", "version")) as version:
Expand All @@ -170,6 +175,16 @@ def _initialize(self, path):
except ValueError:
raise utils.FormatError("Invalid version format")

# Check for the config file
config_file_path = Path(self.root) / ".fmf/config"
try:
self.config = YAML(typ="safe").load(config_file_path.read_text())
log.debug(f"Config file '{config_file_path}' loaded.")
except FileNotFoundError:
log.debug("Config file not found.")
except YAMLError as error:
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 """

Expand Down Expand Up @@ -593,6 +608,21 @@ def child(self, name, data, source=None):
self.children[name].sources.append(source)
self.children[name]._raw_data = copy.deepcopy(data)

@property
def explore_include(self):
""" Additional filenames to be explored """
try:
explore_include = self.config["explore"]["include"]
if not isinstance(explore_include, list):
raise utils.GeneralError(
f"The 'include' config section should be a list, found '{explore_include}'.")
if ".fmf" in explore_include:
raise utils.GeneralError(
"The '.fmf' directory cannot be used for storing fmf metadata.")
except KeyError:
explore_include = []
return explore_include

def grow(self, path):
"""
Grow the metadata tree for the given directory path
Expand All @@ -613,16 +643,18 @@ def grow(self, path):
except StopIteration:
log.debug("Skipping '{0}' (not accessible).".format(path))
return

# Investigate main.fmf as the first file (for correct inheritance)
filenames = sorted(
[filename for filename in filenames if filename.endswith(SUFFIX)])
try:
filenames.insert(0, filenames.pop(filenames.index(MAIN)))
except ValueError:
pass

# Check every metadata file and load data (ignore hidden)
for filename in filenames:
if filename.startswith("."):
if filename.startswith(".") and filename not in self.explore_include:
continue
fullpath = os.path.abspath(os.path.join(dirpath, filename))
log.info("Checking file {0}".format(fullpath))
Expand All @@ -643,9 +675,10 @@ def grow(self, path):
# Handle other *.fmf files as children
else:
self.child(os.path.splitext(filename)[0], data, fullpath)

# Explore every child directory (ignore hidden dirs and subtrees)
for dirname in sorted(dirnames):
if dirname.startswith("."):
if dirname.startswith(".") and dirname not in self.explore_include:
continue
fulldir = os.path.join(dirpath, dirname)
if os.path.islink(fulldir):
Expand All @@ -665,6 +698,7 @@ def grow(self, path):
log.debug("Ignoring metadata tree '{0}'.".format(dirname))
continue
self.child(dirname, os.path.join(path, dirname))

# Ignore directories with no metadata (remove all child nodes which
# do not have children and their data haven't been updated)
for name in list(self.children.keys()):
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def test_basic(self):
def test_hidden(self):
""" 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 """
Expand Down
Loading