This is the first of a set of three posts aimed at showing how to create a full featured Python3 project: since the topic is quite massive, I decided to split it into three different posts. In this post we do not only quickly see how to develop a full featured Python application, since I wanted to do something that shows a lot of things, such as:

  • creating Python objects
  • put the custom Python objects inside a Python package within the scope of our own namespace
  • develop accordingly to encapsulation rules, by implementing getters and setters methods that look like regular attributes by exploiting decorators
  • use the standard Python logging facility, configuring everything with an external settings file
  • altering the __eq__ comparison so to consider two objects as equal when one of their attribute has the same value
  • implementing comparison methods and the __iter__ method, so to be able to use Python standard functions such as sorted() to sort it also in reverse order
  • exploit total_ordering to make an object fully sortable

The next parts of this post will be on the following topics

The operating environment used in this post is Red Hat Enterprise Linux 8 or CentOS 8 - using a different environment may lead to having to adapt or even change things.

Designing

Never code anything without design it first, so let's start from the requisites:

I need an application that can store a list of items so that:

  • items must have a name
  • the list must be iterable using the "classic" pythonic syntax for the items in the list
  • the list must support a flag to set it to store only unique items or allow duplicates
  • the list must be sortable by the name of the items using Python standard function such as "sorted()"
  • it should implement logging by using Python logging facilities

the fields of the list item can of course be extended later on.

The outcome is developing the following Python classes:

  • FoolistItem
  • Foolist

and pack them together into the "foolist" Python package.

The best practice is to define a container namespace: this avoids name collisions with packages with the same name developed by other parties. To meet this recommendation, we put the "foolist" package inside the "carcano" namespace.

As for the application itself, we call it "fooapp.py".

Create the Workspace

The very first thing to do is creating the workspace for the project - simply create a directory with the name of the project:

mkdir ~/fooproject

this is the root directory of the project; let's change directory to it:

cd ~/fooproject

here we must create the directory tree of the project: lets' start from the root directory for the directory tree containing the Python sources

mkdir src

application files such as "fooapp.py" must be put inside the "bin" directory within "src" directory.

Let's create this directory as follows:

mkdir src/bin
We created the  "src" directory to keep all the sources together: as we'll see in the next posts, we'll create other directories at the same level of the "src" directory, such as "RPM": they serve to different purposes: "src" contains code, whereas "RPM" will contain manifest and other stuff to create the RPM package used to wrap everything up and have a convenient deliverable unit .

Foolist Python Package

Create the Namespace

Creating our custom namespace is as easy as creating a directory beneath "src"

mkdir src/carcano
This is not our case, but if you'd need to split your packages across more than one namespace, you should repeat the previous step for any of the namespaces you want to create.

Create the Package

Creating a Python package requires just to create the related directory with the name of the package itself: since our package is "foolist", and it is contained into the "carcano" namespace, we must create it within "carcano" directory:

mkdir src/carcano/foolist

then we must create an empty "__init__.py" file beneath "src/carcano/foolist" directory - it is said "empty" since it has no directives out of the initial line and a few metadata:

# __init__.py
__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'
These metadata dunders are used to specify the author of the package as well as the version of the package and are specific to the package: each file that implements a class that is imported by the package must have its own metadata that refers to the author of the class and the version of the class.

Add Objects to the Package

Now that we have created the package, we must add FoolistItem and Foolist objects to it: since the best practice is to have a dedicated file to each of the objects, we put the FoolistItem object into "foolistitem.py" file and the Foolist object into "foolist.py" file.

FoolistItem object

FoolistItem is the object we have to implement first: for your convenience I put the code here as a whole, and we'll discuss every interesting snippet one by one. So create "src/carcano/foolist/foolistitem.py" file with the following contents:

__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

import functools


