fspath package

enjoy in scripting

semantic path names and much more

return42/fspath@GitHub

contributed by return42

tired in os.path?

are you tired in juggling with …

parent_dir = os.path.abspath(
                 os.path.join(
                     os.path.dirname(__file__)
                     , os.path.pardir))

and all that bloody stuff? do you think this ..

parent_dir = FSPath(__file__).DIRNAME.ABSPATH / '..'

is much more readable .. than continue.

install

from PyPI

$ pip install [--user] fspath

or a bleeding edge installation from GitHub

$ pip install --user git+http://github.com/return42/fspath.git

Content

semantic path

>>> from fspath import FSPath
>>> tmp = FSPath('~/tmp')
>>> tmp
'/home/user/tmp'
>>> tmp.EXISTS
False

no additional import, no juggling with os.join(...)

simply slash / and foo.<method> calls

>>> [(tmp/x).makedirs() for x in ('foo', 'bar')]
True, True
>>> for n in tmp.listdir():
...     print(tmp / n)
...
/home/user/tmp/foo
/home/user/tmp/bar

behaves as expected

confused by makedirs ‘Changed in ..’?

>>> foo = tmp / 'foo'
>>> import os
>>> os.makedirs(foo) &&
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/os.py", line 241, in makedirs
  mkdir(name, mode)
  FileExistsError: [Errno 17] File exists:'/home/user/tmp/foo'

aargh, creates intermediate but raise if exists?!

>>> foo.makedirs()
False

FSPath behaves as expected :)

return of dispersed operations

tired in meaningless foo, foo2 and fooN functions?

def copyfile(self, dest, preserve=False):
   if preserve:
      shutil.copy2(self, dest)
   else:
      shutil.copy(self, dest)

you think delete means delete!

def delete(self):
    if self.ISDIR:
        self.rmtree()
    else:
        os.remove(self)

be expressive in daily use cases

just read my entire text file

readme = FSPath('README.txt').readFile()

open a path name with its associated desktop application

>>> FSPath('index.html').startFile() # opens HTML-browser showing
>>>
>>> FSPath('.').startFile()          # opens file-explorer at CWD

M$-Win has nativ support in Python. On Darwin and FreeBSD the open command is used. On other OS (e.g. Linux) the xdg-open is used.

be expressive in daily use cases

FSPath gives us prototypes with meaningful defaults

def openTextFile(self
                 , mode='rt', encoding='utf-8'
                 , errors='strict', buffering=1
                 , newline=None):

and without meaningless arguments

def openBinaryFile(self
                   , mode='rb', errors='strict'
                   , buffering=None):

if you have time, compare this with open

be expressive in daily use cases

just download and extract

>>> arch = foo / 'fspath.zip'
>>> url = 'https://github.com/return42/fspath/archive/master.zip'

.download – super easy download + segmentation + ticker

>>> arch.download(url, chunksize=1024, ticker=True)
/home/user/tmp/foo/fspath.zip: [87.9 KB][==============    ]  83%

.extract – extract in one step, no matter ZIP or TAR

