Skip to content

Commit

Permalink
cue: add Context and Context.compile
Browse files Browse the repository at this point in the history
Add `Context` class representing a CUE context along with `compile`
function from Python str or bytes to CUE Value.

Expand definition of `Value` to take both a `Context` and the
resource from `libcue`.

Make both `Context` and `Value` release Go resources when deallocated.

Test `compile`. Since at the moment we can't inspect CUE value,
this reduces to testing that compilation is either successful in
producing a CUE value (as a Python `Value`, or errors out with an
expected message.

Fixes #3071.
Fixes #3072.
Updates #3100.
Updates #3073.
Updates #3074.

Signed-off-by: Aram Hăvărneanu <[email protected]>
Change-Id: Ib98c06aca3bcceb8dc5d3f42a3acae289bc324c0
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue-py/+/1194797
TryBot-Result: CUEcueckoo <[email protected]>
Reviewed-by: Daniel Martí <[email protected]>
  • Loading branch information
4ad committed Jun 3, 2024
1 parent f18cc7e commit e5cbfc2
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 0 deletions.
19 changes: 19 additions & 0 deletions cue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""
cue: Python bindings to CUE
CUE is an open source data constraint language which aims to simplify
tasks involving defining and using data.
This package provide programmatic access to CUE from Python. The
Python API roughly follows the Go API documented at
https://pkg.go.dev/cuelang.org/go/cue, but uses Python idioms when
appropiate.
A Value represents a CUE value. Values are created from a Context.
Multiple values involved in some operation must use the same Context.
For more information about the CUE language see https://cuelang.org.
"""

from .build import (
BuildOption,
FileName,
ImportPath,
InferBuiltins,
Scope,
)
from .context import Context
from .error import Error
from .eval import (
All,
Expand All @@ -43,6 +61,7 @@
'Attributes',
'BuildOption',
'Concrete',
'Context',
'Definitions',
'DisallowCycles',
'Docs',
Expand Down
46 changes: 46 additions & 0 deletions cue/compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2024 The CUE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Compile CUE code.
"""

from cue.build import BuildOption, encode_build_opts
from cue.value import Value
from cue.error import Error
import libcue

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cue.context import Context

def compile(ctx: 'Context', s: str, *opts: BuildOption) -> Value:
val_ptr = libcue.ffi.new("cue_value*")
buf = libcue.ffi.new("char[]", s.encode("utf-8"))

build_opts = encode_build_opts(*opts)
err = libcue.compile_string(ctx._ctx, buf, build_opts, val_ptr)
if err != 0:
raise Error(err)
return Value(ctx, val_ptr[0])

def compile_bytes(ctx: 'Context', buf: bytes, *opts: BuildOption) -> Value:
val_ptr = libcue.ffi.new("cue_value*")
buf_ptr = libcue.ffi.from_buffer(buf)

build_opts = encode_build_opts(*opts)
err = libcue.compile_bytes(ctx._ctx, buf_ptr, len(buf), build_opts, val_ptr)
if err != 0:
raise Error(err)
return Value(ctx, val_ptr[0])
73 changes: 73 additions & 0 deletions cue/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2024 The CUE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Create CUE values.
"""

from functools import singledispatchmethod
from typing import final
from cue.value import Value
from cue.build import BuildOption
from cue.compile import compile, compile_bytes
import libcue

@final
class Context:
"""
Create new CUE values.
Any operation that involves two values should originate from
the same Context.
Corresponding Go functionality is documented at:
https://pkg.go.dev/cuelang.org/go/cue#Context
"""

_ctx: int

def __init__(self):
self._ctx = libcue.newctx()

def __del__(self):
libcue.free(self._ctx)

@singledispatchmethod
def compile(self, s, *opts: BuildOption) -> Value:
"""
Compile CUE code returning corresponding Value.
Corresponding Go functionality is documented at:
https://pkg.go.dev/cuelang.org/go/cue#Context.CompileString and
https://pkg.go.dev/cuelang.org/go/cue#Context.CompileBytes
Args:
s: CUE code to compile, can be or type str or bytes.
*opts: build options to use.
Returns:
Value: the CUE value corresponding to s.
Raises:
Error: if any error occurred.
"""
raise NotImplementedError

@compile.register
def _(self, s: str, *opts: BuildOption) -> Value:
return compile(self, s, *opts)

@compile.register
def _(self, b: bytes, *opts: BuildOption) -> Value:
return compile_bytes(self, b, *opts)
33 changes: 33 additions & 0 deletions cue/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,38 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Perform operations on CUE values.
"""

from typing import final
import libcue

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cue.context import Context

@final
class Value:
"""
A CUE value.
Value holds any value that can be encoded by CUE.
Corresponding Go functionality is documented at:
https://pkg.go.dev/cuelang.org/go/cue#Value.
"""

_ctx: 'Context'
_val: int

def __init__(self, ctx: 'Context', v: int):
self._ctx = ctx
self._val = v

def __del__(self):
libcue.free(self._val)

def context(self) -> 'Context':
"""The Context that created this Value."""
return self._ctx
17 changes: 17 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2024 The CUE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
CUE Python tests.
"""
140 changes: 140 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Copyright 2024 The CUE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
cue.Context tests.
"""

import pytest
import cue

def test_compile_empty():
ctx = cue.Context()

val = ctx.compile("")
assert isinstance(val, cue.Value)

val = ctx.compile(b"")
assert isinstance(val, cue.Value)

def test_compile_empty_with_options():
ctx = cue.Context()

val = ctx.compile("", cue.FileName("empty.cue"), cue.ImportPath("example.com/foo/bar"))
assert isinstance(val, cue.Value)

val = ctx.compile(b"", cue.FileName("empty.cue"), cue.ImportPath("example.com/foo/bar"))
assert isinstance(val, cue.Value)

def test_compile():
ctx = cue.Context()

val = ctx.compile("true")
assert isinstance(val, cue.Value)

val = ctx.compile("42")
assert isinstance(val, cue.Value)

val = ctx.compile("1.2345")
assert isinstance(val, cue.Value)

val = ctx.compile('"hello"')
assert isinstance(val, cue.Value)

val = ctx.compile("'world'")
assert isinstance(val, cue.Value)

val = ctx.compile("int")
assert isinstance(val, cue.Value)

val = ctx.compile("{}")
assert isinstance(val, cue.Value)

val = ctx.compile("x: 42")
assert isinstance(val, cue.Value)

val = ctx.compile("x: y: z: true")
assert isinstance(val, cue.Value)

def test_compile_bytes():
ctx = cue.Context()

val = ctx.compile(b"true")
assert isinstance(val, cue.Value)

val = ctx.compile(b"42")
assert isinstance(val, cue.Value)

val = ctx.compile(b"1.2345")
assert isinstance(val, cue.Value)

val = ctx.compile(b'"hello"')
assert isinstance(val, cue.Value)

val = ctx.compile(b"'world'")
assert isinstance(val, cue.Value)

val = ctx.compile(b"int")
assert isinstance(val, cue.Value)

val = ctx.compile(b"{}")
assert isinstance(val, cue.Value)

val = ctx.compile(b"x: 42")
assert isinstance(val, cue.Value)

val = ctx.compile(b"x: y: z: true")
assert isinstance(val, cue.Value)

def test_compile_with_options():
ctx = cue.Context()

val = ctx.compile("true", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("42", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("1.2345", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile('"hello"', cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("'world'", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("int", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("{}", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("x: 42", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

val = ctx.compile("x: y: z: true", cue.FileName("empty.cue"))
assert isinstance(val, cue.Value)

def test_compile_error():
ctx = cue.Context()

with pytest.raises(cue.Error):
ctx.compile("<")

with pytest.raises(cue.Error, match="expected operand, found 'EOF'"):
ctx.compile("a: b: -")

with pytest.raises(cue.Error, match="expected operand, found 'EOF'"):
ctx.compile(b"a: b: -")
29 changes: 29 additions & 0 deletions tests/test_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2024 The CUE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
cue.Value tests.
"""

import pytest
import cue

def test_context():
ctx = cue.Context()

val = ctx.compile("")
assert val.context() == ctx

val = ctx.compile("x: 42")
assert val.context() == ctx

0 comments on commit e5cbfc2

Please sign in to comment.