diff --git a/.cvsignore b/.gitignore similarity index 100% rename from .cvsignore rename to .gitignore diff --git a/0001-Patch-for-RHBZ-750694.patch b/0001-Patch-for-RHBZ-750694.patch new file mode 100644 index 0000000..65c3561 --- /dev/null +++ b/0001-Patch-for-RHBZ-750694.patch @@ -0,0 +1,34 @@ +From ebc1a41118befdc7a92e0ccd04819d488775316d Mon Sep 17 00:00:00 2001 +From: Dan Callaghan +Date: Fri, 18 Nov 2011 15:13:59 +1000 +Subject: [PATCH 1/5] Patch for RHBZ#750694 + +--- + xmltramp.py | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/xmltramp.py b/xmltramp.py +index 9a4908a..7819b25 100644 +--- a/xmltramp.py ++++ b/xmltramp.py +@@ -224,7 +224,9 @@ def startElementNS(self, name, qname, attrs): + + attrs = dict(attrs) + newprefixes = {} +- for k in self.prefixes.keys(): newprefixes[k] = self.prefixes[k][-1] ++ for k in self.prefixes.keys(): ++ if self.prefixes[k]: ++ newprefixes[k] = self.prefixes[k][-1] + + self.stack.append(Element(name, attrs, prefixes=newprefixes.copy())) + +@@ -357,5 +359,6 @@ def unittest(): + + assert parse('').__repr__(1) == '' + assert parse('').__repr__(1) == '' ++ assert parse('').__repr__(1) == '' + + if __name__ == '__main__': unittest() +-- +2.14.4 + diff --git a/0002-fix-imports-and-syntax-for-Python-3.patch b/0002-fix-imports-and-syntax-for-Python-3.patch new file mode 100644 index 0000000..91265de --- /dev/null +++ b/0002-fix-imports-and-syntax-for-Python-3.patch @@ -0,0 +1,123 @@ +From cc474a11cda8bae8f26f651a9d11e63209427184 Mon Sep 17 00:00:00 2001 +From: Dan Callaghan +Date: Thu, 5 Jul 2018 15:41:51 +1000 +Subject: [PATCH 2/5] fix imports and syntax for Python 3 + +--- + xmltramp.py | 36 +++++++++++++++++++++++------------- + 1 file changed, 23 insertions(+), 13 deletions(-) + +diff --git a/xmltramp.py b/xmltramp.py +index 7819b25..a3a188f 100644 +--- a/xmltramp.py ++++ b/xmltramp.py +@@ -5,7 +5,11 @@ + __credits__ = "Many thanks to pjz, bitsko, and DanC." + __copyright__ = "(C) 2003-2006 Aaron Swartz. GNU GPL 2." + +-if not hasattr(__builtins__, 'True'): True, False = 1, 0 ++try: ++ text_type = unicode ++except NameError: # PY3 ++ text_type = str ++ + def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u'')) + def islst(f): return isinstance(f, type(())) or isinstance(f, type([])) + +@@ -87,7 +91,7 @@ def arep(a, inprefixes, addns=1): + elif isinstance(x, Element): + out += x.__repr__(recursive+1, multiline, inprefixes.copy()) + else: +- raise TypeError, "I wasn't expecting "+`x`+"." ++ raise TypeError("I wasn't expecting "+repr(x)+".") + if multiline and content: out += '\n' + ('\t' * (recursive-1)) + else: + if self._dir: out += '...' +@@ -99,25 +103,25 @@ def arep(a, inprefixes, addns=1): + def __unicode__(self): + text = '' + for x in self._dir: +- text += unicode(x) ++ text += text_type(x) + return ' '.join(text.split()) + + def __str__(self): + return self.__unicode__().encode('utf-8') + + def __getattr__(self, n): +- if n[0] == '_': raise AttributeError, "Use foo['"+n+"'] to access the child element." ++ if n[0] == '_': raise AttributeError("Use foo['"+n+"'] to access the child element.") + if self._dNS: n = (self._dNS, n) + for x in self._dir: + if isinstance(x, Element) and x._name == n: return x +- raise AttributeError, 'No child element named %s' % repr(n) ++ raise AttributeError('No child element named %s' % repr(n)) + + def __hasattr__(self, n): + for x in self._dir: + if isinstance(x, Element) and x._name == n: return True + return False + +- def __setattr__(self, n, v): ++ def __setattr__(self, n, v): + if n[0] == '_': self.__dict__[n] = v + else: self[n] = v + +@@ -140,7 +144,7 @@ def __getitem__(self, n): + if self._dNS and not islst(n): n = (self._dNS, n) + for x in self._dir: + if isinstance(x, Element) and x._name == n: return x +- raise KeyError, n ++ raise KeyError(n) + + def __setitem__(self, n, v): + if isinstance(n, type(0)): # d[1] +@@ -213,7 +217,7 @@ def __init__(self): + ContentHandler.__init__(self) + + def startPrefixMapping(self, prefix, uri): +- if not self.prefixes.has_key(prefix): self.prefixes[prefix] = [] ++ if prefix not in self.prefixes: self.prefixes[prefix] = [] + self.prefixes[prefix].append(uri) + def endPrefixMapping(self, prefix): + self.prefixes[prefix].pop() +@@ -255,12 +259,18 @@ def seed(fileobj): + return seeder.result + + def parse(text): +- from StringIO import StringIO ++ try: ++ from StringIO import StringIO ++ except ImportError: # PY3 ++ from io import StringIO + return seed(StringIO(text)) + + def load(url): +- import urllib +- return seed(urllib.urlopen(url)) ++ try: ++ from urllib.request import urlopen ++ except ImportError: # PY2 ++ from urllib import urlopen ++ return seed(urlopen(url)) + + def unittest(): + parse('afoobara').__repr__(1,1) == \ +@@ -276,12 +286,12 @@ def unittest(): + + try: + d._doesnotexist +- raise "ExpectedError", "but found success. Damn." ++ raise AssertionError("Expected error, but found success. Damn.") + except AttributeError: pass + assert d.bar._name == 'bar' + try: + d.doesnotexist +- raise "ExpectedError", "but found success. Damn." ++ raise AssertionError("Expected error, but found success. Damn.") + except AttributeError: pass + + assert hasattr(d, 'bar') == True +-- +2.14.4 + diff --git a/0003-__str__-needs-to-return-str-not-bytes-on-Python-3.patch b/0003-__str__-needs-to-return-str-not-bytes-on-Python-3.patch new file mode 100644 index 0000000..f178f93 --- /dev/null +++ b/0003-__str__-needs-to-return-str-not-bytes-on-Python-3.patch @@ -0,0 +1,36 @@ +From 3dd8fe4955d28e8dee1c890d487288163a760419 Mon Sep 17 00:00:00 2001 +From: Dan Callaghan +Date: Thu, 5 Jul 2018 15:42:10 +1000 +Subject: [PATCH 3/5] __str__ needs to return str not bytes on Python 3 + +--- + xmltramp.py | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/xmltramp.py b/xmltramp.py +index a3a188f..e2522f2 100644 +--- a/xmltramp.py ++++ b/xmltramp.py +@@ -5,6 +5,7 @@ + __credits__ = "Many thanks to pjz, bitsko, and DanC." + __copyright__ = "(C) 2003-2006 Aaron Swartz. GNU GPL 2." + ++import sys + try: + text_type = unicode + except NameError: # PY3 +@@ -107,7 +108,10 @@ def __unicode__(self): + return ' '.join(text.split()) + + def __str__(self): +- return self.__unicode__().encode('utf-8') ++ if sys.version_info[0] > 2: ++ return self.__unicode__() ++ else: ++ return self.__unicode__().encode('utf-8') + + def __getattr__(self, n): + if n[0] == '_': raise AttributeError("Use foo['"+n+"'] to access the child element.") +-- +2.14.4 + diff --git a/0004-empty-slice-is-slice-None-None-None-on-Python-3.patch b/0004-empty-slice-is-slice-None-None-None-on-Python-3.patch new file mode 100644 index 0000000..9f0ba6e --- /dev/null +++ b/0004-empty-slice-is-slice-None-None-None-on-Python-3.patch @@ -0,0 +1,27 @@ +From b9aebcfc971d55fa6f2bf0e895e34a19994b6fd3 Mon Sep 17 00:00:00 2001 +From: Dan Callaghan +Date: Thu, 5 Jul 2018 15:42:35 +1000 +Subject: [PATCH 4/5] empty slice is slice(None, None, None) on Python 3 + +On Python 2, the empty slice is slice(0, 9223372036854775807, None) so +this extra conditional was not necessary. +--- + xmltramp.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/xmltramp.py b/xmltramp.py +index e2522f2..0653112 100644 +--- a/xmltramp.py ++++ b/xmltramp.py +@@ -135,7 +135,7 @@ def __getitem__(self, n): + return self._dir[n] + elif isinstance(n, slice(0).__class__): + # numerical slices +- if isinstance(n.start, type(0)): return self._dir[n.start:n.stop] ++ if n.start is None or isinstance(n.start, type(0)): return self._dir[n.start:n.stop] + + # d['foo':] == all s + n = n.start +-- +2.14.4 + diff --git a/0005-use-OrderedDict-for-attributes-and-namespaces.patch b/0005-use-OrderedDict-for-attributes-and-namespaces.patch new file mode 100644 index 0000000..55f1562 --- /dev/null +++ b/0005-use-OrderedDict-for-attributes-and-namespaces.patch @@ -0,0 +1,91 @@ +From 1303e9a3797c48b1681532c7c7e2d70a64907258 Mon Sep 17 00:00:00 2001 +From: Dan Callaghan +Date: Thu, 5 Jul 2018 15:46:58 +1000 +Subject: [PATCH 5/5] use OrderedDict for attributes and namespaces + +This is to ensure consistent iteration order, mainly so that the tests +will work regardless of dict hashing behaviour which varies across +Python 2 and 3. It should be nicer for callers as well. +--- + xmltramp.py | 22 +++++++++++++--------- + 1 file changed, 13 insertions(+), 9 deletions(-) + +diff --git a/xmltramp.py b/xmltramp.py +index 0653112..d3f1541 100644 +--- a/xmltramp.py ++++ b/xmltramp.py +@@ -10,6 +10,10 @@ + text_type = unicode + except NameError: # PY3 + text_type = str ++try: ++ from collections import OrderedDict ++except ImportError: # PY<=2.6 ++ OrderedDict = dict + + def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u'')) + def islst(f): return isinstance(f, type(())) or isinstance(f, type([])) +@@ -26,18 +30,18 @@ class Element: + def __init__(self, name, attrs=None, children=None, prefixes=None): + if islst(name) and name[0] == None: name = name[1] + if attrs: +- na = {} ++ na = OrderedDict() + for k in attrs.keys(): + if islst(k) and k[0] == None: na[k[1]] = attrs[k] + else: na[k] = attrs[k] + attrs = na + + self._name = name +- self._attrs = attrs or {} ++ self._attrs = attrs or OrderedDict() + self._dir = children or [] + +- prefixes = prefixes or {} +- self._prefixes = dict(zip(prefixes.values(), prefixes.keys())) ++ prefixes = prefixes or OrderedDict() ++ self._prefixes = OrderedDict(zip(prefixes.values(), prefixes.keys())) + + if prefixes: self._dNS = prefixes.get(None, None) + else: self._dNS = None +@@ -67,7 +71,7 @@ def arep(a, inprefixes, addns=1): + + return out + +- inprefixes = inprefixes or {u'http://www.w3.org/XML/1998/namespace':'xml'} ++ inprefixes = inprefixes or OrderedDict({u'http://www.w3.org/XML/1998/namespace':'xml'}) + + # need to call first to set inprefixes: + attributes = arep(self._attrs, inprefixes, recursive) +@@ -217,7 +221,7 @@ class Seeder(EntityResolver, DTDHandler, ContentHandler, ErrorHandler): + def __init__(self): + self.stack = [] + self.ch = '' +- self.prefixes = {} ++ self.prefixes = OrderedDict() + ContentHandler.__init__(self) + + def startPrefixMapping(self, prefix, uri): +@@ -231,7 +235,7 @@ def startElementNS(self, name, qname, attrs): + if ch and not ch.isspace(): self.stack[-1]._dir.append(ch) + + attrs = dict(attrs) +- newprefixes = {} ++ newprefixes = OrderedDict() + for k in self.prefixes.keys(): + if self.prefixes[k]: + newprefixes[k] = self.prefixes[k][-1] +@@ -333,8 +337,8 @@ def unittest(): + """) + + assert repr(d) == '...' +- assert d.__repr__(1) == 'John Polk and John PalfreyJohn PolkJohn PalfreyBuffy' +- assert d.__repr__(1,1) == '\n\tJohn Polk and John Palfrey\n\tJohn Polk\n\tJohn Palfrey\n\tBuffy\n' ++ assert d.__repr__(1) == 'John Polk and John PalfreyJohn PolkJohn PalfreyBuffy' ++ assert d.__repr__(1,1) == '\n\tJohn Polk and John Palfrey\n\tJohn Polk\n\tJohn Palfrey\n\tBuffy\n' + + assert repr(parse("")) == '' + +-- +2.14.4 + diff --git a/Makefile b/Makefile deleted file mode 100644 index 905fc44..0000000 --- a/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -# Makefile for source rpm: python-xmltramp -# $Id$ -NAME := python-xmltramp -SPECFILE = $(firstword $(wildcard *.spec)) - -define find-makefile-common -for d in common ../common ../../common ; do if [ -f $$d/Makefile.common ] ; then if [ -f $$d/CVS/Root -a -w $$/Makefile.common ] ; then cd $$d ; cvs -Q update ; fi ; echo "$$d/Makefile.common" ; break ; fi ; done -endef - -MAKEFILE_COMMON := $(shell $(find-makefile-common)) - -ifeq ($(MAKEFILE_COMMON),) -# attept a checkout -define checkout-makefile-common -test -f CVS/Root && { cvs -Q -d $$(cat CVS/Root) checkout common && echo "common/Makefile.common" ; } || { echo "ERROR: I can't figure out how to checkout the 'common' module." ; exit -1 ; } >&2 -endef - -MAKEFILE_COMMON := $(shell $(checkout-makefile-common)) -endif - -include $(MAKEFILE_COMMON) diff --git a/python-xmltramp.spec b/python-xmltramp.spec new file mode 100644 index 0000000..5144833 --- /dev/null +++ b/python-xmltramp.spec @@ -0,0 +1,213 @@ + +Name: python-xmltramp +Version: 2.18 +Release: 29%{?dist} +Summary: Pythonic API for XML + +# Automatically converted from old format: GPLv2 - review is highly recommended. +License: GPL-2.0-only +# License text is not present in the upstream file, though clearly marked as GPLv2 +# See https://www.redhat.com/archives/fedora-legal-list/2008-January/msg00010.html + +URL: http://www.aaronsw.com/2002/xmltramp/ +Source0: http://www.aaronsw.com/2002/xmltramp/xmltramp-%{version}.py +Patch1: 0001-Patch-for-RHBZ-750694.patch +Patch2: 0002-fix-imports-and-syntax-for-Python-3.patch +Patch3: 0003-__str__-needs-to-return-str-not-bytes-on-Python-3.patch +Patch4: 0004-empty-slice-is-slice-None-None-None-on-Python-3.patch +Patch5: 0005-use-OrderedDict-for-attributes-and-namespaces.patch + +BuildArch: noarch +BuildRequires: python3-devel + +%global _description\ +xmltramp is a simple Pythonic API for working with XML. + +%description %_description + +%package -n python3-xmltramp +Summary: %summary +%{?python_provide:%python_provide python3-xmltramp} + +%description -n python3-xmltramp %_description + +%prep +%setup -c -T +cp -p %{SOURCE0} xmltramp.py +%patch -P1 -p1 +%patch -P2 -p1 +%patch -P3 -p1 +%patch -P4 -p1 +%patch -P5 -p1 + +%build +# noarch + +%install +install -Dm0644 -t %{buildroot}%{python3_sitelib}/ xmltramp.py + +%check +%{__python3} xmltramp.py + +%files -n python3-xmltramp +%{python3_sitelib}/xmltramp.py* +%{python3_sitelib}/__pycache__/ + + +%changelog +* Fri Sep 19 2025 Python Maint - 2.18-29 +- Rebuilt for Python 3.14.0rc3 bytecode + +* Fri Aug 15 2025 Python Maint - 2.18-28 +- Rebuilt for Python 3.14.0rc2 bytecode + +* Fri Jul 25 2025 Fedora Release Engineering - 2.18-27 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild + +* Mon Jun 02 2025 Python Maint - 2.18-26 +- Rebuilt for Python 3.14 + +* Sat Jan 18 2025 Fedora Release Engineering - 2.18-25 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild + +* Mon Jul 29 2024 Miroslav Suchý - 2.18-24 +- convert license to SPDX + +* Fri Jul 19 2024 Fedora Release Engineering - 2.18-23 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild + +* Fri Jun 07 2024 Python Maint - 2.18-22 +- Rebuilt for Python 3.13 + +* Fri Jan 26 2024 Fedora Release Engineering - 2.18-21 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Mon Jan 22 2024 Fedora Release Engineering - 2.18-20 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Fri Jul 21 2023 Fedora Release Engineering - 2.18-19 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild + +* Tue Jun 13 2023 Python Maint - 2.18-18 +- Rebuilt for Python 3.12 + +* Fri Jan 20 2023 Fedora Release Engineering - 2.18-17 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild + +* Fri Jul 22 2022 Fedora Release Engineering - 2.18-16 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild + +* Mon Jun 13 2022 Python Maint - 2.18-15 +- Rebuilt for Python 3.11 + +* Fri Jan 21 2022 Fedora Release Engineering - 2.18-14 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild + +* Fri Jul 23 2021 Fedora Release Engineering - 2.18-13 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild + +* Fri Jun 04 2021 Python Maint - 2.18-12 +- Rebuilt for Python 3.10 + +* Wed Jan 27 2021 Fedora Release Engineering - 2.18-11 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Wed Jul 29 2020 Fedora Release Engineering - 2.18-10 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild + +* Tue May 26 2020 Miro Hrončok - 2.18-9 +- Rebuilt for Python 3.9 + +* Thu Jan 30 2020 Fedora Release Engineering - 2.18-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Thu Oct 03 2019 Miro Hrončok - 2.18-7 +- Rebuilt for Python 3.8.0rc1 (#1748018) + +* Mon Aug 19 2019 Miro Hrončok - 2.18-6 +- Rebuilt for Python 3.8 + +* Fri Jul 26 2019 Fedora Release Engineering - 2.18-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Sat Feb 02 2019 Fedora Release Engineering - 2.18-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Fri Oct 12 2018 Zbigniew Jędrzejewski-Szmek - 2.18-3 +- Python2 binary package has been removed + See https://fedoraproject.org/wiki/Changes/Mass_Python_2_Package_Removal + +* Sat Jul 14 2018 Fedora Release Engineering - 2.18-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild + +* Thu Jul 05 2018 Dan Callaghan - 2.18-1 +- Updated to upstream release 2.18 (only ~10 years late...) +- Minimal fixes to support Python 3 + +* Fri Feb 09 2018 Fedora Release Engineering - 2.17-21 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild + +* Wed Jan 31 2018 Iryna Shcherbina - 2.17-20 +- Update Python 2 dependency declarations to new packaging standards + (See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3) + +* Sat Aug 19 2017 Zbigniew Jędrzejewski-Szmek - 2.17-19 +- Python 2 binary package renamed to python2-xmltramp + See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3 + +* Thu Jul 27 2017 Fedora Release Engineering - 2.17-18 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild + +* Sat Feb 11 2017 Fedora Release Engineering - 2.17-17 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild + +* Thu Feb 04 2016 Fedora Release Engineering - 2.17-16 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild + +* Thu Jun 18 2015 Fedora Release Engineering - 2.17-15 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild + +* Sun Jun 08 2014 Fedora Release Engineering - 2.17-14 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild + +* Sun Aug 04 2013 Fedora Release Engineering - 2.17-13 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild + +* Fri Jun 07 2013 Dan Callaghan - 2.17-12 +- Patch for RHBZ#750694 + +* Thu Feb 14 2013 Fedora Release Engineering - 2.17-11 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_19_Mass_Rebuild + +* Sat Jul 21 2012 Fedora Release Engineering - 2.17-10 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild + +* Sat Jan 14 2012 Fedora Release Engineering - 2.17-9 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild + +* Wed Feb 09 2011 Fedora Release Engineering - 2.17-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild + +* Thu Jul 22 2010 David Malcolm - 2.17-7 +- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild + +* Sun Jul 26 2009 Fedora Release Engineering - 2.17-6 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild + +* Thu Feb 26 2009 Fedora Release Engineering - 2.17-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild + +* Sat Nov 29 2008 Ignacio Vazquez-Abrams - 2.17-4 +- Rebuild for Python 2.6 + +* Fri Jan 18 2008 David Malcolm - 2.17-3 +- add comment in specfile about the License text + +* Tue Nov 13 2007 David Malcolm - 2.17-2 +- fix License tag +- fix capitalization of Summary tag +- preserve timestamp when installing + +* Fri Aug 3 2007 David Malcolm - 2.17-1 +- initial packaging + diff --git a/xmltramp-2.18.py b/xmltramp-2.18.py new file mode 100644 index 0000000..9a4908a --- /dev/null +++ b/xmltramp-2.18.py @@ -0,0 +1,361 @@ +"""xmltramp: Make XML documents easily accessible.""" + +__version__ = "2.18" +__author__ = "Aaron Swartz" +__credits__ = "Many thanks to pjz, bitsko, and DanC." +__copyright__ = "(C) 2003-2006 Aaron Swartz. GNU GPL 2." + +if not hasattr(__builtins__, 'True'): True, False = 1, 0 +def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u'')) +def islst(f): return isinstance(f, type(())) or isinstance(f, type([])) + +empty = {'http://www.w3.org/1999/xhtml': ['img', 'br', 'hr', 'meta', 'link', 'base', 'param', 'input', 'col', 'area']} + +def quote(x, elt=True): + if elt and '<' in x and len(x) > 24 and x.find(']]>') == -1: return "" + else: x = x.replace('&', '&').replace('<', '<').replace(']]>', ']]>') + if not elt: x = x.replace('"', '"') + return x + +class Element: + def __init__(self, name, attrs=None, children=None, prefixes=None): + if islst(name) and name[0] == None: name = name[1] + if attrs: + na = {} + for k in attrs.keys(): + if islst(k) and k[0] == None: na[k[1]] = attrs[k] + else: na[k] = attrs[k] + attrs = na + + self._name = name + self._attrs = attrs or {} + self._dir = children or [] + + prefixes = prefixes or {} + self._prefixes = dict(zip(prefixes.values(), prefixes.keys())) + + if prefixes: self._dNS = prefixes.get(None, None) + else: self._dNS = None + + def __repr__(self, recursive=0, multiline=0, inprefixes=None): + def qname(name, inprefixes): + if islst(name): + if inprefixes[name[0]] is not None: + return inprefixes[name[0]]+':'+name[1] + else: + return name[1] + else: + return name + + def arep(a, inprefixes, addns=1): + out = '' + + for p in self._prefixes.keys(): + if not p in inprefixes.keys(): + if addns: out += ' xmlns' + if addns and self._prefixes[p]: out += ':'+self._prefixes[p] + if addns: out += '="'+quote(p, False)+'"' + inprefixes[p] = self._prefixes[p] + + for k in a.keys(): + out += ' ' + qname(k, inprefixes)+ '="' + quote(a[k], False) + '"' + + return out + + inprefixes = inprefixes or {u'http://www.w3.org/XML/1998/namespace':'xml'} + + # need to call first to set inprefixes: + attributes = arep(self._attrs, inprefixes, recursive) + out = '<' + qname(self._name, inprefixes) + attributes + + if not self._dir and (self._name[0] in empty.keys() + and self._name[1] in empty[self._name[0]]): + out += ' />' + return out + + out += '>' + + if recursive: + content = 0 + for x in self._dir: + if isinstance(x, Element): content = 1 + + pad = '\n' + ('\t' * recursive) + for x in self._dir: + if multiline and content: out += pad + if isstr(x): out += quote(x) + elif isinstance(x, Element): + out += x.__repr__(recursive+1, multiline, inprefixes.copy()) + else: + raise TypeError, "I wasn't expecting "+`x`+"." + if multiline and content: out += '\n' + ('\t' * (recursive-1)) + else: + if self._dir: out += '...' + + out += '' + + return out + + def __unicode__(self): + text = '' + for x in self._dir: + text += unicode(x) + return ' '.join(text.split()) + + def __str__(self): + return self.__unicode__().encode('utf-8') + + def __getattr__(self, n): + if n[0] == '_': raise AttributeError, "Use foo['"+n+"'] to access the child element." + if self._dNS: n = (self._dNS, n) + for x in self._dir: + if isinstance(x, Element) and x._name == n: return x + raise AttributeError, 'No child element named %s' % repr(n) + + def __hasattr__(self, n): + for x in self._dir: + if isinstance(x, Element) and x._name == n: return True + return False + + def __setattr__(self, n, v): + if n[0] == '_': self.__dict__[n] = v + else: self[n] = v + + + def __getitem__(self, n): + if isinstance(n, type(0)): # d[1] == d._dir[1] + return self._dir[n] + elif isinstance(n, slice(0).__class__): + # numerical slices + if isinstance(n.start, type(0)): return self._dir[n.start:n.stop] + + # d['foo':] == all s + n = n.start + if self._dNS and not islst(n): n = (self._dNS, n) + out = [] + for x in self._dir: + if isinstance(x, Element) and x._name == n: out.append(x) + return out + else: # d['foo'] == first + if self._dNS and not islst(n): n = (self._dNS, n) + for x in self._dir: + if isinstance(x, Element) and x._name == n: return x + raise KeyError, n + + def __setitem__(self, n, v): + if isinstance(n, type(0)): # d[1] + self._dir[n] = v + elif isinstance(n, slice(0).__class__): + # d['foo':] adds a new foo + n = n.start + if self._dNS and not islst(n): n = (self._dNS, n) + + nv = Element(n) + self._dir.append(nv) + + else: # d["foo"] replaces first and dels rest + if self._dNS and not islst(n): n = (self._dNS, n) + + nv = Element(n); nv._dir.append(v) + replaced = False + + todel = [] + for i in range(len(self)): + if self[i]._name == n: + if replaced: + todel.append(i) + else: + self[i] = nv + replaced = True + if not replaced: self._dir.append(nv) + for i in todel: del self[i] + + def __delitem__(self, n): + if isinstance(n, type(0)): del self._dir[n] + elif isinstance(n, slice(0).__class__): + # delete all s + n = n.start + if self._dNS and not islst(n): n = (self._dNS, n) + + for i in range(len(self)): + if self[i]._name == n: del self[i] + else: + # delete first foo + for i in range(len(self)): + if self[i]._name == n: del self[i] + break + + def __call__(self, *_pos, **_set): + if _set: + for k in _set.keys(): self._attrs[k] = _set[k] + if len(_pos) > 1: + for i in range(0, len(_pos), 2): + self._attrs[_pos[i]] = _pos[i+1] + if len(_pos) == 1: + return self._attrs[_pos[0]] + if len(_pos) == 0: + return self._attrs + + def __len__(self): return len(self._dir) + +class Namespace: + def __init__(self, uri): self.__uri = uri + def __getattr__(self, n): return (self.__uri, n) + def __getitem__(self, n): return (self.__uri, n) + +from xml.sax.handler import EntityResolver, DTDHandler, ContentHandler, ErrorHandler + +class Seeder(EntityResolver, DTDHandler, ContentHandler, ErrorHandler): + def __init__(self): + self.stack = [] + self.ch = '' + self.prefixes = {} + ContentHandler.__init__(self) + + def startPrefixMapping(self, prefix, uri): + if not self.prefixes.has_key(prefix): self.prefixes[prefix] = [] + self.prefixes[prefix].append(uri) + def endPrefixMapping(self, prefix): + self.prefixes[prefix].pop() + + def startElementNS(self, name, qname, attrs): + ch = self.ch; self.ch = '' + if ch and not ch.isspace(): self.stack[-1]._dir.append(ch) + + attrs = dict(attrs) + newprefixes = {} + for k in self.prefixes.keys(): newprefixes[k] = self.prefixes[k][-1] + + self.stack.append(Element(name, attrs, prefixes=newprefixes.copy())) + + def characters(self, ch): + self.ch += ch + + def endElementNS(self, name, qname): + ch = self.ch; self.ch = '' + if ch and not ch.isspace(): self.stack[-1]._dir.append(ch) + + element = self.stack.pop() + if self.stack: + self.stack[-1]._dir.append(element) + else: + self.result = element + +from xml.sax import make_parser +from xml.sax.handler import feature_namespaces + +def seed(fileobj): + seeder = Seeder() + parser = make_parser() + parser.setFeature(feature_namespaces, 1) + parser.setContentHandler(seeder) + parser.parse(fileobj) + return seeder.result + +def parse(text): + from StringIO import StringIO + return seed(StringIO(text)) + +def load(url): + import urllib + return seed(urllib.urlopen(url)) + +def unittest(): + parse('afoobara').__repr__(1,1) == \ + '\n\ta\n\t\tfoobar\n\ta\n' + + assert str(parse("")) == "" + assert str(parse("I love you.")) == "I love you." + assert parse("\nmom\nwow\n")[0].strip() == "mom\nwow" + assert str(parse(' center ')) == "center" + assert str(parse('\xcf\x80')) == '\xcf\x80' + + d = Element('foo', attrs={'foo':'bar'}, children=['hit with a', Element('bar'), Element('bar')]) + + try: + d._doesnotexist + raise "ExpectedError", "but found success. Damn." + except AttributeError: pass + assert d.bar._name == 'bar' + try: + d.doesnotexist + raise "ExpectedError", "but found success. Damn." + except AttributeError: pass + + assert hasattr(d, 'bar') == True + + assert d('foo') == 'bar' + d(silly='yes') + assert d('silly') == 'yes' + assert d() == d._attrs + + assert d[0] == 'hit with a' + d[0] = 'ice cream' + assert d[0] == 'ice cream' + del d[0] + assert d[0]._name == "bar" + assert len(d[:]) == len(d._dir) + assert len(d[1:]) == len(d._dir) - 1 + assert len(d['bar':]) == 2 + d['bar':] = 'baz' + assert len(d['bar':]) == 3 + assert d['bar']._name == 'bar' + + d = Element('foo') + + doc = Namespace("http://example.org/bar") + bbc = Namespace("http://example.org/bbc") + dc = Namespace("http://purl.org/dc/elements/1.1/") + d = parse(""" + John Polk and John Palfrey + John Polk + John Palfrey + Buffy + """) + + assert repr(d) == '...' + assert d.__repr__(1) == 'John Polk and John PalfreyJohn PolkJohn PalfreyBuffy' + assert d.__repr__(1,1) == '\n\tJohn Polk and John Palfrey\n\tJohn Polk\n\tJohn Palfrey\n\tBuffy\n' + + assert repr(parse("")) == '' + + assert str(d.author) == str(d['author']) == "John Polk and John Palfrey" + assert d.author._name == doc.author + assert str(d[dc.creator]) == "John Polk" + assert d[dc.creator]._name == dc.creator + assert str(d[dc.creator:][1]) == "John Palfrey" + d[dc.creator] = "Me!!!" + assert str(d[dc.creator]) == "Me!!!" + assert len(d[dc.creator:]) == 1 + d[dc.creator:] = "You!!!" + assert len(d[dc.creator:]) == 2 + + assert d[bbc.show](bbc.station) == "4" + d[bbc.show](bbc.station, "5") + assert d[bbc.show](bbc.station) == "5" + + e = Element('e') + e.c = '' + assert e.__repr__(1) == '<img src="foo">' + e.c = '2 > 4' + assert e.__repr__(1) == '2 > 4' + e.c = 'CDATA sections are closed with ]]>.' + assert e.__repr__(1) == 'CDATA sections are <em>closed</em> with ]]>.' + e.c = parse('
i
love
you
') + assert e.__repr__(1) == '
i
love
you
' + + e = Element('e') + e('c', 'that "sucks"') + assert e.__repr__(1) == '' + + + assert quote("]]>") == "]]>" + assert quote('< dkdkdsd dkd sksdksdfsd fsdfdsf]]> kfdfkg >') == '< dkdkdsd dkd sksdksdfsd fsdfdsf]]> kfdfkg >' + + assert parse('').__repr__(1) == '' + assert parse('').__repr__(1) == '' + +if __name__ == '__main__': unittest()