Bobby
Bobby

Reputation: 1482

Python: Elegent way to get an iterable of all parents in a path

Working with a path-based resource system, the application needs to find the first working resource that manages a given resource based on paths. I need a succinct, pythonic way to generate the following:

Input:

/house/dogs/ralph/bone

Output:

/house/dogs/ralph/bone
/house/dogs/ralph
/house/dogs
/house

Note: It's okay to use os.path or similar built-ins, but these are not filesystem resources. Output can be anything iterable (list, set, generator, etc.).

Upvotes: 4

Views: 2272

Answers (3)

Mad Physicist
Mad Physicist

Reputation: 114440

Use pathlib. PurePaths offer an abstract interface to path-like objects that have no relationship to the file system. In particular, PurePosixPath is the kind that uses forward slashes (/) as separators:

>>> from pathlib import PurePosixPath
>>> p = PurePosixPath('/house/dogs/ralph/bone')
>>> str(p.parent)
/house/dogs/ralph
>>> str(p.parent.parent)
/house/dogs

You can loop this easily:

p = PurePosixPath(...)
while str(p) != p.root:
    # Do stuff to p
    p = p.parent

A fairly pythonic finishing touch would be to make it a generator:

def receding_path(p):
    p = PurePosixPath(p)
    while str(p) != p.root:
        yield str(p)
        p = p.parent

for item in receding_path('/house/dogs/ralph/bone'):
    # do stuff to each item

Upvotes: 6

Jeffrey Froman
Jeffrey Froman

Reputation: 6623

Something of a combination of the previous two answers:

import pathlib
import os
def resources(path):
  parts = pathlib.Path(path).parts
  for n in range(len(parts), 1, -1):
    yield os.path.join(*parts[:n])

Upvotes: 2

pault
pault

Reputation: 43524

One way would be to split the string on "/" and take successive slices.

in_string = "/house/dogs/ralph/bone"
s = in_string.split("/")
out_strings = list(filter(None, ("/".join(s[:i+1]) for i in range(len(s)))))
print(out_strings)
#['/house', '/house/dogs', '/house/dogs/ralph', '/house/dogs/ralph/bone']

The filter(None, ...) is used to remove empty strings.

Or reverse the range if you want the output in the order you specified in your post:

out_strings = list(filter(None, ("/".join(s[:i]) for i in range(len(s), 0, -1))))
print(out_strings)
#['/house/dogs/ralph/bone',
# '/house/dogs/ralph',
# '/house/dogs',
# '/house']

Upvotes: 1

Related Questions