logilab-common-1.4.1/ 0000755 0002025 0002025 00000000000 13131135544 015762 5 ustar dlaxalde dlaxalde 0000000 0000000 logilab-common-1.4.1/COPYING 0000644 0002025 0002025 00000043103 12651420631 017016 0 ustar dlaxalde dlaxalde 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Section\'s description.
Blabla bla Section\'s description.\nBlabla bla Sub section description blablaSection title
Section title
\nSubsection
\n\n
\n'''
nested_list = '''
| head1 | \nhead2 | \n
| cell1 | \ncell2 | \n
| f1 | \nv1 | \n
| f22 | \nv22 | \n
| f333 | \nv333 | \n
| field | \nvalue | \n
|---|---|
| f1 | \nv1 | \n
| f22 | \nv22 | \n
| f333 | \nv333 | \n
| toi perdu ? | \n\n |
blablabla''' if __name__ == '__main__': unittest_main() logilab-common-1.4.1/test/unittest_configuration.py 0000644 0002025 0002025 00000035525 13131131507 024126 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. # # logilab-common is free software: you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation, either version 2.1 of the License, or (at your option) any # later version. # # logilab-common 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License along # with logilab-common. If not, see
| %s | ' % cell) else: self.writeln(u'%s | ' % cell) self.writeln(u'
|---|
)""" self.write(u'
') self.format_children(layout) self.write(u'
') def visit_span(self, layout): """display links (using)""" self.write(u'' % self.handle_attrs(layout)) self.format_children(layout) self.write(u'') def visit_link(self, layout): """display links (using )""" self.write(u' %s' % (layout.url, self.handle_attrs(layout), layout.label)) def visit_verbatimtext(self, layout): """display verbatim text (using
)"""
self.write(u'')
self.write(layout.data.replace(u'&', u'&').replace(u'<', u'<'))
self.write(u'')
def visit_text(self, layout):
"""add some text"""
data = layout.data
if layout.escaped:
data = data.replace(u'&', u'&').replace(u'<', u'<')
self.write(data)
logilab-common-1.4.1/logilab/common/ureports/docbook_writer.py 0000644 0002025 0002025 00000013112 12651420631 026132 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""HTML formatting drivers for ureports"""
__docformat__ = "restructuredtext en"
from six.moves import range
from logilab.common.ureports import HTMLWriter
class DocbookWriter(HTMLWriter):
"""format layouts as HTML"""
def begin_format(self, layout):
"""begin to format a layout"""
super(HTMLWriter, self).begin_format(layout)
if self.snippet is None:
self.writeln('')
self.writeln("""
""")
def end_format(self, layout):
"""finished to format a layout"""
if self.snippet is None:
self.writeln(' ')
def visit_section(self, layout):
"""display a section (using (level 0) or )"""
if self.section == 0:
tag = "chapter"
else:
tag = "section"
self.section += 1
self.writeln(self._indent('<%s%s>' % (tag, self.handle_attrs(layout))))
self.format_children(layout)
self.writeln(self._indent('%s>'% tag))
self.section -= 1
def visit_title(self, layout):
"""display a title using """
self.write(self._indent(' ' % self.handle_attrs(layout)))
self.format_children(layout)
self.writeln(' ')
def visit_table(self, layout):
"""display a table as html"""
self.writeln(self._indent(' %s ' \
% (self.handle_attrs(layout), layout.title)))
self.writeln(self._indent(' '% layout.cols))
for i in range(layout.cols):
self.writeln(self._indent(' ' % i))
table_content = self.get_table_content(layout)
# write headers
if layout.cheaders:
self.writeln(self._indent(' '))
self._write_row(table_content[0])
self.writeln(self._indent(' '))
table_content = table_content[1:]
elif layout.rcheaders:
self.writeln(self._indent(' '))
self._write_row(table_content[-1])
self.writeln(self._indent(' '))
table_content = table_content[:-1]
# write body
self.writeln(self._indent(' '))
for i in range(len(table_content)):
row = table_content[i]
self.writeln(self._indent(' '))
for j in range(len(row)):
cell = row[j] or ' '
self.writeln(self._indent(' %s ' % cell))
self.writeln(self._indent('
'))
self.writeln(self._indent(' '))
self.writeln(self._indent(' '))
self.writeln(self._indent('
'))
def _write_row(self, row):
"""write content of row (using )"""
self.writeln(' ')
for j in range(len(row)):
cell = row[j] or ' '
self.writeln(' %s ' % cell)
self.writeln(self._indent('
'))
def visit_list(self, layout):
"""display a list (using )"""
self.writeln(self._indent(' ' % self.handle_attrs(layout)))
for row in list(self.compute_content(layout)):
self.writeln(' %s ' % row)
self.writeln(self._indent(' '))
def visit_paragraph(self, layout):
"""display links (using )"""
self.write(self._indent(' '))
self.format_children(layout)
self.writeln(' ')
def visit_span(self, layout):
"""display links (using )"""
#TODO: translate in docbook
self.write('' % self.handle_attrs(layout))
self.format_children(layout)
self.write(' ')
def visit_link(self, layout):
"""display links (using )"""
self.write('%s ' % (layout.url,
self.handle_attrs(layout),
layout.label))
def visit_verbatimtext(self, layout):
"""display verbatim text (using )"""
self.writeln(self._indent(' '))
self.write(layout.data.replace('&', '&').replace('<', '<'))
self.writeln(self._indent(' '))
def visit_text(self, layout):
"""add some text"""
self.write(layout.data.replace('&', '&').replace('<', '<'))
def _indent(self, string):
"""correctly indent string according to section"""
return ' ' * 2*(self.section) + string
logilab-common-1.4.1/logilab/common/ureports/__init__.py 0000644 0002025 0002025 00000013741 12651420631 024665 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Universal report objects and some formatting drivers.
A way to create simple reports using python objects, primarily designed to be
formatted as text and html.
"""
__docformat__ = "restructuredtext en"
import sys
from logilab.common.compat import StringIO
from logilab.common.textutils import linesep
def get_nodes(node, klass):
"""return an iterator on all children node of the given klass"""
for child in node.children:
if isinstance(child, klass):
yield child
# recurse (FIXME: recursion controled by an option)
for grandchild in get_nodes(child, klass):
yield grandchild
def layout_title(layout):
"""try to return the layout's title as string, return None if not found
"""
for child in layout.children:
if isinstance(child, Title):
return u' '.join([node.data for node in get_nodes(child, Text)])
def build_summary(layout, level=1):
"""make a summary for the report, including X level"""
assert level > 0
level -= 1
summary = List(klass=u'summary')
for child in layout.children:
if not isinstance(child, Section):
continue
label = layout_title(child)
if not label and not child.id:
continue
if not child.id:
child.id = label.replace(' ', '-')
node = Link(u'#'+child.id, label=label or child.id)
# FIXME: Three following lines produce not very compliant
# docbook: there are some useless . They might be
# replaced by the three commented lines but this then produces
# a bug in html display...
if level and [n for n in child.children if isinstance(n, Section)]:
node = Paragraph([node, build_summary(child, level)])
summary.append(node)
# summary.append(node)
# if level and [n for n in child.children if isinstance(n, Section)]:
# summary.append(build_summary(child, level))
return summary
class BaseWriter(object):
"""base class for ureport writers"""
def format(self, layout, stream=None, encoding=None):
"""format and write the given layout into the stream object
unicode policy: unicode strings may be found in the layout;
try to call stream.write with it, but give it back encoded using
the given encoding if it fails
"""
if stream is None:
stream = sys.stdout
if not encoding:
encoding = getattr(stream, 'encoding', 'UTF-8')
self.encoding = encoding or 'UTF-8'
self.__compute_funcs = []
self.out = stream
self.begin_format(layout)
layout.accept(self)
self.end_format(layout)
def format_children(self, layout):
"""recurse on the layout children and call their accept method
(see the Visitor pattern)
"""
for child in getattr(layout, 'children', ()):
child.accept(self)
def writeln(self, string=u''):
"""write a line in the output buffer"""
self.write(string + linesep)
def write(self, string):
"""write a string in the output buffer"""
try:
self.out.write(string)
except UnicodeEncodeError:
self.out.write(string.encode(self.encoding))
def begin_format(self, layout):
"""begin to format a layout"""
self.section = 0
def end_format(self, layout):
"""finished to format a layout"""
def get_table_content(self, table):
"""trick to get table content without actually writing it
return an aligned list of lists containing table cells values as string
"""
result = [[]]
cols = table.cols
for cell in self.compute_content(table):
if cols == 0:
result.append([])
cols = table.cols
cols -= 1
result[-1].append(cell)
# fill missing cells
while len(result[-1]) < cols:
result[-1].append(u'')
return result
def compute_content(self, layout):
"""trick to compute the formatting of children layout before actually
writing it
return an iterator on strings (one for each child element)
"""
# use cells !
def write(data):
try:
stream.write(data)
except UnicodeEncodeError:
stream.write(data.encode(self.encoding))
def writeln(data=u''):
try:
stream.write(data+linesep)
except UnicodeEncodeError:
stream.write(data.encode(self.encoding)+linesep)
self.write = write
self.writeln = writeln
self.__compute_funcs.append((write, writeln))
for child in layout.children:
stream = StringIO()
child.accept(self)
yield stream.getvalue()
self.__compute_funcs.pop()
try:
self.write, self.writeln = self.__compute_funcs[-1]
except IndexError:
del self.write
del self.writeln
from logilab.common.ureports.nodes import *
from logilab.common.ureports.text_writer import TextWriter
from logilab.common.ureports.html_writer import HTMLWriter
logilab-common-1.4.1/logilab/common/ureports/text_writer.py 0000644 0002025 0002025 00000012134 12651420631 025501 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Text formatting drivers for ureports"""
from __future__ import print_function
__docformat__ = "restructuredtext en"
from six.moves import range
from logilab.common.textutils import linesep
from logilab.common.ureports import BaseWriter
TITLE_UNDERLINES = [u'', u'=', u'-', u'`', u'.', u'~', u'^']
BULLETS = [u'*', u'-']
class TextWriter(BaseWriter):
"""format layouts as text
(ReStructured inspiration but not totally handled yet)
"""
def begin_format(self, layout):
super(TextWriter, self).begin_format(layout)
self.list_level = 0
self.pending_urls = []
def visit_section(self, layout):
"""display a section as text
"""
self.section += 1
self.writeln()
self.format_children(layout)
if self.pending_urls:
self.writeln()
for label, url in self.pending_urls:
self.writeln(u'.. _`%s`: %s' % (label, url))
self.pending_urls = []
self.section -= 1
self.writeln()
def visit_title(self, layout):
title = u''.join(list(self.compute_content(layout)))
self.writeln(title)
try:
self.writeln(TITLE_UNDERLINES[self.section] * len(title))
except IndexError:
print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT")
def visit_paragraph(self, layout):
"""enter a paragraph"""
self.format_children(layout)
self.writeln()
def visit_span(self, layout):
"""enter a span"""
self.format_children(layout)
def visit_table(self, layout):
"""display a table as text"""
table_content = self.get_table_content(layout)
# get columns width
cols_width = [0]*len(table_content[0])
for row in table_content:
for index in range(len(row)):
col = row[index]
cols_width[index] = max(cols_width[index], len(col))
if layout.klass == 'field':
self.field_table(layout, table_content, cols_width)
else:
self.default_table(layout, table_content, cols_width)
self.writeln()
def default_table(self, layout, table_content, cols_width):
"""format a table"""
cols_width = [size+1 for size in cols_width]
format_strings = u' '.join([u'%%-%ss'] * len(cols_width))
format_strings = format_strings % tuple(cols_width)
format_strings = format_strings.split(' ')
table_linesep = u'\n+' + u'+'.join([u'-'*w for w in cols_width]) + u'+\n'
headsep = u'\n+' + u'+'.join([u'='*w for w in cols_width]) + u'+\n'
# FIXME: layout.cheaders
self.write(table_linesep)
for i in range(len(table_content)):
self.write(u'|')
line = table_content[i]
for j in range(len(line)):
self.write(format_strings[j] % line[j])
self.write(u'|')
if i == 0 and layout.rheaders:
self.write(headsep)
else:
self.write(table_linesep)
def field_table(self, layout, table_content, cols_width):
"""special case for field table"""
assert layout.cols == 2
format_string = u'%s%%-%ss: %%s' % (linesep, cols_width[0])
for field, value in table_content:
self.write(format_string % (field, value))
def visit_list(self, layout):
"""display a list layout as text"""
bullet = BULLETS[self.list_level % len(BULLETS)]
indent = ' ' * self.list_level
self.list_level += 1
for child in layout.children:
self.write(u'%s%s%s ' % (linesep, indent, bullet))
child.accept(self)
self.list_level -= 1
def visit_link(self, layout):
"""add a hyperlink"""
if layout.label != layout.url:
self.write(u'`%s`_' % layout.label)
self.pending_urls.append( (layout.label, layout.url) )
else:
self.write(layout.url)
def visit_verbatimtext(self, layout):
"""display a verbatim layout as text (so difficult ;)
"""
self.writeln(u'::\n')
for line in layout.data.splitlines():
self.writeln(u' ' + line)
self.writeln()
def visit_text(self, layout):
"""add some text"""
self.write(u'%s' % layout.data)
logilab-common-1.4.1/logilab/common/ureports/nodes.py 0000644 0002025 0002025 00000013316 12651420631 024234 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Micro reports objects.
A micro report is a tree of layout and content objects.
"""
__docformat__ = "restructuredtext en"
from logilab.common.tree import VNode
from six import string_types
class BaseComponent(VNode):
"""base report component
attributes
* id : the component's optional id
* klass : the component's optional klass
"""
def __init__(self, id=None, klass=None):
VNode.__init__(self, id)
self.klass = klass
class BaseLayout(BaseComponent):
"""base container node
attributes
* BaseComponent attributes
* children : components in this table (i.e. the table's cells)
"""
def __init__(self, children=(), **kwargs):
super(BaseLayout, self).__init__(**kwargs)
for child in children:
if isinstance(child, BaseComponent):
self.append(child)
else:
self.add_text(child)
def append(self, child):
"""overridden to detect problems easily"""
assert child not in self.parents()
VNode.append(self, child)
def parents(self):
"""return the ancestor nodes"""
assert self.parent is not self
if self.parent is None:
return []
return [self.parent] + self.parent.parents()
def add_text(self, text):
"""shortcut to add text data"""
self.children.append(Text(text))
# non container nodes #########################################################
class Text(BaseComponent):
"""a text portion
attributes :
* BaseComponent attributes
* data : the text value as an encoded or unicode string
"""
def __init__(self, data, escaped=True, **kwargs):
super(Text, self).__init__(**kwargs)
#if isinstance(data, unicode):
# data = data.encode('ascii')
assert isinstance(data, string_types), data.__class__
self.escaped = escaped
self.data = data
class VerbatimText(Text):
"""a verbatim text, display the raw data
attributes :
* BaseComponent attributes
* data : the text value as an encoded or unicode string
"""
class Link(BaseComponent):
"""a labelled link
attributes :
* BaseComponent attributes
* url : the link's target (REQUIRED)
* label : the link's label as a string (use the url by default)
"""
def __init__(self, url, label=None, **kwargs):
super(Link, self).__init__(**kwargs)
assert url
self.url = url
self.label = label or url
class Image(BaseComponent):
"""an embedded or a single image
attributes :
* BaseComponent attributes
* filename : the image's filename (REQUIRED)
* stream : the stream object containing the image data (REQUIRED)
* title : the image's optional title
"""
def __init__(self, filename, stream, title=None, **kwargs):
super(Image, self).__init__(**kwargs)
assert filename
assert stream
self.filename = filename
self.stream = stream
self.title = title
# container nodes #############################################################
class Section(BaseLayout):
"""a section
attributes :
* BaseLayout attributes
a title may also be given to the constructor, it'll be added
as a first element
a description may also be given to the constructor, it'll be added
as a first paragraph
"""
def __init__(self, title=None, description=None, **kwargs):
super(Section, self).__init__(**kwargs)
if description:
self.insert(0, Paragraph([Text(description)]))
if title:
self.insert(0, Title(children=(title,)))
class Title(BaseLayout):
"""a title
attributes :
* BaseLayout attributes
A title must not contains a section nor a paragraph!
"""
class Span(BaseLayout):
"""a title
attributes :
* BaseLayout attributes
A span should only contains Text and Link nodes (in-line elements)
"""
class Paragraph(BaseLayout):
"""a simple text paragraph
attributes :
* BaseLayout attributes
A paragraph must not contains a section !
"""
class Table(BaseLayout):
"""some tabular data
attributes :
* BaseLayout attributes
* cols : the number of columns of the table (REQUIRED)
* rheaders : the first row's elements are table's header
* cheaders : the first col's elements are table's header
* title : the table's optional title
"""
def __init__(self, cols, title=None,
rheaders=0, cheaders=0, rrheaders=0, rcheaders=0,
**kwargs):
super(Table, self).__init__(**kwargs)
assert isinstance(cols, int)
self.cols = cols
self.title = title
self.rheaders = rheaders
self.cheaders = cheaders
self.rrheaders = rrheaders
self.rcheaders = rcheaders
class List(BaseLayout):
"""some list data
attributes :
* BaseLayout attributes
"""
logilab-common-1.4.1/logilab/common/graph.py 0000644 0002025 0002025 00000024007 12651420631 022341 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Graph manipulation utilities.
(dot generation adapted from pypy/translator/tool/make_dot.py)
"""
__docformat__ = "restructuredtext en"
__metaclass__ = type
import os.path as osp
import os
import sys
import tempfile
import codecs
import errno
def escape(value):
"""Make usable in a dot file."""
lines = [line.replace('"', '\\"') for line in value.split('\n')]
data = '\\l'.join(lines)
return '\\n' + data
def target_info_from_filename(filename):
"""Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
basename = osp.basename(filename)
storedir = osp.dirname(osp.abspath(filename))
target = filename.split('.')[-1]
return storedir, basename, target
class DotBackend:
"""Dot File backend."""
def __init__(self, graphname, rankdir=None, size=None, ratio=None,
charset='utf-8', renderer='dot', additionnal_param={}):
self.graphname = graphname
self.renderer = renderer
self.lines = []
self._source = None
self.emit("digraph %s {" % normalize_node_id(graphname))
if rankdir:
self.emit('rankdir=%s' % rankdir)
if ratio:
self.emit('ratio=%s' % ratio)
if size:
self.emit('size="%s"' % size)
if charset:
assert charset.lower() in ('utf-8', 'iso-8859-1', 'latin1'), \
'unsupported charset %s' % charset
self.emit('charset="%s"' % charset)
for param in sorted(additionnal_param.items()):
self.emit('='.join(param))
def get_source(self):
"""returns self._source"""
if self._source is None:
self.emit("}\n")
self._source = '\n'.join(self.lines)
del self.lines
return self._source
source = property(get_source)
def generate(self, outputfile=None, dotfile=None, mapfile=None):
"""Generates a graph file.
:param outputfile: filename and path [defaults to graphname.png]
:param dotfile: filename and path [defaults to graphname.dot]
:rtype: str
:return: a path to the generated file
"""
import subprocess # introduced in py 2.4
name = self.graphname
if not dotfile:
# if 'outputfile' is a dot file use it as 'dotfile'
if outputfile and outputfile.endswith(".dot"):
dotfile = outputfile
else:
dotfile = '%s.dot' % name
if outputfile is not None:
storedir, basename, target = target_info_from_filename(outputfile)
if target != "dot":
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
os.close(pdot)
else:
dot_sourcepath = osp.join(storedir, dotfile)
else:
target = 'png'
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
ppng, outputfile = tempfile.mkstemp(".png", name)
os.close(pdot)
os.close(ppng)
pdot = codecs.open(dot_sourcepath, 'w', encoding='utf8')
pdot.write(self.source)
pdot.close()
if target != 'dot':
if sys.platform == 'win32':
use_shell = True
else:
use_shell = False
try:
if mapfile:
subprocess.call([self.renderer, '-Tcmapx', '-o', mapfile, '-T', target, dot_sourcepath, '-o', outputfile],
shell=use_shell)
else:
subprocess.call([self.renderer, '-T', target,
dot_sourcepath, '-o', outputfile],
shell=use_shell)
except OSError as e:
if e.errno == errno.ENOENT:
e.strerror = 'File not found: {0}'.format(self.renderer)
raise
os.unlink(dot_sourcepath)
return outputfile
def emit(self, line):
"""Adds to final output."""
self.lines.append(line)
def emit_edge(self, name1, name2, **props):
"""emit an edge from to .
edge properties: see http://www.graphviz.org/doc/info/attrs.html
"""
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
self.emit('%s -> %s [%s];' % (n_from, n_to, ', '.join(sorted(attrs))) )
def emit_node(self, name, **props):
"""emit a node with given properties.
node properties: see http://www.graphviz.org/doc/info/attrs.html
"""
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
self.emit('%s [%s];' % (normalize_node_id(name), ', '.join(sorted(attrs))))
def normalize_node_id(nid):
"""Returns a suitable DOT node id for `nid`."""
return '"%s"' % nid
class GraphGenerator:
def __init__(self, backend):
# the backend is responsible to output the graph in a particular format
self.backend = backend
# XXX doesn't like space in outpufile / mapfile
def generate(self, visitor, propshdlr, outputfile=None, mapfile=None):
# the visitor
# the property handler is used to get node and edge properties
# according to the graph and to the backend
self.propshdlr = propshdlr
for nodeid, node in visitor.nodes():
props = propshdlr.node_properties(node)
self.backend.emit_node(nodeid, **props)
for subjnode, objnode, edge in visitor.edges():
props = propshdlr.edge_properties(edge, subjnode, objnode)
self.backend.emit_edge(subjnode, objnode, **props)
return self.backend.generate(outputfile=outputfile, mapfile=mapfile)
class UnorderableGraph(Exception):
pass
def ordered_nodes(graph):
"""takes a dependency graph dict as arguments and return an ordered tuple of
nodes starting with nodes without dependencies and up to the outermost node.
If there is some cycle in the graph, :exc:`UnorderableGraph` will be raised.
Also the given graph dict will be emptied.
"""
# check graph consistency
cycles = get_cycles(graph)
if cycles:
cycles = '\n'.join([' -> '.join(cycle) for cycle in cycles])
raise UnorderableGraph('cycles in graph: %s' % cycles)
vertices = set(graph)
to_vertices = set()
for edges in graph.values():
to_vertices |= set(edges)
missing_vertices = to_vertices - vertices
if missing_vertices:
raise UnorderableGraph('missing vertices: %s' % ', '.join(missing_vertices))
# order vertices
order = []
order_set = set()
old_len = None
while graph:
if old_len == len(graph):
raise UnorderableGraph('unknown problem with %s' % graph)
old_len = len(graph)
deps_ok = []
for node, node_deps in graph.items():
for dep in node_deps:
if dep not in order_set:
break
else:
deps_ok.append(node)
order.append(deps_ok)
order_set |= set(deps_ok)
for node in deps_ok:
del graph[node]
result = []
for grp in reversed(order):
result.extend(sorted(grp))
return tuple(result)
def get_cycles(graph_dict, vertices=None):
'''given a dictionary representing an ordered graph (i.e. key are vertices
and values is a list of destination vertices representing edges), return a
list of detected cycles
'''
if not graph_dict:
return ()
result = []
if vertices is None:
vertices = graph_dict.keys()
for vertice in vertices:
_get_cycles(graph_dict, [], set(), result, vertice)
return result
def _get_cycles(graph_dict, path, visited, result, vertice):
"""recursive function doing the real work for get_cycles"""
if vertice in path:
cycle = [vertice]
for node in path[::-1]:
if node == vertice:
break
cycle.insert(0, node)
# make a canonical representation
start_from = min(cycle)
index = cycle.index(start_from)
cycle = cycle[index:] + cycle[0:index]
# append it to result if not already in
if not cycle in result:
result.append(cycle)
return
path.append(vertice)
try:
for node in graph_dict[vertice]:
# don't check already visited nodes again
if node not in visited:
_get_cycles(graph_dict, path, visited, result, node)
visited.add(node)
except KeyError:
pass
path.pop()
def has_path(graph_dict, fromnode, tonode, path=None):
"""generic function taking a simple graph definition as a dictionary, with
node has key associated to a list of nodes directly reachable from it.
Return None if no path exists to go from `fromnode` to `tonode`, else the
first path found (as a list including the destination node at last)
"""
if path is None:
path = []
elif fromnode in path:
return None
path.append(fromnode)
for destnode in graph_dict[fromnode]:
if destnode == tonode or has_path(graph_dict, destnode, tonode, path):
return path[1:] + [tonode]
path.pop()
return None
logilab-common-1.4.1/logilab/common/compat.py 0000644 0002025 0002025 00000005041 12651420631 022520 0 ustar dlaxalde dlaxalde 0000000 0000000 # pylint: disable=E0601,W0622,W0611
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Wrappers around some builtins introduced in python 2.3, 2.4 and
2.5, making them available in for earlier versions of python.
See another compatibility snippets from other projects:
:mod:`lib2to3.fixes`
:mod:`coverage.backward`
:mod:`unittest2.compatibility`
"""
__docformat__ = "restructuredtext en"
import os
import sys
import types
from warnings import warn
# not used here, but imported to preserve API
from six.moves import builtins
if sys.version_info < (3, 0):
str_to_bytes = str
def str_encode(string, encoding):
if isinstance(string, unicode):
return string.encode(encoding)
return str(string)
else:
def str_to_bytes(string):
return str.encode(string)
# we have to ignore the encoding in py3k to be able to write a string into a
# TextIOWrapper or like object (which expect an unicode string)
def str_encode(string, encoding):
return str(string)
# See also http://bugs.python.org/issue11776
if sys.version_info[0] == 3:
def method_type(callable, instance, klass):
# api change. klass is no more considered
return types.MethodType(callable, instance)
else:
# alias types otherwise
method_type = types.MethodType
# Pythons 2 and 3 differ on where to get StringIO
if sys.version_info < (3, 0):
from cStringIO import StringIO
FileIO = file
BytesIO = StringIO
reload = reload
else:
from io import FileIO, BytesIO, StringIO
from imp import reload
from logilab.common.deprecation import deprecated
# Other projects import these from here, keep providing them for
# backwards compat
any = deprecated('use builtin "any"')(any)
all = deprecated('use builtin "all"')(all)
logilab-common-1.4.1/logilab/common/registry.py 0000644 0002025 0002025 00000123437 13063007140 023111 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of Logilab-common.
#
# Logilab-common is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 2.1 of the License, or (at your
# option) any later version.
#
# Logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Logilab-common. If not, see .
"""This module provides bases for predicates dispatching (the pattern in use
here is similar to what's refered as multi-dispatch or predicate-dispatch in the
literature, though a bit different since the idea is to select across different
implementation 'e.g. classes), not to dispatch a message to a function or
method. It contains the following classes:
* :class:`RegistryStore`, the top level object which loads implementation
objects and stores them into registries. You'll usually use it to access
registries and their contained objects;
* :class:`Registry`, the base class which contains objects semantically grouped
(for instance, sharing a same API, hence the 'implementation' name). You'll
use it to select the proper implementation according to a context. Notice you
may use registries on their own without using the store.
.. Note::
implementation objects are usually designed to be accessed through the
registry and not by direct instantiation, besides to use it as base classe.
The selection procedure is delegated to a selector, which is responsible for
scoring the object according to some context. At the end of the selection, if an
implementation has been found, an instance of this class is returned. A selector
is built from one or more predicates combined together using AND, OR, NOT
operators (actually `&`, `|` and `~`). You'll thus find some base classes to
build predicates:
* :class:`Predicate`, the abstract base predicate class
* :class:`AndPredicate`, :class:`OrPredicate`, :class:`NotPredicate`, which you
shouldn't have to use directly. You'll use `&`, `|` and '~' operators between
predicates directly
* :func:`objectify_predicate`
You'll eventually find one concrete predicate: :class:`yes`
.. autoclass:: RegistryStore
.. autoclass:: Registry
Predicates
----------
.. autoclass:: Predicate
.. autofunction:: objectify_predicate
.. autoclass:: yes
.. autoclass:: AndPredicate
.. autoclass:: OrPredicate
.. autoclass:: NotPredicate
Debugging
---------
.. autoclass:: traced_selection
Exceptions
----------
.. autoclass:: RegistryException
.. autoclass:: RegistryNotFound
.. autoclass:: ObjectNotFound
.. autoclass:: NoSelectableObject
"""
from __future__ import print_function
__docformat__ = "restructuredtext en"
import sys
import pkgutil
import types
import weakref
import traceback as tb
from os import listdir, stat
from os.path import join, isdir, exists
from logging import getLogger
from warnings import warn
from six import string_types, add_metaclass
from logilab.common.modutils import modpath_from_file
from logilab.common.logging_ext import set_log_methods
from logilab.common.decorators import classproperty
from logilab.common.deprecation import deprecated
class RegistryException(Exception):
"""Base class for registry exception."""
class RegistryNotFound(RegistryException):
"""Raised when an unknown registry is requested.
This is usually a programming/typo error.
"""
class ObjectNotFound(RegistryException):
"""Raised when an unregistered object is requested.
This may be a programming/typo or a misconfiguration error.
"""
class NoSelectableObject(RegistryException):
"""Raised when no object is selectable for a given context."""
def __init__(self, args, kwargs, objects):
self.args = args
self.kwargs = kwargs
self.objects = objects
def __str__(self):
return ('args: %s, kwargs: %s\ncandidates: %s'
% (self.args, self.kwargs.keys(), self.objects))
class SelectAmbiguity(RegistryException):
"""Raised when several objects compete at selection time with an equal
score.
"""
def _modname_from_path(path, extrapath=None):
modpath = modpath_from_file(path, extrapath)
# omit '__init__' from package's name to avoid loading that module
# once for each name when it is imported by some other object
# module. This supposes import in modules are done as::
#
# from package import something
#
# not::
#
# from package.__init__ import something
#
# which seems quite correct.
if modpath[-1] == '__init__':
modpath.pop()
return '.'.join(modpath)
def _toload_info(path, extrapath, _toload=None):
"""Return a dictionary of : and an ordered list of
(file, module name) to load
"""
if _toload is None:
assert isinstance(path, list)
_toload = {}, []
for fileordir in path:
if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
_toload_info(subfiles, extrapath, _toload)
elif fileordir[-3:] == '.py':
modname = _modname_from_path(fileordir, extrapath)
_toload[0][modname] = fileordir
_toload[1].append((fileordir, modname))
return _toload
class RegistrableObject(object):
"""This is the base class for registrable objects which are selected
according to a context.
:attr:`__registry__`
name of the registry for this object (string like 'views',
'templates'...). You may want to define `__registries__` directly if your
object should be registered in several registries.
:attr:`__regid__`
object's identifier in the registry (string like 'main',
'primary', 'folder_box')
:attr:`__select__`
class'selector
Moreover, the `__abstract__` attribute may be set to True to indicate that a
class is abstract and should not be registered.
You don't have to inherit from this class to put it in a registry (having
`__regid__` and `__select__` is enough), though this is needed for classes
that should be automatically registered.
"""
__registry__ = None
__regid__ = None
__select__ = None
__abstract__ = True # see doc snipppets below (in Registry class)
@classproperty
def __registries__(cls):
if cls.__registry__ is None:
return ()
return (cls.__registry__,)
class RegistrableInstance(RegistrableObject):
"""Inherit this class if you want instances of the classes to be
automatically registered.
"""
def __new__(cls, *args, **kwargs):
"""Add a __module__ attribute telling the module where the instance was
created, for automatic registration.
"""
module = kwargs.pop('__module__', None)
obj = super(RegistrableInstance, cls).__new__(cls)
if module is None:
warn('instantiate {0} with '
'__module__=__name__'.format(cls.__name__),
DeprecationWarning)
# XXX subclass must no override __new__
filepath = tb.extract_stack(limit=2)[0][0]
obj.__module__ = _modname_from_path(filepath)
else:
obj.__module__ = module
return obj
def __init__(self, __module__=None):
super(RegistrableInstance, self).__init__()
class Registry(dict):
"""The registry store a set of implementations associated to identifier:
* to each identifier are associated a list of implementations
* to select an implementation of a given identifier, you should use one of the
:meth:`select` or :meth:`select_or_none` method
* to select a list of implementations for a context, you should use the
:meth:`possible_objects` method
* dictionary like access to an identifier will return the bare list of
implementations for this identifier.
To be usable in a registry, the only requirement is to have a `__select__`
attribute.
At the end of the registration process, the :meth:`__registered__`
method is called on each registered object which have them, given the
registry in which it's registered as argument.
Registration methods:
.. automethod:: register
.. automethod:: unregister
Selection methods:
.. automethod:: select
.. automethod:: select_or_none
.. automethod:: possible_objects
.. automethod:: object_by_id
"""
def __init__(self, debugmode):
super(Registry, self).__init__()
self.debugmode = debugmode
def __getitem__(self, name):
"""return the registry (list of implementation objects) associated to
this name
"""
try:
return super(Registry, self).__getitem__(name)
except KeyError:
exc = ObjectNotFound(name)
exc.__traceback__ = sys.exc_info()[-1]
raise exc
@classmethod
def objid(cls, obj):
"""returns a unique identifier for an object stored in the registry"""
return '%s.%s' % (obj.__module__, cls.objname(obj))
@classmethod
def objname(cls, obj):
"""returns a readable name for an object stored in the registry"""
return getattr(obj, '__name__', id(obj))
def initialization_completed(self):
"""call method __registered__() on registered objects when the callback
is defined"""
for objects in self.values():
for objectcls in objects:
registered = getattr(objectcls, '__registered__', None)
if registered:
registered(self)
if self.debugmode:
wrap_predicates(_lltrace)
def register(self, obj, oid=None, clear=False):
"""base method to add an object in the registry"""
assert not '__abstract__' in obj.__dict__, obj
assert obj.__select__, obj
oid = oid or obj.__regid__
assert oid, ('no explicit name supplied to register object %s, '
'which has no __regid__ set' % obj)
if clear:
objects = self[oid] = []
else:
objects = self.setdefault(oid, [])
assert not obj in objects, 'object %s is already registered' % obj
objects.append(obj)
def register_and_replace(self, obj, replaced):
"""remove and register """
# XXXFIXME this is a duplication of unregister()
# remove register_and_replace in favor of unregister + register
# or simplify by calling unregister then register here
if not isinstance(replaced, string_types):
replaced = self.objid(replaced)
# prevent from misspelling
assert obj is not replaced, 'replacing an object by itself: %s' % obj
registered_objs = self.get(obj.__regid__, ())
for index, registered in enumerate(registered_objs):
if self.objid(registered) == replaced:
del registered_objs[index]
break
else:
self.warning('trying to replace %s that is not registered with %s',
replaced, obj)
self.register(obj)
def unregister(self, obj):
"""remove object from this registry"""
objid = self.objid(obj)
oid = obj.__regid__
for registered in self.get(oid, ()):
# use self.objid() to compare objects because vreg will probably
# have its own version of the object, loaded through execfile
if self.objid(registered) == objid:
self[oid].remove(registered)
break
else:
self.warning('can\'t remove %s, no id %s in the registry',
objid, oid)
def all_objects(self):
"""return a list containing all objects in this registry.
"""
result = []
for objs in self.values():
result += objs
return result
# dynamic selection methods ################################################
def object_by_id(self, oid, *args, **kwargs):
"""return object with the `oid` identifier. Only one object is expected
to be found.
raise :exc:`ObjectNotFound` if there are no object with id `oid` in this
registry
raise :exc:`AssertionError` if there is more than one object there
"""
objects = self[oid]
assert len(objects) == 1, objects
return objects[0](*args, **kwargs)
def select(self, __oid, *args, **kwargs):
"""return the most specific object among those with the given oid
according to the given context.
raise :exc:`ObjectNotFound` if there are no object with id `oid` in this
registry
raise :exc:`NoSelectableObject` if no object can be selected
"""
obj = self._select_best(self[__oid], *args, **kwargs)
if obj is None:
raise NoSelectableObject(args, kwargs, self[__oid] )
return obj
def select_or_none(self, __oid, *args, **kwargs):
"""return the most specific object among those with the given oid
according to the given context, or None if no object applies.
"""
try:
return self._select_best(self[__oid], *args, **kwargs)
except ObjectNotFound:
return None
def possible_objects(self, *args, **kwargs):
"""return an iterator on possible objects in this registry for the given
context
"""
for objects in self.values():
obj = self._select_best(objects, *args, **kwargs)
if obj is None:
continue
yield obj
def _select_best(self, objects, *args, **kwargs):
"""return an instance of the most specific object according
to parameters
return None if not object apply (don't raise `NoSelectableObject` since
it's costly when searching objects using `possible_objects`
(e.g. searching for hooks).
"""
score, winners = 0, None
for obj in objects:
objectscore = obj.__select__(obj, *args, **kwargs)
if objectscore > score:
score, winners = objectscore, [obj]
elif objectscore > 0 and objectscore == score:
winners.append(obj)
if winners is None:
return None
if len(winners) > 1:
# log in production environement / test, error while debugging
msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
if self.debugmode:
# raise bare exception in debug mode
raise SelectAmbiguity(msg % (winners, args, kwargs.keys()))
self.error(msg, winners, args, kwargs.keys())
# return the result of calling the object
return self.selected(winners[0], args, kwargs)
def selected(self, winner, args, kwargs):
"""override here if for instance you don't want "instanciation"
"""
return winner(*args, **kwargs)
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
def obj_registries(cls, registryname=None):
"""return a tuple of registry names (see __registries__)"""
if registryname:
return (registryname,)
return cls.__registries__
class RegistryStore(dict):
"""This class is responsible for loading objects and storing them
in their registry which is created on the fly as needed.
It handles dynamic registration of objects and provides a
convenient api to access them. To be recognized as an object that
should be stored into one of the store's registry
(:class:`Registry`), an object must provide the following
attributes, used control how they interact with the registry:
:attr:`__registries__`
list of registry names (string like 'views', 'templates'...) into which
the object should be registered
:attr:`__regid__`
object identifier in the registry (string like 'main',
'primary', 'folder_box')
:attr:`__select__`
the object predicate selectors
Moreover, the :attr:`__abstract__` attribute may be set to `True`
to indicate that an object is abstract and should not be registered
(such inherited attributes not considered).
.. Note::
When using the store to load objects dynamically, you *always* have
to use **super()** to get the methods and attributes of the
superclasses, and not use the class identifier. If not, you'll get into
trouble at reload time.
For example, instead of writing::
class Thing(Parent):
__regid__ = 'athing'
__select__ = yes()
def f(self, arg1):
Parent.f(self, arg1)
You must write::
class Thing(Parent):
__regid__ = 'athing'
__select__ = yes()
def f(self, arg1):
super(Thing, self).f(arg1)
Controlling object registration
-------------------------------
Dynamic loading is triggered by calling the :meth:`register_modnames`
method, given a list of modules names to inspect.
.. automethod:: register_modnames
For each module, by default, all compatible objects are registered
automatically. However if some objects come as replacement of
other objects, or have to be included only if some condition is
met, you'll have to define a `registration_callback(vreg)`
function in the module and explicitly register **all objects** in
this module, using the api defined below.
.. automethod:: RegistryStore.register_all
.. automethod:: RegistryStore.register_and_replace
.. automethod:: RegistryStore.register
.. automethod:: RegistryStore.unregister
.. Note::
Once the function `registration_callback(vreg)` is implemented in a
module, all the objects from this module have to be explicitly
registered as it disables the automatic object registration.
Examples:
.. sourcecode:: python
def registration_callback(store):
# register everything in the module except BabarClass
store.register_all(globals().values(), __name__, (BabarClass,))
# conditionally register BabarClass
if 'babar_relation' in store.schema:
store.register(BabarClass)
In this example, we register all application object classes defined in the module
except `BabarClass`. This class is then registered only if the 'babar_relation'
relation type is defined in the instance schema.
.. sourcecode:: python
def registration_callback(store):
store.register(Elephant)
# replace Babar by Celeste
store.register_and_replace(Celeste, Babar)
In this example, we explicitly register classes one by one:
* the `Elephant` class
* the `Celeste` to replace `Babar`
If at some point we register a new appobject class in this module, it won't be
registered at all without modification to the `registration_callback`
implementation. The first example will register it though, thanks to the call
to the `register_all` method.
Controlling registry instantiation
----------------------------------
The `REGISTRY_FACTORY` class dictionary allows to specify which class should
be instantiated for a given registry name. The class associated to `None`
key will be the class used when there is no specific class for a name.
"""
def __init__(self, debugmode=False):
super(RegistryStore, self).__init__()
self.debugmode = debugmode
def reset(self):
"""clear all registries managed by this store"""
# don't use self.clear, we want to keep existing subdictionaries
for subdict in self.values():
subdict.clear()
self._lastmodifs = {}
def __getitem__(self, name):
"""return the registry (dictionary of class objects) associated to
this name
"""
try:
return super(RegistryStore, self).__getitem__(name)
except KeyError:
exc = RegistryNotFound(name)
exc.__traceback__ = sys.exc_info()[-1]
raise exc
# methods for explicit (un)registration ###################################
# default class, when no specific class set
REGISTRY_FACTORY = {None: Registry}
def registry_class(self, regid):
"""return existing registry named regid or use factory to create one and
return it"""
try:
return self.REGISTRY_FACTORY[regid]
except KeyError:
return self.REGISTRY_FACTORY[None]
def setdefault(self, regid):
try:
return self[regid]
except RegistryNotFound:
self[regid] = self.registry_class(regid)(self.debugmode)
return self[regid]
def register_all(self, objects, modname, butclasses=()):
"""register registrable objects into `objects`.
Registrable objects are properly configured subclasses of
:class:`RegistrableObject`. Objects which are not defined in the module
`modname` or which are in `butclasses` won't be registered.
Typical usage is:
.. sourcecode:: python
store.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
So you get partially automatic registration, keeping manual registration
for some object (to use
:meth:`~logilab.common.registry.RegistryStore.register_and_replace` for
instance).
"""
assert isinstance(modname, string_types), \
'modname expected to be a module name (ie string), got %r' % modname
for obj in objects:
if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses:
if isinstance(obj, type):
self._load_ancestors_then_object(modname, obj, butclasses)
else:
self.register(obj)
def register(self, obj, registryname=None, oid=None, clear=False):
"""register `obj` implementation into `registryname` or
`obj.__registries__` if not specified, with identifier `oid` or
`obj.__regid__` if not specified.
If `clear` is true, all objects with the same identifier will be
previously unregistered.
"""
assert not obj.__dict__.get('__abstract__'), obj
for registryname in obj_registries(obj, registryname):
registry = self.setdefault(registryname)
registry.register(obj, oid=oid, clear=clear)
self.debug("register %s in %s['%s']",
registry.objname(obj), registryname, oid or obj.__regid__)
self._loadedmods.setdefault(obj.__module__, {})[registry.objid(obj)] = obj
def unregister(self, obj, registryname=None):
"""unregister `obj` object from the registry `registryname` or
`obj.__registries__` if not specified.
"""
for registryname in obj_registries(obj, registryname):
registry = self[registryname]
registry.unregister(obj)
self.debug("unregister %s from %s['%s']",
registry.objname(obj), registryname, obj.__regid__)
def register_and_replace(self, obj, replaced, registryname=None):
"""register `obj` object into `registryname` or
`obj.__registries__` if not specified. If found, the `replaced` object
will be unregistered first (else a warning will be issued as it is
generally unexpected).
"""
for registryname in obj_registries(obj, registryname):
registry = self[registryname]
registry.register_and_replace(obj, replaced)
self.debug("register %s in %s['%s'] instead of %s",
registry.objname(obj), registryname, obj.__regid__,
registry.objname(replaced))
# initialization methods ###################################################
def init_registration(self, path, extrapath=None):
"""reset registry and walk down path to return list of (path, name)
file modules to be loaded"""
# XXX make this private by renaming it to _init_registration ?
self.reset()
# compute list of all modules that have to be loaded
self._toloadmods, filemods = _toload_info(path, extrapath)
# XXX is _loadedmods still necessary ? It seems like it's useful
# to avoid loading same module twice, especially with the
# _load_ancestors_then_object logic but this needs to be checked
self._loadedmods = {}
return filemods
@deprecated('use register_modnames() instead')
def register_objects(self, path, extrapath=None):
"""register all objects found walking down """
# load views from each directory in the instance's path
# XXX inline init_registration ?
filemods = self.init_registration(path, extrapath)
for filepath, modname in filemods:
self.load_file(filepath, modname)
self.initialization_completed()
def register_modnames(self, modnames):
"""register all objects found in """
self.reset()
self._loadedmods = {}
self._toloadmods = {}
toload = []
for modname in modnames:
filepath = pkgutil.find_loader(modname).get_filename()
if filepath[-4:] in ('.pyc', '.pyo'):
# The source file *must* exists
filepath = filepath[:-1]
self._toloadmods[modname] = filepath
toload.append((filepath, modname))
for filepath, modname in toload:
self.load_file(filepath, modname)
self.initialization_completed()
def initialization_completed(self):
"""call initialization_completed() on all known registries"""
for reg in self.values():
reg.initialization_completed()
def _mdate(self, filepath):
""" return the modification date of a file path """
try:
return stat(filepath)[-2]
except OSError:
# this typically happens on emacs backup files (.#foo.py)
self.warning('Unable to load %s. It is likely to be a backup file',
filepath)
return None
def is_reload_needed(self, path):
"""return True if something module changed and the registry should be
reloaded
"""
lastmodifs = self._lastmodifs
for fileordir in path:
if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
if self.is_reload_needed([join(fileordir, fname)
for fname in listdir(fileordir)]):
return True
elif fileordir[-3:] == '.py':
mdate = self._mdate(fileordir)
if mdate is None:
continue # backup file, see _mdate implementation
elif "flymake" in fileordir:
# flymake + pylint in use, don't consider these they will corrupt the registry
continue
if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
self.info('File %s changed since last visit', fileordir)
return True
return False
def load_file(self, filepath, modname):
""" load registrable objects (if any) from a python file """
if modname in self._loadedmods:
return
self._loadedmods[modname] = {}
mdate = self._mdate(filepath)
if mdate is None:
return # backup file, see _mdate implementation
elif "flymake" in filepath:
# flymake + pylint in use, don't consider these they will corrupt the registry
return
# set update time before module loading, else we get some reloading
# weirdness in case of syntax error or other error while importing the
# module
self._lastmodifs[filepath] = mdate
# load the module
if sys.version_info < (3,) and not isinstance(modname, str):
modname = str(modname)
module = __import__(modname, fromlist=modname.split('.')[:-1])
self.load_module(module)
def load_module(self, module):
"""Automatically handle module objects registration.
Instances are registered as soon as they are hashable and have the
following attributes:
* __regid__ (a string)
* __select__ (a callable)
* __registries__ (a tuple/list of string)
For classes this is a bit more complicated :
- first ensure parent classes are already registered
- class with __abstract__ == True in their local dictionary are skipped
- object class needs to have registries and identifier properly set to a
non empty string to be registered.
"""
self.info('loading %s from %s', module.__name__, module.__file__)
if hasattr(module, 'registration_callback'):
module.registration_callback(self)
else:
self.register_all(vars(module).values(), module.__name__)
def _load_ancestors_then_object(self, modname, objectcls, butclasses=()):
"""handle class registration according to rules defined in
:meth:`load_module`
"""
# backward compat, we used to allow whatever else than classes
if not isinstance(objectcls, type):
if self.is_registrable(objectcls) and objectcls.__module__ == modname:
self.register(objectcls)
return
# imported classes
objmodname = objectcls.__module__
if objmodname != modname:
# The module of the object is not the same as the currently
# worked on module, or this is actually an instance, which
# has no module at all
if objmodname in self._toloadmods:
# if this is still scheduled for loading, let's proceed immediately,
# but using the object module
self.load_file(self._toloadmods[objmodname], objmodname)
return
# ensure object hasn't been already processed
clsid = '%s.%s' % (modname, objectcls.__name__)
if clsid in self._loadedmods[modname]:
return
self._loadedmods[modname][clsid] = objectcls
# ensure ancestors are registered
for parent in objectcls.__bases__:
self._load_ancestors_then_object(modname, parent, butclasses)
# ensure object is registrable
if objectcls in butclasses or not self.is_registrable(objectcls):
return
# backward compat
reg = self.setdefault(obj_registries(objectcls)[0])
if reg.objname(objectcls)[0] == '_':
warn("[lgc 0.59] object whose name start with '_' won't be "
"skipped anymore at some point, use __abstract__ = True "
"instead (%s)" % objectcls, DeprecationWarning)
return
# register, finally
self.register(objectcls)
@classmethod
def is_registrable(cls, obj):
"""ensure `obj` should be registered
as arbitrary stuff may be registered, do a lot of check and warn about
weird cases (think to dumb proxy objects)
"""
if isinstance(obj, type):
if not issubclass(obj, RegistrableObject):
# ducktyping backward compat
if not (getattr(obj, '__registries__', None)
and getattr(obj, '__regid__', None)
and getattr(obj, '__select__', None)):
return False
elif issubclass(obj, RegistrableInstance):
return False
elif not isinstance(obj, RegistrableInstance):
return False
if not obj.__regid__:
return False # no regid
registries = obj.__registries__
if not registries:
return False # no registries
selector = obj.__select__
if not selector:
return False # no selector
if obj.__dict__.get('__abstract__', False):
return False
# then detect potential problems that should be warned
if not isinstance(registries, (tuple, list)):
cls.warning('%s has __registries__ which is not a list or tuple', obj)
return False
if not callable(selector):
cls.warning('%s has not callable __select__', obj)
return False
return True
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
# init logging
set_log_methods(RegistryStore, getLogger('registry.store'))
set_log_methods(Registry, getLogger('registry'))
# helpers for debugging selectors
TRACED_OIDS = None
def _trace_selector(cls, selector, args, ret):
vobj = args[0]
if TRACED_OIDS == 'all' or vobj.__regid__ in TRACED_OIDS:
print('%s -> %s for %s(%s)' % (cls, ret, vobj, vobj.__regid__))
def _lltrace(selector):
"""use this decorator on your predicates so they become traceable with
:class:`traced_selection`
"""
def traced(cls, *args, **kwargs):
ret = selector(cls, *args, **kwargs)
if TRACED_OIDS is not None:
_trace_selector(cls, selector, args, ret)
return ret
traced.__name__ = selector.__name__
traced.__doc__ = selector.__doc__
return traced
class traced_selection(object): # pylint: disable=C0103
"""
Typical usage is :
.. sourcecode:: python
>>> from logilab.common.registry import traced_selection
>>> with traced_selection():
... # some code in which you want to debug selectors
... # for all objects
This will yield lines like this in the logs::
selector one_line_rset returned 0 for
You can also give to :class:`traced_selection` the identifiers of objects on
which you want to debug selection ('oid1' and 'oid2' in the example above).
.. sourcecode:: python
>>> with traced_selection( ('regid1', 'regid2') ):
... # some code in which you want to debug selectors
... # for objects with __regid__ 'regid1' and 'regid2'
A potentially useful point to set up such a tracing function is
the `logilab.common.registry.Registry.select` method body.
"""
def __init__(self, traced='all'):
self.traced = traced
def __enter__(self):
global TRACED_OIDS
TRACED_OIDS = self.traced
def __exit__(self, exctype, exc, traceback):
global TRACED_OIDS
TRACED_OIDS = None
return traceback is None
# selector base classes and operations ########################################
def objectify_predicate(selector_func):
"""Most of the time, a simple score function is enough to build a selector.
The :func:`objectify_predicate` decorator turn it into a proper selector
class::
@objectify_predicate
def one(cls, req, rset=None, **kwargs):
return 1
class MyView(View):
__select__ = View.__select__ & one()
"""
return type(selector_func.__name__, (Predicate,),
{'__doc__': selector_func.__doc__,
'__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
_PREDICATES = {}
def wrap_predicates(decorator):
for predicate in _PREDICATES.values():
if not '_decorators' in predicate.__dict__:
predicate._decorators = set()
if decorator in predicate._decorators:
continue
predicate._decorators.add(decorator)
predicate.__call__ = decorator(predicate.__call__)
class PredicateMetaClass(type):
def __new__(mcs, *args, **kwargs):
# use __new__ so subclasses doesn't have to call Predicate.__init__
inst = type.__new__(mcs, *args, **kwargs)
proxy = weakref.proxy(inst, lambda p: _PREDICATES.pop(id(p)))
_PREDICATES[id(proxy)] = proxy
return inst
@add_metaclass(PredicateMetaClass)
class Predicate(object):
"""base class for selector classes providing implementation
for operators ``&``, ``|`` and ``~``
This class is only here to give access to binary operators, the selector
logic itself should be implemented in the :meth:`__call__` method. Notice it
should usually accept any arbitrary arguments (the context), though that may
vary depending on your usage of the registry.
a selector is called to help choosing the correct object for a
particular context by returning a score (`int`) telling how well
the implementation given as first argument fit to the given context.
0 score means that the class doesn't apply.
"""
@property
def func_name(self):
# backward compatibility
return self.__class__.__name__
def search_selector(self, selector):
"""search for the given selector, selector instance or tuple of
selectors in the selectors tree. Return None if not found.
"""
if self is selector:
return self
if (isinstance(selector, type) or isinstance(selector, tuple)) and \
isinstance(self, selector):
return self
return None
def __str__(self):
return self.__class__.__name__
def __and__(self, other):
return AndPredicate(self, other)
def __rand__(self, other):
return AndPredicate(other, self)
def __iand__(self, other):
return AndPredicate(self, other)
def __or__(self, other):
return OrPredicate(self, other)
def __ror__(self, other):
return OrPredicate(other, self)
def __ior__(self, other):
return OrPredicate(self, other)
def __invert__(self):
return NotPredicate(self)
# XXX (function | function) or (function & function) not managed yet
def __call__(self, cls, *args, **kwargs):
return NotImplementedError("selector %s must implement its logic "
"in its __call__ method" % self.__class__)
def __repr__(self):
return u'' % (self.__class__.__name__, id(self))
class MultiPredicate(Predicate):
"""base class for compound selector classes"""
def __init__(self, *selectors):
self.selectors = self.merge_selectors(selectors)
def __str__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join(str(s) for s in self.selectors))
@classmethod
def merge_selectors(cls, selectors):
"""deal with selector instanciation when necessary and merge
multi-selectors if possible:
AndPredicate(AndPredicate(sel1, sel2), AndPredicate(sel3, sel4))
==> AndPredicate(sel1, sel2, sel3, sel4)
"""
merged_selectors = []
for selector in selectors:
# XXX do we really want magic-transformations below?
# if so, wanna warn about them?
if isinstance(selector, types.FunctionType):
selector = objectify_predicate(selector)()
if isinstance(selector, type) and issubclass(selector, Predicate):
selector = selector()
assert isinstance(selector, Predicate), selector
if isinstance(selector, cls):
merged_selectors += selector.selectors
else:
merged_selectors.append(selector)
return merged_selectors
def search_selector(self, selector):
"""search for the given selector or selector instance (or tuple of
selectors) in the selectors tree. Return None if not found
"""
for childselector in self.selectors:
if childselector is selector:
return childselector
found = childselector.search_selector(selector)
if found is not None:
return found
# if not found in children, maybe we are looking for self?
return super(MultiPredicate, self).search_selector(selector)
class AndPredicate(MultiPredicate):
"""and-chained selectors"""
def __call__(self, cls, *args, **kwargs):
score = 0
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if not partscore:
return 0
score += partscore
return score
class OrPredicate(MultiPredicate):
"""or-chained selectors"""
def __call__(self, cls, *args, **kwargs):
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if partscore:
return partscore
return 0
class NotPredicate(Predicate):
"""negation selector"""
def __init__(self, selector):
self.selector = selector
def __call__(self, cls, *args, **kwargs):
score = self.selector(cls, *args, **kwargs)
return int(not score)
def __str__(self):
return 'NOT(%s)' % self.selector
class yes(Predicate): # pylint: disable=C0103
"""Return the score given as parameter, with a default score of 0.5 so any
other selector take precedence.
Usually used for objects which can be selected whatever the context, or
also sometimes to add arbitrary points to a score.
Take care, `yes(0)` could be named 'no'...
"""
def __init__(self, score=0.5):
self.score = score
def __call__(self, *args, **kwargs):
return self.score
# deprecated stuff #############################################################
@deprecated('[lgc 0.59] use Registry.objid class method instead')
def classid(cls):
return '%s.%s' % (cls.__module__, cls.__name__)
@deprecated('[lgc 0.59] use obj_registries function instead')
def class_registries(cls, registryname):
return obj_registries(cls, registryname)
logilab-common-1.4.1/logilab/common/tasksqueue.py 0000644 0002025 0002025 00000005653 12651420631 023440 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Prioritized tasks queue"""
__docformat__ = "restructuredtext en"
from bisect import insort_left
from six.moves import queue
LOW = 0
MEDIUM = 10
HIGH = 100
PRIORITY = {
'LOW': LOW,
'MEDIUM': MEDIUM,
'HIGH': HIGH,
}
REVERSE_PRIORITY = dict((values, key) for key, values in PRIORITY.items())
class PrioritizedTasksQueue(queue.Queue):
def _init(self, maxsize):
"""Initialize the queue representation"""
self.maxsize = maxsize
# ordered list of task, from the lowest to the highest priority
self.queue = []
def _put(self, item):
"""Put a new item in the queue"""
for i, task in enumerate(self.queue):
# equivalent task
if task == item:
# if new task has a higher priority, remove the one already
# queued so the new priority will be considered
if task < item:
item.merge(task)
del self.queue[i]
break
# else keep it so current order is kept
task.merge(item)
return
insort_left(self.queue, item)
def _get(self):
"""Get an item from the queue"""
return self.queue.pop()
def __iter__(self):
return iter(self.queue)
def remove(self, tid):
"""remove a specific task from the queue"""
# XXX acquire lock
for i, task in enumerate(self):
if task.id == tid:
self.queue.pop(i)
return
raise ValueError('not task of id %s in queue' % tid)
class Task(object):
def __init__(self, tid, priority=LOW):
# task id
self.id = tid
# task priority
self.priority = priority
def __repr__(self):
return '' % (self.id, id(self))
def __cmp__(self, other):
return cmp(self.priority, other.priority)
def __lt__(self, other):
return self.priority < other.priority
def __eq__(self, other):
return self.id == other.id
__hash__ = object.__hash__
def merge(self, other):
pass
logilab-common-1.4.1/logilab/common/vcgutils.py 0000644 0002025 0002025 00000016771 12651420631 023111 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Functions to generate files readable with Georg Sander's vcg
(Visualization of Compiler Graphs).
You can download vcg at http://rw4.cs.uni-sb.de/~sander/html/gshome.html
Note that vcg exists as a debian package.
See vcg's documentation for explanation about the different values that
maybe used for the functions parameters.
"""
__docformat__ = "restructuredtext en"
import string
ATTRS_VAL = {
'algos': ('dfs', 'tree', 'minbackward',
'left_to_right', 'right_to_left',
'top_to_bottom', 'bottom_to_top',
'maxdepth', 'maxdepthslow', 'mindepth', 'mindepthslow',
'mindegree', 'minindegree', 'minoutdegree',
'maxdegree', 'maxindegree', 'maxoutdegree'),
'booleans': ('yes', 'no'),
'colors': ('black', 'white', 'blue', 'red', 'green', 'yellow',
'magenta', 'lightgrey',
'cyan', 'darkgrey', 'darkblue', 'darkred', 'darkgreen',
'darkyellow', 'darkmagenta', 'darkcyan', 'gold',
'lightblue', 'lightred', 'lightgreen', 'lightyellow',
'lightmagenta', 'lightcyan', 'lilac', 'turquoise',
'aquamarine', 'khaki', 'purple', 'yellowgreen', 'pink',
'orange', 'orchid'),
'shapes': ('box', 'ellipse', 'rhomb', 'triangle'),
'textmodes': ('center', 'left_justify', 'right_justify'),
'arrowstyles': ('solid', 'line', 'none'),
'linestyles': ('continuous', 'dashed', 'dotted', 'invisible'),
}
# meaning of possible values:
# O -> string
# 1 -> int
# list -> value in list
GRAPH_ATTRS = {
'title': 0,
'label': 0,
'color': ATTRS_VAL['colors'],
'textcolor': ATTRS_VAL['colors'],
'bordercolor': ATTRS_VAL['colors'],
'width': 1,
'height': 1,
'borderwidth': 1,
'textmode': ATTRS_VAL['textmodes'],
'shape': ATTRS_VAL['shapes'],
'shrink': 1,
'stretch': 1,
'orientation': ATTRS_VAL['algos'],
'vertical_order': 1,
'horizontal_order': 1,
'xspace': 1,
'yspace': 1,
'layoutalgorithm': ATTRS_VAL['algos'],
'late_edge_labels': ATTRS_VAL['booleans'],
'display_edge_labels': ATTRS_VAL['booleans'],
'dirty_edge_labels': ATTRS_VAL['booleans'],
'finetuning': ATTRS_VAL['booleans'],
'manhattan_edges': ATTRS_VAL['booleans'],
'smanhattan_edges': ATTRS_VAL['booleans'],
'port_sharing': ATTRS_VAL['booleans'],
'edges': ATTRS_VAL['booleans'],
'nodes': ATTRS_VAL['booleans'],
'splines': ATTRS_VAL['booleans'],
}
NODE_ATTRS = {
'title': 0,
'label': 0,
'color': ATTRS_VAL['colors'],
'textcolor': ATTRS_VAL['colors'],
'bordercolor': ATTRS_VAL['colors'],
'width': 1,
'height': 1,
'borderwidth': 1,
'textmode': ATTRS_VAL['textmodes'],
'shape': ATTRS_VAL['shapes'],
'shrink': 1,
'stretch': 1,
'vertical_order': 1,
'horizontal_order': 1,
}
EDGE_ATTRS = {
'sourcename': 0,
'targetname': 0,
'label': 0,
'linestyle': ATTRS_VAL['linestyles'],
'class': 1,
'thickness': 0,
'color': ATTRS_VAL['colors'],
'textcolor': ATTRS_VAL['colors'],
'arrowcolor': ATTRS_VAL['colors'],
'backarrowcolor': ATTRS_VAL['colors'],
'arrowsize': 1,
'backarrowsize': 1,
'arrowstyle': ATTRS_VAL['arrowstyles'],
'backarrowstyle': ATTRS_VAL['arrowstyles'],
'textmode': ATTRS_VAL['textmodes'],
'priority': 1,
'anchor': 1,
'horizontal_order': 1,
}
# Misc utilities ###############################################################
def latin_to_vcg(st):
"""Convert latin characters using vcg escape sequence.
"""
for char in st:
if char not in string.ascii_letters:
try:
num = ord(char)
if num >= 192:
st = st.replace(char, r'\fi%d'%ord(char))
except:
pass
return st
class VCGPrinter:
"""A vcg graph writer.
"""
def __init__(self, output_stream):
self._stream = output_stream
self._indent = ''
def open_graph(self, **args):
"""open a vcg graph
"""
self._stream.write('%sgraph:{\n'%self._indent)
self._inc_indent()
self._write_attributes(GRAPH_ATTRS, **args)
def close_graph(self):
"""close a vcg graph
"""
self._dec_indent()
self._stream.write('%s}\n'%self._indent)
def node(self, title, **args):
"""draw a node
"""
self._stream.write('%snode: {title:"%s"' % (self._indent, title))
self._write_attributes(NODE_ATTRS, **args)
self._stream.write('}\n')
def edge(self, from_node, to_node, edge_type='', **args):
"""draw an edge from a node to another.
"""
self._stream.write(
'%s%sedge: {sourcename:"%s" targetname:"%s"' % (
self._indent, edge_type, from_node, to_node))
self._write_attributes(EDGE_ATTRS, **args)
self._stream.write('}\n')
# private ##################################################################
def _write_attributes(self, attributes_dict, **args):
"""write graph, node or edge attributes
"""
for key, value in args.items():
try:
_type = attributes_dict[key]
except KeyError:
raise Exception('''no such attribute %s
possible attributes are %s''' % (key, attributes_dict.keys()))
if not _type:
self._stream.write('%s%s:"%s"\n' % (self._indent, key, value))
elif _type == 1:
self._stream.write('%s%s:%s\n' % (self._indent, key,
int(value)))
elif value in _type:
self._stream.write('%s%s:%s\n' % (self._indent, key, value))
else:
raise Exception('''value %s isn\'t correct for attribute %s
correct values are %s''' % (value, key, _type))
def _inc_indent(self):
"""increment indentation
"""
self._indent = ' %s' % self._indent
def _dec_indent(self):
"""decrement indentation
"""
self._indent = self._indent[:-2]
logilab-common-1.4.1/logilab/common/umessage.py 0000644 0002025 0002025 00000013567 12651420631 023062 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Unicode email support (extends email from stdlib)"""
__docformat__ = "restructuredtext en"
import email
from encodings import search_function
import sys
from email.utils import parseaddr, parsedate
from email.header import decode_header
from datetime import datetime
from six import text_type, binary_type
try:
from mx.DateTime import DateTime
except ImportError:
DateTime = datetime
import logilab.common as lgc
def decode_QP(string):
parts = []
for decoded, charset in decode_header(string):
if not charset :
charset = 'iso-8859-15'
# python 3 sometimes returns str and sometimes bytes.
# the 'official' fix is to use the new 'policy' APIs
# https://bugs.python.org/issue24797
# let's just handle this bug ourselves for now
if isinstance(decoded, binary_type):
decoded = decoded.decode(charset, 'replace')
assert isinstance(decoded, text_type)
parts.append(decoded)
if sys.version_info < (3, 3):
# decoding was non-RFC compliant wrt to whitespace handling
# see http://bugs.python.org/issue1079
return u' '.join(parts)
return u''.join(parts)
def message_from_file(fd):
try:
return UMessage(email.message_from_file(fd))
except email.errors.MessageParseError:
return ''
def message_from_string(string):
try:
return UMessage(email.message_from_string(string))
except email.errors.MessageParseError:
return ''
class UMessage:
"""Encapsulates an email.Message instance and returns only unicode objects.
"""
def __init__(self, message):
self.message = message
# email.Message interface #################################################
def get(self, header, default=None):
value = self.message.get(header, default)
if value:
return decode_QP(value)
return value
def __getitem__(self, header):
return self.get(header)
def get_all(self, header, default=()):
return [decode_QP(val) for val in self.message.get_all(header, default)
if val is not None]
def is_multipart(self):
return self.message.is_multipart()
def get_boundary(self):
return self.message.get_boundary()
def walk(self):
for part in self.message.walk():
yield UMessage(part)
def get_payload(self, index=None, decode=False):
message = self.message
if index is None:
payload = message.get_payload(index, decode)
if isinstance(payload, list):
return [UMessage(msg) for msg in payload]
if message.get_content_maintype() != 'text':
return payload
if isinstance(payload, text_type):
return payload
charset = message.get_content_charset() or 'iso-8859-1'
if search_function(charset) is None:
charset = 'iso-8859-1'
return text_type(payload or b'', charset, "replace")
else:
payload = UMessage(message.get_payload(index, decode))
return payload
def get_content_maintype(self):
return text_type(self.message.get_content_maintype())
def get_content_type(self):
return text_type(self.message.get_content_type())
def get_filename(self, failobj=None):
value = self.message.get_filename(failobj)
if value is failobj:
return value
try:
return text_type(value)
except UnicodeDecodeError:
return u'error decoding filename'
# other convenience methods ###############################################
def headers(self):
"""return an unicode string containing all the message's headers"""
values = []
for header in self.message.keys():
values.append(u'%s: %s' % (header, self.get(header)))
return '\n'.join(values)
def multi_addrs(self, header):
"""return a list of 2-uple (name, address) for the given address (which
is expected to be an header containing address such as from, to, cc...)
"""
persons = []
for person in self.get_all(header, ()):
name, mail = parseaddr(person)
persons.append((name, mail))
return persons
def date(self, alternative_source=False, return_str=False):
"""return a datetime object for the email's date or None if no date is
set or if it can't be parsed
"""
value = self.get('date')
if value is None and alternative_source:
unix_from = self.message.get_unixfrom()
if unix_from is not None:
try:
value = unix_from.split(" ", 2)[2]
except IndexError:
pass
if value is not None:
datetuple = parsedate(value)
if datetuple:
if lgc.USE_MX_DATETIME:
return DateTime(*datetuple[:6])
return datetime(*datetuple[:6])
elif not return_str:
return None
return value
logilab-common-1.4.1/logilab/common/__init__.py 0000644 0002025 0002025 00000012436 12723763004 023006 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Logilab common library (aka Logilab's extension to the standard library).
:type STD_BLACKLIST: tuple
:var STD_BLACKLIST: directories ignored by default by the functions in
this package which have to recurse into directories
:type IGNORED_EXTENSIONS: tuple
:var IGNORED_EXTENSIONS: file extensions that may usually be ignored
"""
__docformat__ = "restructuredtext en"
import sys
import types
import pkg_resources
__version__ = pkg_resources.get_distribution('logilab-common').version
# deprecated, but keep compatibility with pylint < 1.4.4
__pkginfo__ = types.ModuleType('__pkginfo__')
__pkginfo__.__package__ = __name__
__pkginfo__.version = __version__
sys.modules['logilab.common.__pkginfo__'] = __pkginfo__
STD_BLACKLIST = ('CVS', '.svn', '.hg', '.git', '.tox', 'debian', 'dist', 'build')
IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc', '~', '.swp', '.orig')
# set this to False if you've mx DateTime installed but you don't want your db
# adapter to use it (should be set before you got a connection)
USE_MX_DATETIME = True
class attrdict(dict):
"""A dictionary for which keys are also accessible as attributes."""
def __getattr__(self, attr):
try:
return self[attr]
except KeyError:
raise AttributeError(attr)
class dictattr(dict):
def __init__(self, proxy):
self.__proxy = proxy
def __getitem__(self, attr):
try:
return getattr(self.__proxy, attr)
except AttributeError:
raise KeyError(attr)
class nullobject(object):
def __repr__(self):
return ''
def __bool__(self):
return False
__nonzero__ = __bool__
class tempattr(object):
def __init__(self, obj, attr, value):
self.obj = obj
self.attr = attr
self.value = value
def __enter__(self):
self.oldvalue = getattr(self.obj, self.attr)
setattr(self.obj, self.attr, self.value)
return self.obj
def __exit__(self, exctype, value, traceback):
setattr(self.obj, self.attr, self.oldvalue)
# flatten -----
# XXX move in a specific module and use yield instead
# do not mix flatten and translate
#
# def iterable(obj):
# try: iter(obj)
# except: return False
# return True
#
# def is_string_like(obj):
# try: obj +''
# except (TypeError, ValueError): return False
# return True
#
#def is_scalar(obj):
# return is_string_like(obj) or not iterable(obj)
#
#def flatten(seq):
# for item in seq:
# if is_scalar(item):
# yield item
# else:
# for subitem in flatten(item):
# yield subitem
def flatten(iterable, tr_func=None, results=None):
"""Flatten a list of list with any level.
If tr_func is not None, it should be a one argument function that'll be called
on each final element.
:rtype: list
>>> flatten([1, [2, 3]])
[1, 2, 3]
"""
if results is None:
results = []
for val in iterable:
if isinstance(val, (list, tuple)):
flatten(val, tr_func, results)
elif tr_func is None:
results.append(val)
else:
results.append(tr_func(val))
return results
# XXX is function below still used ?
def make_domains(lists):
"""
Given a list of lists, return a list of domain for each list to produce all
combinations of possibles values.
:rtype: list
Example:
>>> make_domains(['a', 'b'], ['c','d', 'e'])
[['a', 'b', 'a', 'b', 'a', 'b'], ['c', 'c', 'd', 'd', 'e', 'e']]
"""
from six.moves import range
domains = []
for iterable in lists:
new_domain = iterable[:]
for i in range(len(domains)):
domains[i] = domains[i]*len(iterable)
if domains:
missing = (len(domains[0]) - len(iterable)) / len(iterable)
i = 0
for j in range(len(iterable)):
value = iterable[j]
for dummy in range(missing):
new_domain.insert(i, value)
i += 1
i += 1
domains.append(new_domain)
return domains
# private stuff ################################################################
def _handle_blacklist(blacklist, dirnames, filenames):
"""remove files/directories in the black list
dirnames/filenames are usually from os.walk
"""
for norecurs in blacklist:
if norecurs in dirnames:
dirnames.remove(norecurs)
elif norecurs in filenames:
filenames.remove(norecurs)
logilab-common-1.4.1/logilab/common/testlib.py 0000644 0002025 0002025 00000056415 13013556007 022716 0 ustar dlaxalde dlaxalde 0000000 0000000 # -*- coding: utf-8 -*-
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Run tests.
This will find all modules whose name match a given prefix in the test
directory, and run them. Various command line options provide
additional facilities.
Command line options:
-v verbose -- run tests in verbose mode with output to stdout
-q quiet -- don't print anything except if a test fails
-t testdir -- directory where the tests will be found
-x exclude -- add a test to exclude
-p profile -- profiled execution
-d dbc -- enable design-by-contract
-m match -- only run test matching the tag pattern which follow
If no non-option arguments are present, prefixes used are 'test',
'regrtest', 'smoketest' and 'unittest'.
"""
from __future__ import print_function
__docformat__ = "restructuredtext en"
# modified copy of some functions from test/regrtest.py from PyXml
# disable camel case warning
# pylint: disable=C0103
from contextlib import contextmanager
import sys
import os, os.path as osp
import re
import difflib
import tempfile
import math
import warnings
from shutil import rmtree
from operator import itemgetter
from inspect import isgeneratorfunction
from six import PY2, add_metaclass, string_types
from six.moves import builtins, range, configparser, input
from logilab.common.deprecation import class_deprecated, deprecated
import unittest as unittest_legacy
if not getattr(unittest_legacy, "__package__", None):
try:
import unittest2 as unittest
from unittest2 import SkipTest
except ImportError:
raise ImportError("You have to install python-unittest2 to use %s" % __name__)
else:
import unittest as unittest
from unittest import SkipTest
from functools import wraps
from logilab.common.debugger import Debugger
from logilab.common.decorators import cached, classproperty
from logilab.common import textutils
__all__ = ['unittest_main', 'find_tests', 'nocoverage', 'pause_trace']
DEFAULT_PREFIXES = ('test', 'regrtest', 'smoketest', 'unittest',
'func', 'validation')
is_generator = deprecated('[lgc 0.63] use inspect.isgeneratorfunction')(isgeneratorfunction)
# used by unittest to count the number of relevant levels in the traceback
__unittest = 1
@deprecated('with_tempdir is deprecated, use {0}.TemporaryDirectory.'.format(
'tempfile' if not PY2 else 'backports.tempfile'))
def with_tempdir(callable):
"""A decorator ensuring no temporary file left when the function return
Work only for temporary file created with the tempfile module"""
if isgeneratorfunction(callable):
def proxy(*args, **kwargs):
old_tmpdir = tempfile.gettempdir()
new_tmpdir = tempfile.mkdtemp(prefix="temp-lgc-")
tempfile.tempdir = new_tmpdir
try:
for x in callable(*args, **kwargs):
yield x
finally:
try:
rmtree(new_tmpdir, ignore_errors=True)
finally:
tempfile.tempdir = old_tmpdir
return proxy
@wraps(callable)
def proxy(*args, **kargs):
old_tmpdir = tempfile.gettempdir()
new_tmpdir = tempfile.mkdtemp(prefix="temp-lgc-")
tempfile.tempdir = new_tmpdir
try:
return callable(*args, **kargs)
finally:
try:
rmtree(new_tmpdir, ignore_errors=True)
finally:
tempfile.tempdir = old_tmpdir
return proxy
def in_tempdir(callable):
"""A decorator moving the enclosed function inside the tempfile.tempfdir
"""
@wraps(callable)
def proxy(*args, **kargs):
old_cwd = os.getcwd()
os.chdir(tempfile.tempdir)
try:
return callable(*args, **kargs)
finally:
os.chdir(old_cwd)
return proxy
def within_tempdir(callable):
"""A decorator run the enclosed function inside a tmpdir removed after execution
"""
proxy = with_tempdir(in_tempdir(callable))
proxy.__name__ = callable.__name__
return proxy
def find_tests(testdir,
prefixes=DEFAULT_PREFIXES, suffix=".py",
excludes=(),
remove_suffix=True):
"""
Return a list of all applicable test modules.
"""
tests = []
for name in os.listdir(testdir):
if not suffix or name.endswith(suffix):
for prefix in prefixes:
if name.startswith(prefix):
if remove_suffix and name.endswith(suffix):
name = name[:-len(suffix)]
if name not in excludes:
tests.append(name)
tests.sort()
return tests
## PostMortem Debug facilities #####
def start_interactive_mode(result):
"""starts an interactive shell so that the user can inspect errors
"""
debuggers = result.debuggers
descrs = result.error_descrs + result.fail_descrs
if len(debuggers) == 1:
# don't ask for test name if there's only one failure
debuggers[0].start()
else:
while True:
testindex = 0
print("Choose a test to debug:")
# order debuggers in the same way than errors were printed
print("\n".join(['\t%s : %s' % (i, descr) for i, (_, descr)
in enumerate(descrs)]))
print("Type 'exit' (or ^D) to quit")
print()
try:
todebug = input('Enter a test name: ')
if todebug.strip().lower() == 'exit':
print()
break
else:
try:
testindex = int(todebug)
debugger = debuggers[descrs[testindex][0]]
except (ValueError, IndexError):
print("ERROR: invalid test number %r" % (todebug, ))
else:
debugger.start()
except (EOFError, KeyboardInterrupt):
print()
break
# coverage pausing tools #####################################################
@contextmanager
def replace_trace(trace=None):
"""A context manager that temporary replaces the trace function"""
oldtrace = sys.gettrace()
sys.settrace(trace)
try:
yield
finally:
# specific hack to work around a bug in pycoverage, see
# https://bitbucket.org/ned/coveragepy/issue/123
if (oldtrace is not None and not callable(oldtrace) and
hasattr(oldtrace, 'pytrace')):
oldtrace = oldtrace.pytrace
sys.settrace(oldtrace)
pause_trace = replace_trace
def nocoverage(func):
"""Function decorator that pauses tracing functions"""
if hasattr(func, 'uncovered'):
return func
func.uncovered = True
def not_covered(*args, **kwargs):
with pause_trace():
return func(*args, **kwargs)
not_covered.uncovered = True
return not_covered
# test utils ##################################################################
# Add deprecation warnings about new api used by module level fixtures in unittest2
# http://www.voidspace.org.uk/python/articles/unittest2.shtml#setupmodule-and-teardownmodule
class _DebugResult(object): # simplify import statement among unittest flavors..
"Used by the TestSuite to hold previous class when running in debug."
_previousTestClass = None
_moduleSetUpFailed = False
shouldStop = False
# backward compatibility: TestSuite might be imported from lgc.testlib
TestSuite = unittest.TestSuite
class keywords(dict):
"""Keyword args (**kwargs) support for generative tests."""
class starargs(tuple):
"""Variable arguments (*args) for generative tests."""
def __new__(cls, *args):
return tuple.__new__(cls, args)
unittest_main = unittest.main
class InnerTestSkipped(SkipTest):
"""raised when a test is skipped"""
pass
def parse_generative_args(params):
args = []
varargs = ()
kwargs = {}
flags = 0 # 2 <=> starargs, 4 <=> kwargs
for param in params:
if isinstance(param, starargs):
varargs = param
if flags:
raise TypeError('found starargs after keywords !')
flags |= 2
args += list(varargs)
elif isinstance(param, keywords):
kwargs = param
if flags & 4:
raise TypeError('got multiple keywords parameters')
flags |= 4
elif flags & 2 or flags & 4:
raise TypeError('found parameters after kwargs or args')
else:
args.append(param)
return args, kwargs
class InnerTest(tuple):
def __new__(cls, name, *data):
instance = tuple.__new__(cls, data)
instance.name = name
return instance
class Tags(set):
"""A set of tag able validate an expression"""
def __init__(self, *tags, **kwargs):
self.inherit = kwargs.pop('inherit', True)
if kwargs:
raise TypeError("%s are an invalid keyword argument for this function" % kwargs.keys())
if len(tags) == 1 and not isinstance(tags[0], string_types):
tags = tags[0]
super(Tags, self).__init__(tags, **kwargs)
def __getitem__(self, key):
return key in self
def match(self, exp):
return eval(exp, {}, self)
def __or__(self, other):
return Tags(*super(Tags, self).__or__(other))
# duplicate definition from unittest2 of the _deprecate decorator
def _deprecate(original_func):
def deprecated_func(*args, **kwargs):
warnings.warn(
('Please use %s instead.' % original_func.__name__),
DeprecationWarning, 2)
return original_func(*args, **kwargs)
return deprecated_func
class TestCase(unittest.TestCase):
"""A unittest.TestCase extension with some additional methods."""
maxDiff = None
tags = Tags()
def __init__(self, methodName='runTest'):
super(TestCase, self).__init__(methodName)
self.__exc_info = sys.exc_info
self.__testMethodName = self._testMethodName
self._current_test_descr = None
self._options_ = None
@classproperty
@cached
def datadir(cls): # pylint: disable=E0213
"""helper attribute holding the standard test's data directory
NOTE: this is a logilab's standard
"""
mod = sys.modules[cls.__module__]
return osp.join(osp.dirname(osp.abspath(mod.__file__)), 'data')
# cache it (use a class method to cache on class since TestCase is
# instantiated for each test run)
@classmethod
def datapath(cls, *fname):
"""joins the object's datadir and `fname`"""
return osp.join(cls.datadir, *fname)
def set_description(self, descr):
"""sets the current test's description.
This can be useful for generative tests because it allows to specify
a description per yield
"""
self._current_test_descr = descr
# override default's unittest.py feature
def shortDescription(self):
"""override default unittest shortDescription to handle correctly
generative tests
"""
if self._current_test_descr is not None:
return self._current_test_descr
return super(TestCase, self).shortDescription()
def quiet_run(self, result, func, *args, **kwargs):
try:
func(*args, **kwargs)
except (KeyboardInterrupt, SystemExit):
raise
except unittest.SkipTest as e:
if hasattr(result, 'addSkip'):
result.addSkip(self, str(e))
else:
warnings.warn("TestResult has no addSkip method, skips not reported",
RuntimeWarning, 2)
result.addSuccess(self)
return False
except:
result.addError(self, self.__exc_info())
return False
return True
def _get_test_method(self):
"""return the test method"""
return getattr(self, self._testMethodName)
def optval(self, option, default=None):
"""return the option value or default if the option is not define"""
return getattr(self._options_, option, default)
def __call__(self, result=None, runcondition=None, options=None):
"""rewrite TestCase.__call__ to support generative tests
This is mostly a copy/paste from unittest.py (i.e same
variable names, same logic, except for the generative tests part)
"""
if result is None:
result = self.defaultTestResult()
self._options_ = options
# if result.cvg:
# result.cvg.start()
testMethod = self._get_test_method()
if (getattr(self.__class__, "__unittest_skip__", False) or
getattr(testMethod, "__unittest_skip__", False)):
# If the class or method was skipped.
try:
skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
or getattr(testMethod, '__unittest_skip_why__', ''))
if hasattr(result, 'addSkip'):
result.addSkip(self, skip_why)
else:
warnings.warn("TestResult has no addSkip method, skips not reported",
RuntimeWarning, 2)
result.addSuccess(self)
finally:
result.stopTest(self)
return
if runcondition and not runcondition(testMethod):
return # test is skipped
result.startTest(self)
try:
if not self.quiet_run(result, self.setUp):
return
generative = isgeneratorfunction(testMethod)
# generative tests
if generative:
self._proceed_generative(result, testMethod,
runcondition)
else:
status = self._proceed(result, testMethod)
success = (status == 0)
if not self.quiet_run(result, self.tearDown):
return
if not generative and success:
result.addSuccess(self)
finally:
# if result.cvg:
# result.cvg.stop()
result.stopTest(self)
def _proceed_generative(self, result, testfunc, runcondition=None):
# cancel startTest()'s increment
result.testsRun -= 1
success = True
try:
for params in testfunc():
if runcondition and not runcondition(testfunc,
skipgenerator=False):
if not (isinstance(params, InnerTest)
and runcondition(params)):
continue
if not isinstance(params, (tuple, list)):
params = (params, )
func = params[0]
args, kwargs = parse_generative_args(params[1:])
# increment test counter manually
result.testsRun += 1
status = self._proceed(result, func, args, kwargs)
if status == 0:
result.addSuccess(self)
success = True
else:
success = False
# XXX Don't stop anymore if an error occured
#if status == 2:
# result.shouldStop = True
if result.shouldStop: # either on error or on exitfirst + error
break
except self.failureException:
result.addFailure(self, self.__exc_info())
success = False
except SkipTest as e:
result.addSkip(self, e)
except:
# if an error occurs between two yield
result.addError(self, self.__exc_info())
success = False
return success
def _proceed(self, result, testfunc, args=(), kwargs=None):
"""proceed the actual test
returns 0 on success, 1 on failure, 2 on error
Note: addSuccess can't be called here because we have to wait
for tearDown to be successfully executed to declare the test as
successful
"""
kwargs = kwargs or {}
try:
testfunc(*args, **kwargs)
except self.failureException:
result.addFailure(self, self.__exc_info())
return 1
except KeyboardInterrupt:
raise
except InnerTestSkipped as e:
result.addSkip(self, e)
return 1
except SkipTest as e:
result.addSkip(self, e)
return 0
except:
result.addError(self, self.__exc_info())
return 2
return 0
def innerSkip(self, msg=None):
"""mark a generative test as skipped for the reason"""
msg = msg or 'test was skipped'
raise InnerTestSkipped(msg)
if sys.version_info >= (3,2):
assertItemsEqual = unittest.TestCase.assertCountEqual
else:
assertCountEqual = unittest.TestCase.assertItemsEqual
TestCase.assertItemsEqual = deprecated('assertItemsEqual is deprecated, use assertCountEqual')(
TestCase.assertItemsEqual)
import doctest
class SkippedSuite(unittest.TestSuite):
def test(self):
"""just there to trigger test execution"""
self.skipped_test('doctest module has no DocTestSuite class')
class DocTestFinder(doctest.DocTestFinder):
def __init__(self, *args, **kwargs):
self.skipped = kwargs.pop('skipped', ())
doctest.DocTestFinder.__init__(self, *args, **kwargs)
def _get_test(self, obj, name, module, globs, source_lines):
"""override default _get_test method to be able to skip tests
according to skipped attribute's value
"""
if getattr(obj, '__name__', '') in self.skipped:
return None
return doctest.DocTestFinder._get_test(self, obj, name, module,
globs, source_lines)
@add_metaclass(class_deprecated)
class DocTest(TestCase):
"""trigger module doctest
I don't know how to make unittest.main consider the DocTestSuite instance
without this hack
"""
__deprecation_warning__ = 'use stdlib doctest module with unittest API directly'
skipped = ()
def __call__(self, result=None, runcondition=None, options=None):\
# pylint: disable=W0613
try:
finder = DocTestFinder(skipped=self.skipped)
suite = doctest.DocTestSuite(self.module, test_finder=finder)
# XXX iirk
doctest.DocTestCase._TestCase__exc_info = sys.exc_info
except AttributeError:
suite = SkippedSuite()
# doctest may gork the builtins dictionnary
# This happen to the "_" entry used by gettext
old_builtins = builtins.__dict__.copy()
try:
return suite.run(result)
finally:
builtins.__dict__.clear()
builtins.__dict__.update(old_builtins)
run = __call__
def test(self):
"""just there to trigger test execution"""
class MockConnection:
"""fake DB-API 2.0 connexion AND cursor (i.e. cursor() return self)"""
def __init__(self, results):
self.received = []
self.states = []
self.results = results
def cursor(self):
"""Mock cursor method"""
return self
def execute(self, query, args=None):
"""Mock execute method"""
self.received.append( (query, args) )
def fetchone(self):
"""Mock fetchone method"""
return self.results[0]
def fetchall(self):
"""Mock fetchall method"""
return self.results
def commit(self):
"""Mock commiy method"""
self.states.append( ('commit', len(self.received)) )
def rollback(self):
"""Mock rollback method"""
self.states.append( ('rollback', len(self.received)) )
def close(self):
"""Mock close method"""
pass
def mock_object(**params):
"""creates an object using params to set attributes
>>> option = mock_object(verbose=False, index=range(5))
>>> option.verbose
False
>>> option.index
[0, 1, 2, 3, 4]
"""
return type('Mock', (), params)()
def create_files(paths, chroot):
"""Creates directories and files found in .
:param paths: list of relative paths to files or directories
:param chroot: the root directory in which paths will be created
>>> from os.path import isdir, isfile
>>> isdir('/tmp/a')
False
>>> create_files(['a/b/foo.py', 'a/b/c/', 'a/b/c/d/e.py'], '/tmp')
>>> isdir('/tmp/a')
True
>>> isdir('/tmp/a/b/c')
True
>>> isfile('/tmp/a/b/c/d/e.py')
True
>>> isfile('/tmp/a/b/foo.py')
True
"""
dirs, files = set(), set()
for path in paths:
path = osp.join(chroot, path)
filename = osp.basename(path)
# path is a directory path
if filename == '':
dirs.add(path)
# path is a filename path
else:
dirs.add(osp.dirname(path))
files.add(path)
for dirpath in dirs:
if not osp.isdir(dirpath):
os.makedirs(dirpath)
for filepath in files:
open(filepath, 'w').close()
class AttrObject: # XXX cf mock_object
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def tag(*args, **kwargs):
"""descriptor adding tag to a function"""
def desc(func):
assert not hasattr(func, 'tags')
func.tags = Tags(*args, **kwargs)
return func
return desc
def require_version(version):
""" Compare version of python interpreter to the given one. Skip the test
if older.
"""
def check_require_version(f):
version_elements = version.split('.')
try:
compare = tuple([int(v) for v in version_elements])
except ValueError:
raise ValueError('%s is not a correct version : should be X.Y[.Z].' % version)
current = sys.version_info[:3]
if current < compare:
def new_f(self, *args, **kwargs):
self.skipTest('Need at least %s version of python. Current version is %s.' % (version, '.'.join([str(element) for element in current])))
new_f.__name__ = f.__name__
return new_f
else:
return f
return check_require_version
def require_module(module):
""" Check if the given module is loaded. Skip the test if not.
"""
def check_require_module(f):
try:
__import__(module)
return f
except ImportError:
def new_f(self, *args, **kwargs):
self.skipTest('%s can not be imported.' % module)
new_f.__name__ = f.__name__
return new_f
return check_require_module
logilab-common-1.4.1/logilab/common/changelog.py 0000644 0002025 0002025 00000020300 12723763004 023163 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with logilab-common. If not, see .
"""Manipulation of upstream change log files.
The upstream change log files format handled is simpler than the one
often used such as those generated by the default Emacs changelog mode.
Sample ChangeLog format::
Change log for project Yoo
==========================
--
* add a new functionality
2002-02-01 -- 0.1.1
* fix bug #435454
* fix bug #434356
2002-01-01 -- 0.1
* initial release
There is 3 entries in this change log, one for each released version and one
for the next version (i.e. the current entry).
Each entry contains a set of messages corresponding to changes done in this
release.
All the non empty lines before the first entry are considered as the change
log title.
"""
__docformat__ = "restructuredtext en"
import sys
from stat import S_IWRITE
import codecs
from six import string_types
BULLET = '*'
SUBBULLET = '-'
INDENT = ' ' * 4
class NoEntry(Exception):
"""raised when we are unable to find an entry"""
class EntryNotFound(Exception):
"""raised when we are unable to find a given entry"""
class Version(tuple):
"""simple class to handle soft version number has a tuple while
correctly printing it as X.Y.Z
"""
def __new__(cls, versionstr):
if isinstance(versionstr, string_types):
versionstr = versionstr.strip(' :') # XXX (syt) duh?
parsed = cls.parse(versionstr)
else:
parsed = versionstr
return tuple.__new__(cls, parsed)
@classmethod
def parse(cls, versionstr):
versionstr = versionstr.strip(' :')
try:
return [int(i) for i in versionstr.split('.')]
except ValueError as ex:
raise ValueError("invalid literal for version '%s' (%s)" %
(versionstr, ex))
def __str__(self):
return '.'.join([str(i) for i in self])
# upstream change log #########################################################
class ChangeLogEntry(object):
"""a change log entry, i.e. a set of messages associated to a version and
its release date
"""
version_class = Version
def __init__(self, date=None, version=None, **kwargs):
self.__dict__.update(kwargs)
if version:
self.version = self.version_class(version)
else:
self.version = None
self.date = date
self.messages = []
def add_message(self, msg):
"""add a new message"""
self.messages.append(([msg], []))
def complete_latest_message(self, msg_suite):
"""complete the latest added message
"""
if not self.messages:
raise ValueError('unable to complete last message as '
'there is no previous message)')
if self.messages[-1][1]: # sub messages
self.messages[-1][1][-1].append(msg_suite)
else: # message
self.messages[-1][0].append(msg_suite)
def add_sub_message(self, sub_msg, key=None):
if not self.messages:
raise ValueError('unable to complete last message as '
'there is no previous message)')
if key is None:
self.messages[-1][1].append([sub_msg])
else:
raise NotImplementedError('sub message to specific key '
'are not implemented yet')
def write(self, stream=sys.stdout):
"""write the entry to file """
stream.write(u'%s -- %s\n' % (self.date or '', self.version or ''))
for msg, sub_msgs in self.messages:
stream.write(u'%s%s %s\n' % (INDENT, BULLET, msg[0]))
stream.write(u''.join(msg[1:]))
if sub_msgs:
stream.write(u'\n')
for sub_msg in sub_msgs:
stream.write(u'%s%s %s\n' %
(INDENT * 2, SUBBULLET, sub_msg[0]))
stream.write(u''.join(sub_msg[1:]))
stream.write(u'\n')
stream.write(u'\n\n')
class ChangeLog(object):
"""object representation of a whole ChangeLog file"""
entry_class = ChangeLogEntry
def __init__(self, changelog_file, title=u''):
self.file = changelog_file
assert isinstance(title, type(u'')), 'title must be a unicode object'
self.title = title
self.additional_content = u''
self.entries = []
self.load()
def __repr__(self):
return '' % (self.file, id(self),
len(self.entries))
def add_entry(self, entry):
"""add a new entry to the change log"""
self.entries.append(entry)
def get_entry(self, version='', create=None):
""" return a given changelog entry
if version is omitted, return the current entry
"""
if not self.entries:
if version or not create:
raise NoEntry()
self.entries.append(self.entry_class())
if not version:
if self.entries[0].version and create is not None:
self.entries.insert(0, self.entry_class())
return self.entries[0]
version = self.version_class(version)
for entry in self.entries:
if entry.version == version:
return entry
raise EntryNotFound()
def add(self, msg, create=None):
"""add a new message to the latest opened entry"""
entry = self.get_entry(create=create)
entry.add_message(msg)
def load(self):
""" read a logilab's ChangeLog from file """
try:
stream = codecs.open(self.file, encoding='utf-8')
except IOError:
return
last = None
expect_sub = False
for line in stream:
sline = line.strip()
words = sline.split()
# if new entry
if len(words) == 1 and words[0] == '--':
expect_sub = False
last = self.entry_class()
self.add_entry(last)
# if old entry
elif len(words) == 3 and words[1] == '--':
expect_sub = False
last = self.entry_class(words[0], words[2])
self.add_entry(last)
# if title
elif sline and last is None:
self.title = '%s%s' % (self.title, line)
# if new entry
elif sline and sline[0] == BULLET:
expect_sub = False
last.add_message(sline[1:].strip())
# if new sub_entry
elif expect_sub and sline and sline[0] == SUBBULLET:
last.add_sub_message(sline[1:].strip())
# if new line for current entry
elif sline and last.messages:
last.complete_latest_message(line)
else:
expect_sub = True
self.additional_content += line
stream.close()
def format_title(self):
return u'%s\n\n' % self.title.strip()
def save(self):
"""write back change log"""
# filetutils isn't importable in appengine, so import locally
from logilab.common.fileutils import ensure_fs_mode
ensure_fs_mode(self.file, S_IWRITE)
self.write(codecs.open(self.file, 'w', encoding='utf-8'))
def write(self, stream=sys.stdout):
"""write changelog to stream"""
stream.write(self.format_title())
for entry in self.entries:
entry.write(stream)
logilab-common-1.4.1/logilab/common/sphinx_ext.py 0000644 0002025 0002025 00000006401 12651420631 023427 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
from logilab.common.decorators import monkeypatch
from sphinx.ext import autodoc
class DocstringOnlyModuleDocumenter(autodoc.ModuleDocumenter):
objtype = 'docstring'
def format_signature(self):
pass
def add_directive_header(self, sig):
pass
def document_members(self, all_members=False):
pass
def resolve_name(self, modname, parents, path, base):
if modname is not None:
return modname, parents + [base]
return (path or '') + base, []
#autodoc.add_documenter(DocstringOnlyModuleDocumenter)
def setup(app):
app.add_autodocumenter(DocstringOnlyModuleDocumenter)
from sphinx.ext.autodoc import (ViewList, Options, AutodocReporter, nodes,
assemble_option_dict, nested_parse_with_titles)
@monkeypatch(autodoc.AutoDirective)
def run(self):
self.filename_set = set() # a set of dependent filenames
self.reporter = self.state.document.reporter
self.env = self.state.document.settings.env
self.warnings = []
self.result = ViewList()
# find out what documenter to call
objtype = self.name[4:]
doc_class = self._registry[objtype]
# process the options with the selected documenter's option_spec
self.genopt = Options(assemble_option_dict(
self.options.items(), doc_class.option_spec))
# generate the output
documenter = doc_class(self, self.arguments[0])
documenter.generate(more_content=self.content)
if not self.result:
return self.warnings
# record all filenames as dependencies -- this will at least
# partially make automatic invalidation possible
for fn in self.filename_set:
self.env.note_dependency(fn)
# use a custom reporter that correctly assigns lines to source
# filename/description and lineno
old_reporter = self.state.memo.reporter
self.state.memo.reporter = AutodocReporter(self.result,
self.state.memo.reporter)
if self.name in ('automodule', 'autodocstring'):
node = nodes.section()
# necessary so that the child nodes get the right source/line set
node.document = self.state.document
nested_parse_with_titles(self.state, self.result, node)
else:
node = nodes.paragraph()
node.document = self.state.document
self.state.nested_parse(self.result, 0, node)
self.state.memo.reporter = old_reporter
return self.warnings + node.children
logilab-common-1.4.1/logilab/common/daemon.py 0000644 0002025 0002025 00000006411 12651420631 022502 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""A daemonize function (for Unices)"""
__docformat__ = "restructuredtext en"
import os
import errno
import signal
import sys
import time
import warnings
from six.moves import range
def setugid(user):
"""Change process user and group ID
Argument is a numeric user id or a user name"""
try:
from pwd import getpwuid
passwd = getpwuid(int(user))
except ValueError:
from pwd import getpwnam
passwd = getpwnam(user)
if hasattr(os, 'initgroups'): # python >= 2.7
os.initgroups(passwd.pw_name, passwd.pw_gid)
else:
import ctypes
if ctypes.CDLL(None).initgroups(passwd.pw_name, passwd.pw_gid) < 0:
err = ctypes.c_int.in_dll(ctypes.pythonapi,"errno").value
raise OSError(err, os.strerror(err), 'initgroups')
os.setgid(passwd.pw_gid)
os.setuid(passwd.pw_uid)
os.environ['HOME'] = passwd.pw_dir
def daemonize(pidfile=None, uid=None, umask=0o77):
"""daemonize a Unix process. Set paranoid umask by default.
Return 1 in the original process, 2 in the first fork, and None for the
second fork (eg daemon process).
"""
# http://www.faqs.org/faqs/unix-faq/programmer/faq/
#
# fork so the parent can exit
if os.fork(): # launch child and...
return 1
# disconnect from tty and create a new session
os.setsid()
# fork again so the parent, (the session group leader), can exit.
# as a non-session group leader, we can never regain a controlling
# terminal.
if os.fork(): # launch child again.
return 2
# move to the root to avoit mount pb
os.chdir('/')
# redirect standard descriptors
null = os.open('/dev/null', os.O_RDWR)
for i in range(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != errno.EBADF:
raise
os.close(null)
# filter warnings
warnings.filterwarnings('ignore')
# write pid in a file
if pidfile:
# ensure the directory where the pid-file should be set exists (for
# instance /var/run/cubicweb may be deleted on computer restart)
piddir = os.path.dirname(pidfile)
if not os.path.exists(piddir):
os.makedirs(piddir)
f = file(pidfile, 'w')
f.write(str(os.getpid()))
f.close()
# set umask if specified
if umask is not None:
os.umask(umask)
# change process uid
if uid:
setugid(uid)
return None
logilab-common-1.4.1/logilab/common/sphinxutils.py 0000644 0002025 0002025 00000010534 12651420631 023632 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Sphinx utils
ModuleGenerator: Generate a file that lists all the modules of a list of
packages in order to pull all the docstring.
This should not be used in a makefile to systematically generate sphinx
documentation!
Typical usage:
>>> from logilab.common.sphinxutils import ModuleGenerator
>>> mgen = ModuleGenerator('logilab common', '/home/adim/src/logilab/common')
>>> mgen.generate('api_logilab_common.rst', exclude_dirs=('test',))
"""
import os, sys
import os.path as osp
import inspect
from logilab.common import STD_BLACKLIST
from logilab.common.shellutils import globfind
from logilab.common.modutils import load_module_from_file, modpath_from_file
def module_members(module):
members = []
for name, value in inspect.getmembers(module):
if getattr(value, '__module__', None) == module.__name__:
members.append( (name, value) )
return sorted(members)
def class_members(klass):
return sorted([name for name in vars(klass)
if name not in ('__doc__', '__module__',
'__dict__', '__weakref__')])
class ModuleGenerator:
file_header = """.. -*- coding: utf-8 -*-\n\n%s\n"""
module_def = """
:mod:`%s`
=======%s
.. automodule:: %s
:members: %s
"""
class_def = """
.. autoclass:: %s
:members: %s
"""
def __init__(self, project_title, code_dir):
self.title = project_title
self.code_dir = osp.abspath(code_dir)
def generate(self, dest_file, exclude_dirs=STD_BLACKLIST):
"""make the module file"""
self.fn = open(dest_file, 'w')
num = len(self.title) + 6
title = "=" * num + "\n %s API\n" % self.title + "=" * num
self.fn.write(self.file_header % title)
self.gen_modules(exclude_dirs=exclude_dirs)
self.fn.close()
def gen_modules(self, exclude_dirs):
"""generate all modules"""
for module in self.find_modules(exclude_dirs):
modname = module.__name__
classes = []
modmembers = []
for objname, obj in module_members(module):
if inspect.isclass(obj):
classmembers = class_members(obj)
classes.append( (objname, classmembers) )
else:
modmembers.append(objname)
self.fn.write(self.module_def % (modname, '=' * len(modname),
modname,
', '.join(modmembers)))
for klass, members in classes:
self.fn.write(self.class_def % (klass, ', '.join(members)))
def find_modules(self, exclude_dirs):
basepath = osp.dirname(self.code_dir)
basedir = osp.basename(basepath) + osp.sep
if basedir not in sys.path:
sys.path.insert(1, basedir)
for filepath in globfind(self.code_dir, '*.py', exclude_dirs):
if osp.basename(filepath) in ('setup.py', '__pkginfo__.py'):
continue
try:
module = load_module_from_file(filepath)
except: # module might be broken or magic
dotted_path = modpath_from_file(filepath)
module = type('.'.join(dotted_path), (), {}) # mock it
yield module
if __name__ == '__main__':
# example :
title, code_dir, outfile = sys.argv[1:]
generator = ModuleGenerator(title, code_dir)
# XXX modnames = ['logilab']
generator.generate(outfile, ('test', 'tests', 'examples',
'data', 'doc', '.hg', 'migration'))
logilab-common-1.4.1/logilab/common/clcommands.py 0000644 0002025 0002025 00000025745 12651420631 023372 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Helper functions to support command line tools providing more than
one command.
e.g called as "tool command [options] args..." where and are
command'specific
"""
from __future__ import print_function
__docformat__ = "restructuredtext en"
import sys
import logging
from os.path import basename
from logilab.common.configuration import Configuration
from logilab.common.logging_ext import init_log, get_threshold
from logilab.common.deprecation import deprecated
class BadCommandUsage(Exception):
"""Raised when an unknown command is used or when a command is not
correctly used (bad options, too much / missing arguments...).
Trigger display of command usage.
"""
class CommandError(Exception):
"""Raised when a command can't be processed and we want to display it and
exit, without traceback nor usage displayed.
"""
# command line access point ####################################################
class CommandLine(dict):
"""Usage:
>>> LDI = cli.CommandLine('ldi', doc='Logilab debian installer',
version=version, rcfile=RCFILE)
>>> LDI.register(MyCommandClass)
>>> LDI.register(MyOtherCommandClass)
>>> LDI.run(sys.argv[1:])
Arguments:
* `pgm`, the program name, default to `basename(sys.argv[0])`
* `doc`, a short description of the command line tool
* `copyright`, additional doc string that will be appended to the generated
doc
* `version`, version number of string of the tool. If specified, global
--version option will be available.
* `rcfile`, path to a configuration file. If specified, global --C/--rc-file
option will be available? self.rcfile = rcfile
* `logger`, logger to propagate to commands, default to
`logging.getLogger(self.pgm))`
"""
def __init__(self, pgm=None, doc=None, copyright=None, version=None,
rcfile=None, logthreshold=logging.ERROR,
check_duplicated_command=True):
if pgm is None:
pgm = basename(sys.argv[0])
self.pgm = pgm
self.doc = doc
self.copyright = copyright
self.version = version
self.rcfile = rcfile
self.logger = None
self.logthreshold = logthreshold
self.check_duplicated_command = check_duplicated_command
def register(self, cls, force=False):
"""register the given :class:`Command` subclass"""
assert not self.check_duplicated_command or force or not cls.name in self, \
'a command %s is already defined' % cls.name
self[cls.name] = cls
return cls
def run(self, args):
"""main command line access point:
* init logging
* handle global options (-h/--help, --version, -C/--rc-file)
* check command
* run command
Terminate by :exc:`SystemExit`
"""
init_log(debug=True, # so that we use StreamHandler
logthreshold=self.logthreshold,
logformat='%(levelname)s: %(message)s')
try:
arg = args.pop(0)
except IndexError:
self.usage_and_exit(1)
if arg in ('-h', '--help'):
self.usage_and_exit(0)
if self.version is not None and arg in ('--version'):
print(self.version)
sys.exit(0)
rcfile = self.rcfile
if rcfile is not None and arg in ('-C', '--rc-file'):
try:
rcfile = args.pop(0)
arg = args.pop(0)
except IndexError:
self.usage_and_exit(1)
try:
command = self.get_command(arg)
except KeyError:
print('ERROR: no %s command' % arg)
print()
self.usage_and_exit(1)
try:
sys.exit(command.main_run(args, rcfile))
except KeyboardInterrupt as exc:
print('Interrupted', end=' ')
if str(exc):
print(': %s' % exc, end=' ')
print()
sys.exit(4)
except BadCommandUsage as err:
print('ERROR:', err)
print()
print(command.help())
sys.exit(1)
def create_logger(self, handler, logthreshold=None):
logger = logging.Logger(self.pgm)
logger.handlers = [handler]
if logthreshold is None:
logthreshold = get_threshold(self.logthreshold)
logger.setLevel(logthreshold)
return logger
def get_command(self, cmd, logger=None):
if logger is None:
logger = self.logger
if logger is None:
logger = self.logger = logging.getLogger(self.pgm)
logger.setLevel(get_threshold(self.logthreshold))
return self[cmd](logger)
def usage(self):
"""display usage for the main program (i.e. when no command supplied)
and exit
"""
print('usage:', self.pgm, end=' ')
if self.rcfile:
print('[--rc-file=]', end=' ')
print(' [options] ...')
if self.doc:
print('\n%s' % self.doc)
print('''
Type "%(pgm)s --help" for more information about a specific
command. Available commands are :\n''' % self.__dict__)
max_len = max([len(cmd) for cmd in self])
padding = ' ' * max_len
for cmdname, cmd in sorted(self.items()):
if not cmd.hidden:
print(' ', (cmdname + padding)[:max_len], cmd.short_description())
if self.rcfile:
print('''
Use --rc-file= / -C before the command
to specify a configuration file. Default to %s.
''' % self.rcfile)
print('''%(pgm)s -h/--help
display this usage information and exit''' % self.__dict__)
if self.version:
print('''%(pgm)s -v/--version
display version configuration and exit''' % self.__dict__)
if self.copyright:
print('\n', self.copyright)
def usage_and_exit(self, status):
self.usage()
sys.exit(status)
# base command classes #########################################################
class Command(Configuration):
"""Base class for command line commands.
Class attributes:
* `name`, the name of the command
* `min_args`, minimum number of arguments, None if unspecified
* `max_args`, maximum number of arguments, None if unspecified
* `arguments`, string describing arguments, used in command usage
* `hidden`, boolean flag telling if the command should be hidden, e.g. does
not appear in help's commands list
* `options`, options list, as allowed by :mod:configuration
"""
arguments = ''
name = ''
# hidden from help ?
hidden = False
# max/min args, None meaning unspecified
min_args = None
max_args = None
@classmethod
def description(cls):
return cls.__doc__.replace(' ', '')
@classmethod
def short_description(cls):
return cls.description().split('.')[0]
def __init__(self, logger):
usage = '%%prog %s %s\n\n%s' % (self.name, self.arguments,
self.description())
Configuration.__init__(self, usage=usage)
self.logger = logger
def check_args(self, args):
"""check command's arguments are provided"""
if self.min_args is not None and len(args) < self.min_args:
raise BadCommandUsage('missing argument')
if self.max_args is not None and len(args) > self.max_args:
raise BadCommandUsage('too many arguments')
def main_run(self, args, rcfile=None):
"""Run the command and return status 0 if everything went fine.
If :exc:`CommandError` is raised by the underlying command, simply log
the error and return status 2.
Any other exceptions, including :exc:`BadCommandUsage` will be
propagated.
"""
if rcfile:
self.load_file_configuration(rcfile)
args = self.load_command_line_configuration(args)
try:
self.check_args(args)
self.run(args)
except CommandError as err:
self.logger.error(err)
return 2
return 0
def run(self, args):
"""run the command with its specific arguments"""
raise NotImplementedError()
class ListCommandsCommand(Command):
"""list available commands, useful for bash completion."""
name = 'listcommands'
arguments = '[command]'
hidden = True
def run(self, args):
"""run the command with its specific arguments"""
if args:
command = args.pop()
cmd = _COMMANDS[command]
for optname, optdict in cmd.options:
print('--help')
print('--' + optname)
else:
commands = sorted(_COMMANDS.keys())
for command in commands:
cmd = _COMMANDS[command]
if not cmd.hidden:
print(command)
# deprecated stuff #############################################################
_COMMANDS = CommandLine()
DEFAULT_COPYRIGHT = '''\
Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
@deprecated('use cls.register(cli)')
def register_commands(commands):
"""register existing commands"""
for command_klass in commands:
_COMMANDS.register(command_klass)
@deprecated('use args.pop(0)')
def main_run(args, doc=None, copyright=None, version=None):
"""command line tool: run command specified by argument list (without the
program name). Raise SystemExit with status 0 if everything went fine.
>>> main_run(sys.argv[1:])
"""
_COMMANDS.doc = doc
_COMMANDS.copyright = copyright
_COMMANDS.version = version
_COMMANDS.run(args)
@deprecated('use args.pop(0)')
def pop_arg(args_list, expected_size_after=None, msg="Missing argument"):
"""helper function to get and check command line arguments"""
try:
value = args_list.pop(0)
except IndexError:
raise BadCommandUsage(msg)
if expected_size_after is not None and len(args_list) > expected_size_after:
raise BadCommandUsage('too many arguments')
return value
logilab-common-1.4.1/logilab/common/date.py 0000644 0002025 0002025 00000026020 12723763004 022156 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Date manipulation helper functions."""
from __future__ import division
__docformat__ = "restructuredtext en"
import math
import re
import sys
from locale import getlocale, LC_TIME
from datetime import date, time, datetime, timedelta
from time import strptime as time_strptime
from calendar import monthrange, timegm
from six.moves import range
try:
from mx.DateTime import RelativeDateTime, Date, DateTimeType
except ImportError:
endOfMonth = None
DateTimeType = datetime
else:
endOfMonth = RelativeDateTime(months=1, day=-1)
# NOTE: should we implement a compatibility layer between date representations
# as we have in lgc.db ?
FRENCH_FIXED_HOLIDAYS = {
'jour_an': '%s-01-01',
'fete_travail': '%s-05-01',
'armistice1945': '%s-05-08',
'fete_nat': '%s-07-14',
'assomption': '%s-08-15',
'toussaint': '%s-11-01',
'armistice1918': '%s-11-11',
'noel': '%s-12-25',
}
FRENCH_MOBILE_HOLIDAYS = {
'paques2004': '2004-04-12',
'ascension2004': '2004-05-20',
'pentecote2004': '2004-05-31',
'paques2005': '2005-03-28',
'ascension2005': '2005-05-05',
'pentecote2005': '2005-05-16',
'paques2006': '2006-04-17',
'ascension2006': '2006-05-25',
'pentecote2006': '2006-06-05',
'paques2007': '2007-04-09',
'ascension2007': '2007-05-17',
'pentecote2007': '2007-05-28',
'paques2008': '2008-03-24',
'ascension2008': '2008-05-01',
'pentecote2008': '2008-05-12',
'paques2009': '2009-04-13',
'ascension2009': '2009-05-21',
'pentecote2009': '2009-06-01',
'paques2010': '2010-04-05',
'ascension2010': '2010-05-13',
'pentecote2010': '2010-05-24',
'paques2011': '2011-04-25',
'ascension2011': '2011-06-02',
'pentecote2011': '2011-06-13',
'paques2012': '2012-04-09',
'ascension2012': '2012-05-17',
'pentecote2012': '2012-05-28',
}
# XXX this implementation cries for multimethod dispatching
def get_step(dateobj, nbdays=1):
# assume date is either a python datetime or a mx.DateTime object
if isinstance(dateobj, date):
return ONEDAY * nbdays
return nbdays # mx.DateTime is ok with integers
def datefactory(year, month, day, sampledate):
# assume date is either a python datetime or a mx.DateTime object
if isinstance(sampledate, datetime):
return datetime(year, month, day)
if isinstance(sampledate, date):
return date(year, month, day)
return Date(year, month, day)
def weekday(dateobj):
# assume date is either a python datetime or a mx.DateTime object
if isinstance(dateobj, date):
return dateobj.weekday()
return dateobj.day_of_week
def str2date(datestr, sampledate):
# NOTE: datetime.strptime is not an option until we drop py2.4 compat
year, month, day = [int(chunk) for chunk in datestr.split('-')]
return datefactory(year, month, day, sampledate)
def days_between(start, end):
if isinstance(start, date):
delta = end - start
# datetime.timedelta.days is always an integer (floored)
if delta.seconds:
return delta.days + 1
return delta.days
else:
return int(math.ceil((end - start).days))
def get_national_holidays(begin, end):
"""return french national days off between begin and end"""
begin = datefactory(begin.year, begin.month, begin.day, begin)
end = datefactory(end.year, end.month, end.day, end)
holidays = [str2date(datestr, begin)
for datestr in FRENCH_MOBILE_HOLIDAYS.values()]
for year in range(begin.year, end.year+1):
for datestr in FRENCH_FIXED_HOLIDAYS.values():
date = str2date(datestr % year, begin)
if date not in holidays:
holidays.append(date)
return [day for day in holidays if begin <= day < end]
def add_days_worked(start, days):
"""adds date but try to only take days worked into account"""
step = get_step(start)
weeks, plus = divmod(days, 5)
end = start + ((weeks * 7) + plus) * step
if weekday(end) >= 5: # saturday or sunday
end += (2 * step)
end += len([x for x in get_national_holidays(start, end + step)
if weekday(x) < 5]) * step
if weekday(end) >= 5: # saturday or sunday
end += (2 * step)
return end
def nb_open_days(start, end):
assert start <= end
step = get_step(start)
days = days_between(start, end)
weeks, plus = divmod(days, 7)
if weekday(start) > weekday(end):
plus -= 2
elif weekday(end) == 6:
plus -= 1
open_days = weeks * 5 + plus
nb_week_holidays = len([x for x in get_national_holidays(start, end+step)
if weekday(x) < 5 and x < end])
open_days -= nb_week_holidays
if open_days < 0:
return 0
return open_days
def date_range(begin, end, incday=None, incmonth=None):
"""yields each date between begin and end
:param begin: the start date
:param end: the end date
:param incr: the step to use to iterate over dates. Default is
one day.
:param include: None (means no exclusion) or a function taking a
date as parameter, and returning True if the date
should be included.
When using mx datetime, you should *NOT* use incmonth argument, use instead
oneDay, oneHour, oneMinute, oneSecond, oneWeek or endOfMonth (to enumerate
months) as `incday` argument
"""
assert not (incday and incmonth)
begin = todate(begin)
end = todate(end)
if incmonth:
while begin < end:
yield begin
begin = next_month(begin, incmonth)
else:
incr = get_step(begin, incday or 1)
while begin < end:
yield begin
begin += incr
# makes py datetime usable #####################################################
ONEDAY = timedelta(days=1)
ONEWEEK = timedelta(days=7)
try:
strptime = datetime.strptime
except AttributeError: # py < 2.5
from time import strptime as time_strptime
def strptime(value, format):
return datetime(*time_strptime(value, format)[:6])
def strptime_time(value, format='%H:%M'):
return time(*time_strptime(value, format)[3:6])
def todate(somedate):
"""return a date from a date (leaving unchanged) or a datetime"""
if isinstance(somedate, datetime):
return date(somedate.year, somedate.month, somedate.day)
assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
return somedate
def totime(somedate):
"""return a time from a time (leaving unchanged), date or datetime"""
# XXX mx compat
if not isinstance(somedate, time):
return time(somedate.hour, somedate.minute, somedate.second)
assert isinstance(somedate, (time)), repr(somedate)
return somedate
def todatetime(somedate):
"""return a date from a date (leaving unchanged) or a datetime"""
# take care, datetime is a subclass of date
if isinstance(somedate, datetime):
return somedate
assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
return datetime(somedate.year, somedate.month, somedate.day)
def datetime2ticks(somedate):
return timegm(somedate.timetuple()) * 1000 + int(getattr(somedate, 'microsecond', 0) / 1000)
def ticks2datetime(ticks):
miliseconds, microseconds = divmod(ticks, 1000)
try:
return datetime.fromtimestamp(miliseconds)
except (ValueError, OverflowError):
epoch = datetime.fromtimestamp(0)
nb_days, seconds = divmod(int(miliseconds), 86400)
delta = timedelta(nb_days, seconds=seconds, microseconds=microseconds)
try:
return epoch + delta
except (ValueError, OverflowError):
raise
def days_in_month(somedate):
return monthrange(somedate.year, somedate.month)[1]
def days_in_year(somedate):
feb = date(somedate.year, 2, 1)
if days_in_month(feb) == 29:
return 366
else:
return 365
def previous_month(somedate, nbmonth=1):
while nbmonth:
somedate = first_day(somedate) - ONEDAY
nbmonth -= 1
return somedate
def next_month(somedate, nbmonth=1):
while nbmonth:
somedate = last_day(somedate) + ONEDAY
nbmonth -= 1
return somedate
def first_day(somedate):
return date(somedate.year, somedate.month, 1)
def last_day(somedate):
return date(somedate.year, somedate.month, days_in_month(somedate))
def ustrftime(somedate, fmt='%Y-%m-%d'):
"""like strftime, but returns a unicode string instead of an encoded
string which may be problematic with localized date.
"""
if sys.version_info >= (3, 3):
# datetime.date.strftime() supports dates since year 1 in Python >=3.3.
return somedate.strftime(fmt)
else:
try:
if sys.version_info < (3, 0):
encoding = getlocale(LC_TIME)[1] or 'ascii'
return unicode(somedate.strftime(str(fmt)), encoding)
else:
return somedate.strftime(fmt)
except ValueError:
if somedate.year >= 1900:
raise
# datetime is not happy with dates before 1900
# we try to work around this, assuming a simple
# format string
fields = {'Y': somedate.year,
'm': somedate.month,
'd': somedate.day,
}
if isinstance(somedate, datetime):
fields.update({'H': somedate.hour,
'M': somedate.minute,
'S': somedate.second})
fmt = re.sub('%([YmdHMS])', r'%(\1)02d', fmt)
return unicode(fmt) % fields
def utcdatetime(dt):
if dt.tzinfo is None:
return dt
return (dt.replace(tzinfo=None) - dt.utcoffset())
def utctime(dt):
if dt.tzinfo is None:
return dt
return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None)
def datetime_to_seconds(date):
"""return the number of seconds since the begining of the day for that date
"""
return date.second+60*date.minute + 3600*date.hour
def timedelta_to_days(delta):
"""return the time delta as a number of seconds"""
return delta.days + delta.seconds / (3600*24)
def timedelta_to_seconds(delta):
"""return the time delta as a fraction of days"""
return delta.days*(3600*24) + delta.seconds
logilab-common-1.4.1/logilab/common/textutils.py 0000644 0002025 0002025 00000041701 13063007140 023277 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""Some text manipulation utility functions.
:group text formatting: normalize_text, normalize_paragraph, pretty_match,\
unquote, colorize_ansi
:group text manipulation: searchall, splitstrip
:sort: text formatting, text manipulation
:type ANSI_STYLES: dict(str)
:var ANSI_STYLES: dictionary mapping style identifier to ANSI terminal code
:type ANSI_COLORS: dict(str)
:var ANSI_COLORS: dictionary mapping color identifier to ANSI terminal code
:type ANSI_PREFIX: str
:var ANSI_PREFIX:
ANSI terminal code notifying the start of an ANSI escape sequence
:type ANSI_END: str
:var ANSI_END:
ANSI terminal code notifying the end of an ANSI escape sequence
:type ANSI_RESET: str
:var ANSI_RESET:
ANSI terminal code resetting format defined by a previous ANSI escape sequence
"""
__docformat__ = "restructuredtext en"
import sys
import re
import os.path as osp
from warnings import warn
from unicodedata import normalize as _uninormalize
try:
from os import linesep
except ImportError:
linesep = '\n' # gae
from logilab.common.deprecation import deprecated
MANUAL_UNICODE_MAP = {
u'\xa1': u'!', # INVERTED EXCLAMATION MARK
u'\u0142': u'l', # LATIN SMALL LETTER L WITH STROKE
u'\u2044': u'/', # FRACTION SLASH
u'\xc6': u'AE', # LATIN CAPITAL LETTER AE
u'\xa9': u'(c)', # COPYRIGHT SIGN
u'\xab': u'"', # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
u'\xe6': u'ae', # LATIN SMALL LETTER AE
u'\xae': u'(r)', # REGISTERED SIGN
u'\u0153': u'oe', # LATIN SMALL LIGATURE OE
u'\u0152': u'OE', # LATIN CAPITAL LIGATURE OE
u'\xd8': u'O', # LATIN CAPITAL LETTER O WITH STROKE
u'\xf8': u'o', # LATIN SMALL LETTER O WITH STROKE
u'\xbb': u'"', # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
u'\xdf': u'ss', # LATIN SMALL LETTER SHARP S
u'\u2013': u'-', # HYPHEN
u'\u2019': u"'", # SIMPLE QUOTE
}
def unormalize(ustring, ignorenonascii=None, substitute=None):
"""replace diacritical characters with their corresponding ascii characters
Convert the unicode string to its long normalized form (unicode character
will be transform into several characters) and keep the first one only.
The normal form KD (NFKD) will apply the compatibility decomposition, i.e.
replace all compatibility characters with their equivalents.
:type substitute: str
:param substitute: replacement character to use if decomposition fails
:see: Another project about ASCII transliterations of Unicode text
http://pypi.python.org/pypi/Unidecode
"""
# backward compatibility, ignorenonascii was a boolean
if ignorenonascii is not None:
warn("ignorenonascii is deprecated, use substitute named parameter instead",
DeprecationWarning, stacklevel=2)
if ignorenonascii:
substitute = ''
res = []
for letter in ustring[:]:
try:
replacement = MANUAL_UNICODE_MAP[letter]
except KeyError:
replacement = _uninormalize('NFKD', letter)[0]
if ord(replacement) >= 2 ** 7:
if substitute is None:
raise ValueError("can't deal with non-ascii based characters")
replacement = substitute
res.append(replacement)
return u''.join(res)
def unquote(string):
"""remove optional quotes (simple or double) from the string
:type string: str or unicode
:param string: an optionally quoted string
:rtype: str or unicode
:return: the unquoted string (or the input string if it wasn't quoted)
"""
if not string:
return string
if string[0] in '"\'':
string = string[1:]
if string[-1] in '"\'':
string = string[:-1]
return string
_BLANKLINES_RGX = re.compile('\r?\n\r?\n')
_NORM_SPACES_RGX = re.compile('\s+')
def normalize_text(text, line_len=80, indent='', rest=False):
"""normalize a text to display it with a maximum line size and
optionally arbitrary indentation. Line jumps are normalized but blank
lines are kept. The indentation string may be used to insert a
comment (#) or a quoting (>) mark for instance.
:type text: str or unicode
:param text: the input text to normalize
:type line_len: int
:param line_len: expected maximum line's length, default to 80
:type indent: str or unicode
:param indent: optional string to use as indentation
:rtype: str or unicode
:return:
the input text normalized to fit on lines with a maximized size
inferior to `line_len`, and optionally prefixed by an
indentation string
"""
if rest:
normp = normalize_rest_paragraph
else:
normp = normalize_paragraph
result = []
for text in _BLANKLINES_RGX.split(text):
result.append(normp(text, line_len, indent))
return ('%s%s%s' % (linesep, indent, linesep)).join(result)
def normalize_paragraph(text, line_len=80, indent=''):
"""normalize a text to display it with a maximum line size and
optionally arbitrary indentation. Line jumps are normalized. The
indentation string may be used top insert a comment mark for
instance.
:type text: str or unicode
:param text: the input text to normalize
:type line_len: int
:param line_len: expected maximum line's length, default to 80
:type indent: str or unicode
:param indent: optional string to use as indentation
:rtype: str or unicode
:return:
the input text normalized to fit on lines with a maximized size
inferior to `line_len`, and optionally prefixed by an
indentation string
"""
text = _NORM_SPACES_RGX.sub(' ', text)
line_len = line_len - len(indent)
lines = []
while text:
aline, text = splittext(text.strip(), line_len)
lines.append(indent + aline)
return linesep.join(lines)
def normalize_rest_paragraph(text, line_len=80, indent=''):
"""normalize a ReST text to display it with a maximum line size and
optionally arbitrary indentation. Line jumps are normalized. The
indentation string may be used top insert a comment mark for
instance.
:type text: str or unicode
:param text: the input text to normalize
:type line_len: int
:param line_len: expected maximum line's length, default to 80
:type indent: str or unicode
:param indent: optional string to use as indentation
:rtype: str or unicode
:return:
the input text normalized to fit on lines with a maximized size
inferior to `line_len`, and optionally prefixed by an
indentation string
"""
toreport = ''
lines = []
line_len = line_len - len(indent)
for line in text.splitlines():
line = toreport + _NORM_SPACES_RGX.sub(' ', line.strip())
toreport = ''
while len(line) > line_len:
# too long line, need split
line, toreport = splittext(line, line_len)
lines.append(indent + line)
if toreport:
line = toreport + ' '
toreport = ''
else:
line = ''
if line:
lines.append(indent + line.strip())
return linesep.join(lines)
def splittext(text, line_len):
"""split the given text on space according to the given max line size
return a 2-uple:
* a line <= line_len if possible
* the rest of the text which has to be reported on another line
"""
if len(text) <= line_len:
return text, ''
pos = min(len(text)-1, line_len)
while pos > 0 and text[pos] != ' ':
pos -= 1
if pos == 0:
pos = min(len(text), line_len)
while len(text) > pos and text[pos] != ' ':
pos += 1
return text[:pos], text[pos+1:].strip()
def splitstrip(string, sep=','):
"""return a list of stripped string by splitting the string given as
argument on `sep` (',' by default). Empty string are discarded.
>>> splitstrip('a, b, c , 4,,')
['a', 'b', 'c', '4']
>>> splitstrip('a')
['a']
>>>
:type string: str or unicode
:param string: a csv line
:type sep: str or unicode
:param sep: field separator, default to the comma (',')
:rtype: str or unicode
:return: the unquoted string (or the input string if it wasn't quoted)
"""
return [word.strip() for word in string.split(sep) if word.strip()]
get_csv = deprecated('get_csv is deprecated, use splitstrip')(splitstrip)
def split_url_or_path(url_or_path):
"""return the latest component of a string containing either an url of the
form :// or a local file system path
"""
if '://' in url_or_path:
return url_or_path.rstrip('/').rsplit('/', 1)
return osp.split(url_or_path.rstrip(osp.sep))
def text_to_dict(text):
"""parse multilines text containing simple 'key=value' lines and return a
dict of {'key': 'value'}. When the same key is encountered multiple time,
value is turned into a list containing all values.
>>> d = text_to_dict('''multiple=1
... multiple= 2
... single =3
... ''')
>>> d['single']
'3'
>>> d['multiple']
['1', '2']
"""
res = {}
if not text:
return res
for line in text.splitlines():
line = line.strip()
if line and not line.startswith('#'):
key, value = [w.strip() for w in line.split('=', 1)]
if key in res:
try:
res[key].append(value)
except AttributeError:
res[key] = [res[key], value]
else:
res[key] = value
return res
_BLANK_URE = r'(\s|,)+'
_BLANK_RE = re.compile(_BLANK_URE)
__VALUE_URE = r'-?(([0-9]+\.[0-9]*)|((0x?)?[0-9]+))'
__UNITS_URE = r'[a-zA-Z]+'
_VALUE_RE = re.compile(r'(?P%s)(?P%s)?'%(__VALUE_URE, __UNITS_URE))
_VALIDATION_RE = re.compile(r'^((%s)(%s))*(%s)?$' % (__VALUE_URE, __UNITS_URE,
__VALUE_URE))
BYTE_UNITS = {
"b": 1,
"kb": 1024,
"mb": 1024 ** 2,
"gb": 1024 ** 3,
"tb": 1024 ** 4,
}
TIME_UNITS = {
"ms": 0.0001,
"s": 1,
"min": 60,
"h": 60 * 60,
"d": 60 * 60 *24,
}
def apply_units(string, units, inter=None, final=float, blank_reg=_BLANK_RE,
value_reg=_VALUE_RE):
"""Parse the string applying the units defined in units
(e.g.: "1.5m",{'m',60} -> 80).
:type string: str or unicode
:param string: the string to parse
:type units: dict (or any object with __getitem__ using basestring key)
:param units: a dict mapping a unit string repr to its value
:type inter: type
:param inter: used to parse every intermediate value (need __sum__)
:type blank_reg: regexp
:param blank_reg: should match every blank char to ignore.
:type value_reg: regexp with "value" and optional "unit" group
:param value_reg: match a value and it's unit into the
"""
if inter is None:
inter = final
fstring = _BLANK_RE.sub('', string)
if not (fstring and _VALIDATION_RE.match(fstring)):
raise ValueError("Invalid unit string: %r." % string)
values = []
for match in value_reg.finditer(fstring):
dic = match.groupdict()
lit, unit = dic["value"], dic.get("unit")
value = inter(lit)
if unit is not None:
try:
value *= units[unit.lower()]
except KeyError:
raise KeyError('invalid unit %s. valid units are %s' %
(unit, units.keys()))
values.append(value)
return final(sum(values))
_LINE_RGX = re.compile('\r\n|\r+|\n')
def pretty_match(match, string, underline_char='^'):
"""return a string with the match location underlined:
>>> import re
>>> print(pretty_match(re.search('mange', 'il mange du bacon'), 'il mange du bacon'))
il mange du bacon
^^^^^
>>>
:type match: _sre.SRE_match
:param match: object returned by re.match, re.search or re.finditer
:type string: str or unicode
:param string:
the string on which the regular expression has been applied to
obtain the `match` object
:type underline_char: str or unicode
:param underline_char:
character to use to underline the matched section, default to the
carret '^'
:rtype: str or unicode
:return:
the original string with an inserted line to underline the match
location
"""
start = match.start()
end = match.end()
string = _LINE_RGX.sub(linesep, string)
start_line_pos = string.rfind(linesep, 0, start)
if start_line_pos == -1:
start_line_pos = 0
result = []
else:
result = [string[:start_line_pos]]
start_line_pos += len(linesep)
offset = start - start_line_pos
underline = ' ' * offset + underline_char * (end - start)
end_line_pos = string.find(linesep, end)
if end_line_pos == -1:
string = string[start_line_pos:]
result.append(string)
result.append(underline)
else:
end = string[end_line_pos + len(linesep):]
string = string[start_line_pos:end_line_pos]
result.append(string)
result.append(underline)
result.append(end)
return linesep.join(result).rstrip()
# Ansi colorization ###########################################################
ANSI_PREFIX = '\033['
ANSI_END = 'm'
ANSI_RESET = '\033[0m'
ANSI_STYLES = {
'reset': "0",
'bold': "1",
'italic': "3",
'underline': "4",
'blink': "5",
'inverse': "7",
'strike': "9",
}
ANSI_COLORS = {
'reset': "0",
'black': "30",
'red': "31",
'green': "32",
'yellow': "33",
'blue': "34",
'magenta': "35",
'cyan': "36",
'white': "37",
}
def _get_ansi_code(color=None, style=None):
"""return ansi escape code corresponding to color and style
:type color: str or None
:param color:
the color name (see `ANSI_COLORS` for available values)
or the color number when 256 colors are available
:type style: str or None
:param style:
style string (see `ANSI_COLORS` for available values). To get
several style effects at the same time, use a coma as separator.
:raise KeyError: if an unexistent color or style identifier is given
:rtype: str
:return: the built escape code
"""
ansi_code = []
if style:
style_attrs = splitstrip(style)
for effect in style_attrs:
ansi_code.append(ANSI_STYLES[effect])
if color:
if color.isdigit():
ansi_code.extend(['38', '5'])
ansi_code.append(color)
else:
ansi_code.append(ANSI_COLORS[color])
if ansi_code:
return ANSI_PREFIX + ';'.join(ansi_code) + ANSI_END
return ''
def colorize_ansi(msg, color=None, style=None):
"""colorize message by wrapping it with ansi escape codes
:type msg: str or unicode
:param msg: the message string to colorize
:type color: str or None
:param color:
the color identifier (see `ANSI_COLORS` for available values)
:type style: str or None
:param style:
style string (see `ANSI_COLORS` for available values). To get
several style effects at the same time, use a coma as separator.
:raise KeyError: if an unexistent color or style identifier is given
:rtype: str or unicode
:return: the ansi escaped string
"""
# If both color and style are not defined, then leave the text as is
if color is None and style is None:
return msg
escape_code = _get_ansi_code(color, style)
# If invalid (or unknown) color, don't wrap msg with ansi codes
if escape_code:
return '%s%s%s' % (escape_code, msg, ANSI_RESET)
return msg
DIFF_STYLE = {'separator': 'cyan', 'remove': 'red', 'add': 'green'}
def diff_colorize_ansi(lines, out=sys.stdout, style=DIFF_STYLE):
for line in lines:
if line[:4] in ('--- ', '+++ '):
out.write(colorize_ansi(line, style['separator']))
elif line[0] == '-':
out.write(colorize_ansi(line, style['remove']))
elif line[0] == '+':
out.write(colorize_ansi(line, style['add']))
elif line[:4] == '--- ':
out.write(colorize_ansi(line, style['separator']))
elif line[:4] == '+++ ':
out.write(colorize_ansi(line, style['separator']))
else:
out.write(line)
logilab-common-1.4.1/logilab/__init__.py 0000644 0002025 0002025 00000000070 12651420631 021501 0 ustar dlaxalde dlaxalde 0000000 0000000 __import__('pkg_resources').declare_namespace(__name__)
logilab-common-1.4.1/COPYING.LESSER 0000644 0002025 0002025 00000063637 12651420631 020030 0 ustar dlaxalde dlaxalde 0000000 0000000
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations
below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it
becomes a de-facto standard. To achieve this, non-free programs must
be allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control
compilation and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least
three years, to give the same user the materials specified in
Subsection 6a, above, for a charge no more than the cost of
performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply, and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License
may add an explicit geographical distribution limitation excluding those
countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms
of the ordinary General Public License).
To apply these terms, attach the following notices to the library.
It is safest to attach them to the start of each source file to most
effectively convey the exclusion of warranty; and each file should
have at least the "copyright" line and a pointer to where the full
notice is found.
Copyright (C)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or
your school, if any, to sign a "copyright disclaimer" for the library,
if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James
Random Hacker.
, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
logilab-common-1.4.1/doc/ 0000755 0002025 0002025 00000000000 13131135544 016527 5 ustar dlaxalde dlaxalde 0000000 0000000 logilab-common-1.4.1/doc/logilab-pytest.1 0000644 0002025 0002025 00000002537 13013556007 021557 0 ustar dlaxalde dlaxalde 0000000 0000000 .TH logilab-pytest "1" "January 2008" logilab-pytest
.SH NAME
.B logilab-pytest
\- run python unit tests
.SH SYNOPSIS
usage: logilab-pytest [OPTIONS] [testfile [testpattern]]
.PP
examples:
.PP
logilab-pytest path/to/mytests.py
logilab-pytest path/to/mytests.py TheseTests
logilab-pytest path/to/mytests.py TheseTests.test_thisone
.PP
logilab-pytest one (will run both test_thisone and test_thatone)
logilab-pytest path/to/mytests.py \fB\-s\fR not (will skip test_notthisone)
.PP
logilab-pytest \fB\-\-coverage\fR test_foo.py
.IP
(only if logilab.devtools is available)
.SS "options:"
.TP
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-t\fR TESTDIR
directory where the tests will be found
.TP
\fB\-d\fR
enable design\-by\-contract
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Verbose output
.TP
\fB\-i\fR, \fB\-\-pdb\fR
Enable test failure inspection (conflicts with
\fB\-\-coverage\fR)
.TP
\fB\-x\fR, \fB\-\-exitfirst\fR
Exit on first failure (only make sense when logilab-pytest run
one test file)
.TP
\fB\-s\fR SKIPPED, \fB\-\-skip\fR=\fISKIPPED\fR
test names matching this name will be skipped to skip
several patterns, use commas
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Minimal output
.TP
\fB\-P\fR PROFILE, \fB\-\-profile\fR=\fIPROFILE\fR
Profile execution and store data in the given file
.TP
\fB\-\-coverage\fR
run tests with pycoverage (conflicts with \fB\-\-pdb\fR)
logilab-common-1.4.1/doc/makefile 0000644 0002025 0002025 00000000414 12651420631 020226 0 ustar dlaxalde dlaxalde 0000000 0000000 all: epydoc
epydoc:
mkdir -p apidoc
-epydoc --parse-only -o apidoc --html -v --no-private --exclude='test' --exclude="__pkginfo__" --exclude="setup" -n "Logilab's common library" $(shell dirname $(CURDIR))/build/lib/logilab/common >/dev/null
clean:
rm -rf apidoc
logilab-common-1.4.1/__pkginfo__.py 0000644 0002025 0002025 00000003704 13131131662 020566 0 ustar dlaxalde dlaxalde 0000000 0000000 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common. If not, see .
"""logilab.common packaging information"""
__docformat__ = "restructuredtext en"
import sys
import os
distname = 'logilab-common'
modname = 'common'
subpackage_of = 'logilab'
subpackage_master = True
numversion = (1, 4, 1)
version = '.'.join([str(num) for num in numversion])
license = 'LGPL' # 2.1 or later
description = "collection of low-level Python packages and modules used by Logilab projects"
web = "http://www.logilab.org/project/%s" % distname
mailinglist = "mailto://python-projects@lists.logilab.org"
author = "Logilab"
author_email = "contact@logilab.fr"
from os.path import join
scripts = [join('bin', 'logilab-pytest')]
include_dirs = [join('test', 'data')]
install_requires = [
'setuptools',
'six >= 1.4.0',
]
tests_require = [
'pytz',
'egenix-mx-base',
]
if sys.version_info < (2, 7):
install_requires.append('unittest2 >= 0.5.1')
if os.name == 'nt':
install_requires.append('colorama')
classifiers = ["Topic :: Utilities",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
]
logilab-common-1.4.1/README 0000644 0002025 0002025 00000010444 13013556007 016645 0 ustar dlaxalde dlaxalde 0000000 0000000 Logilab's common library
========================
What's this ?
-------------
This package contains some modules used by different Logilab projects.
It is released under the GNU Lesser General Public License.
There is no documentation available yet but the source code should be clean and
well documented.
Designed to ease:
* handling command line options and configuration files
* writing interactive command line tools
* manipulation of files and character strings
* manipulation of common structures such as graph, tree, and pattern such as visitor
* generating text and HTML reports
* more...
Installation
------------
Extract the tarball, jump into the created directory and run ::
python setup.py install
For installation options, see ::
python setup.py install --help
Provided modules
----------------
Here is a brief description of the available modules.
Modules providing high-level features
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `cache`, a cache implementation with a least recently used algorithm.
* `changelog`, a tiny library to manipulate our simplified ChangeLog file format.
* `clcommands`, high-level classes to define command line programs handling
different subcommands. It is based on `configuration` to get easy command line
/ configuration file handling.
* `configuration`, some classes to handle unified configuration from both
command line (using optparse) and configuration file (using ConfigParser).
* `proc`, interface to Linux /proc.
* `umessage`, unicode email support.
* `ureports`, micro-reports, a way to create simple reports using python objects
without care of the final formatting. ReST and html formatters are provided.
Modules providing low-level functions and structures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `compat`, provides a transparent compatibility layer between different python
versions.
* `date`, a set of date manipulation functions.
* `daemon`, a daemon function and mix-in class to properly start an Unix daemon
process.
* `decorators`, function decorators such as cached, timed...
* `deprecation`, decorator, metaclass & all to mark functions / classes as
deprecated or moved
* `fileutils`, some file / file path manipulation utilities.
* `graph`, graph manipulations functions such as cycle detection, bases for dot
file generation.
* `modutils`, python module manipulation functions.
* `shellutils`, some powerful shell like functions to replace shell scripts with
python scripts.
* `tasksqueue`, a prioritized tasks queue implementation.
* `textutils`, some text manipulation functions (ansi colorization, line wrapping,
rest support...).
* `tree`, base class to represent tree structure, and some others to make it
works with the visitor implementation (see below).
* `visitor`, a generic visitor pattern implementation.
Modules extending some standard modules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `debugger`, `pdb` customization.
* `logging_ext`, extensions to `logging` module such as a colorized formatter
and an easier initialization function.
* `optik_ext`, defines some new option types (regexp, csv, color, date, etc.)
for `optik` / `optparse`
Modules extending some external modules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* `sphinx_ext`, Sphinx_ plugin defining a `autodocstring` directive.
* `vcgutils` , utilities functions to generate file readable with Georg Sander's
vcg tool (Visualization of Compiler Graphs).
To be deprecated modules
~~~~~~~~~~~~~~~~~~~~~~~~
Those `logilab.common` modules will much probably be deprecated in future
versions:
* `testlib`: use `unittest2`_ instead
* `interface`: use `zope.interface`_ if you really want this
* `table`, `xmlutils`: is that used?
* `sphinxutils`: we won't go that way imo (i == syt)
Comments, support, bug reports
------------------------------
Project page https://www.logilab.org/project/logilab-common
Use the python-projects@lists.logilab.org mailing list.
You can subscribe to this mailing list at
https://lists.logilab.org/mailman/listinfo/python-projects
Archives are available at
https://lists.logilab.org/pipermail/python-projects/
.. _Sphinx: http://sphinx.pocoo.org/
.. _`unittest2`: http://pypi.python.org/pypi/unittest2
.. _`discover`: http://pypi.python.org/pypi/discover
.. _`zope.interface`: http://pypi.python.org/pypi/zope.interface