@functools.total_ordering
class FoolistItem():
    """
    Object used as Item into Foolist class
    """
    _name = ""
    _enabled = False
    _next = None

    def __init__(self, name, enabled=False):
        """
        Initalize an Item assigning a name. You can optionally assign a value
        to the "enabled" boolean attribute, that defaults to False

        :param name: the name to assign to this FoolistItem Object
        :param enabled: a flag to mark this FoolistItem Object as enabled
                        or not
        """
        self._name = name
        self._enabled = enabled
        self._next = None

    def __str__(self):
        """
        Implements the representation of the Item

        :returns: pretty-print of this FoolistItem Object
        """
        return f"Name: {self._name}, Enabled: {self._enabled}"

    def __repr__(self):
        """
        Implements the representation of the Item

        :returns: an unambiguous representation of this FoolistItem Object
        """
        return f"Id: {id(self)}, Name: {self._name}, Enabled: {self._enabled}"

    @property
    def next(self):
        """
        a reference to the next Item when used into a list
        """
        return self._next

    @next.setter
    def next(self, node):
        self._next = node

    @property
    def name(self):
        """
        the name of this FoolistItem Object
        """
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def enabled(self):
        """
        wether or not this FoolistItem Object is enabled
        """
        return self._enabled

    @enabled.setter
    def enabled(self, enabled):
        self._enabled = enabled

    def __lt__(self, other):
        """
        Lower_than comparison implementation
        """
        return self.name < other.name

    def __eq__(self, other):
        """
        Equal_to comparison implementation
        """
        if other is not None:
            return (self.name) == (other.name)
        else:
            return (self.name) is None

"carcano.foolist" Python package is implemented by "src/carcano/fooolist/__init__.py" file: when developing applications that will use it, developers  will import the contents of "carcano.foolist" package using statements such as "from carcano.foolist import * " or "import carcano.foolist". To let this kind of statements work, we must add the import directive within the __init__.py file it so to have "foolistitem.py" contents automtically imported:

# __init__.py
__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

from .foolistitem import FoolistItem

We can now discuss the interesting snippets of "foolistitem.py" file:

Metadata information

The very first lines of "src/carcano/fooolist/foolistitem.py"  file provide metadata information to FoolistItem object:

__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

Despite the package has its own metadata - the ones into "src/carcano/fooolist/__init__.py" file - we must give metadata information of the objects provided by the package within each of the files that implement them: they are used by pydoc utility so that each object can have a different author and can be individually versioned.

Definition of the class

The very first thing with Python classes is declaring it along with its attributes:

class FoolistItem():
    """
    Object used as Item into Foolist class
    """
    _name = ""
    _enabled = False
    _next = None

as you see FoolistItem class comes with the following attributes:

  • _name
  • _enabled
  • _next

the leading underscore of the name of the attributes is used as a hint to let the other developers know that the attributes should be considered as "private".

Python does not have a "strong" distinction from "public" and "private". Conversely from other programming languages, Python does not enforce private by preventing access to variables with a leading underscore in their names from outside the class: it only avoids importing them. The convention is defined in PEP-8.

Note that right after the row that marks the definition of the class there are some comment lines: these are in docstring format and are used by pydoc utility to automatically pretty-print the documentation page of the class.

Python docstrings can be written using several formats, however the most popular is  reStructuredText (reST), that by the way is also the one recommended by PEP-287

The following snippet defines how "FoolistItem" object gets initialized:

    def __init__(self, name, enabled=False):
        """
        Initalize an Item assigning a name. You can optionally assign a value
        to the "enabled" boolean attribute, that defaults to False

        :param name: the name to assign to this FoolistItem Object
        :param enabled: a flag to mark this FoolistItem Object as enabled
                        or not
        """
        self._name = name
        self._enabled = enabled
        self._next = None
__init__ is the method used to initialize the instance of the object after it has been created: if you need to control the creation itself of the object you should use the  __new__ method. However be wary that Python automatically creates new objects using a Factory - roughly put, you have an object that creates other objects. Implementing the __new__ method is not a "clean" way of doing and you must avoid it unless you cannot do otherwise.
String Representation Method

although this is not mandatory, FoolistItem also implements the __str__ method: this is the method Python uses to pretty-print the instance of the class:

    def __str__(self):
        """
        Implements the representation of the Item

        :returns: pretty-print of this FoolistItem Object
        """
        return f"Name: {self._name}, Enabled: {self._enabled}"
Unique Representation Method

although this is not mandatory, FoolistItem also implements the __repr__ method: this is the method Python uses to uniquely render the instance of the class:

    def __repr__(self):
        """
        Implements the representation of the Item

        :returns: an unambiguous representation of this FoolistItem Object
        """
        return f"Id: {id(self)}, Name: {self._name}, Enabled: {self._enabled}"
