[Python-3000] Path Reform: Get the ball rolling

Talin talin at acm.org
Wed Nov 1 09:17:25 CET 2006


More comments...

Mike Orr wrote:
> Talin wrote:
>> 1) Does os.path need to be refactored at all?
> 
> Yes.  Functions are scattered arbitrarily across six modules: os,
> os.path, shutil, stat, glob, fnmatch.  You have to search through five
> scattered doc pages in the Python library to find your function, plus
> the os module doc is split into five sections.  You may think
> 'shlutil' has to do with shells, not paths.  shutil.copy2 is
> riduculously named: what's so "2" about it?  Why is 'split' in os.path
> but 'stat' and 'mkdir' and 'remove' are in os?  Don't they all operate
> on paths?
> 
> The lack of method chaning means you have to use nested functions,
> which must be read "inside out" rather than left-to-right like paths
> normally go. Say you want to add the absolute path of "../../lib" to
> the Python path in a platform-independent manner, relative to an
> absolute path (__file__):
> 
>     # Assume __file__ is "/toplevel/app1/bin/main_program.py".
>     # Result is "/toplevel/app1/lib".
>     p = os.path.join(os.path.dirname(os.path.dirname(__file__)), "lib")
> 
> PEP 355 proposes a much easier-to-read:
> 
>     # The Path object emulates "/toplevel/app1/bin/main_program.py".
>     p = Path(__file__).parent.parent.join("lib")

Actually I generally use:

       p = os.path.normpath( os.path.join( __file__, "../..", "lib" ) )

or even:

       p = os.path.normpath( os.path.join( __file__, "../../lib" ) )

...which isn't quite as concise as what you wrote, but is better than 
the first example. (The reason this works is because 'normpath' doesn't 
know whether the last component is a file or a directory -- it simply 
interprets the ".." as an instruction to strip off the last component.)

What I'd like to see is a version of "join" that automatically 
simplifies as it goes. Lets call it "combine":

       p = os.path.combine( __file__, "../..", "lib" )

or:

       p = os.path.combine( __file__, "../../lib" )

That's even easier to read than any of the above versions IMHO.

> Noam Raphael's directory-component object would make this even more
> straightforward:
> 
>     # The Path object emulates ("/", "toplevel", "app1", "bin",
> "main_program.py")
>     p = Path(__file__)[:-2] + "lib"

I don't know if I would describe this as 'straightforward'. 'Concise', 
certainly; 'terse', yes, and 'clever'. But also 'cute', and 'tricky'. I 
see a couple of problems with it:

-- Its only intuitive if you remember that array elements are path 
components and not strings. In other words, if you attempt to "read" the 
[:-2] as if the path were a string, you'll get the wrong answer.

-- Is path[ 0 ] a string or a path? What if I really do want to get the 
first two *characters* of the path, and not the first to components? Do 
I have to say something like:

    str( path )[ :2 ]

> Stat handling has grown cruft over the years. To check the modify time
> of a file:
> 
>     os.path.getmtime("/FOO")
>     os.stat("/FOO").st_mtime
>     os.stat("/FOO")[stat.ST_MTIME]  # List subscript, deprecated usage.
> 
> If you want to check whether a file is a type for which there is no
> os.path.is*() method:
> 
>     stat.S_ISSOCK( os.stat("/FOO").st_mode )  # Is the file a socket?
> 
> Compare to the directory-component proposal:
> 
>     Path("/foo").stat().issock

This is the part I really don't like. A path is not a file.

Imagine that if instead of paths we were doing SQL queries. Take 
SQLObject for example; say we have a table of addresses:

    Address.select( query_string )

Now, suppose you say that you want to be able to perform manipulations 
on the query string, and therefore it should be an object. So we'll 
define a new class, SQLQuery( string ):

    Address.select( SQLQuery( string ) )

And we will allow queries to be conjoined using boolean operators:

    Address.select( SQLQuery( string ) | SQLQuery( string2 ) )

Nothing controversial so far - this is the way many such systems work 
already.

But now you say "Well, since SQLQuery is an object, it would be more 
elegant to have all of the query-related functions be methods of the 
query object." So for example, if you wanted to run the query string 
against the Address table, and see how many records came back, you would 
have to do something like:

    SQLQuery( string ).select( Address ).count()

..which is exactly backwards, and here's why: Generally when creating 
member functions of objects, you arrange them in the form:

    actor.operation( subject )

Where 'actor' is considered to be the 'active agent' of the operation, 
while the 'subject' is the passive input parameter.

I would argue that both paths and query strings are passive, whereas 
tables and file systems are, if not exactly lively, at least more 
'actor-like' than paths or queries.

Now, that being said, I wouldn't have a problem with there being an 
"abstract filesystem object" that represents an entity on disk (be it 
file, directory, etc.), which would have a path inside it that would do 
some of the things you suggest.

> os.path functions are too low-level.  Say you want to recursively
> delete a path you're about to overwrite, no matter whether it exists
> or is a file or directory.  You can't do it in one line of code, darn,
> you gotta write this function or inline the code everywhere you use
> it:
> 
>     def purge(p):
>         if os.path.isdir(p):
>             shutil.rmtree(p)       # Raises error if nonexistent or
> not a directory.
>         elif os.path.exists():
>             # isfile follows symlinks and returns False for special
> files, so it's
>             # not a reliable guide of whether we can call os.remove.
>             os.remove(p)          # Raises error if nonexistent or a directory.
>         if os.path.isfile(p):  # Includes all symlinks.
>             os.remove(p)

I don't deny that such a function ought to exist. But it shouldn't be a 
member function on a path object IMHO.

>> 2) is there anything that the existing os.path *won't do* that we desperately need it to do?
> 
> For filesystem files, no.  Though you really mean all six modules
> above and not just os.path.  It has been proposed to support
> non-filesystem directories (zip files, CSV/Subversion sandboxes, URLs,
> FTP objects) under a new Path API.
> 
>>  3) Assuming that the answer to #1 is "yes", the next question is:
> "evolution or revolution?"
> 
> Revolution.  It needs a clean new API.  However, this can live
> alongside the existing functions if necessary:  posixpath.PosixPath,
> path.Path, etc.
> 
>> 4) My third question is: Who are we going to steal our ideas from?
> Boost, Java, C# and others - all are worthy of being the, ahem, target
> of our inspiration. Or we have some alternative that's so cool that it
> makes sense to "Think Different(ly)"?
> 
> Java is the only one I'm familiar with.  The existing Python proposals
> are summarized below.
> 
>> 5) Must there be one ring to rule them all? I suggested earlier that we
> might have a "low-level" and a "high-level" API, one built on top of the
> other. Is this a good idea, or a non-starter?
> 
> It's worth discussing.  One question is whether the dichotomy does
> anything useful or just adds unnecessary complexity.  But that can
> only be answered for a specific API proposal.  Whatever we do will be
> "low-level" compared to third-party extensions that will be built on
> top of it, so we should plan for extensibility.

Actually, I was considering the PEP 355 to be "high-level" and the 
current os.path to be "low-level".

-- Talin


More information about the Python-3000 mailing list