Repository: tefra/xsdata
Pull request #441 (2021-03-30 20:56:06)

JsonParser allow parsing from filename string
This was a hidden feature of the XmlParser as well, time to sync their entrypoints

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=441&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=441&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=441&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=h1) Report
> Merging [#441](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=desc) (37d2f3e) into [master](https://codecov.io/gh/tefra/xsdata/commit/e1a4950f4e04f54fb777f0479541c91309264205?el=desc) (e1a4950) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/441/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl)](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #441   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           74        74           
  Lines         6913      6912    -1     
  Branches      1224      1225    +1     
=========================================
- Hits          6913      6912    -1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/bindings.py](https://codecov.io/gh/tefra/xsdata/pull/441/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvYmluZGluZ3MucHk=) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes.py](https://codecov.io/gh/tefra/xsdata/pull/441/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMucHk=) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/parsers/json.py](https://codecov.io/gh/tefra/xsdata/pull/441/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvanNvbi5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=footer). Last update [e1a4950...37d2f3e](https://codecov.io/gh/tefra/xsdata/pull/441?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/codegen.rst b/docs/codegen.rst
index 69913b58..f3cfd4af 100644
--- a/docs/codegen.rst
+++ b/docs/codegen.rst
@@ -13,8 +13,8 @@ Make sure the cli requirements are installed.
 .. command-output:: xsdata --help
 
 
-Generate Models
-===============
+Generate Code
+=============
 
 .. command-output:: xsdata generate --help
 
@@ -25,28 +25,26 @@ Generate Models
     - :ref:`Docstring styles`
 
 
-Examples
---------
 
 .. code-block:: console
-    :name: directory source
+    :caption: Scan directory for xsd, wsdl, xml files
 
     $ xsdata amadeus/schemas --package amadeus.models
 
 
 .. code-block:: console
-    :name: file path
+    :caption: Convert a local schema
 
     $ xsdata air_v48_0/AirReqRsp.xsd rail_v48_0/RailReqRsp.xsd --package travelport.models
 
 
 .. code-block:: console
-    :name: uri resource
+    :caption: Convert a remote schema
 
     $ xsdata http://www.gstatic.com/localfeed/local_feed.xsd --package feeds --print
 
 .. code-block:: console
-    :name: xml url
+    :caption: Convert a remote xml file
 
     $ xsdata https://musicbrainz.org/ws/2/artist/1f9df192-a621-4f54-8850-2c5373b7eac9 --print
 
@@ -78,4 +76,4 @@ Download Schemas
 .. admonition:: Examples
     :class: hint
 
-    Check the :doc:`samples` and the `samples repo <https://github.com/tefra/xsdata-samples>`_ for more ✨✨✨
+    Check the :doc:`examples` and the `samples repo <https://github.com/tefra/xsdata-samples>`_ for more ✨✨✨
diff --git a/docs/json.rst b/docs/json.rst
index 8855dfdd..49625101 100644
--- a/docs/json.rst
+++ b/docs/json.rst
@@ -7,6 +7,7 @@ types doing roundtrip conversions is not always possible.
 
 .. testsetup:: *
 
+    import io
     from pathlib import Path
     from xsdata.formats.dataclass.context import XmlContext
     from xsdata.formats.dataclass.parsers import JsonParser
@@ -16,35 +17,48 @@ types doing roundtrip conversions is not always possible.
 
     json_path = fixtures_dir.joinpath("defxmlschema/chapter05.json")
     parser = JsonParser(context=XmlContext())
-    order = Order(
-        items=ItemsType(
-            product=[
-                Product(
-                    number=557,
-                    name='Short-Sleeved Linen Blouse',
-                    size=SizeType(value=None, system=None)
-                )
-            ]
-        )
-    )
 
 Parsing JSON
 ============
 
-From Path
----------
+
+From filename
+-------------
 
 .. doctest::
 
     >>> from pathlib import Path
     >>> from xsdata.formats.dataclass.context import XmlContext
     >>> from xsdata.formats.dataclass.parsers import JsonParser
-    >>> from tests import fixtures_dir
+    >>> from tests import fixtures_dir # pathlib.Path
     >>> from tests.fixtures.defxmlschema.chapter05 import Order
     ...
-    >>> json_path = fixtures_dir.joinpath("defxmlschema/chapter05.json")
+    >>> filename = str(fixtures_dir.joinpath("defxmlschema/chapter05.json"))
     >>> parser = JsonParser(context=XmlContext())
-    >>> order = parser.from_path(json_path, Order)
+    >>> order = parser.parse(filename, Order)
+    >>> order.items.product[0]
+    Product(number=557, name='Short-Sleeved Linen Blouse', size=SizeType(value=None, system=None))
+
+
+From file object
+----------------
+
+.. doctest::
+
+    >>> json_path = fixtures_dir.joinpath("defxmlschema/chapter05.json")
+    >>> with json_path.open("rb") as fp:
+    ...     order = parser.parse(fp, Order)
+    >>> order.items.product[0]
+    Product(number=557, name='Short-Sleeved Linen Blouse', size=SizeType(value=None, system=None))
+
+
+From stream
+-----------
+
+.. doctest::
+
+    >>> import io
+    >>> order = parser.parse(io.BytesIO(json_path.read_bytes()), Order)
     >>> order.items.product[0]
     Product(number=557, name='Short-Sleeved Linen Blouse', size=SizeType(value=None, system=None))
 
@@ -69,6 +83,16 @@ From Bytes
     Product(number=557, name='Short-Sleeved Linen Blouse', size=SizeType(value=None, system=None))
 
 
+From path
+---------
+
+.. doctest::
+
+    >>> order = parser.from_path(json_path, Order)
+    >>> order.items.product[0]
+    Product(number=557, name='Short-Sleeved Linen Blouse', size=SizeType(value=None, system=None))
+
+
 Unknown target type
 -------------------
 
diff --git a/docs/xml.rst b/docs/xml.rst
index 0689deb1..7cfcd996 100644
--- a/docs/xml.rst
+++ b/docs/xml.rst
@@ -20,6 +20,7 @@ context instance.
 
 .. testsetup:: *
 
+    import io
     from xsdata.formats.dataclass.context import XmlContext
     from xsdata.formats.dataclass.parsers import XmlParser
     from xsdata.formats.dataclass.serializers import XmlSerializer
@@ -29,20 +30,6 @@ context instance.
     from tests.fixtures.primer import PurchaseOrder, Usaddress
 
     xml_path = fixtures_dir.joinpath("primer/order.xml")
-    books = Books(
-         book=[
-             BookForm(
-                 id="bk001",
-                 author="Hightower, Kim",
-                 title="The First Book",
-                 genre="Fiction",
-                 price=44.95,
-                 pub_date="2000-10-01",
-                 review="An amazing story of nothing.",
-             )
-         ]
-     )
-
     parser = XmlParser()
     serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
 
@@ -50,24 +37,46 @@ context instance.
 Parsing XML
 ===========
 
-
-From Path
----------
+From filename
+-------------
 
 .. doctest::
 
     >>> from xsdata.formats.dataclass.context import XmlContext
     >>> from xsdata.formats.dataclass.parsers import XmlParser
-    >>> from tests import fixtures_dir
+    >>> from tests import fixtures_dir # pathlib.Path
     >>> from tests.fixtures.primer import PurchaseOrder, Usaddress
     ...
-    >>> xml_path = fixtures_dir.joinpath("primer/order.xml")
+    >>> filename = str(fixtures_dir.joinpath("primer/order.xml"))
     >>> parser = XmlParser(context=XmlContext())
-    >>> order = parser.from_path(xml_path, PurchaseOrder)
+    >>> order = parser.parse(filename, PurchaseOrder)
     >>> order.bill_to
     Usaddress(name='Robert Smith', street='8 Oak Avenue', city='Old Town', state='PA', zip=Decimal('95819'), country='US')
 
 
+From file object
+----------------
+
+.. doctest::
+
+    >>> xml_path = fixtures_dir.joinpath("primer/order.xml")
+    >>> with xml_path.open("rb") as fp:
+    ...     order = parser.parse(fp, PurchaseOrder)
+    >>> order.bill_to.street
+    '8 Oak Avenue'
+
+
+From stream
+-----------
+
+.. doctest::
+
+    >>> import io
+    >>> order = parser.parse(io.BytesIO(xml_path.read_bytes()), PurchaseOrder)
+    >>> order.bill_to.street
+    '8 Oak Avenue'
+
+
 From string
 -----------
 
@@ -88,6 +97,16 @@ From bytes
     '8 Oak Avenue'
 
 
+From path
+---------
+
+.. doctest::
+
+    >>> order = parser.from_path(xml_path, PurchaseOrder)
+    >>> order.bill_to.street
+    '8 Oak Avenue'
+
+
 Unknown target type
 -------------------
 
diff --git a/xsdata/formats/bindings.py b/xsdata/formats/bindings.py
index da446b83..fa2e3735 100644
--- a/xsdata/formats/bindings.py
+++ b/xsdata/formats/bindings.py
@@ -30,4 +30,5 @@ class AbstractParser(abc.ABC):
 
     @abc.abstractmethod
     def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input stream and return the resulting object tree."""
+        """Parse the input stream or filename and return the resulting object
+        tree."""
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index 3cf147c7..c909999b 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -1,6 +1,4 @@
-import io
 import json
-import pathlib
 import warnings
 from dataclasses import dataclass
 from dataclasses import field
@@ -23,7 +21,6 @@ from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.utils.constants import EMPTY_MAP
 
-
 ANY_KEYS = {f.name for f in fields(AnyElement)}
 DERIVED_KEYS = {f.name for f in fields(DerivedElement)}
 
@@ -38,13 +35,15 @@ class JsonParser(AbstractParser):
 
     context: XmlContext = field(default_factory=XmlContext)
 
-    def from_path(self, path: pathlib.Path, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input file path and return the resulting object tree."""
-        return self.from_bytes(path.read_bytes(), clazz)
+    def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
+        """Parse the input stream or filename and return the resulting object
+        tree."""
 
-    def parse(self, source: io.BytesIO, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the JSON input stream and return the resulting object tree."""
-        ctx = json.load(source)
+        if not hasattr(source, "read"):
+            with open(source, "rb") as fp:
+                ctx = json.load(fp)
+        else:
+            ctx = json.load(source)
 
         if clazz is None:
             clazz = self.context.find_type_by_fields(set(ctx.keys()))
diff --git a/xsdata/formats/dataclass/parsers/nodes.py b/xsdata/formats/dataclass/parsers/nodes.py
index fd2c370c..0483cfcf 100644
--- a/xsdata/formats/dataclass/parsers/nodes.py
+++ b/xsdata/formats/dataclass/parsers/nodes.py
@@ -440,7 +440,8 @@ class NodeParser(PushParser):
     ns_map: Dict = field(init=False, default_factory=dict)
 
     def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the XML input stream and return the resulting object tree."""
+        """Parse the input stream or filename and return the resulting object
+        tree."""
         handler = self.handler(clazz=clazz, parser=self)
         result = handler.parse(source)
 


Test patch:
diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py
index 9c04b309..0c873b9f 100644
--- a/tests/formats/dataclass/parsers/test_json.py
+++ b/tests/formats/dataclass/parsers/test_json.py
@@ -51,6 +51,18 @@ class JsonParserTests(TestCase):
             books.book[1],
         )
 
+    def test_parser_entry_points(self):
+        path = fixtures_dir.joinpath("books/books.json")
+
+        books = self.parser.from_string(path.read_text(), Books)
+        self.assertIsInstance(books, Books)
+
+        books = self.parser.from_bytes(path.read_bytes(), Books)
+        self.assertIsInstance(books, Books)
+
+        books = self.parser.parse(str(path), Books)
+        self.assertIsInstance(books, Books)
+
     def test_parser_with_unknown_class(self):
         path = fixtures_dir.joinpath("books/books.json")
         books = self.parser.from_path(path)