10
\$\begingroup\$

I need to display a path within a GUI application. Displaying the whole path would be an overkill, so I made a custom path shortener.

def cut_path(path):
    ''' This function takes a path and outputs the path cut with the ... after the 
     the first folder after the first character and completes the name with the filename or the last folder
    Example: 
    C:/Users/Mauro/Desktop/DenoiseAvg/DenoiseAverage/test_cut_path.py
    C:/Users/.../test_cut_path.py
     '''      
    endidx = 5
    char = path[endidx]    
    while char != '/' and endidx < len(path):
        endidx += 1
        char = path[endidx]

    inpath = path[:endidx + 1 ]

    rpath = path[::-1]    
    idx = 1   
    char = rpath[idx]    
    while char != "/" and idx < len(rpath):
        idx += 1
        char = rpath[idx]

    endpath = path[len(path) - idx - 1 : ]

    outpath = inpath + "..." + endpath
    return outpath

Pastebin for syntax highlighting

Is there a shorter, more readable, pythonic way to do it? Is there a way to limit how many characters the path would be long? How would you do it?

\$\endgroup\$
  • \$\begingroup\$ Why not use “…” (U+2026, horizontal ellipsis) instead of “...”? \$\endgroup\$ – Konrad Rudolph Jul 14 '17 at 14:35
19
\$\begingroup\$

Python is 'batteries included', this means offloading code to the core library is a good thing, and often possible. In this case, if you're running Python 3.4+ you could make use of pathlib. All you need is to know the pathlib.PurePath constructor and PurePath.parts.

And so you can change your code to:

import pathlib

def cut_path(path):
    parts = list(pathlib.PurePath(path).parts)
    if len(parts) >= 4:
        parts [2:-1] = ['...']
    return pathlib.PurePath(*parts)

However, as you're doing it on how long the path is, you could dynamically build the new path. And so could use something like:

def cut_path(path, max_size=5):
    if not path:
        return path

    parts = list(pathlib.PurePath(path).parts)

    path = pathlib.PurePath(parts[0])
    for part in parts[1:-1]:
        path /= part
        if len(str(path)) >= max_size:
            path /= '...'
            break
    if len(parts) > 1:
        path /= parts[-1]
    return path

If however you're using Python 2 or Python <3.4 then you can't use the nice new interface, and would have to use the old interface, os.path. Which doesn't support splitting and joining the paths very well. First you may want to use os.path.normcase to normalize the path. Which may destroy the path if you have a Posix path on Windows. After this you'd want to take the drive with os.path.splitdrive, and then split the rest of the parts using os.sep. Then you want to do the same as above, and replace the middle section of the path, and finally os.path.join them all together.

Which could leave you with something like:

def cut_path(path):
    path = os.path.normcase(path)
    drive, path = os.path.splitdrive(path)
    parts = path.split(os.sep)
    parts[0] = drive + parts[0]
    # Join plays funny, so you have to add a separator in these cases.
    if parts[0][-1] == ':':
        parts[0] += os.sep
    if len(parts) >= 4:
        parts [2:-1] = ['...']
    return os.path.join(*parts)
\$\endgroup\$
  • \$\begingroup\$ Though I prefer your solution. I would point out it generated a different result to the original code. Yours has a minimum of 4 segments. The original had a minimum of 4 characters. \$\endgroup\$ – Martin York Jul 14 '17 at 16:00
  • \$\begingroup\$ @LokiAstari Oh my bad. Thank you for telling me, I'll look into fixing that now, :) \$\endgroup\$ – Peilonrayz Jul 14 '17 at 16:04
5
\$\begingroup\$

Rather than hardcoding a / it would be more os agnostic to use os.path.sep for the separator.

\$\endgroup\$
3
\$\begingroup\$

No I don't think that is idiomatic python (though I am not the perfect person to answer the question).

I think it is more idiomatic to split the string (into an array). Then use the parts of the array to build the result.

There also seems to be a bug in your for short paths:

cut_path("c:/a/a.y") 

This is how I would do it

def cut_path2(path):
    segments = path.split('/')
    siz = 0
    pat = 0
    output = ""

    # Your code assumes the prefix must be at least 
    # 5 characters long I record this in siz
    while (siz < 5) and (pat < len(segments)):
        output += segments[pat]
        output += "/"
        siz += len(segments[pat]) + 1
        pat += 1

    # You don't want to shorten if the
    # the first loop prints the whole path.
    if pat < (len(segments)):
        output += ".../"
    if pat <= (len(segments)):
        output += segments[len(segments) - 1]    
    return output

It tested this with the unit test you provided but I can guarantee that it will work for all paths.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.