Skip to content
Snippets Groups Projects
Commit b32179e4 authored by Julian Gilbey's avatar Julian Gilbey
Browse files

New upstream version 1.2.1

parent c7146830
No related branches found
No related tags found
No related merge requests found
[flake8]
exclude = .git,__pycache__,docs/conf.py,old,build,dist,.tox
ignore = E501
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
name: build
on: push
jobs:
test:
name: Test Python ${{ matrix.python-version }} (${{ matrix.os }})
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: make setup
- name: Lint
run: make lint
- name: Run tests
run: make test
release:
name: Release
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install requirements
run: python -m pip install wheel setuptools build
- name: Build a distribution
run: python -m build
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
- 3.8
- pypy
install:
- pip install coveralls
- pip install tox
script:
coverage run --source=untangle ./setup.py test
after_success:
coveralls
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/4e8e4267eec52e11a384
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false
......@@ -2,14 +2,26 @@ Changelog
---------
Unreleased
- flake8 now runs as part of the unit tests. Fully replaced nose with tox as a test runner.
1.2.1
- (SECURITY) Use [defusedxml](https://github.com/tiran/defusedxml) to prevent XML SAX vulnerabilities ([#94](https://github.com/stchris/untangle/pull/94))
1.2.0
- (SECURITY) Prevent XML SAX vulnerability: External Entities injection ([#60](https://github.com/stchris/untangle/issues/60))
- support for python keywords as element names ([#43](https://github.com/stchris/untangle/pull/43))
- support Element truthiness on Python 3 ([#68](https://github.com/stchris/untangle/pull/68/))
- dropped support for Python 3.4-3.6 and pypy, untangle currently support Python 3.7-3.10
- fixed setup.py warning ([#77](https://github.com/stchris/untangle/pull/77/))
- dropped support for Python 2.6, 3.3
- fixed support for Python 3.6 ([#57](https://github.com/stchris/untangle/pull/57))
- formatted code with black
- flake8 linter enforced in CI
- `main` is now the default branch
- switch to Github Actions
- switch to poetry and pytest
1.1.1
- addded generic SAX feature toggle ([#26](https://github.com/stchris/untangle/pull/26))
- added generic SAX feature toggle ([#26](https://github.com/stchris/untangle/pull/26))
- added support for `hasattribute`/`getattribute` ([#15](https://github.com/stchris/untangle/pull/15))
- added support for `len()` on parsed objects ([https://github.com/stchris/untangle/commit/31f3078]())
- fixed a potential bug when trying to detect URLs ([https://github.com/stchris/untangle/commit/cfa11d16]())
......
include README.md
......@@ -4,8 +4,16 @@
compile:
python -m compileall -q untangle.py tests/tests.py
setup:
python -m pip install poetry
poetry install
lint:
poetry run flake8 .
poetry run black --check .
test:
tox
poetry run pytest -v
# needs python-stdeb
package_deb:
......
untangle
========
[![Build Status](https://secure.travis-ci.org/stchris/untangle.png?branch=master)](http://travis-ci.org/stchris/untangle)
[![Build Status](https://github.com/stchris/untangle/actions/workflows/build.yml/badge.svg)](https://github.com/stchris/untangle/actions)
[![PyPi version](https://img.shields.io/pypi/v/untangle.svg)](https://pypi.python.org/pypi/untangle)
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
......@@ -12,7 +12,7 @@ untangle
* Children can be accessed with ``parent.child``, attributes with ``element['attribute']``.
* You can call the ``parse()`` method with a filename, an URL or an XML string.
* Substitutes ``-``, ``.`` and ``:`` with ``_`` ``<foobar><foo-bar/></foobar>`` can be accessed with ``foobar.foo_bar``, ``<foo.bar.baz/>`` can be accessed with ``foo_bar_baz`` and ``<foo:bar><foo:baz/></foo:bar>`` can be accessed with ``foo_bar.foo_baz``
* Works with Python 2.7 and 3.4, 3.5, 3.6, 3.7, 3.8 and pypy
* Works with Python 3.7 - 3.10
Installation
------------
......@@ -31,7 +31,7 @@ Conda feedstock maintained by @htenkanen. Issues and questions about conda-forge
Usage
-----
(See and run <a href="https://github.com/stchris/untangle/blob/master/examples.py">examples.py</a> or this blog post: [Read XML painlessly](http://pythonadventures.wordpress.com/2011/10/30/read-xml-painlessly/) for more info)
(See and run <a href="https://github.com/stchris/untangle/blob/main/examples.py">examples.py</a> or this blog post: [Read XML painlessly](http://pythonadventures.wordpress.com/2011/10/30/read-xml-painlessly/) for more info)
```python
import untangle
......
......@@ -41,8 +41,8 @@ source_suffix = ".rst"
master_doc = "index"
# General information about the project.
project = u"untangle"
copyright = u"2012, Christian Stefanescu"
project = "untangle"
copyright = "2012, Christian Stefanescu"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
......@@ -185,8 +185,8 @@ latex_documents = [
(
"index",
"untangle.tex",
u"untangle Documentation",
u"Christian Stefanescu",
"untangle Documentation",
"Christian Stefanescu",
"manual",
),
]
......@@ -217,7 +217,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
("index", "untangle", u"untangle Documentation", [u"Christian Stefanescu"], 1)
("index", "untangle", "untangle Documentation", ["Christian Stefanescu"], 1)
]
# If true, show URL addresses after external links.
......@@ -233,8 +233,8 @@ texinfo_documents = [
(
"index",
"untangle",
u"untangle Documentation",
u"Christian Stefanescu",
"untangle Documentation",
"Christian Stefanescu",
"untangle",
"One line description of project.",
"Miscellaneous",
......
......@@ -7,7 +7,7 @@ untangle: Convert XML to Python objects
=======================================
`untangle <https://stchris.github.com/untangle/>`_ is a tiny Python library which converts an XML
document to a Python object. It is available under the `MIT license <https://github.com/stchris/untangle/blob/master/LICENSE/>`_.
document to a Python object. It is available under the `MIT license <https://github.com/stchris/untangle/blob/main/LICENSE/>`_.
.. contents::
......@@ -45,17 +45,15 @@ and assuming it's available in a variable called `xml`, we could use untangle li
For text/data inbetween tags, this is described as cdata. After specifying the relevant element as explained above, the data/cdata can be accessed by adding ".cdata" (without the quotes) to the end of your dictionary call.
For more examples, have a look at (and launch) `examples.py <https://github.com/stchris/untangle/blob/master/examples.py/>`_.
For more examples, have a look at (and launch) `examples.py <https://github.com/stchris/untangle/blob/main/examples.py/>`_.
Installation
------------
It is recommended to use pip, which will always download the latest stable release: ::
::
pip install untangle
untangle works with Python versions 2.6, 2.7, 3.3, 3.4, 3.5, 3.6 and pypy.
Alternatively, you can install untangle with conda from conda-forge: ::
conda install -c conda-forge untangle
......@@ -99,7 +97,7 @@ This will toggle the SAX handler feature described `here <https://docs.python.or
Changelog
---------
see https://github.com/stchris/untangle/blob/master/CHANGELOG.md
see https://github.com/stchris/untangle/blob/main/CHANGELOG.md
Indices and tables
......
This diff is collapsed.
[tool.poetry]
name = "untangle"
version = "1.2.1"
description = "Converts XML to Python objects"
authors = ["Christian Stefanescu <hello@stchris.net>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.7"
defusedxml = "^0.7.1"
[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
flake8 = "^4.0.1"
black = "^22.6.0"
build = "^0.8.0"
setuptools = "^62.6.0"
wheel = "^0.37.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
untangle
--------
.. image:: https://secure.travis-ci.org/stchris/untangle.png?branch=master
untangle parses an XML document and returns a Python object which makes it
easy to access the data you want.
Example:
::
import untangle
obj = untangle.parse('<root><child name="child1"/></root>')
assert obj.root.child['name'] == u'child1'
See http://0chris.com/untangle and
http://readthedocs.org/docs/untangle/en/latest/
"""
import os
import sys
import untangle
from setuptools import setup
from setuptools import setup, find_packages
from pathlib import Path
if sys.argv[-1] == "test":
os.system("tox")
sys.exit()
long_description = (Path(__file__).parent / "README.md").read_text()
setup(
name="untangle",
packages=find_packages(),
version=untangle.__version__,
description="Convert XML documents into Python objects",
long_description=__doc__,
long_description_content_type="text/markdown",
long_description=long_description,
author="Christian Stefanescu",
author_email="chris@0chris.com",
author_email="hello@stchris.net",
url="http://github.com/stchris//untangle",
py_modules=["untangle"],
install_requires=["defusedxml"],
include_package_data=True,
license="MIT",
classifiers=(
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Natural Language :: English",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
),
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
)
# vim: set expandtab ts=4 sw=4:
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "pypy"
language: python
script:
- python setup.py test
<!DOCTYPE external [
<!ENTITY ee SYSTEM "http://www.python.org/some.xml">
]>
<root>&ee;</root>
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import unittest
import untangle
import xml
import defusedxml
class FromStringTestCase(unittest.TestCase):
""" Basic parsing tests with input as string """
"""Basic parsing tests with input as string"""
def test_basic(self):
o = untangle.parse("<a><b/><c/></a>")
......@@ -31,6 +33,13 @@ class FromStringTestCase(unittest.TestCase):
self.assertTrue("c" in o.a)
self.assertTrue("d" not in o.a)
def test_truthiness(self):
o = untangle.parse("<a><b/><c/></a>")
self.assertTrue(o)
self.assertTrue(o.a)
self.assertTrue(o.a.b)
self.assertTrue(o.a.c)
def test_with_attributes(self):
o = untangle.parse(
"""
......@@ -118,7 +127,7 @@ class FromStringTestCase(unittest.TestCase):
class InvalidTestCase(unittest.TestCase):
""" Test corner cases """
"""Test corner cases"""
def test_invalid_xml(self):
self.assertRaises(xml.sax.SAXParseException, untangle.parse, "<unclosed>")
......@@ -131,7 +140,7 @@ class InvalidTestCase(unittest.TestCase):
class PomXmlTestCase(unittest.TestCase):
""" Tests parsing a Maven pom.xml """
"""Tests parsing a Maven pom.xml"""
def setUp(self):
self.o = untangle.parse("tests/res/pom.xml")
......@@ -164,7 +173,7 @@ class PomXmlTestCase(unittest.TestCase):
class NamespaceTestCase(unittest.TestCase):
""" Tests for XMLs with namespaces """
"""Tests for XMLs with namespaces"""
def setUp(self):
self.o = untangle.parse("tests/res/some.xslt")
......@@ -197,10 +206,10 @@ class NamespaceTestCase(unittest.TestCase):
class IterationTestCase(unittest.TestCase):
""" Tests various cases of iteration over child nodes. """
"""Tests various cases of iteration over child nodes."""
def test_multiple_children(self):
""" Regular case of iteration. """
"""Regular case of iteration."""
o = untangle.parse("<a><b/><b/></a>")
cnt = 0
for i in o.a.b:
......@@ -208,8 +217,8 @@ class IterationTestCase(unittest.TestCase):
self.assertEqual(2, cnt)
def test_single_child(self):
""" Special case when there is only a single child element.
Does not work without an __iter__ implemented.
"""Special case when there is only a single child element.
Does not work without an __iter__ implemented.
"""
o = untangle.parse("<a><b/></a>")
cnt = 0
......@@ -219,7 +228,7 @@ class IterationTestCase(unittest.TestCase):
class TwimlTestCase(unittest.TestCase):
""" Github Issue #5: can't dir the parsed object """
"""Github Issue #5: can't dir the parsed object"""
def test_twiml_dir(self):
xml = """<?xml version="1.0" encoding="UTF-8"?>
......@@ -232,24 +241,24 @@ class TwimlTestCase(unittest.TestCase):
</Response>
"""
o = untangle.parse(xml)
self.assertEqual([u"Response"], dir(o))
self.assertEqual(["Response"], dir(o))
resp = o.Response
self.assertEqual([u"Gather", u"Redirect"], dir(resp))
self.assertEqual(["Gather", "Redirect"], dir(resp))
gather = resp.Gather
redir = resp.Redirect
self.assertEqual([u"Play"], dir(gather))
self.assertEqual(["Play"], dir(gather))
self.assertEqual([], dir(redir))
self.assertEqual(
u"http://example.com/calls/1/twiml?event=start", o.Response.Redirect.cdata
"http://example.com/calls/1/twiml?event=start", o.Response.Redirect.cdata
)
class UnicodeTestCase(unittest.TestCase):
""" Github issue #8: UnicodeEncodeError """
"""Github issue #8: UnicodeEncodeError"""
def test_unicode_file(self):
o = untangle.parse("tests/res/unicode.xml")
self.assertEqual(u"ðÒÉ×ÅÔ ÍÉÒ", o.page.menu.name)
self.assertEqual("ðÒÉ×ÅÔ ÍÉÒ", o.page.menu.name)
def test_lengths(self):
o = untangle.parse("tests/res/unicode.xml")
......@@ -263,11 +272,16 @@ class UnicodeTestCase(unittest.TestCase):
def test_unicode_string(self):
o = untangle.parse("<Element>valüé ◔‿◔</Element>")
self.assertEqual(u"valüé ◔‿◔", o.Element.cdata)
self.assertEqual("valüé ◔‿◔", o.Element.cdata)
def test_unicode_element(self):
o = untangle.parse("<Francés></Francés>")
self.assertTrue(o is not None)
self.assertTrue(o.Francés is not None)
class FileObjects(unittest.TestCase):
""" Test reading from file-like objects """
"""Test reading from file-like objects"""
def test_file_object(self):
with open("tests/res/pom.xml") as pom_file:
......@@ -283,14 +297,14 @@ class FileObjects(unittest.TestCase):
class Foo(object):
""" Used in UntangleInObjectsTestCase """
"""Used in UntangleInObjectsTestCase"""
def __init__(self):
self.doc = untangle.parse('<a><b x="1">foo</b></a>')
class UntangleInObjectsTestCase(unittest.TestCase):
""" tests usage of untangle in classes """
"""tests usage of untangle in classes"""
def test_object(self):
foo = Foo()
......@@ -299,7 +313,7 @@ class UntangleInObjectsTestCase(unittest.TestCase):
class UrlStringTestCase(unittest.TestCase):
""" tests is_url() function """
"""tests is_url() function"""
def test_is_url(self):
self.assertFalse(untangle.is_url("foo"))
......@@ -310,7 +324,7 @@ class UrlStringTestCase(unittest.TestCase):
class TestSaxHandler(unittest.TestCase):
""" Tests the SAX ContentHandler """
"""Tests the SAX ContentHandler"""
def test_empty_handler(self):
h = untangle.Handler()
......@@ -352,16 +366,16 @@ class ParserFeatureTestCase(unittest.TestCase):
def test_valid_feature(self):
# xml.sax.handler.feature_external_ges -> load external general (text)
# entities, such as DTDs
doc = untangle.parse(self.bad_dtd_xml, feature_external_ges=False)
self.assertEqual(doc.foo["bar"], "baz")
with self.assertRaises(defusedxml.common.ExternalReferenceForbidden):
untangle.parse(self.bad_dtd_xml)
def test_invalid_feature(self):
with self.assertRaises(AttributeError):
untangle.parse(self.bad_dtd_xml, invalid_feature=True)
def test_invalid_external_dtd(self):
with self.assertRaises(IOError):
untangle.parse(self.bad_dtd_xml, feature_external_ges=True)
with self.assertRaises(defusedxml.common.ExternalReferenceForbidden):
untangle.parse(self.bad_dtd_xml)
class TestEquals(unittest.TestCase):
......@@ -378,7 +392,14 @@ class TestEquals(unittest.TestCase):
self.assertTrue(c in listA)
class TestExternalEntityExpansion(unittest.TestCase):
def test_xxe(self):
# from https://pypi.org/project/defusedxml/#external-entity-expansion-remote
with self.assertRaises(defusedxml.common.EntitiesForbidden):
untangle.parse("tests/res/xxe.xml")
if __name__ == "__main__":
unittest.main()
# vim: set expandtab ts=4 sw=4:
# vim: set expandtab ts=4 sw=4
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = flake8, py27, py34, py35, py36, py37, py38 pypy
skip_missing_interpreters = true
[testenv]
commands = py.test tests/tests.py
deps = pytest
[testenv:flake8]
basepython=python
deps=flake8
commands=flake8 .
[flake8]
exclude = .git,__pycache__,docs/conf.py,old,build,dist,.tox
ignore = E501
[testenv:black]
basepython=python
deps=black
commands=black --check .
......@@ -15,7 +15,9 @@
"""
import os
import keyword
from xml.sax import make_parser, handler
from defusedxml.sax import make_parser
from xml.sax import handler
try:
from StringIO import StringIO
......@@ -27,14 +29,13 @@ try:
def is_string(x):
return isinstance(x, StringTypes)
except ImportError:
def is_string(x):
return isinstance(x, str)
__version__ = "1.1.1"
__version__ = "1.2.1"
class Element(object):
......@@ -114,9 +115,11 @@ class Element(object):
self.cdata,
)
def __nonzero__(self):
def __bool__(self):
return self.is_root or self._name is not None
__nonzero__ = __bool__
def __eq__(self, val):
return self.cdata == val
......@@ -173,8 +176,8 @@ def parse(filename, **parser_features):
parses it and returns a Python object which represents the given
document.
Extra arguments to this function are treated as feature values to pass
to ``parser.setFeature()``. For example, ``feature_external_ges=False``
Extra arguments to this function are treated as feature values that are
passed to ``parser.setFeature()``. For example, ``feature_external_ges=False``
will set ``xml.sax.handler.feature_external_ges`` to False, disabling
the parser's inclusion of external general (text) entities such as DTDs.
......@@ -185,6 +188,11 @@ def parse(filename, **parser_features):
Raises ``xml.sax.SAXParseException`` if something goes wrong
during parsing.
Raises ``defusedxml.common.EntitiesForbidden``
or ``defusedxml.common.ExternalReferenceForbidden``
when a potentially malicious entity load is attempted. See also
https://github.com/tiran/defusedxml#attack-vectors
"""
if filename is None or (is_string(filename) and filename.strip()) == "":
raise ValueError("parse() takes a filename, URL or XML string")
......
untangle
--------
.. image:: https://github.com/stchris/untangle/actions/workflows/build.yml/badge.svg
untangle parses an XML document and returns a Python object which makes it
easy to access the data you want.
Example:
::
import untangle
obj = untangle.parse('<root><child name="child1"/></root>')
assert obj.root.child['name'] == u'child1'
See http://github.com/stchris/untangle and
http://readthedocs.org/docs/untangle/en/latest/
#!/bin/bash
python setup.py sdist upload
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment