From 9b4ad968f83441f4ef4c207890285e4d6e24a813 Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Fri, 24 May 2024 22:58:02 +0300 Subject: [PATCH 1/6] Fix sphinx errors and warnings --- docs/API.rst | 10 +++------- docs/conf.py | 1 - pydifact/segmentcollection.py | 12 ++++++------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/API.rst b/docs/API.rst index 0938284..61ab0c9 100644 --- a/docs/API.rst +++ b/docs/API.rst @@ -43,10 +43,6 @@ The base meta class is a PluginMount: .. autoclass:: pydifact.api.PluginMount -Available entry points for plugins are: - -.. autoclass:: pydifact.segments.SegmentProvider - :members: - - .. automethod:: SegmentProvider.__str__ - +:class:`~pydifact.segments.SegmentProvider` uses +:class:`~pydifact.api.PluginMount` and can thus be +extended with plugins. diff --git a/docs/conf.py b/docs/conf.py index fd51d95..fc00475 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,6 @@ html_theme = "sphinx_rtd_theme" html_theme_options = { - "canonical_url": "https://pydifact.readthedocs.io/", # "logo_only": False, "display_version": True, "prev_next_buttons_location": "both", diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index 14f5a87..70d9a69 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -352,12 +352,12 @@ def validate(self): class Message(AbstractSegmentsContainer): """ - A message (started by UNH segment, ended by UNT segment) + A message (started by UNH_ segment, ended by UNT_ segment) Optional features of UNH are not yet supported. - https://www.stylusstudio.com/edifact/40100/UNH_.htm - https://www.stylusstudio.com/edifact/40100/UNT_.htm + .. _UNH: https://www.stylusstudio.com/edifact/40100/UNH_.htm + .. _UNT: https://www.stylusstudio.com/edifact/40100/UNT_.htm """ HEADER_TAG = "UNH" @@ -407,7 +407,7 @@ def validate(self): class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContainer): """ - An interchange (started by UNB segment, ended by UNZ segment) + An interchange (started by UNB_ segment, ended by UNZ_ segment) Optional features of UNB are not yet supported. @@ -417,8 +417,8 @@ class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContaine optional: interchange segments can be accessed without going through messages. - https://www.stylusstudio.com/edifact/40100/UNB_.htm - https://www.stylusstudio.com/edifact/40100/UNZ_.htm + .. _UNB: https://www.stylusstudio.com/edifact/40100/UNB_.htm + .. _UNZ: https://www.stylusstudio.com/edifact/40100/UNZ_.htm """ HEADER_TAG = "UNB" From c4228e54c26e530b8f0ead10dd2f058d083fe2fa Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Sat, 25 May 2024 18:20:49 +0300 Subject: [PATCH 2/6] Refactor the documentation of AbstractSegmentsContainer --- pydifact/segmentcollection.py | 122 ++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index 70d9a69..5957f5a 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -34,15 +34,30 @@ class AbstractSegmentsContainer: - """Represent a collection of EDI Segments for both reading and writing. + """Abstract base class of subclasses containing collection of segments. - You should not instantiate AbstractSegmentsContainer itself, but subclass it use that. + :class:`AbstractSegmentsContainer` is the superclass of several classes such as + :class:`RawSegmentCollection` and :class:`Interchange` and contains methods common + to them. - The segments list in AbstractSegmentsContainer includes header and footer segments too. - Inheriting envelopes must NOT include these elements in .segments, as get_header_element() and - get_footer_element() should provide these elements on-the-fly. + **Implementation detail:** Subclasses must set :attr:`HEADER_TAG` and + :attr:`FOOTER_TAG`. - Inheriting classes must set HEADER_TAG and FOOTER_TAG + :param extra_header_elements: A list of elements to be appended at the end + of the header segment (same format as :class:`~pydifact.segments.Segment` + constructor elements). + + :param characters: The set of control characters + + .. attribute:: segments + + The segments that comprise the container. This does not include the envelope + (that is, the header and footer) segments. To get the envolope segments, use + as :meth:`get_header_segment` and :meth:`get_footer_segment`. + + .. attribute:: characters + + The control characters (a :class:`~pydifact.control.Characters` object). """ HEADER_TAG: str = None @@ -53,13 +68,6 @@ def __init__( extra_header_elements: List[Union[str, List[str]]] = None, characters: Optional[Characters] = None, ): - """ - :param extra_header_elements: a list of elements to be appended at the end - of the header segment (same format as Segment() constructor *elements). - :param characters: the set of control characters - """ - - # The segments that make up this message self.segments = [] # set of control characters @@ -81,10 +89,9 @@ def from_str( ) -> "AbstractSegmentsContainer": """Create an instance from a string. - This method is intended for usage in inheriting classes, not it AbstractSegmentsContainer itself. - :param string: The EDI content - :param parser: A parser to convert the tokens to segments, defaults to `Parser` - :param characters: the set of control characters + :param string: The EDI content. + :param parser: A parser to convert the tokens to segments; defaults to `Parser`. + :param characters: The set of control characters. """ if parser is None: parser = Parser(characters=characters) @@ -99,11 +106,12 @@ def from_segments( segments: Union[List, Iterable], characters: Optional[Characters] = None, ) -> "AbstractSegmentsContainer": - """Create a new AbstractSegmentsContainer instance from a iterable list of segments. + """Create an instance from a list of segments. - :param segments: The segments of the EDI interchange - :param characters: the set of control characters + :param segments: The segments of the EDI interchange. :type segments: list/iterable of Segment + + :param characters: The set of control characters. """ # create a new instance of AbstractSegmentsContainer and return it @@ -115,11 +123,13 @@ def get_segments( name: str, predicate: Callable = None, # Python3.9+ Callable[[Segment], bool] ) -> list: - """Get all the segments that match the requested name. + """Get all segments that match the requested name. - :param name: The name of the segments to return - :param predicate: Optional predicate callable that returns True if the given segment matches a condition - :rtype: list of Segment + :param name: The name of the segments to return. + :param predicate: Optional callable that returns True if the given + segment matches a condition. + + :rtype: list of :class:`Segment` objects. """ for segment in self.segments: if segment.tag == name and (predicate is None or predicate(segment)): @@ -132,10 +142,11 @@ def get_segment( ) -> Optional[Segment]: """Get the first segment that matches the requested name. - :return: The requested segment, or None if not found - :param name: The name of the segment to return + :param name: The name of the segment to return. :param predicate: Optional predicate that must match on the segments - to return + to return. + + :return: The requested segment, or None if not found. """ for segment in self.get_segments(name, predicate): return segment @@ -146,17 +157,18 @@ def split_by( self, start_segment_tag: str, ) -> Iterable: # Python3.9+ Iterable["RawSegmentCollection"] - """Split a segment collection by tag. - - Everything before the first start segment is ignored, so if no matching - start segment is found at all, returned result is empty. + """Split the segment collection by tag. + Assuming the collection contains tags ``["A", "B", "A", "A", "B", "D"]``, + ``split_by("A")`` would return ``[["A", "B"], ["A"], ["A", "B", "D"]]``. + Everything before the first start segment is ignored, so if no matching start + segment is found at all, the returned result is empty. :param start_segment_tag: the segment tag we want to use as separator - :return: generator of segment collections. The start tag is included in - each yielded collection + :return: Generator of segment collections. The start tag is included in + each yielded collection. """ current_list = None @@ -176,13 +188,15 @@ def split_by( def add_segments( self, segments: Union[List[Segment], Iterable] ) -> "AbstractSegmentsContainer": - """Add multiple segments to the collection. Passing a UNA segment means setting/overriding the control - characters and setting the serializer to output the Service String Advice. If you wish to change the control - characters from the default and not output the Service String Advice, change self.characters instead, - without passing a UNA Segment. + """Append a list of segments to the collection. + + Passing a ``UNA`` segment means setting/overriding the control characters and + setting the serializer to output the Service String Advice. If you wish to + change the control characters from the default and not output the Service String + Advice, change :attr:`characters` instead, without passing a UNA Segment. - :param segments: The segments to add - :type segments: list or iterable of Segments + :param segments: The segments to add. + :type segments: List or iterable of :class:`~pydifact.segments.Segment` objects. """ for segment in segments: self.add_segment(segment) @@ -192,7 +206,9 @@ def add_segments( def add_segment(self, segment: Segment) -> "AbstractSegmentsContainer": """Append a segment to the collection. - Note: skips segments that are header or footer tags of this segment container type. + Note: skips segments that are header or footer tags of this segment container + type. + :param segment: The segment to add """ if not segment.tag in (self.HEADER_TAG, self.FOOTER_TAG): @@ -200,23 +216,20 @@ def add_segment(self, segment: Segment) -> "AbstractSegmentsContainer": return self def get_header_segment(self) -> Optional[Segment]: - """Craft and return this container header segment (if any) - - :returns: None if there is no header for that container + """Return the header segment or ``None`` if there is no header. """ return None def get_footer_segment(self) -> Optional[Segment]: - """Craft and return this container footer segment (if any) - - :returns: None if there is no footer for that container + """Return the footer segment or ``None`` if there is no footer. """ return None def serialize(self, break_lines: bool = False) -> str: - """Serialize all the segments added to this object. + """Return the string representation of the object. - :param break_lines: if True, insert line break after each segment terminator. + :param break_lines: If ``True``, inserts line break after each segment + terminator. """ header = self.get_header_segment() footer = self.get_footer_segment() @@ -235,18 +248,13 @@ def serialize(self, break_lines: bool = False) -> str: ) def validate(self): - """Validates this container. + """Validate the object. - This method must be overridden in implementing subclasses, and should make sure that - the container is implemented correctly. - - It does not return anything and should raise an Exception in case of errors. + Raises an exception if the object is invalid. """ raise NotImplementedError def __str__(self) -> str: - """Allow the object to be serialized by casting to a string.""" - return self.serialize() @@ -411,9 +419,9 @@ class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContaine Optional features of UNB are not yet supported. - Functional groups are not yet supported + Functional groups are not yet supported. - Messages are supported, see get_message() and get_message(), but are + Messages are supported (see :meth:`get_message`), but are optional: interchange segments can be accessed without going through messages. From 274ab0811ee80bca0d0877f558bbd0fcbba82758 Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Fri, 2 Aug 2024 13:42:55 +0300 Subject: [PATCH 3/6] Fix some AbstractSegmentsContainer stuff --- pydifact/segmentcollection.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index 5957f5a..e3aaeb6 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -126,8 +126,8 @@ def get_segments( """Get all segments that match the requested name. :param name: The name of the segments to return. - :param predicate: Optional callable that returns True if the given - segment matches a condition. + :param predicate: Optional callable that accepts a segment as argument. + Only segments for which the returned value is ``True'' are returned. :rtype: list of :class:`Segment` objects. """ @@ -190,10 +190,11 @@ def add_segments( ) -> "AbstractSegmentsContainer": """Append a list of segments to the collection. - Passing a ``UNA`` segment means setting/overriding the control characters and - setting the serializer to output the Service String Advice. If you wish to - change the control characters from the default and not output the Service String - Advice, change :attr:`characters` instead, without passing a UNA Segment. + For the :class:`Interchange` subclass, passing a ``UNA`` segment means + setting/overriding the control characters and setting the serializer to output + the Service String Advice. If you wish to change the control characters from the + default and not output the Service String Advice, change :attr:`characters` + instead, without passing a ``UNA`` Segment. :param segments: The segments to add. :type segments: List or iterable of :class:`~pydifact.segments.Segment` objects. From 37883220af0a1e3df455d9f53423cf156e1dd8eb Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Tue, 6 Aug 2024 17:14:58 +0300 Subject: [PATCH 4/6] Don't repeat documentation for inherited methods --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index fc00475..2983956 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc", "sphinx_rtd_theme"] +autodoc_inherit_docstrings = False # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] From c933051b743f2d5932880d2764da7e4e9a21428a Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Tue, 6 Aug 2024 17:16:30 +0300 Subject: [PATCH 5/6] Fix wrong documentation of get_header_segment() and get_footer_segment() --- pydifact/segmentcollection.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index e3aaeb6..7270409 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -217,12 +217,23 @@ def add_segment(self, segment: Segment) -> "AbstractSegmentsContainer": return self def get_header_segment(self) -> Optional[Segment]: - """Return the header segment or ``None`` if there is no header. + """Craft and return a header segment. + + :meth:`get_header_segment` creates and returns an appropriate + :class:`~pydifact.segments.Segment` object that can serve as a header of the + current object. This is useful, for example, when serializing the current object. + + Although the current object may have been created by reading a string (e.g. + with :meth:`from_str`), :meth:`get_header_segment` does not return the header + segment that was read by the string; that segment would have been useful only + during reading and it is the job of :meth:`from_str` to check it. """ return None def get_footer_segment(self) -> Optional[Segment]: - """Return the footer segment or ``None`` if there is no footer. + """Craft and return a footer segment. + + This is similar to :meth:`get_header_segment`, but for the footer segment. """ return None From 954f2a744861215c3993b89f87c33ee69b392032 Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Tue, 6 Aug 2024 17:16:59 +0300 Subject: [PATCH 6/6] Refactor the documentation of Interchange --- pydifact/segmentcollection.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pydifact/segmentcollection.py b/pydifact/segmentcollection.py index 7270409..cd805d7 100644 --- a/pydifact/segmentcollection.py +++ b/pydifact/segmentcollection.py @@ -426,16 +426,17 @@ def validate(self): class Interchange(FileSourcableMixin, UNAHandlingMixin, AbstractSegmentsContainer): - """ - An interchange (started by UNB_ segment, ended by UNZ_ segment) + """An EDIFACT interchange. - Optional features of UNB are not yet supported. + In EDIFACT, the **interchange** is the entire document at the highest level. Except + for its header (a UNB_ segment) and footer (a UNZ_ segment), it consists of one or + more **messages**. - Functional groups are not yet supported. + :class:`Interchange` currently does not support functional groups and optional + features of UNB. - Messages are supported (see :meth:`get_message`), but are - optional: interchange segments can be accessed without going through - messages. + :class:`Interchange` supports all methods of :class:`AbstractSegmentsContainer` plus + some additional methods. .. _UNB: https://www.stylusstudio.com/edifact/40100/UNB_.htm .. _UNZ: https://www.stylusstudio.com/edifact/40100/UNZ_.htm @@ -475,11 +476,6 @@ def get_header_segment(self) -> Segment: ) def get_footer_segment(self) -> Segment: - """:returns a (UNZ) footer segment with correct segment count and control reference. - - It counts either of the number of messages or, if used, of the number of functional groups - in an interchange (TODO).""" - # FIXME: count functional groups (UNG/UNE) correctly cnt = 0 for segment in self.segments: @@ -495,11 +491,12 @@ def get_footer_segment(self) -> Segment: ) def get_messages(self) -> List[Message]: - """parses a list of messages out of the internal segments. + """Get list of messages in the interchange. - :raises EDISyntaxError if constraints are not met (e.g. UNH/UNT both correct) + Using :meth:`get_messages` is optional; interchange segments can be accessed + directly without going through messages. - TODO: parts of this here are better done in the validate() method + :raises: :class:`EDISyntaxError` if the interchange contents are not correct. """ message = None @@ -531,6 +528,7 @@ def get_messages(self) -> List[Message]: raise EDISyntaxError("UNH segment was not closed with a UNT segment.") def add_message(self, message: Message) -> "Interchange": + """Append a message to the interchange.""" segments = ( [message.get_header_segment()] + message.segments