__repr__ is the method used to uniquely represent the object: it should render it in a way that allows it to identify it without doubt. If you want to implement how the object is pretty-printed using a human readable format, you should use __str__ method instead. Stringer uses __str__method to to represent the object as a string, and fall-backs to __repr__ if __str__ has not been implemented.
Property and Setter decoratos

The most straightforward way to prevent runtime errors is avoiding to have values that do not make sense passed to the arguments of functions and methods; we can easily achieve this in Python by defining setter methods: these are methods that are used to set the attributes of the class, ensuring that the passed values make sense with the code that should process them within the other methods of the class.

The only problem with a setter method, is that a developer may not see that there's a setter or simply prefer to directly set an attribute, effectively circumventing the fence.

To overcome this we can use the @property decorator: when a method is preceded by the @property decorator, Python calls the property() built-in function that creates and returns a property object.

The syntax of this function is as follows:

property(fget=None, fset=None, fdel=None, doc=None)

where:

  • fget is the function that returns the value of the attribute
  • fset is the function that sets the value of the attribute
  • fdel is the function that deletes the attribute
  • doc is the string

If you are already familiar with Python decorators, you certainly are thinking that the syntax is suitable to be used with Python decorators. And it is like so indeed.

So for example:

    @property
    def name(self):
        """
        the name of this FoolistItem Object
        """
        return self._name

returns the property object that provides read access to the contents of self._name private property.

Note how even here we put a comment right after the declaration of the method, so to have pydoc utility to automatically pretty-print the documentation of the property in the documentation page of the class.

As for the setter method:

    @name.setter
    def name(self, name):
        self._name = name

this provides access to the function that sets the contents of self._name private property.

The same applies to self._enabled and self._next private attributes, that are bound to enabled and next attributes derived using property and setter decorators

Making the object Sortable

Making the object sortable just requires to implement only the __lt__ (lower than) method:

    def __lt__(self, other):
        """
        Lower_than comparison implementation
        """
        return self.name < other.name

a sortable object can be easily appended to a list and then used with Python standard sorting method. The following snippet is an example of how easily you can make a list of sortable objects leveraging on the  built-in "list" object type :

from carcano.foolist import *


l = []
l.append(FoolistItem("y", True))
l.append(FoolistItem("c", False))

print(l)
print(sorted(l))
print(sorted(l, reverse=True))
In this post we implement a custom list objet called "foolist". We are doing this so to learn how to create a list object with additional features to the standard list object provided by Python. Here for example we add logging statements, but you can exploit this design pattern to have it doing anything you need.
Full Ordering using total_ordering

In this tutorial I wasn't satisfied of just having FoolistItem object sortable: I also wanted to show you how easily you can exploit total_ordering from functools so to achieve full ordering. Total_ordering is a Python module that takes care of guessing all of the other method you do not implement by implementing higher-order functions - higher-order functions are dependent functions that call other functions. The only requisites that should be satisfied to have this working is implementing:

  • the eq method
  • any of the methods
    • lt (less than)
    • le (less than or equal to)
    • gt (greater than)
    • ge (greater than or equal to)

So, the very first thing to do to exploit total_ordering is import it from functools, as by this snippet

import functools


@functools.total_ordering

since I already implemented the __lt__ (lower than) method, I only had to implement __eq__ (equal) method:

    def __eq__(self, other):
        """
        Equal_to comparison implementation
        """
        if other is not None:
            return (self.name) == (other.name)
        else:
            return (self.name) is None
The goal of this method is having Python considering equal two different Foolistitem instances with the same name: we need this since we want to use FoolistItem as the item object of Foolist object, and Foolist object should be able to consider Items with the same name as equal so to support the "unique" constraint (if set).

Foolist object

The last object we have to implement is Foolist: just create "src/carcano/foolist/foolist.py" file with the following contents:

__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

from .foolistitem import FoolistItem

import logging
"""
initialize logger to NullHandler
"""
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())


