From d8e234820a23d94ec5f1c67541d97896fd94af2f Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sat, 15 Jun 2013 13:06:00 +0200 Subject: Initial commit --- README | 6 ++ pomop/__init__.py | 0 pomop/pom.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ pomop/version.py | 54 ++++++++++ setup.cfg | 5 + setup.py | 26 +++++ tests/test_pom.py | 42 ++++++++ 7 files changed, 431 insertions(+) create mode 100644 README create mode 100644 pomop/__init__.py create mode 100644 pomop/pom.py create mode 100644 pomop/version.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/test_pom.py diff --git a/README b/README new file mode 100644 index 0000000..f05e72e --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +pomop +===== +(Maven) Perform POM Operations in Python + +The aim is to be able to quickly parse and manipulate some pom values without +having to drag in $JAVA_UNIVERSE. diff --git a/pomop/__init__.py b/pomop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pomop/pom.py b/pomop/pom.py new file mode 100644 index 0000000..126a47f --- /dev/null +++ b/pomop/pom.py @@ -0,0 +1,298 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2013 Guido Günther +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Some Pom parsing handling + +import tempfile +import os +from lxml import etree + +from . version import Version + +class Pom(object): + """ + Represents a maven POM + + for documentation check I{test_pom.py} + """ + ns = "http://maven.apache.org/POM/4.0.0" + root = "/{%s}project" % ns + + groupid_node = "/{%s}groupId" % ns + artifactid_node = "/{%s}artifactId" % ns + version_node = "/{%s}version" % ns + triple_nodes = (groupid_node, artifactid_node, version_node) + + parent_node = "/{%s}parent" % ns + deps_node = "/{%s}dependencies" % ns + dep_node = "/{%s}dependency" % ns + + version_path = root + version_node + artifactid_path = root + artifactid_node + groupid_path = root + groupid_node + parent_path = root + parent_node + deps_path = root + deps_node + dep_path = root + deps_node + dep_node + + def __init__(self, tree, path=None): + self._path = path + self._tree = tree + + @property + def path(self): + """The path in the filesystem""" + return self._path + + def get_version(self): + searcher = etree.ETXPath(self.version_path) + try: + return Version(searcher(self._tree)[0].text) + except IndexError: + return None + + def set_version(self, version): + searcher = etree.ETXPath(self.version_path) + searcher(self._tree)[0].text = version + + version = property(get_version, set_version) + + @property + def artifactid(self): + """ + The artifactid of this pom + """ + searcher = etree.ETXPath(self.artifactid_path) + return searcher(self._tree)[0].text.strip() + + @property + def groupid(self): + """ + The groupid of this pom. Might be empty. + """ + searcher = etree.ETXPath(self.groupid_path) + try: + return searcher(self._tree)[0].text.strip() + except IndexError: + return None + + def _parent_path(self, node): + return self.root + self.parent_node + node + + def get_parent(self): + """ + Return the POMs parent + + returns: groupid, artifactid, version + """ + p = [] + try: + for node in self.triple_nodes: + searcher = etree.ETXPath(self._parent_path(node)) + p.append(searcher(self._tree)[0].text.strip()) + except IndexError: + return None + return tuple(p) + + def set_parent(self, parent): + """ + Return the POMs parent + + @param parent: groupid, artifactid, version + @type parent: C{tuple} + """ + old_parent = self.parent + if not old_parent: # no parent pom so far + if None in parent: + raise ValueError("Need full parent triple") + searcher = etree.ETXPath(self.root) + root = searcher(self._tree)[0] + # add an empty parent element + parent_elem = etree.SubElement(root, self.parent_node[1:]) + parent_elem.text='\n' + + for node, val in zip(self.triple_nodes, parent): + if not val: + continue + + searcher = etree.ETXPath(self._parent_path(node)) + n = searcher(self._tree) + if n: + n[0].text = val + else: # if the element is missing, add it + n = etree.SubElement(parent_elem, node[1:]) + n.text = val + + parent = property(get_parent, set_parent, doc="The POMs parent as triplet") + + @property + def triplet(self): + """ + groupid, artifactid and version triplet + + The version and groupid can be C{None} which means + "use the parents value" + """ + return (self.groupid, self.artifactid, self.version) + + @property + def full_triplet(self): + """ + groupid, artifactid and version triplet + + All values are guaranteed to be different from C{None}. + since missing values are fetched from the parent + """ + return ((self.groupid or self.parent[0], + self.artifactid or self.parent[1], + self.version or self.parent[2])) + + def is_parent_of(self, child): + """ + Check wheter this is the parent of pom I{child} + + @param child: the pom to check + @type child: L{Pom} + @return: C{True} if L{self} is the parent of L{child}, C{False} + otherwise + @rtype: C{bool} + """ + c = child.parent + if not c: + return False + + t = self.full_triplet + + return (c[0] == t[0] and + c[1] == t[1] and + c[2] == t[2]) + + def is_child_of(self, parent): + """ + Check wheter this is the child of pom I{parent} + + @param parent: the pom to check + @type parent: L{Pom} + @return: C{True} if L{self} is the child of L{parent}, C{False} + otherwise + @rtype: C{bool} + """ + return parent.is_parent_of(self) + + def find_parent(self, poms): + """ + Given a list of poms find our parent in it. + + @param poms: list of poms + @type poms: C{list} of L{Pom}s + @return: the parent pom + @rtype: L{Pom} or None + """ + for pom in poms: + if self.is_child_of(pom): + return pom + return None + + def _strip_ns(self, tag): + """ + Stip the namespace from an XML tag + """ + return tag[len(self.ns)+2:] + + def _parse_dep(self, depelem): + """ + Parse the XML of a POM dependency + + @param depelem: a dependency element + @type depelem: lxml.etree._Element + """ + dep = {} + for child in depelem: + # FIXME: this strips comments + if isinstance(child.tag, (str, unicode)): + tag = self._strip_ns(child.tag) + dep[tag] = child.text + return dep + + def get_dependencies(self): + """ + Get the dependencies as a list + """ + return [ self._parse_dep(e) + for e in self._get_nodes(self.dep_path) if e is not None ] + + def _get_nodes(self, path): + """Return all nodes at xpath I{path} as an Iterator""" + searcher = etree.ETXPath(path) + return searcher(self._tree) + + def _add_dependency(self, dep): + searcher = etree.ETXPath(self.deps_path) + deps = searcher(self._tree)[0] + dep_elem = etree.SubElement(deps, self.dep_node[1:]) + dep_elem.text='\n' + + for node, val in dep.items(): + if not val: + continue + n = etree.SubElement(dep_elem, '{%s}%s' % (self.ns, node)) + n.text = val + n.tail = '\n' + + def _remove_dependencies(self): + searcher = etree.ETXPath(self.deps_path) + deps = searcher(self._tree)[0] + if deps is not None: + deps.clear() + + def set_dependencies(self, deps): + """ + Set the dependencies + """ + if not self.dependencies: + searcher = etree.ETXPath(self.root) + root = searcher(self._tree)[0] + # add an empty parent element + parent_elem = etree.SubElement(root, self.deps_node[1:]) + parent_elem.text='\n' + else: + self._remove_dependencies() + + for dep in deps: + self._add_dependency(dep) + + dependencies = property(get_dependencies, set_dependencies) + + def write(self, path=None): + if path: + self._path = path + + if not self.path: + raise ValueError("Illegal path name") + tmp, name = tempfile.mkstemp(dir=os.path.dirname(self.path)) + self._tree.write(name) + os.chmod(name, 0o644) + os.rename(name, self.path) + + @classmethod + def read(klass, path, sloppy=False): + parser = etree.XMLParser(recover=True) if sloppy else None + return klass(etree.parse(path, parser=parser), path) + + @classmethod + def parse(klass, xml, sloppy=False): + parser = etree.XMLParser(recover=True) if sloppy else None + return klass(etree.fromstring(xml, parser=parser)) diff --git a/pomop/version.py b/pomop/version.py new file mode 100644 index 0000000..b621557 --- /dev/null +++ b/pomop/version.py @@ -0,0 +1,54 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2013 Guido Günther +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Some Pom parsing handling + +class Version(object): + def __init__(self, text): + self._version = text.strip() + + def is_subst(self): + """ + Wheter a version is subject to variable substitution + + >>> Version('1.0').is_subst() + False + >>> Version('${version}').is_subst() + True + """ + return self._version.startswith('${') and self._version.endswith('}') + + def is_snapshot(self): + """ + Wheter a version is a snapthot version + + >>> Version('1.0').is_snapshot() + False + >>> Version('1.0-SNAPSHOT').is_snapshot() + True + """ + return self._version.endswith('-SNAPSHOT') + + def __str__(self): + return self._version + + def __eq__(self, arg): + # Make sure we can compare versions in string context + if isinstance(arg, (str, unicode)): + return self._version.__eq__(arg) + else: + return super(Version, self).__eq__(arg) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..292eb0d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[nosetests] +with-doctest=1 +with-xunit=1 +cover-package=pomop +cover-erase=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bf9f7cf --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/python +# vim: set fileencoding=utf-8 : +# +# (C) 2014 Guido Günther +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from setuptools import setup + +setup(name = "pompop", + author = 'Guido Günther', + author_email = 'agx@sigxcpu.org', + packages = ['pomop'], +) + +# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: diff --git a/tests/test_pom.py b/tests/test_pom.py new file mode 100644 index 0000000..269014e --- /dev/null +++ b/tests/test_pom.py @@ -0,0 +1,42 @@ +# vim: set fileencoding=utf-8 : +# (C) 2013 Guido Günther +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Test L{pomop.pom} config""" + +import unittest + +from pomop.pom import Pom + +pom_xml = """ + + 4.0.0 + + org.sigxcpu.pomop + pomop-parent + pom + 2.7 +""" + + +class TestPom(unittest.TestCase): + def test_parse(self): + """Parse simplistic pom and check values""" + p = Pom.parse(pom_xml) + self.assertIsNotNone(p) + self.assertEqual(p.full_triplet[0], 'org.sigxcpu.pomop') + self.assertEqual(p.full_triplet[1], 'pomop-parent') + self.assertEqual(p.full_triplet[2], '2.7') + self.assertEqual(p.get_parent(), None) -- cgit v1.2.3