Skip to content

Commit

Permalink
Cleanup dependencies and fix OpenCyphal#94 (OpenCyphal#95)
Browse files Browse the repository at this point in the history
Upgrade Parsimonious to
`v0.10.0+39b3d71ee827ba6a19a7226fddfe6b6e51535794`; fix
OpenCyphal#92. Compare the bundled
version of Parsimonious against its latest release, v0.10.0:
erikrose/parsimonious@0.10.0...39b3d71

Remove dependency on `six.py`

Fix OpenCyphal#94 and add a new error
class -- `DataTypeCollisionError`; remove
`MultipleDefinitionsUnderSameVersionError`. This does not affect the API
compatibility because the specific error classes are private.

Bump the minor version number.
  • Loading branch information
pavel-kirienko authored May 1, 2023
1 parent 7f0270c commit f564e42
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 1,259 deletions.
23 changes: 0 additions & 23 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Six
^^^

Copyright (c) 2010-2020 Benjamin Peterson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 0 additions & 1 deletion docs/pages/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ The library is bundled with the following third-party software libraries
(by virtue of being bundled, they are not listed as dependencies and need not be installed by the user):

* `Parsimonious <https://github.com/erikrose/parsimonious>`_ by Erik Rose, MIT license.
* `Six <https://github.com/benjaminp/six>`_ by Benjamin Peterson, MIT license; needed for Parsimonious.

Please refer to the projects' homepages for more information, including the legal information on licensing.
2 changes: 1 addition & 1 deletion pydsdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys as _sys
from pathlib import Path as _Path

__version__ = "1.19.0"
__version__ = "1.20.0"
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
__license__ = "MIT"
__author__ = "OpenCyphal"
Expand Down
37 changes: 17 additions & 20 deletions pydsdl/_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ class RootNamespaceNameCollisionError(_error.InvalidDefinitionError):
"""


class DataTypeNameCollisionError(_error.InvalidDefinitionError):
class DataTypeCollisionError(_error.InvalidDefinitionError):
"""
Raised when there are conflicting data type definitions.
"""


class DataTypeNameCollisionError(DataTypeCollisionError):
"""
Raised when there are conflicting data type names.
"""
Expand All @@ -39,16 +45,6 @@ class FixedPortIDCollisionError(_error.InvalidDefinitionError):
"""


class MultipleDefinitionsUnderSameVersionError(_error.InvalidDefinitionError):
"""
For example::
Type.1.0.dsdl
2800.Type.1.0.dsdl
2801.Type.1.0.dsdl
"""


class VersionsOfDifferentKindError(_error.InvalidDefinitionError):
"""
Definitions that share the same name but are of different kinds.
Expand Down Expand Up @@ -166,7 +162,7 @@ def read_namespace(
lookup_dsdl_definitions += _construct_dsdl_definitions_from_namespace(ld)

# Check for collisions against the lookup definitions also.
_ensure_no_name_collisions(target_dsdl_definitions, lookup_dsdl_definitions)
_ensure_no_collisions(target_dsdl_definitions, lookup_dsdl_definitions)

_logger.debug("Lookup DSDL definitions are listed below:")
for x in lookup_dsdl_definitions:
Expand Down Expand Up @@ -245,7 +241,7 @@ def handler(line_number: int, text: str) -> None:
return types


def _ensure_no_name_collisions(
def _ensure_no_collisions(
target_definitions: List[_dsdl_definition.DSDLDefinition],
lookup_definitions: List[_dsdl_definition.DSDLDefinition],
) -> None:
Expand Down Expand Up @@ -275,6 +271,12 @@ def _ensure_no_name_collisions(
raise DataTypeNameCollisionError(
"This type conflicts with the namespace of %s" % lu.file_path, path=tg.file_path
)
if (
tg_full_name_period == lu_full_name_period
and tg.version == lu.version
and not tg.file_path.samefile(lu.file_path)
): # https://github.com/OpenCyphal/pydsdl/issues/94
raise DataTypeCollisionError("This type is redefined in %s" % lu.file_path, path=tg.file_path)


def _ensure_no_fixed_port_id_collisions(types: List[_serializable.CompositeType]) -> None:
Expand Down Expand Up @@ -320,14 +322,9 @@ def _ensure_minor_version_compatibility_pairwise(
a: _serializable.CompositeType, b: _serializable.CompositeType
) -> None:
assert a is not b
assert a.version.major == b.version.major
assert a.full_name == b.full_name

# Version collision
if a.version.minor == b.version.minor:
raise MultipleDefinitionsUnderSameVersionError(
"This definition shares its version number with %s" % b.source_file_path, path=a.source_file_path
)
assert a.version.major == b.version.major
assert a.version.minor != b.version.minor # This is the whole point of this function.

# Must be of the same kind: both messages or both services
if isinstance(a, _serializable.ServiceType) != isinstance(b, _serializable.ServiceType):
Expand Down
23 changes: 22 additions & 1 deletion pydsdl/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,7 @@ def _unittest_parse_namespace_versioning(wrkspc: Workspace) -> None:
),
)

with raises(_namespace.MultipleDefinitionsUnderSameVersionError):
with raises(_namespace.DataTypeCollisionError):
_namespace.read_namespace((wrkspc.directory / "ns"), [])

wrkspc.drop("ns/Spartans.30.2.dsdl")
Expand Down Expand Up @@ -1604,6 +1604,27 @@ def _unittest_parse_namespace_versioning(wrkspc: Workspace) -> None:
wrkspc.drop("ns/Consistency*")


def _unittest_issue94(wrkspc: Workspace) -> None:
from pytest import raises

wrkspc.new("outer_a/ns/Foo.1.0.dsdl", "@sealed")
wrkspc.new("outer_b/ns/Foo.1.0.dsdl", "@sealed") # Conflict!
wrkspc.new("outer_a/ns/Bar.1.0.dsdl", "Foo.1.0 fo\n@sealed") # Which Foo.1.0?

with raises(_namespace.DataTypeCollisionError):
_namespace.read_namespace(
wrkspc.directory / "outer_a" / "ns",
[wrkspc.directory / "outer_b" / "ns"],
)

wrkspc.drop("outer_b/ns/Foo.1.0.dsdl") # Clear the conflict.
defs = _namespace.read_namespace(
wrkspc.directory / "outer_a" / "ns",
[wrkspc.directory / "outer_b" / "ns"],
)
assert len(defs) == 2


def _unittest_parse_namespace_faults() -> None:
from pytest import raises

Expand Down
49 changes: 34 additions & 15 deletions pydsdl/third_party/parsimonious/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from six import text_type, python_2_unicode_compatible
from textwrap import dedent

from parsimonious.utils import StrAndRepr


@python_2_unicode_compatible
class ParseError(StrAndRepr, Exception):
class ParsimoniousError(Exception):
"""A base exception class to allow library users to catch any Parsimonious error."""
pass


class ParseError(StrAndRepr, ParsimoniousError):
"""A call to ``Expression.parse()`` or ``match()`` didn't match."""

