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 second one is about delivering the object as a Python package
- the third and last post Packaging a Python Wheel as RPM is about how to pack this project into two RPM packages, also seeing how to digitally sign these RPM packages, and in general how to set up an RPM development environment, with a good overview on how to exploit the RPM building tools
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
Foolist Python Package
Create the Namespace
Creating our custom namespace is as easy as creating a directory beneath "src"
mkdir src/carcano
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'
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".
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.
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
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}"
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.
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))
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
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.
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
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.
invain says:
Great tutorial! I learned a lot from this post.