2

I have a code that checks the st_mode of a file:

self.assertEqual(16877, os.stat(my_directory).st_mode)

Only old school unix experts are able to decipher the integer value 16877 fluently.

Is there more readable way to check for exactly this value?

sbeliakov
  • 2,169
  • 1
  • 20
  • 37
guettli
  • 25,042
  • 81
  • 346
  • 663
  • Perhaps, depending on what you mean by "readable". The code you have in your question is hiding a lot of things behind the scenes. For example: `os.stat(my_directory)` returns an `os.stat_return()` object which has several properties, including `st_mode`. As far as demystifying `16877` you could assign the value to a variable that is named appropriately, but that's about all you could do, as the `os.stat_return()` object's `st_mode` property will always be a numeric value. – Alea Kootz Sep 21 '15 at 16:07
  • Does your system set and check this state? Do you have a function which verifies the state? Could you use that in the assertion? Do you have a name for this state in your system (e.g. owner-only)? – Peter Wood Sep 23 '15 at 13:31
  • @PeterWood yes, my system sets this state. Yes, it checks this state. That is the above line. There is not function to verify this. But a test which contains the above line. There is no name up to now. But good idea. A name for the state would be even better. Thank you – guettli Sep 23 '15 at 15:14
  • You're very welcome. About the check: I don't mean in your tests, I mean does your system check, e.g. before or after doing some operation. How does the system refer to it? I know it's `0o40755` or `16877` but giving it a [ubiquitous name](http://www.jamesshore.com/Agile-Book/ubiquitous_language.html) would help everyone. – Peter Wood Sep 23 '15 at 15:51
  • @PeterWood Up to now I would call it "default umask". – guettli Sep 23 '15 at 17:11

4 Answers4

6

You can use defined constats. They can be found in stat (docs). They are:

stat.S_IRUSR for owner read permission,

stat.S_IWUSR for owner write permission,

stat.S_IXUSR for owner execute permission,

stat.S_IRGRP for group read permission,

stat.S_IWGRP for group write permission,

stat.S_IXGRP for group execute permission,

stat.S_IROTH for others read permission,

stat.S_IWOTH for others write permission,

stat.S_IXOTH for others execute permission,

stat.S_IFDIR for directory.

They can be combined using bitwise or |. Then your code can look like:

import stat
import os

permissions = (stat.S_IFDIR | 
               stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
               stat.S_IRGRP | stat.S_IXGRP |
               stat.S_IROTH | stat.S_IXOTH)
self.assertEqual(permissions, os.stat(my_directory).st_mode)
sbeliakov
  • 2,169
  • 1
  • 20
  • 37
3

Here's some information about st_mode from the manual for stat, quoted below.

       S_IFMT     0170000   bit mask for the file type bit field
       S_IFSOCK   0140000   socket
       S_IFLNK    0120000   symbolic link
       S_IFREG    0100000   regular file
       S_IFBLK    0060000   block device
       S_IFDIR    0040000   directory
       S_IFCHR    0020000   character device
       S_IFIFO    0010000   FIFO
       S_ISUID      04000   set-user-ID bit
       S_ISGID      02000   set-group-ID bit (see below)
       S_ISVTX      01000   sticky bit (see below)
       S_IRWXU      00700   owner has read, write, and execute permission
       S_IRUSR      00400   owner has read permission
       S_IWUSR      00200   owner has write permission
       S_IXUSR      00100   owner has execute permission
       S_IRWXG      00070   group has read, write, and execute permission
       S_IRGRP      00040   group has read permission
       S_IWGRP      00020   group has write permission
       S_IXGRP      00010   group has execute permission
       S_IRWXO      00007   others (not in group) have read, write, and
                            execute permission
       S_IROTH      00004   others have read permission
       S_IWOTH      00002   others have write permission
       S_IXOTH      00001   others have execute permission

These are all octal numbers.

oct(16877) is equivalent to 0o40755. This means that the following bits are active:

  • 40000, or S_IFDIR: directory
  • 700, which means that owner can read, write, and execute
  • 50, which means that the group can read and execute, but not write.
  • 5, which means that the world can read and execute, but not write.

(755 is the mode of the directory.)

Some things you might be able to do:

  1. Link the stat manpage in the documentation, or add the codes I pasted. Then, you can use

    self.assertEqual(0o40755, os.stat(my_directory).st_mode)
    

    instead, which means exactly the same thing, but it becomes easier to debug.

  2. You could use the ctypes module to implement the bitfield yourself. The python wiki has more information. (Search for "Bit field".)
  3. You could put 16877 or 0o40755 (and the other outputs) as the value of a constant variable, perhaps global.

    DIR_755 = 0o40755
    

    Then, you could do:

    self.assertEqual(DIR_755, os.stat(my_directory).st_mode)
    

    to check that the mode is correct.

  4. You could make a dictionary of the codes and values:

    st_mode_vals = {
        "S_IFMT": 0170000,  # Or instead, "filetype_bitmask"
        "S_IFSOCK": 0140000,  # Or instead, "socket"
        ...
        "S_IXOTH": 0o1  # Or instead, "other_x"
    }
    

    You would then be able to define them as:

    DIR_755 = st_mode_vals["directory"] +\
              st_mode_vals["usr_rwx"] +\
              st_mode_vals["grp_r"] + st_mode_vals["grp_x"] +\
              st_mode_vals["oth_r"] + st_mode_vals["oth_x"]
    self.assertEqual(DIR_755, os.stat(my_directory).st_mode)
    