class Foolist():
    """
    Object used to implement a list of FoolistItem
    """
    _unique = False

    def __init__(self):
        """
        Initialize the list by setting its head to None
        """
        self.head = None
        self._unique = False

    @property
    def unique(self):
        """
        if true, set the list so to contain only unique items
        """
        return self._unique

    @unique.setter
    def unique(self, isunique):
        log.debug('Package: '+__name__+', setting unique='+str(isunique))
        self._unique = isunique

    def __iter__(self):
        """
        Iterate throughout the list
        """
        node = self.head
        while node is not None:
            yield node
            node = node._next
        raise StopIteration

    def append(self, name, enabled=False):
        """
        Append a FoolistItem to the list

        :param name: the name to assign to the FoolistItem
        :param enabled: a flag to mark it as enabled or not
        """
        log.debug('Package: ' + __name__ +
                ',Method: append, Params: name="' + name +
                '", enabled: ' + str(enabled))
        if self.head is None:
            self.head = FoolistItem(name, enabled)
            return
        if self._unique is True:
            tmp_node = FoolistItem(name, enabled)
        for current_node in self:
            if self._unique is True and current_node == tmp_node:
                log.debug('Package: ' + __name__ +
                        ',Method: append, Msg: skipping since FoolistItem is already present')
                return
            pass
        current_node.next = FoolistItem(name, enabled)

    def remove(self, name):
        """
        Removes an item from the list

        :param name: the name of the FoolistItem to remove
        """
        log.debug('Package: '+__name__+', Method: remove, Params: name="'+name)
        for f in self:
            if f.name == name:
                if f == self.head:
                    self.head = f._next
                else:
                    hold._next = f._next
                del f
                break
            else:
                hold = f

    def __str__(self):
        """
        Implements the representation of the list
        """
        nodes = []
        for node in self:
            nodes.append(str(node))
        return str(nodes)


    def __repr__(self):
        """
        Implements the representation of the list
        """
        nodes = []
        for node in self:
            nodes.append(repr(node))
        return str(nodes)        

same way as we did before, we must add to "src/carcano/fooolist/__init__.py" file the import statement necessary to import  Foolistobject from "foolist.py" file into "carcano.foolist" Python package: it should now it should looks like as follows:

# __init__.py
__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

from .foolistitem import FoolistItem
from .foolist import Foolist

as we did before, we also discuss every interesting snippet.

Importing FoolistITem object

Same way as FoolistItem object, "foolist.py" file begins with author and version metadata: 

__author__ = "Marco Antonio Carcano"
__version__ = '1.0.0'

from .foolistitem import FoolistItem

this time, since Foolist instantiates Foolistitem objects, we also have to the import directive to load the definition of Fooitem class.

Implementing logging using Python logging facilities

One of my objective in this post was also to show how easy is implement logging leveraging on Python's standard logging facility: a good implementation of logging eases debugging when troubleshooting - if you implement logging using standard facilities, besides saving your time, you let also others to save their time,  since they do not have to wonder how do they have to configure logging to have it working your own way:

import logging
"""
initialize logger to NullHandler
"""
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())

besides importing the logging package, we get the logger of Foolist Python object and add NullHandler as the log handler: by doing so we are disabling logging by default, unless the developer or the user do not decide to enable it.

Definition of the class

As before, ... this defines the Foolist class itself

class Foolist():
    """
    Object used to implement a list of FoolistItem
    """
    _unique = False

Note the "_unique" attribute: it is the one used to alter the behavior of the class - if set to false, it lets to store also duplicate FoolistItem objects, otherwise it accepts them only once.

The object gets initialized by processing the __init__() method.

    def __init__(self):
        """
        Initialize the list by setting its head to None
        """
        self.head = None
        self._unique = False

The initialization method simply sets the head of the linked list to None, marking the list as empty

This is the snippet that implements the setter and getter methods for "unique" attribute:

    @property
    def unique(self):
        """
        if true, set the list so to contain only unique items
        """
        return self._unique

    @unique.setter
    def unique(self, isunique):
        log.debug('Package: '+__name__+', setting unique='+str(isunique))
        self._unique = isunique

Here we make use of the standard logging facility for the first time: when the severity is raised to DEBUG, the value of "unique" attribute gets logged.

String Representation Method

