pyinfra icon indicating copy to clipboard operation
pyinfra copied to clipboard

Fix file content check to handle missing files

Open wowi42 opened this issue 3 months ago • 7 comments

Fix #1454

  • [X] Pull request is based on the default branch (3.x at this time)
  • [X] Pull request includes tests for any new/updated operations/facts
  • [X] Pull request includes documentation for any new/updated operations/facts
  • [X] Tests pass (see scripts/dev-test.sh)
  • [X] Type checking & code style passes (see scripts/dev-lint.sh)

wowi42 avatar Sep 25 '25 13:09 wowi42

Not sure if the discussion is better in #1454 or here.

I am not sure this is entirely correct now. Take the case where the file does exist but is empty. For instance something like: touch somefile. This is also a None now. Should it be?


Also, I think this would be better:

    def process(self, output):
        if not output:
            return self.default()
        return output

... if I understood the logic of FactBase correctly.

NichtJens avatar Sep 25 '25 15:09 NichtJens

@NichtJens You are right, fixed it

wowi42 avatar Sep 26 '25 07:09 wowi42

@wowi42 Does this already take my other question into account?

Take the case where the file does exist but is empty. For instance something like: touch somefile. This is also a None now. Should it be?

NichtJens avatar Sep 26 '25 07:09 NichtJens

Nope, I was reading too quickly. I'm unsure how to differentiate between no file and an empty file. Perhaps we should involve @Fizzadar for his input.

wowi42 avatar Sep 26 '25 08:09 wowi42

Yes, this is a problem. Actually, it's exactly why I didn't propose a PR similar to yours and just opened an issue instead. I should probably have specfied what I had tested already. Sorry about that.


Btw., from my experiments, you can go back to the old:

def process(self, output):
    return output

... and it works the same as your current code. The magic happens in get_fact, I think.


In fact, the reason I am asking is since I have the same problem with what I am currently working on. This was also the reason I posted the issue in the first place. I need to update the contents of a json file on the remote side. For this, I wanted a fact that gets the json contents and came up with this:

class JSONFileContents(files.FileContents):
    """
    Returns the contents of a JSON file as dict.
    Returns an empty dict if the file does not exist or cannot be parsed as JSON.
    """

    default = dict

    def command(self, path):
        return make_formatted_string_command("cat {0} || true", QuoteString(path))

    def process(self, output):
        output = super().process(output)
        output = "\n".join(output)
        try:
            output = json.loads(output)
        except json.decoder.JSONDecodeError:
            return self.default()
        return output

This is very similar to yours, except I just kept the cat error message in. I don't think that hurts.


I think these two changes make the PR very minimal (single line change, plus tests). But I am still not sure what the desired behavior really is.

NichtJens avatar Sep 26 '25 11:09 NichtJens

The expected behaviour is:

  • if no file, return None
  • if empty file, return ""

The FindInFile fact solved this same problem already: https://github.com/pyinfra-dev/pyinfra/blob/3.x/src/pyinfra/facts/files.py#L431. Basically we echo a specific string if the file does not exist which we can then use to return the None vs empty string.

Fizzadar avatar Oct 01 '25 14:10 Fizzadar

@wowi42 heads up ^ - also happy to finish this up if you don't mind giving me edit access.

Fizzadar avatar Dec 08 '25 17:12 Fizzadar

@Fizzadar my bad, I will do it

wowi42 avatar Dec 16 '25 15:12 wowi42