Showing posts with label template. Show all posts
Showing posts with label template. Show all posts

Thursday, February 23, 2017

Object-Oriented JavaScript in the UI [2]

Today I'm going to start digging in to the implementation of the first couple of classes in the Jobs-UI object-structure. The classes I'm going to implement are some of the behind-the-scenes items, defining the data-structure of a job, and an object for keeping track of the collection of jobs, adding and removing jobs from that collection, and notifying other objects of changes to the collection. There is also some utility functionality that other classes, particularly the display-oriented ones, will need as well.

A JavaScript class-template

Given my recent attention to Python code-templates, it should maybe come as no great surprise that I also have one for JavaScript classes. I mentioned previously that there are several different ways to define classes in JavaScript, even without the new class and extends syntax-sugar. One of them uses the JavaScript prototype property. A reasonably-complete breakdown of that structure and some of it's permutations can be found on the w3schools website. That works reasonably well, but as far as I've been able to tell, there's no good/real way to emulate private properties and methods. The way that I generally define JavaScript classes, I find that I frequently want private members, so I use what might be called a function-based approach that I know supports them. That approach looks something like this:

function ClassName()
{
    /*
     * TODO: Describe the class (Represents a BLAH, or whatever)
     * 
     * argumentName .... TODO: Describe the argument
     */
    // Processing that needs to happen before definition, if any
    // Instance properties:
    //  + Public properties
//    this.publicName = null;
    //  + Private properties
//    var privateName = null;
    //  + Static properties
//    ClassName.staticName = null;
    // Initialize as needed based on incoming arguments
    // Instance methods:
    //  + public methods
//    this.publicMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    //  + private methods
//    function privateMethod()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Static methods of the instance
//    ClassName.staticMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Event-like methods of the instance
//    this.eventMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Processing that needs to happen before returning the instance, if any
    // Return the instance
    return this;
}
I've made this template-file available for download — The link is at the end of the post.

The UI Markup

The basic markup for the UI (without any styling) will provide a number of data-* attributes that the UI objects will use to detect elements that serve various purposes once the code executes:

<div id="ui-example">
    <fieldset id="currentJobList">
        <legend>Current Jobs [<span id="jobCounter">job-count</span>]</legend>
        <div>
            <span data-sort="number"><strong>Job Number</strong></span>
            <span data-sort="name"><strong>Job Title</strong></span>
            <span data-sort="contact"><strong>Job Contact</strong></span>
        </div>
        <div id="managedJobsList">
            <div data-template="true">
                <span data-field="number">number</span>
                <span data-field="name">name</span>
                <span data-field="contact">contact</span>
                <button data-action="remove">X</button>
            </div>
        </div>
        <div>
            <label > </label>
            <button data-jobaction="startJob">Add A New Job</button>
        </div>
        <fieldset id="newJobForm">
            <legend>Create a New Job</legend>
            <label>Job Number</label>
            <input name="number" type="text" data-jobfield="number"/>
            <br />
            <label>Job Title</label>
            <input name="title" type="text" data-jobfield="name"/>
            <br />
            <label>Job Contact</label>
            <input name="contact" type="text" data-jobfield="contact"/>
            <br />
            <label> </label>
            <button data-jobaction="createJob">OK</button>
            <button data-jobaction="cancelJob">Cancel</button>
        </fieldset>
    </fieldset>

    <fieldset id="jobNameList">
        <legend>All Jobs</legend>
        <div id="managedNameList">
            <div data-template="true" data-field="name">name</div>
        </div>
    </fieldset>

    <fieldset id="jobContactList">
        <legend>All Job Contacts</legend>
        <div id="managedContactlist">
            <div data-template="true" data-field="contact">contact</div>
        </div>
    </fieldset>
</div>

The data-* attributes in play are:

data-sort
Indicates that the element is a sort-control, and provides the name of the Job field that the control will sort by when it is clicked by the user.
data-template
Indicates that the element and its children are to be used as a markup template by the code, allowing the original markup to define what the structure of the display will look like when it's generated.
data-field
Indicates what field from a Job should be displayed in the element's content.
data-action and data-jobaction
Indicates that the element performs some sort of action in the final, rendered UI. The values of this attribute are tied to specific action sequences, with specific events associated, etc.
The idea of tagging items in the markup to control some aspect of the results of the code is something that experience has shown me to be very useful when a web-application has separate (and differently-focussed) developers for logic and design. Allowing a designer to take near-total control over the appearance of an application's UI requires more thought (and more code), but has almost always been a good time-investment.

There will also be some initialization code that creates the various UI-object instances and initializes them, but I've left that out for now. It will be included in the final file-set that I'll make available for download once this series of posts is complete.

Implementing the Job UI Classes

As noted in the previous post, there are seven different classes involved in this UI codebase. In the interests of keeping post-length down to something reasonable (and readable, I hope), I'm going to break the coverage of them out into several posts (three, I think). I'm also leaving all their comments in — even though that adds to the length of the post (maybe more than I'd like), it'll make it easier to discuss interesting and salient points about them, I hope.

The Job class

Job is a dumb data object that represents a single job in the UI. As such, it's mostly just object-properties and instance-representation (through the toString method).

function Job( parameters )
{
    /*
     * Represents a job.
     * 
     * parameters ...... (object, required) The job's data:
     *  +- number ...... (str, required) The job-number of the job;
     *  +- name ........ (str, required) The name of the job;
     *  +- contact ..... (str, required) The contact-of-record for the job
     */
    // Instance properties:
    //  + Public properties
    this.contact = parameters.contact;
    this.number = parameters.number;
    this.name = parameters.name;
    //  + Static properties
    if( typeof Job.fieldNames == 'undefined' )
    { Job.fieldNames = [ 'contact', 'name', 'number' ]; }
    // Instance methods:
    //  + public methods
    this.toString = function()
    {
        /*
         * TODO: Describe the method
         */
        return '<Job number="' + this.number + '" name="' + 
            this.name + '" contact="' + this.contact + '"/>';
    }
    // Return the instance
    return this;
}

Because a Job's properties are used to generate displayed content across several of the other objects in the UI code, they are all defined as public properties. Those property-values can be read and altered without restriction. In a live-system environment, I might spend some time trying to come up with some way to arrange things so that they could not be set once they were defined — in order to keep the UI code from being able to easily alter them on the fly. It would depend heavily on where those Job objects were coming from, though. If they were being provided in real time from a back-end web-service, for example, I would be much less concerned, since that would imply that the service was responsible for managing the data-integrity of the job-objects.

The JobManager class

The JobManager, as the name implies, keeps track of and (to some degree) manages a collection of Jobs, allowing the addition and removal of individual Job objects. It is also responsible for dispatching messages to other objects (the display-oriented ones) that those objects can use to update their content when a change in the managed Jobs has occurred. JobManager is the subject in an Observer pattern relationship with any number of display-component observers.

function JobManager( jobs )
{
    /*
     * Keeps track of the _jobs for the application/UI
     * 
     * _jobs ............ (array of objects, optional) The data of the _jobs 
     *                   to keep track of.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof JobManager.instance != 'undefined' )
    {
        //  Singleton-ish behavior: There can be only one, and it should live 
        //  in JobManager.instance if it exists, so return it
        return JobManager.instance;
    }
    // Instance properties:
    //  + Private properties
    var _displays = [];
    var _jobs = [];
    //  + Static properties
    // NOTE: JobManager.instance is set at the end of instantiation!
    // Instance methods:
    //  + public methods
    this.addDisplay = function( display )
    {
        /*
         * Adds a display to the instance's collection of _displays to be 
         * notified when the collection of _jobs changes
         */
        if ( typeof display.onJobsChanged == 'function' )
        { _displays.push( display ); }
    }
    this.addJob = function( job, updateNow )
    {
        /*
         * Adds a job to the instance's collection of _jobs
         * job ......... (object, required) The job to add to the collection
         * updateNow ... (bool, optional, defaults to true) Indicates whether 
         *               or not to refresh all job-_displays immediately upon 
         *               completion of adding the job.
         */
        if ( typeof updateNow == 'undefined' )
        { updateNow = true; }
        _jobs.push( new Job( job ) );
        if( updateNow )
        { this.jobsChanged(); }
    }
    this.compareJobs = function( job1, job2 )
    {
        /*
         * Compares two Job (or equivalent) objects, returning true if they have 
         * the same field-values, false otherwise
         */
        result = true;
        for( fi=0; fi<Job.fieldNames.length; fi++ )
        {
            field = Job.fieldNames[ fi ];
            try
            {
                if ( job1[ field ] != job2[ field ] )
                {
                    result = false;
                    break;
                }
            }
            catch( error )
            { result = false; }
        }
        return result;
    }
    this.getSortedJobs = function( sortKey, sortOrder )
    {
        /*
         * Gets a COPY of the main job-list (because each different display may 
         * need a different sort-order!), sorted by the sortField specified, in 
         * the order specified by sortOrder
         */
        // Make a copy of the entire current _jobs array
        results = _jobs.slice();
        // Sort it by the specified field-name using a callback 
        switch( sortKey )
        {
            // Define the callback based on the sort-field specified:
            case 'number':
                sortf = function( a, b )
                    {
                        if ( a.number < b.number )
                        { return -1; }
                        if ( a.number > b.number )
                        { return 1; }
                        return 0;
                    };
                    break;
            case 'name':
                sortf = function( a, b )
                    {
                        if ( a.name < b.name )
                        { return -1; }
                        if ( a.name > b.name )
                        { return 1; }
                        return 0;
                    };
                    break;
            case 'contact':
                sortf = function( a, b )
                    {
                        if ( a.contact < b.contact )
                        { return -1; }
                        if ( a.contact > b.contact )
                        { return 1; }
                        return 0;
                    };
                    break;
        }
        // Sort the results with the callback;
        results.sort( sortf );
        // Reverse the results if the sort-orer is "down"
        if ( sortOrder == 'down' )
        { results.reverse(); }
        return results;
    }
    this.removeDisplay = function( display )
    {
        /*
         * Removes a display from the instance's collection of _displays 
         * to be notified when the collection of _jobs changes
         */
        console.log( 'JobManager.removeDisplay( ' + display + ' ) called' );
        console.log( 'JobManager.removeDisplay complete' );
    }
    this.removeJob = function( job, updateNow )
    {
        /*
         * Removes a job from the instance's collection of _jobs
         */
        if ( typeof updateNow == 'undefined' )
        { updateNow = true; }
        new_jobs = [];
        for( ji=0; ji<_jobs.length; ji++ )
        {
            if ( ! this.compareJobs( _jobs[ ji ], job ) )
            { new_jobs.push( _jobs[ ji ] ); }
        }
        _jobs = new_jobs;
        if( updateNow )
        { this.jobsChanged(); }
    }
    //  + private methods
    // Event-like methods of the instance
    this.jobsChanged = function()
    {
        /*
         * Called when a job is added or removed, calls the onjobsChanged method 
         * of all registered _displays
         */
        for( di=0; di<_displays.length; di++ )
        { _displays[ di ].onJobsChanged(); }
    }
    // Processing that needs to happen before returning the instance, if any
    if ( jobs )
    {
        // Add all the jobs one by one, without changing the displays
        for ( ji=0; ji<jobs.length; ji++ )
        { this.addJob( jobs[ ji ], false ); }
    }
    // Store the instance in JobManager.instance, and return that
    JobManager.instance = this;
    return JobManager.instance;
}