although this is not mandatory, Foolist also implements the __str__ method: this is the method Python uses to pretty-print the instance of the class:

    def __str__(self):
        """
        Implements the representation of the list
        """
        nodes = []
        for node in self:
            nodes.append(str(node))
        return str(nodes)
Unique Representation Method

although this is not mandatory, Foolist also implements the __repr__ method: this is the method Python uses to uniquely render the instance of the class:

    def __repr__(self):
        """
        Implements the representation of the list
        """
        nodes = []
        for node in self:
            nodes.append(repr(node))
        return str(nodes)
Making the Class iterable

An iterator is an object that implements the iterator protocol: that is an object that implements the following methods:

  • __iter__ returns the iterator object itself
  • __next__ returns the next element

An iterator object can be used to iterate a collection: when the last item gets reached, the iterator becomes exhausted and then the StopIteration exception is raised, preventing the iterator object from being used again.

The requisite to use an iterator to iterate an object is that that object should be iterable: this means that it should implement the __iter__ method, that returns an iterator.

The following snippet is the implementation of __iter__ method of Foolist class:

    def __iter__(self):
        """
        Iterate throughout the list
        """
        node = self.head
        while node is not None:
            yield node
            node = node._next
        raise StopIteration

I want to remark the use of the yeld directive, that returns the content to the caller but does not return from the function, continuing until the end is reached..

Append and Remove item methods

Since we are implementing a list, this is the append() method

        def append(self, name, enabled=False):
        """
        Append a FoolistItem to the list

        :param name: the name to assign to the FoolistItem
        :param enabled: a flag to mark it as enabled or not
        """
        log.debug('Package: ' + __name__ +
                ',Method: append, Params: name="' + name +
                '", enabled: ' + str(enabled))
        if self.head is None:
            self.head = FoolistItem(name, enabled)
            return
        if self._unique is True:
            tmp_node = FoolistItem(name, enabled)
        for current_node in self:
            if self._unique is True and current_node == tmp_node:
                log.debug('Package: ' + __name__ +
                        ',Method: append, Msg: skipping since FoolistItem is already present')
                return
            pass
        current_node.next = FoolistItem(name, enabled)

in these few lines:

  • it logs a message when severity is DEBUG (lines 8-10)
  • if the list is empty, it adds the new object at the head of the list (lines 11-13)
  • if Foolist has been set to store only unique items, it creates a temporary Foolist item to be used for comparisons with already existing items (lines 14-15)
  • it scrolls the list from the head to the end (line 16)
  • if unique is set to true, and any of the nodes in the list is equal - (that is has the same name) to the temporary node created at line 15, then an error message of DEBUG severity is logged and the method returns
  • if otherwise the end of the list is actually reached, a new FooItem node is created  as the next of the last item of the list (line 22)

As for the remove() method:

    def remove(self, name):
        """
        Removes an item from the list

        :param name: the name of the FoolistItem to remove
        """
        log.debug('Package: '+__name__+', Method: remove, Params: name="'+name)
        for f in self:
            if f.name == name:
                if f == self.head:
                    self.head = f._next
                else:
                    hold._next = f._next
                del f
                break
            else:
                hold = f

here the code:

  • it logs a message when severity is DEBUG (line 7)
  • it iterates the list to its end (line 8)
  • if the name of the current node does not match the name of the node to delete, a reference to the current node is stored in the hold variable (lines 16-17), otherwise
    • if the node is the head of the list, it moves the head to the next node (lines 10-11)
    • otherwise the next of the hold variable (that is the previous node) is set as the next to the current node (lines 12-13)
  • the current node is eventually deleted (line 14)

Implementing unit tests

As the common sense should  suggest, the best practice while developing objects requires to provide unit tests to run at build time: this is one of my goals with this post, so let's create the directory for the file with the tests:

mkdir src/test

now create a "src/test/test_foolist.py" file with the following contents:

#!/usr/bin/env python3
import unittest
from collections.abc import Iterable
from carcano.foolist import *


class Foolist(unittest.TestCase):
    os_list = Foolist()

    def testFoolistIsIterable(self):
        self.assertIsInstance(self.os_list, Iterable)

    def testFoolistAppend(self):
        self.os_list.append('RedHat', True)
        self.os_list.append('CentOS', False)
        self.os_list.append('Suse', True)
        self.assertIn(FoolistItem('CentOS', False), self.os_list)

    def testFoolistRemove(self):
        self.os_list.remove('CentOS')
        self.assertNotIn(FoolistItem('CentOS', False), self.os_list)


