prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Python 3 regression: Undefined Jinja2 variables get rendered as empty string in linting
  ### Solution to issue cannot be found in the documentation.
  
  - [X] I checked the documentation.
  
  ### Issue
  
  For linting, undefined Jinja2 variables get rendered by `conda_smithy.utils.NullUndefined`. That class contains a `__unicode__` method that returns the name of the variable. This is useful to put a clear placeholder where variables will be filled by variants from `conda_build_config.yaml` during the actual build. However, `NullUndefined` doesn't overwrite the `__str__` method of Jinja's own `Undefined`, which returns an empty string.
  
  In effect, linting in a Python 2 environment renders, e.g. `- {{ libjpeg }}` as `- libjpeg`, but in a Python 3 environment, we get `- ` which becomes `None` in the `requirements_section` dictionary.
  
  ### Installed packages
  
  ```shell
  -
  ```
  
  
  ### Environment info
  
  ```shell
  -
  ```
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `conda_smithy/utils.py`
  Content:
  ```
  1 import shutil
  2 import tempfile
  3 import io
  4 import jinja2
  5 import datetime
  6 import time
  7 import os
  8 import sys
  9 from pathlib import Path
  10 from collections import defaultdict
  11 from contextlib import contextmanager
  12 
  13 import ruamel.yaml
  14 
  15 
  16 def get_feedstock_name_from_meta(meta):
  17     """Resolve the feedtstock name from the parsed meta.yaml."""
  18     if "feedstock-name" in meta.meta["extra"]:
  19         return meta.meta["extra"]["feedstock-name"]
  20     elif "parent_recipe" in meta.meta["extra"]:
  21         return meta.meta["extra"]["parent_recipe"]["name"]
  22     else:
  23         return meta.name()
  24 
  25 
  26 def get_feedstock_about_from_meta(meta) -> dict:
  27     """Fetch the feedtstock about from the parsed meta.yaml."""
  28     # it turns out that conda_build would not preserve the feedstock about:
  29     #   - if a subpackage does not have about, it uses the feedstock's
  30     #   - if a subpackage has about, it's used as is
  31     # therefore we need to parse the yaml again just to get the about section...
  32     if "parent_recipe" in meta.meta["extra"]:
  33         recipe_meta = os.path.join(
  34             meta.meta["extra"]["parent_recipe"]["path"], "meta.yaml"
  35         )
  36         with io.open(recipe_meta, "rt") as fh:
  37             content = render_meta_yaml("".join(fh))
  38             meta = get_yaml().load(content)
  39         return dict(meta["about"])
  40     else:
  41         # no parent recipe for any reason, use self's about
  42         return dict(meta.meta["about"])
  43 
  44 
  45 def get_yaml():
  46     # define global yaml API
  47     # roundrip-loader and allowing duplicate keys
  48     # for handling # [filter] / # [not filter]
  49     # Don't use a global variable for this as a global
  50     # variable will make conda-smithy thread unsafe.
  51     yaml = ruamel.yaml.YAML(typ="rt")
  52     yaml.allow_duplicate_keys = True
  53     return yaml
  54 
  55 
  56 @contextmanager
  57 def tmp_directory():
  58     tmp_dir = tempfile.mkdtemp("_recipe")
  59     yield tmp_dir
  60     shutil.rmtree(tmp_dir)
  61 
  62 
  63 class NullUndefined(jinja2.Undefined):
  64     def __unicode__(self):
  65         return self._undefined_name
  66 
  67     def __getattr__(self, name):
  68         return "{}.{}".format(self, name)
  69 
  70     def __getitem__(self, name):
  71         return '{}["{}"]'.format(self, name)
  72 
  73 
  74 class MockOS(dict):
  75     def __init__(self):
  76         self.environ = defaultdict(lambda: "")
  77         self.sep = "/"
  78 
  79 
  80 def stub_compatible_pin(*args, **kwargs):
  81     return f"compatible_pin {args[0]}"
  82 
  83 
  84 def stub_subpackage_pin(*args, **kwargs):
  85     return f"subpackage_pin {args[0]}"
  86 
  87 
  88 def render_meta_yaml(text):
  89     env = jinja2.Environment(undefined=NullUndefined)
  90 
  91     # stub out cb3 jinja2 functions - they are not important for linting
  92     #    if we don't stub them out, the ruamel.yaml load fails to interpret them
  93     #    we can't just use conda-build's api.render functionality, because it would apply selectors
  94     env.globals.update(
  95         dict(
  96             compiler=lambda x: x + "_compiler_stub",
  97             pin_subpackage=stub_subpackage_pin,
  98             pin_compatible=stub_compatible_pin,
  99             cdt=lambda *args, **kwargs: "cdt_stub",
  100             load_file_regex=lambda *args, **kwargs: defaultdict(lambda: ""),
  101             datetime=datetime,
  102             time=time,
  103             target_platform="linux-64",
  104             mpi="mpi",
  105         )
  106     )
  107     mockos = MockOS()
  108     py_ver = "3.7"
  109     context = {"os": mockos, "environ": mockos.environ, "PY_VER": py_ver}
  110     content = env.from_string(text).render(context)
  111     return content
  112 
  113 
  114 @contextmanager
  115 def update_conda_forge_config(forge_yaml):
  116     """Utility method used to update conda forge configuration files
  117 
  118     Uage:
  119     >>> with update_conda_forge_config(somepath) as cfg:
  120     ...     cfg['foo'] = 'bar'
  121     """
  122     if os.path.exists(forge_yaml):
  123         with open(forge_yaml, "r") as fh:
  124             code = get_yaml().load(fh)
  125     else:
  126         code = {}
  127 
  128     # Code could come in as an empty list.
  129     if not code:
  130         code = {}
  131 
  132     yield code
  133 
  134     get_yaml().dump(code, Path(forge_yaml))
  135 
  136 
  137 def merge_dict(src, dest):
  138     """Recursive merge dictionary"""
  139     for key, value in src.items():
  140         if isinstance(value, dict):
  141             # get node or create one
  142             node = dest.setdefault(key, {})
  143             merge_dict(value, node)
  144         else:
  145             dest[key] = value
  146 
  147     return dest
  148
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/conda_smithy/utils.py b/conda_smithy/utils.py
  --- a/conda_smithy/utils.py
  +++ b/conda_smithy/utils.py
  @@ -61,7 +61,7 @@
   
   
   class NullUndefined(jinja2.Undefined):
  -    def __unicode__(self):
  +    def __str__(self):
           return self._undefined_name
   
       def __getattr__(self, name):
  
verification_info: {"golden_diff": "diff --git a/conda_smithy/utils.py b/conda_smithy/utils.py\n--- a/conda_smithy/utils.py\n+++ b/conda_smithy/utils.py\n@@ -61,7 +61,7 @@\n \n \n class NullUndefined(jinja2.Undefined):\n-    def __unicode__(self):\n+    def __str__(self):\n         return self._undefined_name\n \n     def __getattr__(self, name):\n", "issue": "Python 3 regression: Undefined Jinja2 variables get rendered as empty string in linting\n### Solution to issue cannot be found in the documentation.\n\n- [X] I checked the documentation.\n\n### Issue\n\nFor linting, undefined Jinja2 variables get rendered by `conda_smithy.utils.NullUndefined`. That class contains a `__unicode__` method that returns the name of the variable. This is useful to put a clear placeholder where variables will be filled by variants from `conda_build_config.yaml` during the actual build. However, `NullUndefined` doesn't overwrite the `__str__` method of Jinja's own `Undefined`, which returns an empty string.\r\n\r\nIn effect, linting in a Python 2 environment renders, e.g. `- {{ libjpeg }}` as `- libjpeg`, but in a Python 3 environment, we get `- ` which becomes `None` in the `requirements_section` dictionary.\n\n### Installed packages\n\n```shell\n-\n```\n\n\n### Environment info\n\n```shell\n-\n```\n\n", "before_files": [{"content": "import shutil\nimport tempfile\nimport io\nimport jinja2\nimport datetime\nimport time\nimport os\nimport sys\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom contextlib import contextmanager\n\nimport ruamel.yaml\n\n\ndef get_feedstock_name_from_meta(meta):\n    \"\"\"Resolve the feedtstock name from the parsed meta.yaml.\"\"\"\n    if \"feedstock-name\" in meta.meta[\"extra\"]:\n        return meta.meta[\"extra\"][\"feedstock-name\"]\n    elif \"parent_recipe\" in meta.meta[\"extra\"]:\n        return meta.meta[\"extra\"][\"parent_recipe\"][\"name\"]\n    else:\n        return meta.name()\n\n\ndef get_feedstock_about_from_meta(meta) -> dict:\n    \"\"\"Fetch the feedtstock about from the parsed meta.yaml.\"\"\"\n    # it turns out that conda_build would not preserve the feedstock about:\n    #   - if a subpackage does not have about, it uses the feedstock's\n    #   - if a subpackage has about, it's used as is\n    # therefore we need to parse the yaml again just to get the about section...\n    if \"parent_recipe\" in meta.meta[\"extra\"]:\n        recipe_meta = os.path.join(\n            meta.meta[\"extra\"][\"parent_recipe\"][\"path\"], \"meta.yaml\"\n        )\n        with io.open(recipe_meta, \"rt\") as fh:\n            content = render_meta_yaml(\"\".join(fh))\n            meta = get_yaml().load(content)\n        return dict(meta[\"about\"])\n    else:\n        # no parent recipe for any reason, use self's about\n        return dict(meta.meta[\"about\"])\n\n\ndef get_yaml():\n    # define global yaml API\n    # roundrip-loader and allowing duplicate keys\n    # for handling # [filter] / # [not filter]\n    # Don't use a global variable for this as a global\n    # variable will make conda-smithy thread unsafe.\n    yaml = ruamel.yaml.YAML(typ=\"rt\")\n    yaml.allow_duplicate_keys = True\n    return yaml\n\n\n@contextmanager\ndef tmp_directory():\n    tmp_dir = tempfile.mkdtemp(\"_recipe\")\n    yield tmp_dir\n    shutil.rmtree(tmp_dir)\n\n\nclass NullUndefined(jinja2.Undefined):\n    def __unicode__(self):\n        return self._undefined_name\n\n    def __getattr__(self, name):\n        return \"{}.{}\".format(self, name)\n\n    def __getitem__(self, name):\n        return '{}[\"{}\"]'.format(self, name)\n\n\nclass MockOS(dict):\n    def __init__(self):\n        self.environ = defaultdict(lambda: \"\")\n        self.sep = \"/\"\n\n\ndef stub_compatible_pin(*args, **kwargs):\n    return f\"compatible_pin {args[0]}\"\n\n\ndef stub_subpackage_pin(*args, **kwargs):\n    return f\"subpackage_pin {args[0]}\"\n\n\ndef render_meta_yaml(text):\n    env = jinja2.Environment(undefined=NullUndefined)\n\n    # stub out cb3 jinja2 functions - they are not important for linting\n    #    if we don't stub them out, the ruamel.yaml load fails to interpret them\n    #    we can't just use conda-build's api.render functionality, because it would apply selectors\n    env.globals.update(\n        dict(\n            compiler=lambda x: x + \"_compiler_stub\",\n            pin_subpackage=stub_subpackage_pin,\n            pin_compatible=stub_compatible_pin,\n            cdt=lambda *args, **kwargs: \"cdt_stub\",\n            load_file_regex=lambda *args, **kwargs: defaultdict(lambda: \"\"),\n            datetime=datetime,\n            time=time,\n            target_platform=\"linux-64\",\n            mpi=\"mpi\",\n        )\n    )\n    mockos = MockOS()\n    py_ver = \"3.7\"\n    context = {\"os\": mockos, \"environ\": mockos.environ, \"PY_VER\": py_ver}\n    content = env.from_string(text).render(context)\n    return content\n\n\n@contextmanager\ndef update_conda_forge_config(forge_yaml):\n    \"\"\"Utility method used to update conda forge configuration files\n\n    Uage:\n    >>> with update_conda_forge_config(somepath) as cfg:\n    ...     cfg['foo'] = 'bar'\n    \"\"\"\n    if os.path.exists(forge_yaml):\n        with open(forge_yaml, \"r\") as fh:\n            code = get_yaml().load(fh)\n    else:\n        code = {}\n\n    # Code could come in as an empty list.\n    if not code:\n        code = {}\n\n    yield code\n\n    get_yaml().dump(code, Path(forge_yaml))\n\n\ndef merge_dict(src, dest):\n    \"\"\"Recursive merge dictionary\"\"\"\n    for key, value in src.items():\n        if isinstance(value, dict):\n            # get node or create one\n            node = dest.setdefault(key, {})\n            merge_dict(value, node)\n        else:\n            dest[key] = value\n\n    return dest\n", "path": "conda_smithy/utils.py"}], "after_files": [{"content": "import shutil\nimport tempfile\nimport io\nimport jinja2\nimport datetime\nimport time\nimport os\nimport sys\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom contextlib import contextmanager\n\nimport ruamel.yaml\n\n\ndef get_feedstock_name_from_meta(meta):\n    \"\"\"Resolve the feedtstock name from the parsed meta.yaml.\"\"\"\n    if \"feedstock-name\" in meta.meta[\"extra\"]:\n        return meta.meta[\"extra\"][\"feedstock-name\"]\n    elif \"parent_recipe\" in meta.meta[\"extra\"]:\n        return meta.meta[\"extra\"][\"parent_recipe\"][\"name\"]\n    else:\n        return meta.name()\n\n\ndef get_feedstock_about_from_meta(meta) -> dict:\n    \"\"\"Fetch the feedtstock about from the parsed meta.yaml.\"\"\"\n    # it turns out that conda_build would not preserve the feedstock about:\n    #   - if a subpackage does not have about, it uses the feedstock's\n    #   - if a subpackage has about, it's used as is\n    # therefore we need to parse the yaml again just to get the about section...\n    if \"parent_recipe\" in meta.meta[\"extra\"]:\n        recipe_meta = os.path.join(\n            meta.meta[\"extra\"][\"parent_recipe\"][\"path\"], \"meta.yaml\"\n        )\n        with io.open(recipe_meta, \"rt\") as fh:\n            content = render_meta_yaml(\"\".join(fh))\n            meta = get_yaml().load(content)\n        return dict(meta[\"about\"])\n    else:\n        # no parent recipe for any reason, use self's about\n        return dict(meta.meta[\"about\"])\n\n\ndef get_yaml():\n    # define global yaml API\n    # roundrip-loader and allowing duplicate keys\n    # for handling # [filter] / # [not filter]\n    # Don't use a global variable for this as a global\n    # variable will make conda-smithy thread unsafe.\n    yaml = ruamel.yaml.YAML(typ=\"rt\")\n    yaml.allow_duplicate_keys = True\n    return yaml\n\n\n@contextmanager\ndef tmp_directory():\n    tmp_dir = tempfile.mkdtemp(\"_recipe\")\n    yield tmp_dir\n    shutil.rmtree(tmp_dir)\n\n\nclass NullUndefined(jinja2.Undefined):\n    def __str__(self):\n        return self._undefined_name\n\n    def __getattr__(self, name):\n        return \"{}.{}\".format(self, name)\n\n    def __getitem__(self, name):\n        return '{}[\"{}\"]'.format(self, name)\n\n\nclass MockOS(dict):\n    def __init__(self):\n        self.environ = defaultdict(lambda: \"\")\n        self.sep = \"/\"\n\n\ndef stub_compatible_pin(*args, **kwargs):\n    return f\"compatible_pin {args[0]}\"\n\n\ndef stub_subpackage_pin(*args, **kwargs):\n    return f\"subpackage_pin {args[0]}\"\n\n\ndef render_meta_yaml(text):\n    env = jinja2.Environment(undefined=NullUndefined)\n\n    # stub out cb3 jinja2 functions - they are not important for linting\n    #    if we don't stub them out, the ruamel.yaml load fails to interpret them\n    #    we can't just use conda-build's api.render functionality, because it would apply selectors\n    env.globals.update(\n        dict(\n            compiler=lambda x: x + \"_compiler_stub\",\n            pin_subpackage=stub_subpackage_pin,\n            pin_compatible=stub_compatible_pin,\n            cdt=lambda *args, **kwargs: \"cdt_stub\",\n            load_file_regex=lambda *args, **kwargs: defaultdict(lambda: \"\"),\n            datetime=datetime,\n            time=time,\n            target_platform=\"linux-64\",\n            mpi=\"mpi\",\n        )\n    )\n    mockos = MockOS()\n    py_ver = \"3.7\"\n    context = {\"os\": mockos, \"environ\": mockos.environ, \"PY_VER\": py_ver}\n    content = env.from_string(text).render(context)\n    return content\n\n\n@contextmanager\ndef update_conda_forge_config(forge_yaml):\n    \"\"\"Utility method used to update conda forge configuration files\n\n    Uage:\n    >>> with update_conda_forge_config(somepath) as cfg:\n    ...     cfg['foo'] = 'bar'\n    \"\"\"\n    if os.path.exists(forge_yaml):\n        with open(forge_yaml, \"r\") as fh:\n            code = get_yaml().load(fh)\n    else:\n        code = {}\n\n    # Code could come in as an empty list.\n    if not code:\n        code = {}\n\n    yield code\n\n    get_yaml().dump(code, Path(forge_yaml))\n\n\ndef merge_dict(src, dest):\n    \"\"\"Recursive merge dictionary\"\"\"\n    for key, value in src.items():\n        if isinstance(value, dict):\n            # get node or create one\n            node = dest.setdefault(key, {})\n            merge_dict(value, node)\n        else:\n            dest[key] = value\n\n    return dest\n", "path": "conda_smithy/utils.py"}]}