junitparser 5.0.1__tar.gz → 5.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: junitparser
3
- Version: 5.0.1
3
+ Version: 5.0.2
4
4
  Summary: Manipulates JUnit/xUnit Result XML files
5
5
  Keywords: junit,xunit,xml,parser
6
6
  Author: Weiwei Wang
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.7,<0.13.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "junitparser"
7
+ version = "5.0.2"
8
+ description = "Manipulates JUnit/xUnit Result XML files"
9
+ readme = "README.rst"
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Intended Audience :: Developers",
15
+ "Topic :: Text Processing",
16
+ "Programming Language :: Python :: 3",
17
+ ]
18
+ license = "Apache-2.0"
19
+ license-files = ["LICENSE"]
20
+ keywords = [
21
+ "junit",
22
+ "xunit",
23
+ "xml",
24
+ "parser",
25
+ ]
26
+
27
+ [[project.authors]]
28
+ name = "Weiwei Wang"
29
+ email = "gastlygem@gmail.com"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/weiwei/junitparser"
33
+ Issues = "https://github.com/weiwei/junitparser/issues"
34
+ Documentation = "https://junitparser.readthedocs.io/"
35
+
36
+ [project.scripts]
37
+ junitparser = "junitparser.cli:main"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pre-commit>=4.6.0",
42
+ "pytest>=9.0.3",
43
+ "pytest-cov>=7.1.0",
44
+ "ruff>=0.15.12",
45
+ ]
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.11.7,<0.12.0"]
2
+ requires = ["uv_build>=0.11.7,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "junitparser"
7
- version = "5.0.1"
7
+ version = "5.0.2"
8
8
  authors = [
9
9
  { name="Weiwei Wang", email="gastlygem@gmail.com" },
10
10
  ]
@@ -13,7 +13,7 @@ import io
13
13
  import itertools
14
14
  from copy import deepcopy
15
15
  from pathlib import Path
16
- from typing import List, Union, Iterator, IO, Optional
16
+ from typing import List, Iterator, IO
17
17
 
18
18
  try:
19
19
  from lxml import etree
@@ -23,7 +23,7 @@ except ImportError:
23
23
 
24
24
  def write_xml(
25
25
  obj,
26
- file_or_filename: Optional[Union[str, IO, Path]] = None,
26
+ file_or_filename: str | IO | Path | None = None,
27
27
  *,
28
28
  pretty: bool = False,
29
29
  ):
@@ -75,7 +75,7 @@ class JUnitXmlError(Exception):
75
75
  """Exception for JUnit XML related errors."""
76
76
 
77
77
 
78
- class Attr(object):
78
+ class Attr:
79
79
  """An attribute for an XML element.
80
80
 
81
81
  By default they are all string values. To support different value types,
@@ -132,7 +132,7 @@ class FloatAttr(Attr):
132
132
  return float(result.replace(",", "")) if result else None
133
133
 
134
134
  def __set__(self, instance, value: float):
135
- if not (isinstance(value, float) or isinstance(value, int)):
135
+ if not (isinstance(value, (float, int))):
136
136
  raise TypeError("Expected float value.")
137
137
  super().__set__(instance, value)
138
138
 
@@ -149,7 +149,7 @@ class junitxml(type):
149
149
  """Metaclass to decorate the XML class."""
150
150
 
151
151
  def __new__(meta, name, bases, methods):
152
- cls = super(junitxml, meta).__new__(meta, name, bases, methods)
152
+ cls = super().__new__(meta, name, bases, methods)
153
153
  cls = attributed(cls)
154
154
  return cls
155
155
 
@@ -169,12 +169,10 @@ class Element(metaclass=junitxml):
169
169
  tag = self._elem.tag
170
170
  keys = sorted(self._elem.attrib.keys())
171
171
  if keys:
172
- attrs_str = " ".join(
173
- '%s="%s"' % (key, self._elem.attrib[key]) for key in keys
174
- )
175
- return """<Element '%s' %s>""" % (tag, attrs_str)
172
+ attrs_str = " ".join(f'{key}="{self._elem.attrib[key]}"' for key in keys)
173
+ return f"""<Element '{tag}' {attrs_str}>"""
176
174
 
177
- return """<Element '%s'>""" % tag
175
+ return f"""<Element '{tag}'>"""
178
176
 
179
177
  def append(self, sub_elem):
180
178
  """Add the element subelement to the end of this elements internal
@@ -186,7 +184,7 @@ class Element(metaclass=junitxml):
186
184
  """Add elements subelement to the end of this elements internal
187
185
  list of subelements.
188
186
  """
189
- self._elem.extend((sub_elem._elem for sub_elem in sub_elems))
187
+ self._elem.extend(sub_elem._elem for sub_elem in sub_elems)
190
188
 
191
189
  @classmethod
192
190
  def fromstring(cls, text: str):
@@ -243,7 +241,7 @@ class Result(Element):
243
241
  type = Attr()
244
242
 
245
243
  def __init__(self, message: str | None = None, type_: str | None = None):
246
- super(Result, self).__init__(self._tag)
244
+ super().__init__(self._tag)
247
245
  if message:
248
246
  self.message = message
249
247
  if type_:
@@ -344,7 +342,7 @@ class TestCase(Element):
344
342
  __test__ = False
345
343
 
346
344
  # JUnit TestCase children are final results, SystemOut and SystemErr
347
- ITER_TYPES = {t._tag: t for t in (Failure, Error, Skipped, SystemOut, SystemErr)}
345
+ ITER_TYPES = {t._tag: t for t in (Failure, Error, Skipped, SystemOut, SystemErr)} # noqa: RUF012
348
346
 
349
347
  def __init__(
350
348
  self,
@@ -363,7 +361,7 @@ class TestCase(Element):
363
361
  def __hash__(self):
364
362
  return super().__hash__()
365
363
 
366
- def __iter__(self) -> Iterator[Union[Result, System]]:
364
+ def __iter__(self) -> Iterator[Result | System]:
367
365
  for elem in self._elem.iter():
368
366
  if elem.tag in self.ITER_TYPES:
369
367
  yield self.ITER_TYPES[elem.tag].fromelem(elem)
@@ -393,12 +391,12 @@ class TestCase(Element):
393
391
  return any(isinstance(r, Skipped) for r in self.result)
394
392
 
395
393
  @property
396
- def result(self) -> List[FinalResult]:
394
+ def result(self) -> list[FinalResult]:
397
395
  """A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects."""
398
396
  return [entry for entry in self if isinstance(entry, FinalResult)]
399
397
 
400
398
  @result.setter
401
- def result(self, value: Union[FinalResult, List[FinalResult]]):
399
+ def result(self, value: FinalResult | list[FinalResult]):
402
400
  # Check typing
403
401
  if not (
404
402
  isinstance(value, FinalResult)
@@ -575,30 +573,30 @@ class TestSuite(Element):
575
573
  # Merge the two testsuites
576
574
  result = deepcopy(self)
577
575
  for case in other:
578
- result._add_testcase_no_update_stats(case)
576
+ result._add_testcase_no_update_stats(deepcopy(case))
579
577
  for suite in other.testsuites():
580
- result.add_testsuite(suite)
578
+ result.add_testsuite(deepcopy(suite))
581
579
  result.update_statistics()
582
580
  else:
583
581
  # Create a new test result containing two testsuites
584
582
  result = self.root()
585
- result.add_testsuite(self)
586
- result.add_testsuite(other)
583
+ result.add_testsuite(deepcopy(self))
584
+ result.add_testsuite(deepcopy(other))
587
585
  return result
588
586
 
589
587
  def __iadd__(self, other):
590
588
  if self == other:
591
589
  for case in other:
592
- self._add_testcase_no_update_stats(case)
590
+ self._add_testcase_no_update_stats(deepcopy(case))
593
591
  for suite in other.testsuites():
594
- self.add_testsuite(suite)
592
+ self.add_testsuite(deepcopy(suite))
595
593
  self.update_statistics()
596
594
  return self
597
595
 
598
596
  result = self.root()
599
597
  result.filepath = self.filepath
600
598
  result.add_testsuite(self)
601
- result.add_testsuite(other)
599
+ result.add_testsuite(deepcopy(other))
602
600
  return result
603
601
 
604
602
  def remove_testcase(self, testcase: TestCase):
@@ -668,8 +666,7 @@ class TestSuite(Element):
668
666
  props = self.child(Properties)
669
667
  if props is None:
670
668
  return
671
- for prop in props:
672
- yield prop
669
+ yield from props
673
670
 
674
671
  def remove_property(self, property_: Property):
675
672
  """Remove property *property_* from the testsuite."""
@@ -684,9 +681,7 @@ class TestSuite(Element):
684
681
  """Iterate through all testsuites."""
685
682
  yield from self.iterchildren(type(self))
686
683
 
687
- def write(
688
- self, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False
689
- ):
684
+ def write(self, file_or_filename: str | IO | None = None, *, pretty: bool = False):
690
685
  write_xml(self, file_or_filename=file_or_filename, pretty=pretty)
691
686
 
692
687
 
@@ -728,26 +723,25 @@ class JUnitXml(Element):
728
723
  def __add__(self, other):
729
724
  result = type(self)()
730
725
  for suite in self:
731
- result.add_testsuite(suite)
726
+ result.add_testsuite(deepcopy(suite))
732
727
  for suite in other:
733
- result.add_testsuite(suite)
728
+ result.add_testsuite(deepcopy(suite))
734
729
  return result
735
730
 
736
731
  def __iadd__(self, other):
737
732
  if other._elem.tag == "testsuites":
738
733
  for suite in other:
739
- self.add_testsuite(suite)
734
+ self.add_testsuite(deepcopy(suite))
740
735
  elif other._elem.tag == "testsuite":
741
736
  suite = self.testsuite(name=other.name)
742
737
  for case in other:
743
- suite._add_testcase_no_update_stats(case)
738
+ suite._add_testcase_no_update_stats(deepcopy(case))
744
739
  self.add_testsuite(suite)
745
740
  self.update_statistics()
746
741
 
747
742
  return self
748
743
 
749
744
  def add_testsuite(self, suite: TestSuite):
750
- suite = deepcopy(suite)
751
745
  """Add a testsuite."""
752
746
  for existing_suite in self:
753
747
  if existing_suite == suite:
@@ -787,13 +781,13 @@ class JUnitXml(Element):
787
781
  return instance
788
782
 
789
783
  @classmethod
790
- def fromstring(cls, text: Union[str, bytes]) -> "JUnitXml":
784
+ def fromstring(cls, text: str | bytes) -> "JUnitXml":
791
785
  """Construct JUnit objects from an XML string (str or bytes)."""
792
786
  root_elem = etree.fromstring(text) # nosec
793
787
  return cls.fromroot(root_elem)
794
788
 
795
789
  @classmethod
796
- def fromfile(cls, file: Union[str, IO], parse_func=None) -> "JUnitXml":
790
+ def fromfile(cls, file: str | IO, parse_func=None) -> "JUnitXml":
797
791
  """
798
792
  Construct JUnit objects from an XML file.
799
793
 
@@ -813,9 +807,7 @@ class JUnitXml(Element):
813
807
  instance.filepath = file if isinstance(file, str) else None
814
808
  return instance
815
809
 
816
- def write(
817
- self, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False
818
- ):
810
+ def write(self, file_or_filename: str | IO | None = None, *, pretty: bool = False):
819
811
  """Write the object into a JUnit XML file.
820
812
 
821
813
  If `file_or_filename` is not specified, it will write to the original filename.
File without changes
File without changes