if __name__ == '__main__':
    unittest.main()

we use the "unittest" Python package to define a Foolist class to test both Foolist and FoolistItem objects.

These tests:

  • verify that instances of Foolit are iterable (line 11)
  • add a few FoolistItem objects to the list (lines 14-16)
  • make sure that a FoolistItem with name "CentOS" is actually among the elements of the list (17)
  • remove the FoolistItem with name "CentOS" from the list and ensure it is not beneath the list elements anymore (lines 20-21)

Now we configure it for using nosetools: a framework that extends the Python' unittest module. I like to have it a little verbose, so to see it tracing tests as they run: it reads its configuration from "src/setup.cfg" file, so let's create it with the following contents:

[nosetests]
verbosity=2
detailed-errors=1

since the goal is also to package everything, we need also setuptools: let's create a minimal "src/setup.py" with only settings relevant to work with nosetools:

from setuptools import setup

setup(
    setup_requires=['nose>=1.0'],
    test_suite="nose.collector",
    tests_require=["nose"],
)

since nose can be installed via DNF, let's do it "the distro way":

sudo dnf install -y python3-nose

We are now ready to launch the unit tests. We only have to change to "src" directory and then issue:

python3 src/setup.py nosetests >/dev/null

the output should be as follows:

testFoolistAppend (test_foolist.Foolist) ... ok
testFoolistIsIterable (test_foolist.Foolist) ... ok
testFoolistRemove (test_foolist.Foolist) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK

this is a very basic usage of setuptools, ... we'll explore it better in the next post.

As you can see all of the three tests (testFoolistAppend, testFoolistIsIterable, testFoolistRemove) succeeded.

Thoroughly explaining Python's unit test facilities is out of the scope of this post: I just wanted to provide you with a working example you can use for your reference. If you want to learn more, here is the official documentation.

Create an application

We are eventually ready to develop the "fooapp.py" driver application.

Change to the "bin" directory - the one that has the purpose to contain the applications:

cd src/bin

since our application should import the "carcano.foolist" package, but the package is not among the paths specified by "sys.path", we must create a sym-link to "../carcano" directory to have Python able to scan it for packages to load. This is a convenient trick especially while testing and building things:

ln -s ../carcano carcano
You can see the current value of "sys.path" using the command "python -m site"

This is not our case, but if you'd need to split your packages across more than just one namespace, you must repeat the previous step for any of the namespaces you want to create.

Change directory back to the root of the project:

cd ../..

Now create "src/bin/fooapp.py" file with the following contents:

#!/usr/bin/env python3
from carcano.foolist import *

import os
import logging
import logging.config

log_config_paths = [
    os.path.dirname(os.path.realpath(__file__))+'/logging.conf',
    '/etc/fooapp/logging.conf']

log_enabled = False

for log_config_file in log_config_paths:
    if os.path.isfile(log_config_file):
        log_enabled = True
        break
if log_enabled is True:
    logging.config.fileConfig(
        fname=log_config_file,
        disable_existing_loggers=False
    )
    logger = logging.getLogger(__name__)
    logging.info(__file__+': started')

os_list = Foolist()
os_list.unique = True
os_list.append('RedHat', True)
os_list.append('Suse', True)
os_list.append('CentOS', False)
os_list.append('CentOS', False)
print("Print the list:")
for os in os_list:
    print(os)
print("Print the list in ascending order:")
for os in sorted(os_list):
    print(os)
os_list.remove('CentOS')
os_list.append('Rocky Linux', False)
print("Print the list - after the removal of CentOS:")
for os in os_list:
    print(os)

print("Print the list in descending order:")
for os in sorted(os_list, reverse=True):
    print(os)
if log_enabled is True:
    logging.info(__file__+': finished')

this very simple application creates and populates an instance of the Foolist object; besides this:

  • it disables logging by default (line 12)
  • it enables logging if it finds a "logging.conf" settings file (lines 14-17). The file is seek in two different paths (lines 8-10)
  • if logging has been enabled, it initializes the Python standard logging facility (lines 19-23) and logs a message of severity "INFO" (line 24)

