diff --git a/CHANGES b/CHANGES index 614d8708..8aa624af 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,9 @@ +2.3.7: +Bug Fixes: +* Setting negative floats could cause the leading "-" symbol to be replaced + with an unexpcted "0" when specifying a float format, or crash when using + the default format. + 2.3.6: Bug Fixes: * When using yaml-set with --format=folded and --eyamlcrypt, the encrypted diff --git a/setup.py b/setup.py index 0fb557c7..3a6ed317 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="yamlpath", - version="2.3.6", + version="2.3.7", description="Read and change YAML/Compatible data using powerful, intuitive, command-line friendly syntax", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_commands_eyaml_rotate_keys.py b/tests/test_commands_eyaml_rotate_keys.py index a926c0d4..372c1f29 100644 --- a/tests/test_commands_eyaml_rotate_keys.py +++ b/tests/test_commands_eyaml_rotate_keys.py @@ -192,7 +192,7 @@ def test_bad_recryption_key(self, script_runner, tmp_path_factory, old_eyaml_key yaml_file ) assert not result.success, result.stderr - assert "unable to encrypt" in result.stderr + assert "unable to encrypt" in result.stderr or "cannot be run due to exit code: 1" in result.stderr def test_backup_file(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): import os diff --git a/tests/test_processor.py b/tests/test_processor.py index e637a78b..7f377727 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -160,6 +160,10 @@ def test_set_value_in_none_data(self, capsys, quiet_logger): ("aliases[&testAnchor]", "Updated Value", 1, True, YAMLValueFormats.DEFAULT, PathSeperators.AUTO), (YAMLPath("top_scalar"), "New top-level value", 1, False, YAMLValueFormats.DEFAULT, PathSeperators.DOT), ("/top_array/2", 42, 1, False, YAMLValueFormats.INT, PathSeperators.FSLASH), + ("/top_hash/positive_float", 0.009, 1, True, YAMLValueFormats.FLOAT, PathSeperators.FSLASH), + ("/top_hash/negative_float", -0.009, 1, True, YAMLValueFormats.FLOAT, PathSeperators.FSLASH), + ("/top_hash/positive_float", -2.71828, 1, True, YAMLValueFormats.FLOAT, PathSeperators.FSLASH), + ("/top_hash/negative_float", 5283.4, 1, True, YAMLValueFormats.FLOAT, PathSeperators.FSLASH), ]) def test_set_value(self, quiet_logger, yamlpath, value, tally, mustexist, vformat, pathsep): yamldata = """--- @@ -172,6 +176,9 @@ def test_set_value(self, quiet_logger, yamlpath, value, tally, mustexist, vforma - 2 # Comment N top_scalar: Top-level plain scalar string + top_hash: + positive_float: 3.14159265358 + negative_float: -11.034 """ yaml = YAML() data = yaml.load(yamldata) diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index c972a291..64db28ec 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -31,7 +31,7 @@ from yamlpath.wrappers import ConsolePrinter # Implied Constants -MY_VERSION = "1.0.7" +MY_VERSION = "1.0.8" def processcli(): """Process command-line arguments.""" diff --git a/yamlpath/exceptions/yamlpathexception.py b/yamlpath/exceptions/yamlpathexception.py index beb37634..762d0b81 100644 --- a/yamlpath/exceptions/yamlpathexception.py +++ b/yamlpath/exceptions/yamlpathexception.py @@ -34,7 +34,7 @@ def __init__(self, user_message: str, yaml_path: str, self.yaml_path: str = yaml_path self.segment: Optional[str] = segment - super(YAMLPathException, self).__init__( + super().__init__( "user_message: {}, yaml_path: {}, segment: {}" .format(user_message, yaml_path, segment)) diff --git a/yamlpath/eyaml/eyamlprocessor.py b/yamlpath/eyaml/eyamlprocessor.py index 2b7bc617..826bd259 100644 --- a/yamlpath/eyaml/eyamlprocessor.py +++ b/yamlpath/eyaml/eyamlprocessor.py @@ -161,7 +161,7 @@ def decrypt_eyaml(self, value: str) -> str: raise EYAMLCommandException( "The {} command cannot be run due to exit code: {}" .format(self.eyaml, ex.returncode) - ) + ) from ex # Check for bad decryptions self.logger.debug( @@ -227,9 +227,13 @@ def encrypt_eyaml(self, value: str, raise EYAMLCommandException( "The {} command cannot be run due to exit code: {}" .format(self.eyaml, ex.returncode) - ) + ) from ex - if not retval: + # While exceedingly rare and difficult to test for, it is possible + # for custom eyaml commands to produce no output. This is a critical + # error in every conceivable case but pycov will never get a test + # that works multi-platform. So, ignore covering this case. + if not retval: # pragma: no cover raise EYAMLCommandException( ("The {} command was unable to encrypt your value. Please" + " verify this process can run that command and read your" diff --git a/yamlpath/func.py b/yamlpath/func.py index 1f987dfc..f633a872 100644 --- a/yamlpath/func.py +++ b/yamlpath/func.py @@ -230,7 +230,7 @@ def wrap_type(value: Any) -> Any: elif typ is int: wrapped_value = ScalarInt(value) elif typ is float: - wrapped_value = ScalarFloat(value) + wrapped_value = make_float_node(ast_value) elif typ is bool: wrapped_value = ScalarBoolean(value) @@ -264,7 +264,45 @@ def clone_node(node: Any) -> Any: return type(node)(clone_value, anchor=node.anchor.value) return type(node)(clone_value) -# pylint: disable=locally-disabled,too-many-branches,too-many-statements +def make_float_node(value: float, anchor: str = None): + """ + Create a new ScalarFloat data node from a bare float. + + An optional anchor may be attached. + + Parameters: + 1. value (float) The bare float to wrap. + 2. anchor (str) OPTIONAL anchor to add. + + Returns: (ScalarNode) The new node + """ + minus_sign = "-" if value < 0.0 else None + strval = format(value, '.15f').rstrip('0').rstrip('.') + precision = 0 + width = len(strval) + lastdot = strval.rfind(".") + if -1 < lastdot: + precision = strval.rfind(".") + + if anchor is None: + new_node = ScalarFloat( + value, + m_sign=minus_sign, + prec=precision, + width=width + ) + else: + new_node = ScalarFloat( + value + , anchor=anchor + , m_sign=minus_sign + , prec=precision + , width=width + ) + + return new_node + +# pylint: disable=locally-disabled,too-many-branches,too-many-statements,too-many-locals def make_new_node(source_node: Any, value: Any, value_format: YAMLValueFormats) -> Any: """ @@ -297,14 +335,14 @@ def make_new_node(source_node: Any, value: Any, strform = str(value_format) try: valform = YAMLValueFormats.from_str(strform) - except NameError: + except NameError as wrap_ex: raise NameError( "Unknown YAML Value Format: {}".format(strform) + ". Please specify one of: " + ", ".join( [l.lower() for l in YAMLValueFormats.get_names()] ) - ) + ) from wrap_ex if valform == YAMLValueFormats.BARE: new_type = PlainScalarString @@ -330,43 +368,35 @@ def make_new_node(source_node: Any, value: Any, elif valform == YAMLValueFormats.FLOAT: try: new_value = float(value) - except ValueError: + except ValueError as wrap_ex: raise ValueError( ("The requested value format is {}, but '{}' cannot be" + " cast to a floating-point number.") .format(valform, value) - ) - - strval = str(value) - precision = 0 - width = len(strval) - lastdot = strval.rfind(".") - if -1 < lastdot: - precision = strval.rfind(".") + ) from wrap_ex + anchor_val = None if hasattr(source_node, "anchor"): - new_node = ScalarFloat( - new_value - , anchor=source_node.anchor.value - , prec=precision - , width=width - ) - else: - new_node = ScalarFloat(new_value, prec=precision, width=width) + anchor_val = source_node.anchor.value + new_node = make_float_node(new_value, anchor_val) elif valform == YAMLValueFormats.INT: new_type = ScalarInt try: new_value = int(value) - except ValueError: + except ValueError as wrap_ex: raise ValueError( ("The requested value format is {}, but '{}' cannot be" + " cast to an integer number.") .format(valform, value) - ) + ) from wrap_ex else: # Punt to whatever the best type may be - new_type = type(wrap_type(value)) + wrapped_value = wrap_type(value) + new_type = type(wrapped_value) + new_format = YAMLValueFormats.from_node(wrapped_value) + if new_format is not YAMLValueFormats.DEFAULT: + new_node = make_new_node(source_node, value, new_format) if new_node is None: if hasattr(source_node, "anchor"): diff --git a/yamlpath/processor.py b/yamlpath/processor.py index 046e0a04..0d17ff3c 100644 --- a/yamlpath/processor.py +++ b/yamlpath/processor.py @@ -170,8 +170,9 @@ def set_value(self, yaml_path: Union[YAMLPath, str], self.data, yaml_path, value ): self.logger.debug( - "Processor::set_value: Matched optional node coord, {}." - .format(node_coord) + ("Processor::set_value: Matched optional node coord, {};" + + " setting its value to {}<{}>.") + .format(node_coord, value, value_format) ) self._update_node( node_coord.parent, node_coord.parentref, value, @@ -333,13 +334,13 @@ def _get_nodes_by_index( try: intmin: int = int(min_match) intmax: int = int(max_match) - except ValueError: + except ValueError as wrap_ex: raise YAMLPathException( "{} is not an integer array slice" .format(str_stripped), str(yaml_path), str(unstripped_attrs) - ) + ) from wrap_ex if intmin == intmax and len(data) > intmin: yield NodeCoords([data[intmin]], data, intmin) @@ -353,13 +354,13 @@ def _get_nodes_by_index( else: try: idx: int = int(str_stripped) - except ValueError: + except ValueError as wrap_ex: raise YAMLPathException( "{} is not an integer array index" .format(str_stripped), str(yaml_path), str(unstripped_attrs) - ) + ) from wrap_ex if isinstance(data, list) and len(data) > idx: yield NodeCoords(data[idx], data, idx) @@ -762,14 +763,14 @@ def _get_optional_nodes( else: try: newidx = int(str(stripped_attrs)) - except ValueError: + except ValueError as wrap_ex: raise YAMLPathException( ("Cannot add non-integer {} subreference" + " to lists") .format(str(segment_type)), str(yaml_path), except_segment - ) + ) from wrap_ex for _ in range(len(data) - 1, newidx): next_node = build_next_node( yaml_path, depth + 1, value @@ -884,4 +885,18 @@ def recurse(data, parent, parentref, reference_node, replacement_node): change_node = parent[parentref] new_node = make_new_node(change_node, value, value_format) + + self.logger.debug( + ("Processor::_update_node: Changing the following node of" + + " type {} to {}<{}> as {}, a {} YAML element:" + ).format(type(change_node), value, value_format, + new_node, type(new_node)) + ) + self.logger.debug(change_node) + recurse(self.data, parent, parentref, change_node, new_node) + + self.logger.debug( + "Processor::_update_node: Parent after change:" + ) + self.logger.debug(parent) diff --git a/yamlpath/yamlpath.py b/yamlpath/yamlpath.py index d512a79b..0cca97a9 100644 --- a/yamlpath/yamlpath.py +++ b/yamlpath/yamlpath.py @@ -547,12 +547,12 @@ def _parse_path(self, ): try: idx = int(segment_id) - except ValueError: + except ValueError as wrap_ex: raise YAMLPathException( "Not an integer index: {}".format(segment_id) , yaml_path , segment_id - ) + ) from wrap_ex path_segments.append((segment_type, idx)) elif ( segment_type is PathSegmentTypes.SEARCH