def __init__(self, text, pos=-1, expr=None):
Expand All @@ -16,9 +20,9 @@ def __init__(self, text, pos=-1, expr=None):
self.expr = expr

def __str__(self):
rule_name = ((u"'%s'" % self.expr.name) if self.expr.name else
text_type(self.expr))
return u"Rule %s didn't match at '%s' (line %s, column %s)." % (
rule_name = (("'%s'" % self.expr.name) if self.expr.name else
str(self.expr))
return "Rule %s didn't match at '%s' (line %s, column %s)." % (
rule_name,
self.text[self.pos:self.pos + 20],
self.line(),
Expand All @@ -32,31 +36,47 @@ def line(self):
match."""
# This is a method rather than a property in case we ever wanted to
# pass in which line endings we want to use.
return self.text.count('\n', 0, self.pos) + 1
if isinstance(self.text, list): # TokenGrammar
return None
else:
return self.text.count('\n', 0, self.pos) + 1

def column(self):
"""Return the 1-based column where the expression ceased to match."""
# We choose 1-based because that's what Python does with SyntaxErrors.
try:
return self.pos - self.text.rindex('\n', 0, self.pos)
except ValueError:
except (ValueError, AttributeError):
return self.pos + 1


@python_2_unicode_compatible
class LeftRecursionError(ParseError):
def __str__(self):
rule_name = self.expr.name if self.expr.name else str(self.expr)
window = self.text[self.pos:self.pos + 20]
return dedent(f"""
Left recursion in rule {rule_name!r} at {window!r} (line {self.line()}, column {self.column()}).
Parsimonious is a packrat parser, so it can't handle left recursion.
See https://en.wikipedia.org/wiki/Parsing_expression_grammar#Indirect_left_recursion
for how to rewrite your grammar into a rule that does not use left-recursion.
"""
).strip()


class IncompleteParseError(ParseError):
"""A call to ``parse()`` matched a whole Expression but did not consume the
entire text."""

def __str__(self):
return u"Rule '%s' matched in its entirety, but it didn't consume all the text. The non-matching portion of the text begins with '%s' (line %s, column %s)." % (
return "Rule '%s' matched in its entirety, but it didn't consume all the text. The non-matching portion of the text begins with '%s' (line %s, column %s)." % (
self.expr.name,
self.text[self.pos:self.pos + 20],
self.line(),
self.column())


class VisitationError(Exception):
class VisitationError(ParsimoniousError):
"""Something went wrong while traversing a parse tree.
This exception exists to augment an underlying exception with information
Expand All @@ -76,7 +96,7 @@ def __init__(self, exc, exc_class, node):
"""
self.original_class = exc_class
super(VisitationError, self).__init__(
super().__init__(
'%s: %s\n\n'
'Parse tree:\n'
'%s' %
Expand All @@ -85,7 +105,7 @@ def __init__(self, exc, exc_class, node):
node.prettily(error=node)))


class BadGrammar(StrAndRepr, Exception):
class BadGrammar(StrAndRepr, ParsimoniousError):
"""Something was wrong with the definition of a grammar.
Note that a ParseError might be raised instead if the error is in the
Expand All @@ -94,7 +114,6 @@ class BadGrammar(StrAndRepr, Exception):
"""


@python_2_unicode_compatible
class UndefinedLabel(BadGrammar):
"""A rule referenced in a grammar was never defined.
Expand All @@ -106,4 +125,4 @@ def __init__(self, label):
self.label = label

def __str__(self):
return u'The label "%s" was never defined.' % self.label
return 'The label "%s" was never defined.' % self.label
Loading

0 comments on commit f564e42

Please sign in to comment.