I would personally use #1 (link to documentation), with #3 if there are certain codes that are reused often. For all of them, maybe you can add a comment indicating the value in decimal, too?

edit: I did not see the answer that was added before I posted. I had no idea what the stat module did, so I didn't think of it. Either way, I think I would still use #1 and maybe #3. What he wrote, however, would definitely replace #4 (the dictionary) as another option, and would indeed be much better.

Jean Nassar
  • 555
  • 1
  • 5
  • 13
  • Thank you for your opinion and options provided. However I'd like to share some thoughts about it. Usually using library constants is better approach than defining yours. Here's why: first, why to redefine what already exists? Second, if something changes (let's imagine that new version of unix has new permission constants, or os.stat started to use own format, not unix one) then in case of system/library constants the code will still work, but if you use your own constants then you'll have to change them in code, or even worse you'll miss it and will have hard to find bugs or security leaks. – sbeliakov Sep 21 '15 at 18:15
  • That is something I hadn't considered. I'd thought that option #1 was the best because it seemed to me the most understandable for most programmers, compared to S_IFDIR etc. That is also the reason I gave alternative names in the dictionary. But I see your point in maintainability, and concede. Thank you. – Jean Nassar Sep 21 '15 at 18:30
2

If I may extend the question a bit and understand it as “Is there more readable way to check file modes?”, then I'll suggest adding a custom assertion. The target:

self.assertFileMode(my_directory, user="rwx", group="rx", others="rx")

How to do it.

Let's put that assertion in a mixin:

import os
import stat

class FileAssertions(object):
    FILE_PERMS = {
        'user': {'r': stat.S_IRUSR, 'w': stat.S_IWUSR, 'x': stat.S_IXUSR, 's': stat.S_ISUID},
        'group': {'r': stat.S_IRGRP, 'w': stat.S_IWGRP, 'x': stat.S_IXGRP, 's': stat.S_ISGID},
        'others': {'r': stat.S_IROTH, 'w': stat.S_IWOTH, 'x': stat.S_IXOTH},
    }

    def assertFileMode(self, path, **kwargs):
        mode = os.stat(path).st_mode
        for key, perm_defs in self.FILE_PERMS.items():
            expected = kwargs.pop(key, None)
            if expected is not None:
                actual_perms = mode & sum(perm_defs.values())
                expected_perms = sum(perm_defs[flag] for flag in expected)

                if actual_perms != expected_perms:
                    msg = '{key} permissions: {expected} != {actual} for {path}'.format(
                        key=key, path=path,
                        expected=''.join(sorted(expected)),
                        actual=''.join(sorted(flag for flag, value in perm_defs.items()
                                              if value & mode != 0))
                    )
                    raise self.failureException(msg)
        if kwargs:
            raise TypeError('assertFileMode: unknown arguments %s' % ', '.join(kwargs))

Using it

Now, how about we test some file modes?

# We use our mixin
class MyTestCase(FileAssertions, TestCase):
    def test_some_paths(self):
        # Test all permissions
        self.assertFileMode('/foo/bar', user='rwx', group='rx', others='')

        # Only test user permissions
        self.assertFileMode('/foo/bar', user='rwx')

        # We support the suid/sgid bits as well
        self.assertFileMode('/foo/bar', user='rwxs', group='rxs', others='rx')

Example output:

AssertionError: user permissions: rw != rwx for /foo/bar

Notes:

  • Only permissions given to the method are tested. To test that no permissions exist, pass an empty string.
  • Most of the complexity comes from generating a user-friendly message.
  • Permissions are sorted alphabetically in the error messages, so they are easier to eyeball-compare.
  • To keep it simple, I did not handle testing the sticky bit.
spectras
  • 13,105
  • 2
  • 31
  • 53
  • Yes, this is a great re-usable solution. Would be nice to have it as pip installable library. – guettli Sep 27 '15 at 06:01
  • Thanks. I believe there is indeed some room for a python package that would add a bunch of useful — may I say semantic — assertions, especially about file manipulation and content, temporary directories, … Getting it right with proper documentation and thorough testing is too big an endeavor for myself at the moment though. – spectras Sep 27 '15 at 19:07
  • By the way, I hereby place the code in my answer under public domain. Do whatever you want with it. Credit appreciated but not mandatory. – spectras Sep 27 '15 at 20:11
1
self.assertEqual(16877, os.stat(my_directory).st_mode)

Only old school unix experts are able to decipher the integer value 16877 fluently.

Actually, old-school unix experts would probably be in the same boat, since such things are displayed in octal.

I would:

  • switch to octal for specifying the mode
  • add the text representation and a comment

Something like this:

# 'my_directory' should be a directory with full permissions for user and
# read/execute permissions for group and other
# drwxr-xr-x
self.assertEqual(0o40755, os.stat(my_directory).st_mode)
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237