Design Goals And Best Practice¶
General information about design goals and what constitutes good usage of the library, appears here.
The Single Most Important Thing¶
This library works with true application types. Store operations place representations of operational objects in system files and recovery operations result in objects ready for action.
Using this library to read a temporary object that is then transferred into operational objects in a member-by-member fashion, is perhaps consistent with the use of other libraries. However, there are several reasons to not use this library in that way.
Most importantly it creates an additional translation layer, between the operational objects and the library. This undoes a lot of hard work to locate all such translations inside the library. Whenever the objects handled by the library change (e.g. moves, adds and deletes of members), any translation layer is likely to require an update. Across a substantial application that represents a self-inflicted, maintenance headache.
There are a few exceptions to this rule, e.g. applications working with non-UTC timezones, but generally the presence of pre-store or post-recover translations is either a misuse of the library or due to a limitation of the library. The maintainer is automatically interested in any instance of the latter.
Dates, Times And Zones¶
The types associated with time values appear below;
Python |
Ansar |
Notes |
---|---|---|
|
|
An epoch time value. |
|
|
The difference between two |
|
|
A formal date and time value. |
|
|
The difference between two |
The library supports the two styles of time values; float
values that record the number
of seconds since an epoch (probably January 1, 1970) and datetime
objects that hold
explicit year, month (etc) values. In general applications will use datetime
and
timedelta
values but the epoch-associated float
capabilities are retained for
those specific scenarios where the full complexities of daylight saving, war-time
adjustments and leap seconds, can be avoided.
The concept of timezones manifests as the tzinfo
parameter and attribute of the
datetime
class. The associated tzinfo
class is an abstract base class with multiple
concrete implementations. The standard library provides the minimal implementation
as datetime.timezone
but there are several others including dateutil.tz.tzfile
and zoneinfo.ZoneInfo
. These different implementations provide a variety of
options and behaviours such as intepretation of IANA names, Windows zone names and
GNU TZ values.
Note
There are significant differences in the efficacy of the different implementations,
which might be construed as either limitations or bugs (there are definitely bugs).
Suffice to say, time processing involving timezones is truly complex. As a general
rule the implementations have improved over time, i.e. from datetime
, through
pytz
, dateutil
and most recently, zoneinfo
.
There is no mechanism by which the library can support the encoding of every
concrete implementation of the tzinfo
base class. For technical reasons beyond
the scope of this document it is not currently possible.
To provide timezone capability, the library allows instances of datetime.timezone
for the tzinfo
attribute. Assigning a value of any other type will result in the
raising of an exception during encoding. Of course, a value of None
is accepted,
i.e. “naive” datetimes
.
Applications required to manage datetime
objects with a variety of timezones, say
selected by a user from the set of IANA names, must implement pre-store and post-recover
code. In addition, the selected IANA names must be stored independently of the datetime
objects.
Pre-store code will convert all the datetime
objects to the UTC timezone using
methods such as datetime.astimezone
. Note that the UTC timezone is available
as datetime.timezone.utc
, or ar.UTC
(with the appropriate import
statements):
# Storage location.
# Assume a Meeting class that stores a datetime and IANA name.
meeting_file = File('meetings', ar.Vector(Meeting))
meetings = [
Meeting(.., 'Australia/Broken_Hill'),
Meeting(.., 'Pacific/Marquesas'),
Meeting(.., 'Europe/Sofia'),
..
]
..
# Pre-store.
as_utc = [Meeting(m.when.astimezone(ar.UTC), m.iana) for m in meetings]
# Actual store.
meeting_file.store(as_utc)
Post-recover code will convert all the datetime
objects back to their original
timezone:
# Actual recover.
as_utc, _ = meeting_file.recover()
# Post-recover
# Uses the dateutil.tz.gettz function to convert the IANA name
# into a tzinfo object.
meetings = [Meeting(m.when.astimezone(gettz(m.iana)), m.iana) for m in as_utc]
This departure from the standard encode-decode cycle is unfortunate, but a direct
consequence of the design of the datetime
module.
A WorldTime
conforms to ISO 8601 and looks like this:
2008-07-01T19:01:37
A ClockTime
uses the same representation:
2010-06-06T06:46:01.866233
Recovering a ClockTime
produces a float
value appropriate to the host system time. A ClockTime
implements the concept of a particular time on a particular calendar day - that being a different float
value for every timezone in the world.
A TimeSpan
is a relative value or delta, the difference between two ClockTime
values.
A few example representations appear below:
1h2m3s
8h
10m
0.0125s
1d
These are readable text representations of float values. After recovery the resulting
value can be added to any Python time value and that will have the expected effect, i.e. adding
a recovered TimeSpan
of 2m
will add 120.0
to the Python time value (noting that
true time intervals are not nearly as simple as two time values and a bit of arithmetic):
>>> t = ar.text_to_clock('2012-03-06T19:00:30')
>>> t
1331013630.0
>>> d = ar.text_to_span('2m')
>>> d
120.0
>>> t += d
>>> ar.clock_to_text(t)
'2012-03-06T19:02:30'
Functions such as text_to_clock
, text_to_span
and clock_to_text
are the functions
used internally, during store and recover operations.
Pushing Back Against Complexity¶
The technical domain referred to as application persistence or serialization has many pitfalls and caveats. One of the background concerns is the seemingly unstoppable increase in code complexity - the longer an application exists and the more it immerses itself in serialization the more complex it can get. There are reasons to suggest that this is more than the general increase in complexity that manifests in all evolving software. Having to deal with change (i.e. version management) is one such reason.
Guaranteed Structure¶
One way to push back against complexity is to aim for guaranteed structure wherever possible and only retreat
from that position in an explicit fashion. What this means is that wherever there is a sequence, collection or
nested object, there is benefit in preventing assignment of None
. In the case of the who
member of
the Job
object (from examples appearing throughout this documentation), this
avoids the pre-check needed before every access to that member, e.g. len(j.who)
will throw a TypeError
if who
is currently set to None
. This might seem trivial in isolation but it affects every use of that
member throughout a codebase. Multiplying that by every “structural” member and you have a real contributor to
overall complexity.
The declaration of 'who': ar.VectorOf(ar.Unicode),
is an expression of “guaranteed structure” and must be
backed up by code such as self.who = who or ar.default_vector()
in the __init__
method. Both the encoding
and decoding processes will raise a ValueError
exception if a None
value is detected where structure is
expected.
The downside to “guaranteed structure” is that None
is the standard way of representing “not set”. This works
nicely for non-structured types such as numbers and UUIDs but creates a conflict when dealing with types such as
vectors. The desire to reduce coding complexity must compete with any hard requirements that would normally be
implemented using None
.
Optional Structure¶
The declaration of 'who': ar.PointerTo(ar.VectorOf(ar.Unicode)),
is an expression of optional structure
and it allows the value None
to flow through encoding and decoding processes successfully. There is no
actual difference at coding level, i.e. who = j.who
will still assign the member value to the variable, and
that value will either be a Python list or None
. Further information on the PointerTo
facility can be found here.
Three Class Exemplars¶
This section provides three different styles of declaration and initialization of application types. The different styles are a response to what developers might wish to do, versus what is possible within the capabilities of the library.
No Schema Required¶
The following class demonstrates the extent of what can be achieved without schemas. There is a member for
every type that can be automatically inferred during the registration process. Several library types resolve
to the same Python type. For this reason, library types such as ClockTime
do not make an appearance as
the Python float
type is assumed to be a floating point, mathematical value, i.e. a Float8
. All the
structured types - such as vectors and sets - require explicit type declaration, and therefore cannot
appear within this style of application type.
import datetime
import uuid
import ansar.encode as ar
class EmptyType(object):
def __init__(self):
pass
ar.bind(EmptyType)
class NoSchemaRequired(object):
def __init__(self, flag=False,
whole=0, floating=0.0,
buffer=None, text=None, language=None,
when=None, advancement=None,
serial=None, what = None
):
self.flag = flag
self.whole = whole
self.floating = floating
self.buffer = buffer or bytearray()
self.text = text or bytes()
self.language = language or str()
self.when = when or datetime.datetime.now()
self.advancement = advancement or datetime.timedelta()
self.serial = serial or uuid.uuid4()
self.what = what or EmptyType()
ar.bind(NoSchemaRequired)
All members must be initialized to an instance of the proper Python type. A representation of the default instance looks like:
{
"value": {
"advancement": "00:00:00",
"buffer": "",
"flag": false,
"floating": 0.0,
"language": "",
"serial": "1d33bd90-bc38-4588-a353-6e2bd7909d4d",
"text": "",
"what": {},
"when": "2022-10-25T16:15:55.307257",
"whole": 0
}
}
Default Values Required¶
Declaration of an application type should start with something like below, and evolve towards the style appearing in the next section, as necessary. There is a member for nearly every supported type including the structured types. A few types that are internal to the library are omitted for clarity:
import time
import datetime
import uuid
import ansar.encode as ar
class EmptyType(object):
def __init__(self):
pass
ar.bind(EmptyType)
Status = ar.Enumeration(INITIAL=1, OTHER=2)
class DefaultValuesRequired(object):
def __init__(self, flag=None,
octet=None, letter=None, codepoint=None,
whole=None, floating=None,
buffer=None, text=None, language=None,
mode=None,
moment=None, delay=None,
when=None, advancement=None,
serial=None, what=None,
an_array=None, a_vector=None,
a_set=None, a_map=None,
a_deque=None,
handle=None
):
self.flag = flag
self.octet = octet
self.letter = letter
self.codepoint = codepoint
self.whole = whole
self.floating = floating
self.buffer = buffer
self.text = text
self.language = language
self.mode = mode
self.moment = moment
self.delay= delay
self.when = when
self.advancement = advancement
self.serial = serial
self.what = what or EmptyType()
self.an_array = an_array or ar.make(ar.ArrayOf(int, 8))
self.a_vector = a_vector or ar.default_vector()
self.a_set = a_set or ar.default_set()
self.a_map = a_map or ar.default_map()
self.a_deque = a_deque or ar.default_deque()
self.handle = handle
DEFAULT_VALUES_REQUIRED_SCHEMA = {
"flag": ar.Boolean,
"octet": ar.Byte,
"letter": ar.Character,
"codepoint": ar.Rune,
"whole": ar.Integer8,
"floating": ar.Float8,
"buffer": ar.Block,
"text": ar.String,
"language": ar.Unicode,
"mode": Status,
"moment": ar.ClockTime,
"delay": ar.TimeSpan,
"when": ar.WorldTime,
"advancement": ar.TimeDelta,
"serial": ar.UUID,
"what": EmptyType,
"an_array": ar.ArrayOf(int, 8),
"a_vector": ar.VectorOf(float),
"a_set": ar.SetOf(bytes),
"a_map": ar.MapOf(int, str),
"a_deque": ar.DequeOf(bool),
"handle": ar.PointerTo(bool),
}
ar.bind(DefaultValuesRequired, object_schema=DEFAULT_VALUES_REQUIRED_SCHEMA)
All non-structural members are initialized with None
, including types such as times, enumerations and
pointers. Structural members must be initialized with a sensible default value, most conveniently provided
by one of the library functions. All None
values are, of course, omitted from the representation except
where structure dictates that a slot must exist, i.e. in an array. After loading of a DefaultValuesRequired
instance it is guaranteed that dvr.an_array[7]
will exist, i.e. the expression will not result in
a TypeError
(where an_array
is None
) or an IndexError
(where an_array
is too short):
{
"value": {
"a_deque": [],
"a_map": [],
"a_set": [],
"a_vector": [],
"an_array": [
null,
null,
null,
null,
null,
null,
null,
null
],
"what": {}
}
}
Note
The make_self()
library function can be used to automate the construction
of complex objects. Refer to here for an example.
Structure As Optionals¶
Where it is desirable that structural members may be “not set”, the PointerTo
library type is used
to relax the guarantees of the previous section:
import time
import datetime
import uuid
import ansar.encode as ar
class EmptyType(object):
def __init__(self):
pass
ar.bind(EmptyType)
Status = ar.Enumeration(INITIAL=1, OTHER=2)
class StructureAsOptionals(object):
def __init__(self, flag=None,
octet=None, letter=None, codepoint=None,
whole=None, floating=None,
buffer=None, text=None, language=None,
mode=None,
moment=None, delay=None,
when=None, advancement=None,
serial=None, what=None,
an_array=None, a_vector=None,
a_set=None, a_map=None,
a_deque=None,
handle=None
):
self.flag = flag
self.octet = octet
self.letter = letter
self.codepoint = codepoint
self.whole = whole
self.floating = floating
self.buffer = buffer
self.text = text
self.language = language
self.mode = mode
self.moment = moment
self.delay= delay
self.when = when
self.advancement = advancement
self.serial = serial
self.what = what
self.an_array = an_array
self.a_vector = a_vector
self.a_set = a_set
self.a_map = a_map
self.a_deque = a_deque
self.handle = handle
STRUCTURE_AS_OPTIONALS_SCHEMA = {
"flag": ar.Boolean,
"octet": ar.Byte,
"letter": ar.Character,
"codepoint": ar.Rune,
"whole": ar.Integer8,
"floating": ar.Float8,
"buffer": ar.Block,
"text": ar.String,
"language": ar.Unicode,
"mode": Status,
"moment": ar.ClockTime,
"delay": ar.TimeSpan,
"when": ar.WorldTime,
"advancement": ar.TimeDelta,
"serial": ar.UUID,
"what": ar.PointerTo(EmptyType),
"an_array": ar.PointerTo(ar.ArrayOf(int, 8)),
"a_vector": ar.PointerTo(ar.VectorOf(float)),
"a_set": ar.PointerTo(ar.SetOf(bytes)),
"a_map": ar.PointerTo(ar.MapOf(int, str)),
"a_deque": ar.PointerTo(ar.DequeOf(bool)),
"handle": ar.PointerTo(bool),
}
ar.bind(StructureAsOptionals, object_schema=STRUCTURE_AS_OPTIONALS_SCHEMA)
All members are initialized to None
and this results in the default representation as the empty
JSON object:
{
"value": {}
}
An Assessment Of Versioning¶
Versioning within the field of software engineering is not a science. There are certainly successful implementations of versioning but they solve different problems (document vs network API versioning) and use different technologies and “standards” (e.g. semantic vs calendar version tags). The Ansar library automates the version stamping and version detection aspects of versioning. It is up to the application to exploit the version information supplied at every recovery site, to implement version support. And all of this must exist within a world where developers may be pre-committed to their own flavour of versioning.
This library acknowledges three different approaches to versioning and includes features and behaviours that attempt to integrate, tolerate or (gracefully) reject all of them. 1) It can be essentially ignored, or 2) it can be tackled in a lightweight, informal manner, or 3) it can be under full, explicit control. Each approach may be appropriate to a situation. Figuring out the best approach is mostly about accepting a related level of functional failures when loading “older” encodings. Production environments will obviously lean towards full control with the goal of seamless behaviour in those circumstances.
This library supports the third approach to versioning. But before diving into the coding of any version support, there are two further concepts that should be clarified. One concerns exactly what should be versioned (e.g. application types) and the other is about ensuring complete and effective version tags. Proper explanation can be found at the end of this assessment.
Ignore It All And Carry On¶
In this approach, application types can be modified at will. Applications simply accept the failures and other sub-optimal behaviours that can arise when attempting to load older encodings. This can be appropriate during prototyping work, in informal operational environments and where the loss of older encodings is acceptable or recoverable. Where an application is incapable of loading an encoding in the normal manner it may be possible to manually modify the encoding (i.e. just “fix it”!) using an editor.
The appeal to this approach is code simplicity. Application types and the code around those types always reflects the latest operational model, and only that latest model.
The downsides to this approach are significant. Attempts to load older encodings can produce a variety of undesirable behaviours including complete rejection or successful loading but with incomplete content, or unexpected values. The library is somewhat tolerant of mismatches between inbound encodings and the current definitions of application types. Where an inbound array is too long the surplus elements are discarded and where the array is too short the library appends default values. In addition, members that didnt exist in older encodings assume default values.
Perhaps most importantly, this approach can leave an operational site (e.g. a cloud deployment, a user’s desktop, or a remote monitoring station) compromised and with no automated path back to an operational status.
Boxing Clever¶
The second approach is a common default in that it “just evolves”. Without deliberate thought and without support from the development tooling, this is the style of version management that most often results.
There is a more considered approach to the changing of application types. Members are added and deleted, but in the latter case they are not necessarily removed from the type declaration. For the first time the membership of applications types is an accumulation of past and present members. On storing of an application type, the latest code may omit the initialization of older members. On recovery the code may use the absence of older members to infer the version of the contents.
With care and discipline this approach can have some value. It is “lightweight” in that no additional declaration or tooling is required. Decoding operations have more scope to deal with past and present materials because the names and types of older members are still recorded inside the application types. It is possible to load both old and new encodings without any problems.
On the downside the lack of formality means that there are no offical version tags, e.g. the lack of a member value might indicate an older version but there is no unique identity that can be used in codepath selections.
It’s also true that this style of coding does not scale well. It becomes harder and harder to “add another version” and almost impossible to “remove the oldest version”. What seemed like a pragmatic decision in the earlier part of an application’s lifetime, can be exposed as shortsighted when the application goes on to enjoy commercial success and wider adoption.
This approach may suit a circumstance where there are only a few application types involved (perhaps just the one document type) and/or the types are unlikely to experience much change.
Explicit Version Histories¶
The third approach is where a version tag travels alongside every instance of encoded application data. The data is “stamped” with a tag during the creation of a portable representation. The tag is subsequently extracted during decoding and made available to the loading application. The application is expected to use the tag for codepath selection and this forms the basis of all version support work.
Application types are again an accumulation of past and present members but now there are unique identities associated with each step in the evolution of an application type. Versioning is now explicit rather than inferred from the set of members presented in an encoding.
Encodings are improved in that only those members appropriate to a version are included. This is an
explicit variation of the behaviour in the previous style of versioning, where members with a None
value were omitted. This approach goes even further; it looks for members that should be omitted and
checks that they do indeed have a None
value. Any value other than None
causes an exception.
When a versioned application encounters an encoding created by an unversioned application a special tag
is synthesized to distinguish these encodings. Rather than the normal "1.7"
style of tag the loading
application will receive the empty string, i.e. ""
. This value can be used in codepath selection
to implement seamless version support across versioned and unversioned operation. Shifting to version
management does not prevent the application from loading older encodings.
What To Tag¶
This library implements the version tagging of application types, i.e. registered types such as Job
are
assigned a version history. Each entry in a version history has a tag and this is the value that emerges as
the version tag on decoding operations such as recover()
. This works great
with code such as the following:
f = ar.File('job', Job)
j = Job()
f.store(j)
..
r, v = f.recover()
The value v
will contain the version tag from the latest entry in the version history of Job
.
Now consider this different use of a File
:
f = ar.File('job', ar.VectorOf(Job))
This highlights the fact that a File
works with a type expression. The first code example
works because registered application types are instances of type expressions. This is great
but also begs a question; what is the content of v
after complex data (e.g. a vector
of Jobs
) is recovered?
For the purposes of stamping outbound encodings and comparison of inbound encodings, the library evaluates the associated type expression and derives the effective type. It is the effective type that is used for all version-related activites.
Evaluation involves a traversing of the type expression, as if it were an upside-down tree
of branches and leaves. It is looking for the lowest, non-structural type, or terminal leaf.
The Job
expression and the VectorOf(Job)
expression evaluate to the same effective
type, i.e. Job
. This is because evaluation of VectorOf
returns the type of its
content. It does this in a recursive manner, where VectorOf(VectorOf(Job))
will still
evaluate to the Job
class for all version-related activities.
Evaluation behaves in a similar manner for all the containers and sequences, e.g. the
effective type for DequeOf(Job)
is Job
. Again, this is a recursive behaviour
so that the effective type for DequeOf(VectorOf(Job))
is still Job
. Where the
terminal leaf is not an application type, such as with VectorOf(str)
, evaluation
returns a None
and versioning is effectively disabled for that type expression.
There is a twist in the evaluation of an associative array or MapOf
. While MapOf(str, Job)
evaluates to Job
this also means that MapOf(CompositeKey, Job)
(where CompositeKey
is
a hashable and registered application type) raises a problem. Changes to CompositeKey
will not
be reflected in the version information returned by a recover()
operation. A similar issue
exists with ArrayOf(Job, 8)
. Changes to the dimension will not be reflected in the versioning.
This is a weakness in the “effective type” approach to versioning. Other approaches were considered but the intellectual overhead inflicted on the developer and the additional costs during encoding and decoding could not be justified. The workaround is to place the associative array or fixed-size array inside an application type.
Versioning Of A Complex Type¶
Consider the following application types:
import ansar.encode as ar
class Section(object):
def __init__(self, paragraphs=None):
self.paragraphs = paragraphs or ar.default_vector()
SECTION_SCHEMA = {
"paragraphs": ar.VectorOf(ar.Unicode),
}
SECTION_HISTORY = (
('0.0', None, 'Initial version')
)
ar.bind(Section, object_schema=SECTION_SCHEMA, version_history=SECTION_HISTORY)
class Document(object):
def __init__(self, title=None, sections=None):
self.title = title
self.sections = sections or ar.default_vector()
DOCUMENT_HISTORY = (
('0.0', None, 'Initial version')
)
DOCUMENT_SCHEMA = {
"title": ar.Unicode,
"sections": ar.VectorOf(Section),
}
ar.bind(Document, object_schema=DOCUMENT_SCHEMA, version_history=DOCUMENT_HISTORY)
A Document
contains a title and zero or more Section
objects. Both application types
are using full schemas and version histories - version management is active. During decoding
operations, version support will be based on Document
(i.e. it is the effective type):
f = ar.File(open_file.file_name, Document)
..
r, v = f.recover()
The v
value will contain either "0.0"
, ""
or None
depending on the scenario,
and will be used for codepath selection. As the Document
history changes, codepath
selection is extended to include further values of v
.
Schemas for an application type like Document
can refer to zero or more application types. They
may appear as the type of a member or within complex structural declarations, e.g. VectorOf(Section)
.
The top level type is known as a document and any types referred to in the document schema
are known as reachables. The process that determines the reachable types for a given document
is recursive, i.e. it is an accumulation of all the reachables found in the document schema
and all their reachables, etc.
A real difficulty arises when the Section
type changes. The version history is updated but
at runtime, right when it would be expected, there is no change to the value of v
; version
management machinery is still focused on Document
.
To resolve this situation there needs to be a check between the previous configuration of version histories and the latest configuration. And to do that there needs to be a record of what the previous configuration was.
Applications must maintain a module that registers those application types (i.e. documents) that
need full version management. It will import the Ansar
library and the application modules
declaring the messages to be registered. The module will look like this;
import ansar.encode as ar
import document
import settings
ar.released_document(document.Document)
ar.released_document(settings.Settings)
This code scans the specified types, compiling sets of reachable types and their respective versions.
There is a utility provided by the library that can load this same module and compare the compiled information to a previously saved image of the same information. The following command line compares the current version information with the information saved in the named file;
$ ansar-releasing -a check src/module.py application.release
The -a
flag requests a listing of all the issues detected. The contents of the application.release
file
look like this;
$ cat application.release
document.Document:document.Document/0.2,document.Section/0.7,document.Paragraph/0.3,document.TableOfContents/0.9,document.Index/0.8
settings.Settings:settings.Settings/0.11,settings.Device/0.14,settings.NetworkLocation/0.3
There are four possible outcomes from ansar-releasing
;
1. The release file does not exist¶
There has been no previous release. The current version information is used to create a new file and the utility returns a success code.
2. The release file exists and version information has not changed¶
Current version information is compared to the saved image and no difference can be detected. The utility returns a success code.
3. The saved file exists and there have been valid changes to version information¶
Current version information is compared to the saved image and differences are detected. The set of differences are determined to be valid. The utility returns a success code.
4. The saved file file exists and there have been incomplete changes to version information¶
Current version information is compared to the saved image and differences are detected. The set of differences are determined to be incomplete. The current software should not be deployed into production environments as the potential to write successful version support is compromised. The utility prints details of what it found and returns an error code. Most often the resolution is to add a line to a document history (as specified by the utility) and repeat the build process.
Without the influence of ansar-releasing
a sequence of version changes might look
like this:
Type |
A |
B |
C |
D |
E |
---|---|---|---|---|---|
|
2.5 |
2.5 |
2.5 |
2.5 |
2.6 |
|
1.7 |
1.8 |
1.8 |
1.8 |
1.8 |
|
2.3 |
2.3 |
2.3 |
2.4 |
2.4 |
|
1.8 |
1.8 |
1.9 |
1.9 |
1.9 |
Release B includes a change to Section
, C to Index
and D to
the TableOfContents
. Through these changes to reachables, the version of Document
remains constant. Release E records a change to the document itself. For applications
recovering instances of a Document
, there is no detectable difference between releases
A through to D. With the checks imposed by ansar-releasing
it changes to this:
Type |
A |
B |
C |
D |
E |
---|---|---|---|---|---|
|
2.5 |
2.6 |
2.7 |
2.8 |
2.9 |
|
1.7 |
1.8 |
1.8 |
1.8 |
1.8 |
|
2.3 |
2.3 |
2.3 |
2.4 |
2.4 |
|
1.8 |
1.8 |
1.9 |
1.9 |
1.9 |
Now the version of Document
also changes with every change to one of its reachables. A third
number can be appended to the version tag and used to explicitly represent changes to reachables
rather than a change to the type that the tag belongs to.
Type |
A |
B |
C |
D |
E |
---|---|---|---|---|---|
|
2.5 |
2.5.1 |
2.5.2 |
2.5.3 |
2.6 |
|
1.7 |
1.8 |
1.8 |
1.8 |
1.8 |
|
2.3 |
2.3 |
2.3 |
2.4 |
2.4 |
|
1.8 |
1.8 |
1.9 |
1.9 |
1.9 |
Adopting this numbering option produces tags that are visibly different for changes to
reachables, i.e. the tags are longer and the major-minor combination for Document
remains
unchanged through releases B, C and D. It also helps to keep minor numbers small.
The first number in the sequence is always 1
to be distinct from the default value of zero
assigned to a short-form tag, i.e. for comparison purposes 2.5
is equivalent to 2.5.0
.
Once the checks are satisfied, the final step in a build process is to overwrite the previous version information with the latest. The following command line makes that happen;
$ ansar-releasing set src/module.py application.release
In practice the ansar-releasing
command is used as part of an automated build process and
the application.release
file is added to the source repository. This establishes
a development environment with proper version management. Application developers
can be assured that there are distinct versions of documents, as needed for proper
implementation of version support.