nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: ecmwf/earthkit-data
Pull request #179 (2023-09-13 11:59:49)

Fix numpy fieldlist values access


Patch:
diff --git a/earthkit/data/sources/numpy_list.py b/earthkit/data/sources/numpy_list.py
index bbe5de7..0c8bf22 100644
--- a/earthkit/data/sources/numpy_list.py
+++ b/earthkit/data/sources/numpy_list.py
@@ -26,9 +26,11 @@ class NumpyField(Field):
     def _make_metadata(self):
         pass
 
-    @property
-    def values(self):
-        return self._array
+    def _values(self, dtype=None):
+        if dtype is None:
+            return self._array
+        else:
+            return self._array.astype(dtype)
 
     def write(self, f):
         from earthkit.data.writers import write


Test patch:
diff --git a/tests/sources/test_numpy_list.py b/tests/sources/test_numpy_list.py
index 225c3df..523949b 100644
--- a/tests/sources/test_numpy_list.py
+++ b/tests/sources/test_numpy_list.py
@@ -27,27 +27,31 @@ def test_numpy_list_grib_single_field():
 
     assert ds[0].metadata("shortName") == "2t"
 
-    v = ds[0].values
+    lat, lon, v = ds[0].data(flatten=True)
     v1 = v + 1
 
     md = ds[0].metadata()
     md1 = md.override(shortName="msl")
     r = FieldList.from_numpy(v1, md1)
 
-    assert len(r) == 1
-    assert np.allclose(v1, r[0].values)
-    assert r[0].shape == ds[0].shape
-    assert r[0].metadata("shortName") == "msl"
+    def _check_field(r):
+        assert len(r) == 1
+        assert np.allclose(r[0].values, v1)
+        assert r[0].shape == ds[0].shape
+        assert r[0].metadata("shortName") == "msl"
+        _lat, _lon, _v = r[0].data(flatten=True)
+        assert np.allclose(_lat, lat)
+        assert np.allclose(_lon, lon)
+        assert np.allclose(_v, v1)
+
+    _check_field(r)
 
     # save to disk
     tmp = temp_file()
     r.save(tmp.path)
     assert os.path.exists(tmp.path)
     r_tmp = from_source("file", tmp.path)
-    assert len(r_tmp) == 1
-    assert np.allclose(v1, r_tmp[0].values)
-    assert r_tmp[0].shape == ds[0].shape
-    assert r_tmp[0].metadata("shortName") == "msl"
+    _check_field(r_tmp)
 
 
 def test_numpy_list_grib_multi_field():
Repository: ecmwf/earthkit-data
Pull request #182 (2023-09-15 13:19:00)

Write missing values to grib output
When we use `save` on a NumpyFieldLists it will write np.nan-s as missing values into the resulting grib data.


Patch:
diff --git a/earthkit/data/sources/numpy_list.py b/earthkit/data/sources/numpy_list.py
index 0c8bf22..5212224 100644
--- a/earthkit/data/sources/numpy_list.py
+++ b/earthkit/data/sources/numpy_list.py
@@ -35,7 +35,7 @@ class NumpyField(Field):
     def write(self, f):
         from earthkit.data.writers import write
 
-        write(f, self.values, self._metadata, check_nans=False)
+        write(f, self.values, self._metadata, check_nans=True)
 
 
 class NumpyFieldList(FieldList):
diff --git a/earthkit/data/writers/grib.py b/earthkit/data/writers/grib.py
index e4fc547..9ca6b79 100644
--- a/earthkit/data/writers/grib.py
+++ b/earthkit/data/writers/grib.py
@@ -13,14 +13,13 @@ from . import Writer
 class GribWriter(Writer):
     METADATA_TYPE = "GribMetadata"
 
-    def write(self, f, values, metadata, check_nans=False):
+    def write(self, f, values, metadata, check_nans=True):
         handle = metadata._handle
         if check_nans:
             import numpy as np
 
             if np.isnan(values).any():
-                # missing_value = np.finfo(values.dtype).max
-                missing_value = 9999
+                missing_value = handle.MISSING_VALUE
                 values = np.nan_to_num(values, nan=missing_value)
                 handle.set_double("missingValue", missing_value)
                 handle.set_long("bitmapPresent", 1)


