aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuido Günther <agx@sigxcpu.org>2013-06-15 13:06:00 +0200
committerGuido Günther <agx@sigxcpu.org>2014-06-26 20:04:05 +0200
commitd8e234820a23d94ec5f1c67541d97896fd94af2f (patch)
tree2f1dd13b8af9e7d085459492110876312cc15e20
Initial commit
-rw-r--r--README6
-rw-r--r--pomop/__init__.py0
-rw-r--r--pomop/pom.py298
-rw-r--r--pomop/version.py54
-rw-r--r--setup.cfg5
-rw-r--r--setup.py26
-rw-r--r--tests/test_pom.py42
7 files changed, 431 insertions, 0 deletions
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
--- /dev/null
+++ b/pomop/__init__.py
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 <agx@sigxcpu.org>
+# 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 <agx@sigxcpu.org>
+# 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 <agx@sigxcpu.org>
+# 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 <http://www.gnu.org/licenses/>.
+
+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 <agx@sigxcpu.org>
+# 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 <http://www.gnu.org/licenses/>.
+"""Test L{pomop.pom} config"""
+
+import unittest
+
+from pomop.pom import Pom
+
+pom_xml = """<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.sigxcpu.pomop</groupId>
+ <artifactId>pomop-parent</artifactId>
+ <packaging>pom</packaging>
+ <version>2.7 </version>
+</project>"""
+
+
+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)