Skip to content

Commit

Permalink
Replace number stringification hack with custom YAML loader (#4183)
Browse files Browse the repository at this point in the history
* Custom yaml Loader

Instead of monkeypatching the yaml Loader in an attempt to avoid parsing numbers make a custom Loader where the float/int tags are entirely removed.

* Test that all int/floats are parsed as str
  • Loading branch information
kenodegard authored Jul 24, 2023
1 parent 02acf15 commit 25c695e
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 24 deletions.
55 changes: 32 additions & 23 deletions conda_build/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations

import contextlib
import copy
import hashlib
import json
Expand Down Expand Up @@ -41,9 +40,37 @@
)

try:
loader = yaml.CLoader
except:
loader = yaml.Loader
Loader = yaml.CLoader
except AttributeError:
Loader = yaml.Loader


class StringifyNumbersLoader(Loader):
@classmethod
def remove_implicit_resolver(cls, tag):
if "yaml_implicit_resolvers" not in cls.__dict__:
cls.yaml_implicit_resolvers = {
k: v[:] for k, v in cls.yaml_implicit_resolvers.items()
}
for ch in tuple(cls.yaml_implicit_resolvers):
resolvers = [(t, r) for t, r in cls.yaml_implicit_resolvers[ch] if t != tag]
if resolvers:
cls.yaml_implicit_resolvers[ch] = resolvers
else:
del cls.yaml_implicit_resolvers[ch]

@classmethod
def remove_constructor(cls, tag):
if "yaml_constructors" not in cls.__dict__:
cls.yaml_constructors = cls.yaml_constructors.copy()
if tag in cls.yaml_constructors:
del cls.yaml_constructors[tag]


StringifyNumbersLoader.remove_implicit_resolver("tag:yaml.org,2002:float")
StringifyNumbersLoader.remove_implicit_resolver("tag:yaml.org,2002:int")
StringifyNumbersLoader.remove_constructor("tag:yaml.org,2002:float")
StringifyNumbersLoader.remove_constructor("tag:yaml.org,2002:int")

on_win = sys.platform == "win32"

Expand Down Expand Up @@ -261,9 +288,7 @@ def select_lines(data, namespace, variants_in_place):

def yamlize(data):
try:
with stringify_numbers():
loaded_data = yaml.load(data, Loader=loader)
return loaded_data
return yaml.load(data, Loader=StringifyNumbersLoader)
except yaml.error.YAMLError as e:
if "{{" in data:
try:
Expand Down Expand Up @@ -1056,23 +1081,7 @@ def _hash_dependencies(hashing_dependencies, hash_length):
return f"h{hash_.hexdigest()}"[: hash_length + 1]


@contextlib.contextmanager
def stringify_numbers():
# ensure that numbers are not interpreted as ints or floats. That trips up versions
# with trailing zeros.
implicit_resolver_backup = loader.yaml_implicit_resolvers.copy()
for ch in list("0123456789"):
if ch in loader.yaml_implicit_resolvers:
del loader.yaml_implicit_resolvers[ch]
yield
for ch in list("0123456789"):
if ch in implicit_resolver_backup:
loader.yaml_implicit_resolvers[ch] = implicit_resolver_backup[ch]


class MetaData:
__hash__ = None # declare as non-hashable to avoid its use with memoization

def __init__(self, path, config=None, variant=None):
self.undefined_jinja_vars = []
self.config = get_or_merge_config(config, variant=variant)
Expand Down
65 changes: 64 additions & 1 deletion tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest

from conda_build import api
from conda_build.metadata import MetaData, _hash_dependencies, select_lines
from conda_build.metadata import MetaData, _hash_dependencies, select_lines, yamlize
from conda_build.utils import DEFAULT_SUBDIRS

from .utils import metadata_dir, thisdir
Expand Down Expand Up @@ -260,3 +260,66 @@ def test_config_member_decoupling(testing_metadata):
b = testing_metadata.copy()
b.config.some_member = "123"
assert b.config.some_member != testing_metadata.config.some_member


# ensure that numbers are not interpreted as ints or floats, doing so trips up versions
# with trailing zeros
def test_yamlize_zero():
yml = yamlize(
"""
- 0
- 0.
- 0.0
- .0
"""
)

assert yml == ["0", "0.", "0.0", ".0"]


def test_yamlize_positive():
yml = yamlize(
"""
- +1
- +1.
- +1.2
- +.2
"""
)

assert yml == ["+1", "+1.", "+1.2", "+.2"]


def test_yamlize_negative():
yml = yamlize(
"""
- -1
- -1.
- -1.2
- -.2
"""
)

assert yml == ["-1", "-1.", "-1.2", "-.2"]


def test_yamlize_numbers():
yml = yamlize(
"""
- 1
- 1.2
"""
)

assert yml == ["1", "1.2"]


def test_yamlize_versions():
yml = yamlize(
"""
- 1.2.3
- 1.2.3.4
"""
)

assert yml == ["1.2.3", "1.2.3.4"]

0 comments on commit 25c695e

Please sign in to comment.