# This file is part of Checkbox.
#
# Copyright 2012-2014 Canonical Ltd.
# Written by:
# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`plainbox.impl.secure.origin` -- origin objects
====================================================
"""
import functools
import inspect
import os
from plainbox.abc import ITextSource
from plainbox.i18n import gettext as _
from plainbox.impl.symbol import SymbolDef
[docs]class OriginMode(SymbolDef):
"""
A symbol definition (which will become an enumeration in the near future)
that describes all the possible "modes" an :class:`Origin` can operate in.
"""
# NOTE: this should be an enumeration
whole_file = 'whole-file'
single_line = 'single-line'
line_range = 'line-range'
@functools.total_ordering
[docs]class Origin:
"""
Simple class for tracking where something came from
This class supports "pinpointing" something in a block of text. The block
is described by the source attribute. The actual range is described by
line_start (inclusive) and line_end (exclusive).
:ivar source:
Something that describes where the text came frome. Technically it
should implement the :class:`~plainbox.abc.ITextSource` interface.
:ivar line_start:
The number of the line where the record begins. This can be None
when the intent is to cover the whole file. This can also be equal
to line_end (when not None) if the intent is to show a single line.
:ivar line_end:
The number of the line where the record ends
"""
__slots__ = ['source', 'line_start', 'line_end']
def __init__(self, source, line_start=None, line_end=None):
self.source = source
self.line_start = line_start
self.line_end = line_end
[docs] def mode(self):
"""
Compute the "mode" of this origin instance.
:returns:
:attr:`OriginMode.whole_file`, :attr:`OriginMode.single_line`
or :attr:`OriginMode.line_range`.
The mode tells if this instance is describing the whole file,
a range of lines or just a single line. It is mostly used internally
by the implementation.
"""
if self.line_start is None and self.line_end is None:
return OriginMode.whole_file
elif self.line_start == self.line_end:
return OriginMode.single_line
else:
return OriginMode.line_range
def __repr__(self):
return "<{} source:{!r} line_start:{} line_end:{}>".format(
self.__class__.__name__,
self.source, self.line_start, self.line_end)
def __str__(self):
mode = self.mode()
if mode is OriginMode.whole_file:
return str(self.source)
elif mode is OriginMode.single_line:
return "{}:{}".format(self.source, self.line_start)
elif mode is OriginMode.line_range:
return "{}:{}-{}".format(
self.source, self.line_start, self.line_end)
else:
raise NotImplementedError
[docs] def relative_to(self, base_dir):
"""
Create a Origin with source relative to the specified base directory.
:param base_dir:
A base directory name
:returns:
A new Origin with source replaced by the result of calling
relative_to(base_dir) on the current source *iff* the current
source has that method, self otherwise.
This method is useful for obtaining user friendly Origin objects that
have short, understandable filenames.
"""
relative_source = self.source.relative_to(base_dir)
if relative_source is not self.source:
return Origin(relative_source, self.line_start, self.line_end)
else:
return self
[docs] def with_offset(self, offset):
"""
Create a new Origin by adding a offset of a specific number of lines
:param offset:
Number of lines to add (or substract)
:returns:
A new Origin object
"""
mode = self.mode()
if mode is OriginMode.whole_file:
return self
elif mode is OriginMode.single_line or mode is OriginMode.line_range:
return Origin(self.source,
self.line_start + offset, self.line_end + offset)
else:
raise NotImplementedError
[docs] def just_line(self):
"""
Create a new Origin that points to the start line
:returns:
A new Origin with the end_line equal to start_line.
This effectively makes the origin describe a single line.
"""
return Origin(self.source, self.line_start, self.line_start)
[docs] def just_file(self):
"""
create a new Origin that points to the whole file
:returns:
A new Origin with line_end and line_start both set to None.
"""
return Origin(self.source)
def __eq__(self, other):
if isinstance(other, Origin):
return ((self.source, self.line_start, self.line_end) ==
(other.source, other.line_start, other.line_end))
else:
return NotImplemented
def __gt__(self, other):
if isinstance(other, Origin):
return ((self.source, self.line_start, self.line_end) >
(other.source, other.line_start, other.line_end))
else:
return NotImplemented
@classmethod
[docs] def get_caller_origin(cls, back=0):
"""
Create an Origin instance pointing at the call site of this method.
"""
# Create an Origin instance that pinpoints the place that called
# get_caller_origin().
caller_frame, filename, lineno = inspect.stack(0)[2 + back][:3]
try:
source = PythonFileTextSource(filename)
origin = Origin(source, lineno, lineno)
finally:
# Explicitly delete the frame object, this breaks the
# reference cycle and makes this part of the code deterministic
# with regards to the CPython garbage collector.
#
# As recommended by the python documentation:
# http://docs.python.org/3/library/inspect.html#the-interpreter-stack
del caller_frame
return origin
@functools.total_ordering
[docs]class UnknownTextSource(ITextSource):
"""
A :class:`ITextSource` subclass indicating that the source of text is
unknown.
This instances of this class are constructed by gen_rfc822_records() when
no explicit source is provided and the stream has no name. The serve as
non-None values to prevent constructing :class:`PythonFileTextSource` with
origin computed from :meth:`Origin.get_caller_origin()`
"""
def __str__(self):
return _("???")
def __repr__(self):
return "{}()".format(self.__class__.__name__)
def __eq__(self, other):
if isinstance(other, UnknownTextSource):
return True
else:
return False
def __gt__(self, other):
if isinstance(other, UnknownTextSource):
return False
else:
return NotImplemented
[docs] def relative_to(self, path):
return self
@functools.total_ordering
[docs]class FileTextSource(ITextSource):
"""
A :class:`ITextSource` subclass indicating that text came from a file.
:ivar filename:
name of the file something comes from
"""
def __init__(self, filename):
self.filename = filename
def __str__(self):
return self.filename
def __repr__(self):
return "{}({!r})".format(
self.__class__.__name__, self.filename)
def __eq__(self, other):
if isinstance(other, FileTextSource):
return self.filename == other.filename
else:
return False
def __gt__(self, other):
if isinstance(other, FileTextSource):
return self.filename > other.filename
else:
return NotImplemented
[docs] def relative_to(self, base_dir):
"""
Compute a FileTextSource with the filename being a relative path from
the specified base directory.
:param base_dir:
A base directory name
:returns:
A new FileTextSource with filename relative to that base_dir
"""
return self.__class__(os.path.relpath(self.filename, base_dir))
[docs]class PythonFileTextSource(FileTextSource):
"""
A :class:`FileTextSource` subclass indicating the file was a python file.
It implements no differences but in some context it might be helpful to
differentiate on the type of the source field in the origin of a job
definition record.
:ivar filename:
name of the python filename that something comes from
"""
@functools.total_ordering
[docs]class JobOutputTextSource(ITextSource):
"""
A :class:`ITextSource` subclass indicating that text came from job output.
This class is used by
:meth:`SessionState._gen_rfc822_records_from_io_log()` to allow such
(generated) jobs to be traced back to the job that generated them.
:ivar job:
:class:`plainbox.impl.job.JobDefinition` instance that generated the
text
"""
def __init__(self, job):
self.job = job
def __str__(self):
return str(self.job.id)
def __repr__(self):
return "<{} job:{!r}>".format(self.__class__.__name__, self.job)
def __eq__(self, other):
if isinstance(other, JobOutputTextSource):
return self.job == other.job
return NotImplemented
def __gt__(self, other):
if isinstance(other, JobOutputTextSource):
return self.job > other.job
return NotImplemented
[docs] def relative_to(self, base_path):
return self
@functools.total_ordering
[docs]class CommandLineTextSource(ITextSource):
"""
A :class:`ITextSource` describing text that originated arguments to main()
:attr arg_name:
The optional name of the argument that describes the arg_value
:attr arg_value:
The argument that was passed on command line (the actual text)
"""
def __init__(self, arg_name, arg_value):
self.arg_value = arg_value
self.arg_name = arg_name
def __str__(self):
if self.arg_name is not None:
return _("command line argument {}={!a}").format(
self.arg_name, self.arg_value)
else:
return _("command line argument {!a}").format(self.arg_value)
def __repr__(self):
return "<{} arg_name:{!r} arg_value:{!r}>".format(
self.__class__.__name__, self.arg_name, self.arg_value)
def __eq__(self, other):
if isinstance(other, CommandLineTextSource):
return (self.arg_name == other.arg_name
and self.arg_value == other.arg_value)
return NotImplemented
def __gt__(self, other):
if isinstance(other, CommandLineTextSource):
if self.arg_name > other.arg_name:
return True
if self.arg_value > other.arg_value:
return True
return False
return NotImplemented
[docs] def relative_to(self, base_path):
return self