I've defined JobManager as a Singleton, of sorts: There can be only one active instance of the class in a page, and any attempts to generate an instance will either create the first instance (which happens during the execution of the set-up code), or will return that first instance, already populated and active. You'll see that I leverage this behavior in several of the other classes in the codebase, allowing the page to create a JobManager and populate it with the Jobs for the page, then calling new JobManager() to set up a local reference to the original JobManager in instances of the other classes. The real instance is created and stored as a nominally-static property of the JobManager class (JobManager.instance at the end of the object's definition before being returned.

The two properties of JobManager, _displays and _jobs are private. I could not come up with any reason why any other object in this codebase would need to know anything about either of them, other than the need to acquire a sequence of Jobs for display purposes, and that has additional wrinkles that were better handled by a method (more on that later).

The methods of JobManager are, I think, pretty straightforward:

addDisplay( display ) and removeDisplay( display )
Allow the addition (and registration) and removal of observer display-management objects. Once added, any time the jobsChanged() method of the instance is called, all of the registered displays will be notified that they need to update because there's been a change in the Jobs that the JobManager is keeping track of.
addJob( job, updateNow ) and removeJob( job, updateNow )
Provide a mechanism for adding or removing a single Job from the collection of Jobs that the JobManager is keeping track of. The updateNow argument (which defaults to a true value) allows a developer to write code that adds a series of Jobs to the instance, then explicitly calling for an update (with jobsChanged(). That process, minus the explicit call to jobsChanged(), can be seen in the code at the end of the object-definition.
compareJobs( job1, job2 )
A helper-method (used in removeJob) that compares two objects (which may or may not be Job instances, but frequently are), returning true if both objects provided have the same values for their contact, name and number properties. In practice, I've found that one of the two objects being compared is frequently not a Job instance, though in those cases, it has always been a generic object with the same properties as a Job.
getSortedJobs( sortKey, sortOrder )
Returns a sequence of Job objects, sorted by the sortKey property-values of those objects, in the sortOrder direction. This was needed because each display-object in the codebase can have its own sort-criteria:
  • JobContactList will always sort by contact, ascending;
  • JobNameList will always sort by name, ascending; and
  • JobListView can sort by any field, in either direction
getSortedJobs, then, allows each of those display-objects to use their own separate sort-criteria and -order, no matter what the other objects are using.
jobsChanged()
Calls the onJobsChanged methods of all registered _displays.

In my next post, I'll tackle the various display-object classes: JobListView, JobContactList, JobNameList and JobCounter. Before I break for the day, though, here's a download-link for my JavaScript class-template file:

Thursday, February 16, 2017

Templating Other Entities

Today's post will be long from a scrolling standpoint, but since there's not much meat to the template-code, it should go pretty quickly. The goal for today is to generate final templates/snippets for interface, abstract class, class and final class constructs that can be used as starting-points for defining those types in project-code without having to start from scratch every single time. If you cringed, or started to think out a non-Pythonic-themed comment when you saw final, stop right now, and read this first.

I'm going to tackle them in the order noted above.

The Interface Template

These cannot be instantiated, and must be extended into concrete implementations. In most languages that I'm aware of, an interface definition cannot have properties (not even abstract ones), and cannot have concrete method-implementations. Python doesn't have that constraint (it doesn't have a formal interface-construct, but allows for abstract property- and method-definition through the abc module, using its ABCMeta as a __metaclass__ specification to define a class that has some abstract members, and the @abstractmethod and @abstractproperty decorator-functions to define which members are abstract.

@describe.InitClass()
class InterfaceName( object ):
    """TODO: Document InterfaceName
Provides interface requirements and type-identity for objects that can 
REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Static interface attributes (and  #
    # default values?)                  #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Properties               #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # InterfaceName is intended to be an interface,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == InterfaceName:
            raise NotImplementedError( 'InterfaceName is '
                'intended to be an interface, NOT to be instantiated.' )
        # Call parent initializers, if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Instance Methods         #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Abstract Class Methods            #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'InterfaceName' )

Like the module- and package-templates, a lot of this is just structural, organizational comment-blocks, indicating where to put verious types of members. After the initial declaration of the __metaclass__ for the interface, there are seven major blocks:

  • A place to define static(-ish) interface-level attributes/constants;
  • A place to define abstract (required) properties;
  • A concrete __init__, more on that shortly;
  • A block for a __del__ method, if one needs to be required in derived classes;
  • A block for defining abstract instance-methods;
  • A block for defining abstract class-methods; and
  • A block for defining static methods.

The check for the InterfaceName is in place so that an interface could be made more-or-less operational on Python versions that predate the abc module (sometime in the 2.6.x versions). It'd be kinda tedious, but all that would have to be done would be to remove all of the abc.[whatever] calls and references, and the interface would still be non-instantiable. A similar pattern is also in place for the templated abstract-method for the same reason.

The Abstract Class Template

These also cannot be instantiated, but can contain concrete implementations of properties and methods both, as well as abstract requirements for properties and methods. Much of it is very similar to the Interface template, for much the same reasons, but there are a couple additional sections:

  • Blocks for collecting the (usually protected) property-getter, -setter and -deleter methods for any concrete property implementations; and
  • A concrete- and abstract-property location;

@describe.InitClass()
class AbstractClassName( object ):
    """TODO: Document AbstractClassName
Provides baseline functionality, interface requirements and type-identity for 
objects that can REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties (abstract OR  #
    # concrete!)                        #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()
#    PropertyName = describe.makeProperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # AbstractClassName is intended to be an abstract class,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == AbstractClassName:
            raise NotImplementedError( 'AbstractClassName is '
                'intended to be an abstract class, NOT to be instantiated.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'AbstractClassName' )

There are a few more prompt-comments in the __init__ method as well, keeping with my normal programming structure: I explicitly delete all of an instance's properties during initialization so that they will always have a value after instances are first created.

The Class Template

Classes are intended to be instantiated, and can be extended freely. This template doesn't have anything new, that I haven't shown before, so there's not much that can be said about it.

@describe.InitClass()
class ClassName( object ):
    """TODO: Document ClassName
Represents SOMETHING"""
    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties               #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.
        pass # TODO: Remove this line after __init__ is implemented

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'ClassName' )

The Final Class Template

A final class is intended to be instantiated, but not extended. The only significant difference between this template and the Class template above is how the __init__ handles extension-attempts:

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # FinalClassName is intended to be a nominally-final class
        # and is NOT intended to be extended. Alter at your own risk!
        #---------------------------------------------------------------------#
        # TODO: Explain WHY it's nominally final!                             #
        #---------------------------------------------------------------------#
        if self.__class__ != FinalClassName:
            raise NotImplementedError( 'FinalClassName is '
                'intended to be a nominally-final class, NOT to be extended.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

After taking some time to roll the doc_metadata.py file into the new standard template, I can (finally) make good on my promise to make it available for download, as well as the collection of template-files:

109.6kB

It's been fourteen posts now, and just over a month since I started in on the documentation-process. I'm not sure exactly where I want to go next on the Python side of things, though I have a couple of ideas, so I think for my next post, I'll play around a bit with implementing some design-patterns in JavaScript. If nothing else, that will give me some time to ponder what the next logical step is.

Tuesday, February 14, 2017

Module and Package Template Files

Today's post should be pretty light, since all I'm going to cover are the template-files I use as starting-points for modules and packages.

My Module Template

As mentioned (I think) in a previous post, the main reason I use a template file is so that I don't have to waste a lot of time figuring out how to keep things organized every time I start a new module or package. An added benefit, potentially at least, is that all of my module- and package-files should be consistent enough that someone who's never seen my code can likely figure out where things are in any of my code after seeing a few smaller chunks of it elsewhere. I also find that it makes things easier for me to locate something (where is that abstract class?) if I've been away from it for any length of time.

Granted, a good editor/IDE will probably have ways to navigate to something in the current project-structure (Geany and Eclipse both seem to allow ctrl-clicking as a means of jumping to class or variable definitions). When that fails, or is too clunky for some reason (like if there are multiple classes with the same name across several files), I can usually pick out which file I want to look in, and navigate to one of the comment-headers very quickly.

Your mileage may vary...

Another reason is that this sort of organizational structure facilitates part of the habit (drummed into me by years of dealing with source-control systems that weren't as smart as Git is) of keeping things in neat spaces, and alphabetical order whenever possible. If you've used SVN, or a version of SourceGear Vault like the one I managed for a while, you're maybe already nodding in sympathy.

Enough. Here's what my module template looks like:

#!/usr/bin/env python
"""TODO: Document the module:
Provides classes and functionality relating to XXXX."""

#-----------------------------------#
# Standard-library imports.         #
#-----------------------------------#

# Uncomment this if there are any interfaces or abstract classes here.
# import abc

# Need to import sys in order to add the idic library path!
import sys

#-----------------------------------#
# Imports of other third-party      #
# libraries and functionality.      #
#-----------------------------------#

#-----------------------------------#
# idic-library imports.             #
#-----------------------------------#
sys.path.insert( 1, '/usr/local/lib/idic' )
from doc_metadata import describe

# Import documentation-metadata functionality
from doc_metadata import describe

#-----------------------------------#
# File metadata                     #
#-----------------------------------#
__author__  =       'Author Name'
__version__ =       'Version of File'
__copyright__ =     'Copyright Statement'
__license__ =       'License Info'
__credits__ =       ['Author Name', 'Contributor Name']
__maintainer__ =    'Maintainer Name'
__email__ =         'Email to contact Maintainer'
__status__ =        'Status of file'

#-----------------------------------#
# Create an __all__ list to support #
# "from spam import eggs" syntax.   #
#-----------------------------------#
__all__ = []

#-----------------------------------#
# Initialization that needs to      #
# happen before member definition.  #
#-----------------------------------#

#-----------------------------------#
# Defined constants.                #
#-----------------------------------#

#-----------------------------------#
# Defined exceptions.               #
#-----------------------------------#

#-----------------------------------#
# Defined functions.                #
#-----------------------------------#

#-----------------------------------#
# Defined class-interfaces.         #
#-----------------------------------#

#-----------------------------------#
# Defined abstract classes.         #
#-----------------------------------#

#-----------------------------------#
# Defined concrete classes.         #
#-----------------------------------#

#-----------------------------------#
# Imports to resolve circular       #
# dependencies. Avoid if possible.  #
#-----------------------------------#

#-----------------------------------#
# Initialization that needs to      #
# happen after member definition.   #
#-----------------------------------#

#-----------------------------------#
# Code to execute if file is called #
# or run directly.                  #
#-----------------------------------#
if __name__ == '__main__':
    pass

Working from top to bottom, here's what it provides:

A standard shebang and module doc-string:
#!/usr/bin/env python
"""TODO: Document the module:
Provides classes and functionality relating to XXXX."""
A section for standard library imports
These are imports from the main Python installation on the machine
A section for importing other third-party functionality
I've never (knowingly) used this, but if there are other modules/packages, or imports from them, that aren't part of the standard Python distribution, this would be the place to import them.
Set-up and import of my own libraries
#-----------------------------------#
# idic-library imports.             #
#-----------------------------------#
sys.path.insert( 1, '/usr/local/lib/idic' )
from doc_metadata import describe

# Import documentation-metadata functionality
from doc_metadata import describe
Note that the path being added matches the one in the project-structure from my previous post after it's been deployed. Prior to deployment, resolution of the path may have to be set up in the editor/IDE. I know that setting a PYTHONPATH in Geany's project-properties takes care of allowing the F5 execution-command to work, just like it would for a command-line execution:
File meta-data:
#-----------------------------------#
# File metadata                     #
#-----------------------------------#
__author__  =       'Author Name'
__version__ =       'Version of File'
__copyright__ =     'Copyright Statement'
__license__ =       'License Info'
__credits__ =       ['Author Name', 'Contributor Name']
__maintainer__ =    'Maintainer Name'
__email__ =         'Email to contact Maintainer'
__status__ =        'Status of file'
An explicit declaration of __all__
This allows the members of the module to be imported with the
from [module] import [*|name[, name,...]]syntax.
A set of sections for specific type of constructs and processes
From Initialization that needs to happen before member definition through Initialization that needs to happen after member definition.
Definitions of package-level constants, functions, interfaces, abstract classes, and concrete classes are all encompassed in these sections.
The circular imports section may take some explaining, and I don't want to clutter this page with it just now. I hope that I wont actually encounter it, but the section is there anyway, just in case...
Finally — any code that should be run if the module itself is executed
#-----------------------------------#
# Code to execute if file is called #
# or run directly.                  #
#-----------------------------------#
if __name__ == '__main__':
    pass
I frequently use this section to write small chunks of test-code as I'm working through the functionality of a module.

My Package Template

Structurally, the template-file for a package-header file is almost identical to the one for a module — The only difference is near the end of the file, where there is a space intended for adding related packages by appending them to __all__:


#-----------------------------------#
# Child modules and packages.       #
#-----------------------------------#

#-----------------------------------#
# Code to execute if file is called #
# or run directly.                  #
#-----------------------------------#
if __name__ == '__main__':
    pass

That, plus the file being saved as __init__.py in its parent directory are the only differences between modules and package-header files as far as these templates are concerned.

Short, sweet, and to the point, I hope. Next time around, I'll start diving in to the templates I have set up for defining classes.

Thursday, February 9, 2017

Project Structure

With the documentation-decorators ready to be included in my code-templates, it's time to take a look at those. There's a circular-reference kind of thing that I have to resolve first, though — In order to actually use the decorators, they have to import the module that the describe class lives in:

# Make sure the documentation-decorators are available!
from doc_metadata import describe

In order to do that, the Python interpreter has to know where the doc_metadata.py file lives. That, in turn, means that I need to decide where I'm going to put it, which means that I need to drop it into a project.

I'll start a new project for the framwork-level code that doc_metadata.py (arguably) belongs to. That project will be named idic, and it will follow the structure shown below, with any of the duplicated project-directories removed.

A Basic Project-Structure

I'm expecting all of the projects on my current list to share a common folder structure. This structure is based on the final deployed/installed location for project-files in an Ubuntu Linux (and presumably POSIX-compliant) file-system, and looks something like this:

File-system Path
[Project Root Directory]
  etc
    apache2
      sites-available
  usr
    local
      bin
        [project-name]
      etc
        idic
          datastores
      lib
        idic
          [project-name]
    share
      doc
        [project-name]
      icons
        [project-name]
      [project-name]
  var
    cache
      [project-name]
    www
      [project-name]
        media
        scripts
        styles

The directories in this structure have the following roles a the project once it is deployed:

etc
Items to be deployed to the global (root-access-only) configuration-file directory
etc/apache2/sites-available
Configuration-files for Apache websites.
usr
Items to be deployed to the global (root-access-only) usr directory
usr/local/bin/[project-name]
Executable files associated with the project (command-line applications and scripts).
usr/local/etc
Standard local-machine configuration-files directory
usr/local/etc/idic
Configuration-files directory for idic applications
usr/local/etc/idic/datastores
Configuration-files for machine-resident idic data-stores (database-connection credentials)
usr/local/lib
Standard local-machine shared-libraries directory
usr/local/lib/idic
Shared-library directory for all idic-project codebases
usr/local/lib/idic/[project-name]
Shared-library directory for all project-specific shared-library codebases (the top-level package directory for a project's namespace)
usr/share
Standard machine-specific shared-resources directory
usr/share/doc/[project-name]
Documentation-directory for the project
usr/share/icons/[project-name]
Icon-directory for the project
usr/share/[project-name]
Directory for other shared resource-files originating with the project
var
Items to be deployed to the global (root-access-only) var directory
var/cache/[project-name]
Space for storing (reading and writing) cache-files for the project.
var/www/[project-name]
The document-root directory for a website associated with the project
var/www/[project-name]/media
Image (and other media) files for the project website
var/www/[project-name]/scripts
Client-side scripts for the project website
var/www/[project-name]/styles
Style-sheets for the project website

Some projects may not have the entire directory-tree shown — projects that do not include any websites, for example, will not have the var/www/* portion of the tree, nor the site-configuration items in etc/apache2/*. The build-process that I'm planning to create for the projects I cover here will allow variations of files to be defined in the project workspace for each of several distinct environments.

Once I start digging in to the packaging- and deployment-processes part of the build-process I'll be creating, I expect that many of the top-level directories (/etc, /usr and /var) will need some conditional installation/set-up — This structure will work fine if the installer has root access, but not so much for, say, installers wanting to use the codebase on a hosting-provider machine that they don't have root access on (and cannot convince the provider to install the project[s]). Since I don't know yet what the packaging mechanisms are going to be, I'm going to shelve that for the time being, knowing that I'll have to look at it again.

All of the directories shown as [project-name] would be renamed to reflect the actual project-name. If multiple project-name directories need to exist (for example, if a project provides both a client-facing and administrative website), they should be named accordingly, with the project's name in the directory-name somewhere.

I'm planning to provide a structure that would accommodate a perhaps-typical development-and-migration set-up, and will keep track of files that belong to each specific environment by prefixing them with the environment's name. These environments, their purpose/role, and an example environment-specific file name (I picked a website configuration-file as an example) are:

LOCAL-*
The LOCAL environment is one that exists on the developer's machine, that is built and deployed to frequently as part of the code-test-debug cycle of ongoing/daily development.
Example: /etc/apache2/sites-available/LOCAL-site.conf
DEV-*
A DEV environment is the first shared environment accessible to all project-developers. The stability (or perhaps even existence) of a deployed project-copy should not be taken as a given — developers may well destroy and rebuild some or all of a project on its dev-environment over and over again every few minutes while working out integration of code from multiple developers.
Example: /etc/apache2/sites-available/DEV-site.conf
TEST-*
A TEST environment is where integrated and hopefully production-ready code is deployed to for purposes of quality assurance and maybe user acceptance. It may or may not have a complete application data-set available, but if it does, that data is probably a mirrored copy of the live environment's data, not the live data-set itself.
Example: /etc/apache2/sites-available/TEST-site.conf
STAGE-*
A STAGE (or STAGING) environment is the final stop before a project is deployed to a live environment. Ideally, the staging system(s) will be running on identical hardware, and may have direct access to live-environment application data-sets. A staging environment is often where load-testing is undertaken.
Example: /etc/apache2/sites-available/STAGE-site.conf
LIVE-*
The final live/production environment, where an application is available to all of the users it's intended to be available to.
Example: /etc/apache2/sites-available/LIVE-site.conf

So, to be clear, the example files noted above would exist side by side in a project's directory-tree like so:

File-system Path  Purpose
etc   
  apache2 
    sites-available  
      DEV-site.confDev-environment's site-configuration.
      LIVE-site.confLive-environment's site-configuration.
      LOCAL-site.confLocal-environment's site-configuration.
      STAGE-site.confStage-environment's site-configuration.
      TEST-site.confTest-environment's site-configuration.

The build-process for a given environment would then remove all of the files whose names indicate they are not part of the build for that environment (e.g., a LOCAL build would remove DEV-*, TEST-*, STAGE-* and LIVE-* files), and rename the LOCAL-* files to remove their LOCAL- prefixes, leaving, for example, a site.conf file that is specifically aimed at the environment the build is being created for. This is a pretty brute-force process, but it's simple, easily understood (I think), and relatively easily managed in Makefile code.

Each project would also have its own Makefile (not shown) in the top-level project-directory. I'm also expecting a few installation-scripts at a minimum to reside at the same level, but that will depend on the packaging-mechanism decision I mentioned in my previous post, so I'm not showing (or committed to) any specific structure or approach just yet.

I'm not completely sure, but I suspect that his project-structure would work for any project-type (e.g., for projects that aren't centering around a Python codebase). Since I don't really have anything in my project-queue that isn't a Python project, I'm happy enough to use this structure until/unless I find that I need to vary it for some other project.

Oh... And since doc_metadata.py belongs to the idic project (and eventually namespace), it will live at

[project-root]/usr/local/lib/idic/doc_metadata.py

That feels like enough for this post, particularly after all of the longer ones of late. The next few posts will delve into module- and package-file templates, then I'll probably generate and share templates or snippets for classes and other language-level elements that it makes sense to have them for.