Test patch:
diff --git a/tests/sources/test_numpy_list.py b/tests/sources/test_numpy_list.py
index 523949b..1a56c0a 100644
--- a/tests/sources/test_numpy_list.py
+++ b/tests/sources/test_numpy_list.py
@@ -85,6 +85,37 @@ def test_numpy_list_grib_multi_field():
         assert f.metadata("name") == "2 metre dewpoint temperature", f"name {i}"
 
 
+def test_numpy_list_grib_write_missing():
+    ds = from_source("file", earthkit_examples_file("test.grib"))
+
+    assert ds[0].metadata("shortName") == "2t"
+
+    v = ds[0].values
+    v1 = np.array(v)
+    assert not np.isnan(v1[0])
+    assert not np.isnan(v1[1])
+
+    v1[0] = np.nan
+    assert np.isnan(v1[0])
+    assert not np.isnan(v1[1])
+
+    md = ds[0].metadata()
+    md1 = md.override(shortName="msl")
+    r = FieldList.from_numpy(v1, md1)
+
+    assert np.isnan(r[0].values[0])
+    assert not np.isnan(r[0].values[1])
+
+    # save to disk
+    tmp = temp_file()
+    r.save(tmp.path)
+    assert os.path.exists(tmp.path)
+    r_tmp = from_source("file", tmp.path)
+
+    assert np.isnan(r_tmp[0].values[0])
+    assert not np.isnan(r_tmp[0].values[1])
+
+
 if __name__ == "__main__":
     from earthkit.data.testing import main
Repository: ecmwf/earthkit-data
Pull request #183 (2023-09-15 13:46:53)

Add append option to save method on fieldlists
We can call it like this:

```python
ds.save("my_path", append=True)
```


Patch:
diff --git a/earthkit/data/core/fieldlist.py b/earthkit/data/core/fieldlist.py
index f01dd9d..c91d67f 100644
--- a/earthkit/data/core/fieldlist.py
+++ b/earthkit/data/core/fieldlist.py
@@ -1144,16 +1144,19 @@ class FieldList(Index):
                 )
         return False
 
-    def save(self, filename):
-        r"""Write all the fields into a file. The target file will be overwritten if
-        already exists.
+    def save(self, filename, append=False):
+        r"""Write all the fields into a file.
 
         Parameters
         ----------
         filename: str
             The target file path.