>>> arch.ISTAR, arch.ISZIP
(False, True)
>>> arch.extract(foo)
['fspath-master/', 'fspath-master/.gitignore' ...

be expressive in daily use cases

.glob – shell like pattern in a single folder

>>> folder = foo / 'fspath-master'
>>> g_iter = folder.glob('*.py')
>>> type(g_iter), len(list(g_iter))
(<class 'generator'>, 1)

.reMatchFind – search files recursively by regexp

>>> rst_files = folder.reMatchFind(r'.*\.rst$')

example: change suffix of all ‘.rst’ files in your tree

>>> moved_files = [f.move(f.suffix('.txt')) for f in rst_files]

be expressive in daily use cases

.relpath – strip relative pathnames

>>> folder
'/home/user/tmp/foo/fspath-master'
>>> folder.relpath(tmp)
'foo/fspath-master'
>>> py_iter = folder.reMatchFind(r'.*\.py$', relpath=True)
>>> list(py_iter)
['setup.py', 'fspath/_which.py', 'fspath/win.py', ...]

.filesize – (human) readable file size

>>> arch.filesize()            # size in bytes (int)
91502
>>> arch.filesize(precision=3) # switch to human readable output
'89.357 KB'
>>> foo.filesize(precision=0)  # switch to human readable output
'12 MB'

be expressive in daily use cases

run executable without any rocket since

>>> proc = FSPath('arp').Popen('-a',)
>>> stdout, stderr = proc.communicate()
>>> retVal = proc.returncode

callEXE – synchronous call and capture all in one

>>> from fspath import callEXE
>>> out, err, rc = callEXE('arp', '-a', '192.168.1.120')
>>> print("out:'%s...' | err='%s' | exit code=%d"
          % (out[:24], err, rc))
out:'storage (192.168.1.120) ...' | err='' | exit code=0
>>> callEXE('arp', '-a', 'xyz')
('', 'xyz: Unknown host\n', 255)

more file & folder methods

  • .chdir – change current working dir to self

  • .walk – generate filenames of tree (see os.walk)

  • .delete – delete! .. no matter if file or folder

  • .rmtree – remove entire folder

  • .rmfile – remove single file

  • .copyfile – copy file (opt. with permission bits)

  • .copytree – recursively copy the entire tree

  • .filesize – Filesize in bytes or with precision

  • .suffix – return path name with new suffix

common class members

To be complete with path names.

>>> FSPath.getHOME()
'/home/user'
>>> FSPath.getCWD()
'/share/fspath/local'

FSPath.OS – shortcut to common OS properties

>>> pprint(FSPath.OS)
{'altsep'   : None       ,  'curdir'   : '.'  ,
 'extsep'   : '.'        ,  'linesep'  : '\n' ,
 'pathsep'  : ':'        ,  'sep'      : '/'  ,
 'devnull'  : '/dev/null',  'defpath'  : ':/bin:/usr/bin'
 }

file name suffix explained

>>> filename = FSPath('../path/to/folder/filename.ext')

dot is a part of the suffix

>>> filename.SUFFIX
'.ext'

change suffix in place

>>> filename.suffix('.rst')
'../path/to/folder/filename.rst'

or even throw it away

>>> filename.SKIPSUFFIX
'../path/to/folder/filename'

more file & folder properties

>>> filename.DIRNAME
'../path/to/folder'
>>> filename.BASENAME
'filename.ext'
>>> filename.FILENAME
'filename'
>>> filename.ABSPATH
'/share/fspath/local/path/to/folder/filename.ext'
>>> filename.REALPATH
'/share/fspath/path/to/folder/filename.ext'

more file & folder properties

>>> filename.NTPATH
'..\\path\\to\\folder\\filename.ext'
>>> filename.POSIXPATH
'../path/to/folder/filename.ext'

known from shell

>>> home = FSPath("$HOME")
>>> home
'$HOME'
>>> home.EXPANDVARS
'/home/user'
>>> home = FSPath("~/tmp")
>>> home.EXPANDUSERS
'/home/user'

more file & folder properties

  • .EXISTS – True if file/path name exist

  • .SIZE – Size in bytes

  • .READABLE – True if file/path is readable

  • .WRITEABLE – True if file/path is writeable

  • .EXECUTABLE – True if file is executable

  • .ISDIR – True if path is a folder

  • .ISFILE – True if path is a file

  • .ISABSPATH – True if path is absolute

  • .ISLINK – True if path is a symbolic link

  • .ISMOUNT – True if path is a mountpoint

more file & folder properties

  • .MTIME – last modification time

  • .ATIME – last access time

  • .CTIME – last change time

  • .ISZIP – True if path is a ZIP file

  • .ISTAR – True if path is a TAR archive file

the FSPath type

inheritance of unicode in Py2 and str in Py3

class FSPath(six.text_type):
     ...

constructor normalize without asking

>>> FSPath('goes/up/../and/../down')
'goes/down'

works with anyone who accept strings

>>> os.stat(FSPath.getHOME())
os.stat_result(st_mode=16877, st_ino=1966082, ...

the FSPath type

Take in mind, its a string type!

FSPath member call returns FSPath instances

>>> type(folder.splitpath()[-1])
<class 'fspath.fspath.FSPath'>

call of inherited string member returns string types

>>> type(folder.split(home.OS.sep)[-1])
<class 'str'>

OS_ENV

a singleton for the environment

environment variables are attributes

>>> from fspath import OS_ENV
>>> OS_ENV.SHELL
'/bin/bash'

you can get or set

>>> OS_ENV.TMP = '/tmp/xyz'
>>> FSPath('$TMP').EXPANDVARS
'/tmp/xyz'

OS_ENV

unknown environment request raises KeyError

>>> OS_ENV.XYZ
Traceback (most recent call last):
...
KeyError: 'XYZ'

use .get to avoid exceptions

>>> OS_ENV.get('XYZ', 'not defined')
'not defined'
>>> OS_ENV.get('XYZ')
>>>

Command Line Interface

a CLI with a pinch of sugar

# -*- coding: utf-8; mode: python -*-
# file: foobar/main.py

"""foobar CLI"""

import sys
from fspath import CLI

def main():
    cli = CLI(description=__doc__)
    # define CLI
    ...
    # run CLI
    cli()

CLI & setup

in projects setup.py add entry point for main()

setup(name = "foobar"
      ...
      , entry_points = {
          'console_scripts': [
              'foobar = foobar.main:main' ]}
      ...
      )

CLI's subcommands

implement a cli wrapper for each subcommand

def cli_hello(cliArgs):
    """another 'hello world'"""
    print('hello world')

cliArgs.folder we will be of type FSPath

def cli_listdir(cliArgs):
    """list directory"""
    for f in cliArgs.folder.listdir():
        l = f.upper() if cliArgs.upper else f
        f = cliArgs.folder / f
        if cliArgs.verbose:
            l = '[%10s] ' % (f.filesize(precision=0)) + l
        print(l, end = ('\n' if cliArgs.verbose else ', '))

CLI's subcommands

CLI is an argparse implementation.

def main():
    ...
    # define CLI
    hello   = cli.addCMDParser(cli_hello, cmdName='hello')
    listdir = cli.addCMDParser(cli_listdir, cmdName='dir')
    listdir.add_argument("folder", type = FSPath
                         , nargs = "?", default = FSPath(".")
                         , help = "path of the folder")
    listdir.add_argument("--upper", action = 'store_true'
                         , help = "convert to upper letters")

using type=FSPath for file and path name arguments gives us the power of FSPath (see cli_listdir(...))

CLI usage

the over all help

$ foobar --help
usage: foobar [-h] [--debug] [--verbose] [--quiet] \
              {hello, dir} ...

optional arguments:
  -h, --help  show this help message and exit
  --debug     run in debug mode (default: False)
  --verbose   run in verbose mode (default: False)
  --quiet     run in quiet mode (default: False)

commands:
  {hello,dir}
    hello      another 'hello world'
    dir        list directory

CLI subcommands usage

the subcommand --help

$ foobar dir --help

usage: foobar dir [-h] [--upper] [folder]

positional arguments:
  folder      path of the folder (default: .)

optional arguments:
  -h, --help  show this help message and exit
  --upper     convert to upper letters (default: False)

list directory

show how it works

run subcommand dir

$ foobar dir /
initrd.img.old, initrd.img, var, vmlinuz, home ...

and with global option verbose

$ foobar.py --verbose dir /
[     40 MB] initrd.img.old
[     40 MB] initrd.img
[      4 KB] var
[      7 MB] vmlinuz
[      4 KB] home
...

versioning scheme

As long as every new release is fully downward compatible a serial versioning is enough.

Version numbers follow scheme

YYYYMMDD

to be continued

there is much more to show .. in the meantime take a look at the

API docs

This slide show was build with the help of ..