Note how the foolist instance "os_list" has been set so not to accept duplicates (line 27)

The rest of the application:

  • adds some values to os_list (lines 28-31)
  • removes one of the values (line 38)
  • prints the whole list as it is (lines 32-34)
  • prints the list sorted (lines 35-37)
  • prints the list in descending order (lines 44-46)

Note how, since we made Foolist object iterable, we are able to iterate on it by the mean of a simple for loop.

We must of course grant the application the POSIX execution permission, so to be able to run it

chmod 755 src/bin/fooapp.py

let's have a go:

cd src/bin
./fooapp.py

the output should be as follows:

Print the list:
Name: RedHat, Enabled: True
Name: Suse, Enabled: True
Name: CentOS, Enabled: False
Print the list in ascending order:
Name: CentOS, Enabled: False
Name: RedHat, Enabled: True
Name: Suse, Enabled: True
Print the list - after the removal of CentOS:
Name: RedHat, Enabled: True
Name: Suse, Enabled: True
Name: Rocky Linux, Enabled: False
Print the list in descending order:
Name: Suse, Enabled: True
Name: Rocky Linux, Enabled: False
Name: RedHat, Enabled: True

since both the objects and the application supports Pyhton logging facilities, let's create the file containing the logging settings "src/bin/logging.conf":

[loggers]
keys=root,foolistitem,foolist

[handlers]
keys=console,syslog

[formatters]
keys=jsonFormatter

[logger_root]
level=INFO
handlers=syslog
propagate=0

[logger_foolistitem]
level=INFO
handlers=syslog
qualname=carcano.foolist.foolistitem
propagate=0

[logger_foolist]
level=DEBUG
handlers=syslog
qualname=carcano.foolist.foolist
propagate=0

[handler_null]
class = logging.NullHandler
formatter = default
args = ()

[handler_console]
class=StreamHandler
level=DEBUG
formatter=jsonFormatter
args=(sys.stderr,)

[handler_file]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=jsonFormatter
args=('/var/log/carcano/fooapp.log','a',1024,50)

[handler_syslog]
class = handlers.SysLogHandler
args = ('/dev/log', handlers.SysLogHandler.LOG_LOCAL6)
level=DEBUG
formatter=jsonFormatter

[formatter_jsonFormatter]
format={"time": "%(asctime)s", "logger": "%(name)s", "level": "%(levelname)s", "pid": "%(process)d", "src": "%(pathname)s", "line": "%(lineno)d", "msg": "%(message)s"}

since loggers have been configured to use local6 syslog facility, it is certainly nice to provide a sample config file that can be used as a hint or even as an include to configure rsyslog so to log messages of local6 within "/var/log/fooapp.log" file:

create the directory where to store the file

mkdir -p src/share/doc/fooapp/rsyslog

then create the "src/share/doc/fooapp/rsyslog/fooapp.conf" file with the following contents:

local6.*                                                /var/log/fooapp.log

this file can be copied to "/etc/rsyslog.d" directory to reconfigure rsyslog - of course rsyslog should be reloaded so as to apply the configuration.

Footnotes

Here it ends the first of this trilogy of posts dedicated to Python: I hope you enjoyed it. You now know how to structure a full-featured Python project, that is made of a package within the scope of your own namespace. We did all of this by developing a simple but not obvious object that showed you how to exploit advanced Python features such as using the standard logging facility, exploiting decorators, total_ordering, making the object iterable and sortable so as to be able to manage it using standard Python functions. Last but not least, we also saw how to document using comments and pydoc.

In the next post we have a closer look at Python modules, install packages, distribution packages, how to create them using setuptools and how to push everything to a PyPI compatible repository. Don't miss it.

I hate blogs with pop-ups, ads and all the (even worse) other stuff that distracts from the topics you're reading and violates your privacy. I want to offer my readers the best experience possible for free, ... but please be wary that for me it's not really free: on top of the raw costs of running the blog, I usually spend on average 50-60 hours writing each post. I offer all this for free because I think it's nice to help people, but if you think something in this blog has helped you professionally and you want to give concrete support, your contribution is very much appreciated: you can just use the above button.

5 thoughts on “Python Full Featured Project

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>