+        append: bool
+            When it is true append data to the target file. Otherwise
+            the target file be overwritten if already exists.
         """
-        with open(filename, "wb") as f:
+        flag = "wb" if not append else "ab"
+        with open(filename, flag) as f:
             self.write(f)
 
     def write(self, f):


Test patch:
diff --git a/tests/sources/test_numpy_list.py b/tests/sources/test_numpy_list.py
index 1a56c0a..4cdf72c 100644
--- a/tests/sources/test_numpy_list.py
+++ b/tests/sources/test_numpy_list.py
@@ -116,6 +116,37 @@ def test_numpy_list_grib_write_missing():
     assert not np.isnan(r_tmp[0].values[1])
 
 
+def test_numpy_list_grib_write_append():
+    ds = from_source("file", earthkit_examples_file("test.grib"))
+
+    assert ds[0].metadata("shortName") == "2t"
+
+    v = ds[0].values
+    v1 = v + 1
+    v2 = v + 2
+
+    md = ds[0].metadata()
+    md1 = md.override(shortName="msl")
+
+    md = ds[0].metadata()
+    md2 = md.override(shortName="2d")
+
+    r1 = FieldList.from_numpy(v1, md1)
+    r2 = FieldList.from_numpy(v2, md2)
+
+    # save to disk
+    tmp = temp_file()
+    r1.save(tmp.path)
+    assert os.path.exists(tmp.path)
+    r2.save(tmp.path, append=True)
+    assert os.path.exists(tmp.path)
+
+    r_tmp = from_source("file", tmp.path)
+
+    assert len(r_tmp) == 2
+    assert r_tmp.metadata("shortName") == ["msl", "2d"]
+
+
 if __name__ == "__main__":
     from earthkit.data.testing import main
Repository: openconfig/goyang
Pull request #121 (2020-05-06 01:18:14)

Move sort from YangRang Validate to parseRanges
Validate is also called from ygot/ytypes/int_type.go to validate YangRange. This creates race conditions.


Patch:
diff --git a/pkg/yang/types_builtin.go b/pkg/yang/types_builtin.go
index 5ce3245..3f10471 100644
--- a/pkg/yang/types_builtin.go
+++ b/pkg/yang/types_builtin.go
@@ -859,17 +859,17 @@ func parseRanges(s string, decimal bool, fracDigRequired uint8) (YangRange, erro
 		}
 		r[i] = YRange{min, max}
 	}
+	r.Sort()
+	r = coalesce(r)
+
 	if err := r.Validate(); err != nil {
 		return nil, err
 	}
-
-	r = coalesce(r)
 	return r, nil
 }
 
 // coalesce coalesces r into as few ranges as possible.  For example,
 // 1..5|6..10 would become 1..10.  r is assumed to be sorted.
-// r is assumed to be valid (see Validate).
 func coalesce(r YangRange) YangRange {
 	// coalesce the ranges if we have more than 1.
 	if len(r) < 2 {
@@ -944,10 +944,13 @@ func (r YangRange) Less(i, j int) bool {
 	}
 }
 
-// Validate sorts r and returns an error if r has either an invalid range or has
+// Validate returns an error if r has either an invalid range or has
 // overlapping ranges.
+// r is expected to be sorted use YangRange.Sort()
 func (r YangRange) Validate() error {
-	sort.Sort(r)
+	if !sort.IsSorted(r) {
+		return errors.New("range not sorted")
+	}
 	switch {
 	case len(r) == 0:
 		return nil
@@ -964,6 +967,11 @@ func (r YangRange) Validate() error {
 	return nil
 }
 
+// Sort r. Must be called before Validate and coalesce if unsorted
+func (r YangRange) Sort() {
+	sort.Sort(r)
+}
+
 // Equal returns true if ranges r and q are identically equivalent.
 // TODO(borman): should we coalesce ranges in the comparison?
 func (r YangRange) Equal(q YangRange) bool {


Test patch:
diff --git a/pkg/yang/types_builtin_test.go b/pkg/yang/types_builtin_test.go
index 080e047..fef848b 100644
--- a/pkg/yang/types_builtin_test.go
+++ b/pkg/yang/types_builtin_test.go
@@ -305,6 +305,28 @@ func TestCoalesce(t *testing.T) {
 	}
 }
 
+func TestYangRangeSort(t *testing.T) {
+	for x, tt := range []struct {
+		in, out YangRange
+	}{
+		{YangRange{}, YangRange{}},
+		{YangRange{R(1, 4), R(6, 10)}, YangRange{R(1, 4), R(6, 10)}},
+		{YangRange{R(6, 10), R(1, 4)}, YangRange{R(1, 4), R(6, 10)}},
+		{YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}, YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}},
+		{YangRange{Rf(30, 40, 1), Rf(10, 25, 1)}, YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}},
+		{YangRange{R(1, 2)}, YangRange{R(1, 2)}},
+		{YangRange{R(1, 2), R(4, 5)}, YangRange{R(1, 2), R(4, 5)}},
+		{YangRange{R(1, 3), R(2, 5)}, YangRange{R(1, 3), R(2, 5)}},
+		{YangRange{R(1, 10), R(2, 5)}, YangRange{R(1, 10), R(2, 5)}},
+		{YangRange{R(1, 10), R(1, 2), R(4, 5), R(7, 8)}, YangRange{R(1, 2), R(1, 10), R(4, 5), R(7, 8)}},
+	} {
+		tt.in.Sort()
+		if !tt.in.Equal(tt.out) {
+			t.Errorf("#%d: got %v, want %v", x, tt.in, tt.out)
+		}
+	}
+}
+
 func TestParseRangesDecimal(t *testing.T) {
 	tests := []struct {
 		desc             string
Repository: openconfig/goyang
Pull request #124 (2020-06-15 22:26:41)

Add NodePath Helper


Patch:
diff --git a/pkg/yang/node.go b/pkg/yang/node.go
index 344c1ff..39f08e4 100644
--- a/pkg/yang/node.go
+++ b/pkg/yang/node.go
@@ -134,6 +134,16 @@ func RootNode(n Node) *Module {
 	return nil
 }
 
+// NodePath returns the full path of the node from the module name.
+func NodePath(n Node) string {
+	var path string
+	for n != nil {
+		path = "/" + n.NName() + path
+		n = n.ParentNode()
+	}
+	return path
+}
+
 // FindNode finds the node referenced by path relative to n.  If path does not
 // reference a node then nil is returned (i.e. path not found).  The path looks
 // similar to an XPath but curently has no wildcarding.  For example:


Test patch:
diff --git a/pkg/yang/node_test.go b/pkg/yang/node_test.go
index d7414f7..ac70aca 100644
--- a/pkg/yang/node_test.go
+++ b/pkg/yang/node_test.go
@@ -18,8 +18,51 @@ import (
 	"errors"
 	"fmt"
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
 )
 
+func TestNodePath(t *testing.T) {
+	tests := []struct {
+		desc string
+		in   Node
+		want string
+	}{{
+		desc: "basic",
+		in: &Leaf{
+			Name: "bar",
+			Parent: &Container{
+				Name: "c",
+				Parent: &List{
+					Name: "b",
+					Parent: &Module{
+						Name: "foo",
+					},
+				},
+			},
+		},
+		want: "/foo/b/c/bar",
+	}, {
+		desc: "nil input node",
+		in:   nil,
+		want: "",
+	}, {
+		desc: "single node",
+		in: &Module{
+			Name: "foo",
+		},
+		want: "/foo",
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			if diff := cmp.Diff(NodePath(tt.in), tt.want); diff != "" {
+				t.Errorf("(-got, +want):\n%s", diff)
+			}
+		})
+	}
+}
+
 // TestNode provides a framework for processing tests that can check particular
 // nodes being added to the grammar. It can be used to ensure that particular
 // statement combinations are supported, especially where they are opaque to
Repository: openconfig/goyang
Pull request #144 (2020-09-15 13:20:59)

Add prefix support for RPC and Action input and output.
The Find method in entry.go does not support prefixes for input and output. Some ietf models such as [ietf-netconf-with-defaults@2011-06-01.yang](https://github.com/YangModels/yang/blob/cbfbdfcd445bb1fae2c029feea15441a9f9ce4eb/standard/ietf/RFC/ietf-netconf-with-defaults%402011-06-01.yang#L106) use a prefix for the input in an augment: `augment /nc:copy-config/nc:input`.


Patch:
diff --git a/pkg/yang/entry.go b/pkg/yang/entry.go
index 54b228e..3b88c03 100644
--- a/pkg/yang/entry.go
+++ b/pkg/yang/entry.go
@@ -1198,6 +1198,7 @@ func (e *Entry) Find(name string) *Entry {
 		case part == "..":
 			e = e.Parent
 		case e.RPC != nil:
+			_, part = getPrefix(part)
 			switch part {
 			case "input":
 				e = e.RPC.Input


Test patch:
diff --git a/pkg/yang/entry_test.go b/pkg/yang/entry_test.go
index 8958cd5..e8ab70e 100644
--- a/pkg/yang/entry_test.go
+++ b/pkg/yang/entry_test.go
@@ -2200,12 +2200,15 @@ func TestEntryFind(t *testing.T) {
 			"../t:c/d":   "/test/c/d",
 			"../c/t:d":   "/test/c/d",
 			// Find within an absolute directory with prefixes.
-			"/t:c/d":                "/test/c/d",
-			"/c/t:d":                "/test/c/d",
-			"../t:rpc1/input":       "/test/rpc1/input",
-			"/t:rpc1/input":         "/test/rpc1/input",
-			"/t:e/operation/input":  "/test/e/operation/input",
-			"/t:e/operation/output": "/test/e/operation/output",
+			"/t:c/d":                    "/test/c/d",
+			"/c/t:d":                    "/test/c/d",
+			"../t:rpc1/input":           "/test/rpc1/input",
+			"/t:rpc1/input":             "/test/rpc1/input",
+			"/t:rpc1/t:input":           "/test/rpc1/input",
+			"/t:e/operation/input":      "/test/e/operation/input",
+			"/t:e/operation/output":     "/test/e/operation/output",
+			"/t:e/t:operation/t:input":  "/test/e/operation/input",
+			"/t:e/t:operation/t:output": "/test/e/operation/output",
 		},
 	}, {
 		name: "inter-module find",
Repository: openconfig/goyang
Pull request #150 (2020-10-21 14:52:16)

The `mandatory` attribute on leafs was not handled correctly!
The error was not covered in tests, because the deviation code had also an error.


Patch:
diff --git a/pkg/yang/entry.go b/pkg/yang/entry.go
index e1a1905..a5f8eab 100644
--- a/pkg/yang/entry.go
+++ b/pkg/yang/entry.go
@@ -584,6 +584,8 @@ func ToEntry(n Node) (e *Entry) {
 		e.addError(err)
 		e.Prefix = getRootPrefix(e)
 		addExtraKeywordsToLeafEntry(n, e)
+		e.Mandatory, err = tristateValue(s.Mandatory)
+		e.addError(err)
 		return e
 	case *LeafList:
 		// Create the equivalent leaf element that we are a list of.
@@ -1136,7 +1138,7 @@ func (e *Entry) ApplyDeviate() []error {
 					}
 
 					if devSpec.Mandatory != TSUnset {
-						devSpec.Mandatory = TSUnset
+						deviatedNode.Mandatory = TSUnset
 					}
 
 					if devSpec.deviatePresence.hasMinElements {


Test patch:
diff --git a/pkg/yang/entry_test.go b/pkg/yang/entry_test.go
index ead2e27..f0c1222 100644
--- a/pkg/yang/entry_test.go
+++ b/pkg/yang/entry_test.go
@@ -3213,7 +3213,7 @@ func TestDeviation(t *testing.T) {
 	}
 }
 
-func TestLeafEntryTypes(t *testing.T) {
+func TestLeafEntry(t *testing.T) {
 	tests := []struct {
 		name                string
 		inModules           map[string]string
@@ -3243,6 +3243,9 @@ func TestLeafEntryTypes(t *testing.T) {
 			if got, want := e.Type.FractionDigits, 1; got != want {
 				t.Errorf("got %d, want %d", got, want)
 			}
+			if got, want := e.Mandatory, TSUnset; got != want {
+				t.Errorf("got %d, want %d", got, want)
+			}
 			if diff := cmp.Diff(e.Type.Range, YangRange{Rf(-120, 120, 1)}); diff != "" {
 				t.Errorf("Range (-got, +want):\n%s", diff)
 			}
@@ -3304,6 +3307,50 @@ func TestLeafEntryTypes(t *testing.T) {
 			`,
 		},
 		wantErrSubstr: "overriding of fraction-digits not allowed",
+	}, {
+		name: "leaf mandatory true",
+		inModules: map[string]string{
+			"test.yang": `
+			module test {
+				prefix "t";
+				namespace "urn:t";
+
+				leaf "mandatory" {
+					type "string" {
+					}
+					mandatory true;
+				}
+			}
+			`,
+		},
+		wantEntryPath: "/test/mandatory",
+		wantEntryCustomTest: func(t *testing.T, e *Entry) {
+			if got, want := e.Mandatory, TSTrue; got != want {
+				t.Errorf("got %d, want %d", got, want)
+			}
+		},
+	}, {
+		name: "leaf mandatory false",
+		inModules: map[string]string{
+			"test.yang": `
+			module test {
+				prefix "t";
+				namespace "urn:t";
+
+				leaf "mandatory" {
+					type "string" {
+					}
+					mandatory false;
+				}
+			}
+			`,
+		},
+		wantEntryPath: "/test/mandatory",
+		wantEntryCustomTest: func(t *testing.T, e *Entry) {
+			if got, want := e.Mandatory, TSFalse; got != want {
+				t.Errorf("got %d, want %d", got, want)
+			}
+		},
 	}}
 
 	for _, tt := range tests {
Repository: openconfig/goyang
Pull request #192 (2021-07-28 21:48:42)

Fix merge conflict between #163 and #181
@robshakir, I didn't do a merge on the older PRs before merging, leading to this breakage on HEAD. Is there a setting in goyang you can enable to make it similar ygot where you need to merge into master first before merge?


Patch:
diff --git a/pkg/yang/lex.go b/pkg/yang/lex.go
index b5be219..e7eb4e0 100644
--- a/pkg/yang/lex.go
+++ b/pkg/yang/lex.go
@@ -395,16 +395,16 @@ func lexGround(l *lexer) stateFn {
 			l.next()
 			return lexGround
 		default:
-			return lexIdentifier
+			return lexUnquoted
 		}
 	case '+':
 		l.next()
 		switch l.peek() {
 		case '"', '\'':
-			l.emit(tIdentifier)
+			l.emit(tUnquoted)
 			return lexGround
 		default:
-			return lexIdentifier
+			return lexUnquoted
 		}
 	default:
 		return lexUnquoted


Test patch:
diff --git a/pkg/yang/lex_test.go b/pkg/yang/lex_test.go
index 4b97d25..d7ea06d 100644
--- a/pkg/yang/lex_test.go
+++ b/pkg/yang/lex_test.go
@@ -127,11 +127,11 @@ Tests:
 		{line(), `
 pattern '[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+';
 `, []*token{
-			T(tIdentifier, "pattern"),
+			T(tUnquoted, "pattern"),
 			T(tString, "[a-zA-Z0-9!#$%&"),
-			T(tIdentifier, "+"),
+			T(tUnquoted, "+"),
 			T(tString, "'"),
-			T(tIdentifier, "+"),
+			T(tUnquoted, "+"),
 			T(tString, "*+/=?^_`{|}~-]+"),
 			T(';', ";"),
 		}},
Repository: openconfig/goyang
Pull request #244 (2023-05-02 02:23:34)

Retain Extra fields over Uses,Augments
Hi,

In a YANG model, uses or augments can be used to extend modules. It's possible to define under those nodes an IfFeature attribute. But when turning those into an Entry (yang.ToEntry), IfFeature field disappears. It should become an attribute located under Extras, but when using Uses, it simply isn't copied hence some information is lost.

This PR adds support for moving all the Extra fields defined in Uses or Merge to the augment element.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/59591176/badge)](https://coveralls.io/builds/59591176)

Coverage: 84.039% (+0.02%) from 84.021% when pulling **aadd62555a216bb5d6bd495b805d900603dbb7b4 on bclasse:extra-uses-augment-handling** into **9a319ef4f89f0161defd22aae73270e38e0b3a01 on openconfig:master**.

wenovus: Thanks!

Patch:
diff --git a/pkg/yang/entry.go b/pkg/yang/entry.go
index e2c5309..ce0014e 100644
--- a/pkg/yang/entry.go
+++ b/pkg/yang/entry.go
@@ -1461,6 +1461,12 @@ func (e *Entry) dup() *Entry {
 			ne.Dir[k] = de
 		}
 	}
+
+	ne.Extra = make(map[string][]interface{})
+	for k, v := range e.Extra {
+		ne.Extra[k] = v
+	}
+
 	return &ne
 }
 
@@ -1485,6 +1491,9 @@ func (e *Entry) merge(prefix *Value, namespace *Value, oe *Entry) {
 		} else {
 			v.Parent = e
 			v.Exts = append(v.Exts, oe.Exts...)
+			for lk := range oe.Extra {
+				v.Extra[lk] = append(v.Extra[lk], oe.Extra[lk]...)
+			}
 			e.Dir[k] = v
 		}
 	}


Test patch:
diff --git a/pkg/yang/entry_test.go b/pkg/yang/entry_test.go
index b87d948..2000238 100644
--- a/pkg/yang/entry_test.go
+++ b/pkg/yang/entry_test.go
@@ -2058,6 +2058,7 @@ var testIfFeatureModules = []struct {
   feature ft-identity;
   feature ft-uses;
   feature ft-refine;
+  feature ft-augment-uses;
 
   container cont {
     if-feature ft-container;
@@ -2118,6 +2119,9 @@ var testIfFeatureModules = []struct {
 
   augment "/cont" {
     if-feature ft-augment;
+	uses g {
+		if-feature ft-augment-uses;
+	}
   }
 
   identity id {
@@ -2130,7 +2134,10 @@ var testIfFeatureModules = []struct {
       if-feature ft-refine;
     }
   }
-  grouping g {}
+
+  grouping g {
+	container gc {}
+  }
 }
 `,
 	},
@@ -2269,6 +2276,18 @@ func TestIfFeature(t *testing.T) {
 			inIfFeatures:   ms.Modules["if-feature"].Uses[0].IfFeature,
 			wantIfFeatures: []string{"ft-uses"},
 		},
+		{
+			// Verify that if-feature field defined in "uses" is correctly propagated to container
+			name:           "uses",
+			inIfFeatures:   entryIfFeatures(mod.Dir["gc"]),
+			wantIfFeatures: []string{"ft-uses"},
+		},
+		{
+			// Verify that if-feature field defined in "augment" and in "augment > uses" is correctly propagated to container
+			name:           "augment-uses",
+			inIfFeatures:   entryIfFeatures(mod.Dir["cont"].Dir["gc"]),
+			wantIfFeatures: []string{"ft-augment-uses", "ft-augment"},
+		},
 	}
 
 	for _, tc := range testcases {
Repository: jlowin/fastmcp
Pull request #214 (2025-04-20 16:28:34)

Add an init to make mcp_mixin a module
Allow usage of mcp_mixin via 

```
from contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
```


Patch:
diff --git a/src/contrib/mcp_mixin/README.md b/src/contrib/mcp_mixin/README.md
index 4d1f4d3..3acb661 100644
--- a/src/contrib/mcp_mixin/README.md
+++ b/src/contrib/mcp_mixin/README.md
@@ -10,7 +10,7 @@ Inherit from `MCPMixin` and use the decorators on the methods you want to regist
 
 ```python
 from fastmcp import FastMCP
-from contrib.mcp_mixin.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
+from contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
 
 class MyComponent(MCPMixin):
     @mcp_tool(name="my_tool", description="Does something cool.")
diff --git a/src/contrib/mcp_mixin/__init__.py b/src/contrib/mcp_mixin/__init__.py
new file mode 100644
index 0000000..8b4cca0
--- /dev/null
+++ b/src/contrib/mcp_mixin/__init__.py
@@ -0,0 +1,8 @@
+from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
+
+__all__ = [
+    "MCPMixin",
+    "mcp_tool",
+    "mcp_resource",
+    "mcp_prompt",
+]
diff --git a/src/contrib/mcp_mixin/example.py b/src/contrib/mcp_mixin/example.py
index f3e8aef..5e99663 100644
--- a/src/contrib/mcp_mixin/example.py
+++ b/src/contrib/mcp_mixin/example.py
@@ -2,7 +2,7 @@
 
 import asyncio
 
-from contrib.mcp_mixin.mcp_mixin import (
+from contrib.mcp_mixin import (
     MCPMixin,
     mcp_prompt,
     mcp_resource,


Test patch:
diff --git a/tests/contrib/test_mcp_mixin.py b/tests/contrib/test_mcp_mixin.py
index 6f69f54..b31919c 100644
--- a/tests/contrib/test_mcp_mixin.py
+++ b/tests/contrib/test_mcp_mixin.py
@@ -2,15 +2,17 @@
 
 import pytest
 
-from contrib.mcp_mixin.mcp_mixin import (
-    _DEFAULT_SEPARATOR_PROMPT,
-    _DEFAULT_SEPARATOR_RESOURCE,
-    _DEFAULT_SEPARATOR_TOOL,
+from contrib.mcp_mixin import (
     MCPMixin,
     mcp_prompt,
     mcp_resource,
     mcp_tool,
 )
+from contrib.mcp_mixin.mcp_mixin import (
+    _DEFAULT_SEPARATOR_PROMPT,
+    _DEFAULT_SEPARATOR_RESOURCE,
+    _DEFAULT_SEPARATOR_TOOL,
+)
 from fastmcp import FastMCP