Describe the bug
Summary
When the Python emitter (@azure-tools/typespec-python, backed by @typespec/http-client-python) is run with --option "@azure-tools/typespec-python.models-mode=none", code generation crashes instead of producing a modelless client:
jinja2.exceptions.UndefinedError: 'pygen.codegen.serializers.model_serializer.MsrestModelSerializer object' has no attribute 'get_properties_to_declare
The root cause is in pygen’s OptionsDict.__init__: it stores the constructor-supplied options directly (self._data = options.copy()), bypassing the _validate_and_transform step that every other write path (__setitem__) goes through. That transform is what converts the string "none" into the falsy False. Because it’s skipped, models-mode stays the truthy string "none", the serializer wrongly enters the “write models folder” path, and it does so with MsrestModelSerializer (which has no get_properties_to_declare), crashing the render.
Environment
| Component |
Version |
@typespec/compiler |
1.12.0 |
@azure-tools/typespec-python (emitter) |
0.62.1 |
@typespec/http-client-python |
0.29.2 |
| Python (emitter venv) |
3.12.3 |
| Node.js |
22.23.0 |
| OS |
Linux (Ubuntu / WSL2) |
Actual behavior
Generation aborts with the following traceback (paths abbreviated):
Traceback (most recent call last):
File ".../pygen/codegen/__init__.py", line 83, in process
File ".../pygen/codegen/serializers/__init__.py", line 212, in serialize
File ".../pygen/codegen/serializers/__init__.py", line 305, in _serialize_and_write_models_folder
File ".../pygen/codegen/serializers/model_serializer.py", line 98, in serialize
File ".../jinja2/environment.py", line 1295, in render
File ".../pygen/codegen/templates/model_container.py.jinja2", line 13, in top-level template code
File ".../pygen/codegen/templates/model_dpg.py.jinja2", line 24, in top-level template code
{% for p in serializer.get_properties_to_declare(model) %}
jinja2.exceptions.UndefinedError:
'pygen.codegen.serializers.model_serializer.MsrestModelSerializer object' has no attribute 'get_properties_to_declare'
error @typespec/http-client-python/unknown-error: Can't generate Python client code from this TypeSpec.
Please open an issue on https://github.com/microsoft/typespec.
Note the emitter still enters _serialize_and_write_models_folder even though --models-mode=none was passed.
Expected behavior
models-mode=none should generate a modelless client (no models/ folder; operations take/return plain JSON = MutableMapping[str, Any]), exactly as the __setitem__ normalization already implies - not crash.
Root cause
In pygen/__init__.py, class OptionsDict(MutableMapping):
# __init__ — stores options RAW, skipping the transform:
def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
self._data = options.copy() if options else {} # <-- bypasses _validate_and_transform
self._validate_combinations()
# __setitem__ — the normal write path, which DOES transform:
def __setitem__(self, key: str, value: Any) -> None:
validated_value = self._validate_and_transform(key, value)
self._data[key] = validated_value
# _validate_and_transform — where "none" becomes falsy False:
def _validate_and_transform(self, key: str, value: Any) -> Any:
...
if key == "models-mode" and value == "none":
value = False # switch to falsy value for easier code writing
...
return value
Because __init__ assigns self._data directly, options passed to the constructor never run through _validate_and_transform. So models-mode="none" is kept as the truthy string "none" instead of False. Downstream serialization treats that as “emit models”, picks MsrestModelSerializer, and renders model_dpg.py.jinja2, which calls get_properties_to_declare - a method that doesn’t exist on MsrestModelSerializer - producing the crash.
Proposed fix
Route constructor-supplied options through the same _validate_and_transform used by __setitem__:
def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
- self._data = options.copy() if options else {}
+ self._data = {}
+ if options:
+ for key, value in options.items():
+ self._data[key] = self._validate_and_transform(key, value)
self._validate_combinations()
With this change, OptionsDict({"models-mode": "none"})["models-mode"] returns False, generation honors models-mode=none, and a modelless client is produced successfully.
Reproduction
Minimal reproduction (isolates the bug, no spec needed)
from pygen import OptionsDict
# Same option, two write paths:
via_ctor = OptionsDict({"models-mode": "none"})["models-mode"]
o = OptionsDict(); o["models-mode"] = "none"
via_setitem = o["models-mode"]
print("via constructor :", repr(via_ctor)) # 'none' <-- BUG: stays a truthy string
print("via __setitem__ :", repr(via_setitem)) # False <-- correct (normalized)
Output:
via constructor : 'none'
via __setitem__ : False
The two paths disagree. The emitter constructs OptionsDict(options) with the CLI options (including models-mode=none), so it always takes the buggy constructor path.
Full reproduction (end-to-end)
Compile any ARM management-plane TypeSpec spec with models-mode=none:
tsp compile <spec>/client.tsp \
--emit @azure-tools/typespec-python \
--option "@azure-tools/typespec-python.models-mode=none" \
--option "@azure-tools/typespec-python.namespace=my.pkg.mgmt" \
--option "@azure-tools/typespec-python.emitter-output-dir=$PWD/out"
Checklist
Describe the bug
Summary
When the Python emitter (
@azure-tools/typespec-python, backed by@typespec/http-client-python) is run with--option "@azure-tools/typespec-python.models-mode=none", code generation crashes instead of producing a modelless client:The root cause is in
pygen’sOptionsDict.__init__: it stores the constructor-supplied options directly (self._data = options.copy()), bypassing the_validate_and_transformstep that every other write path (__setitem__) goes through. That transform is what converts the string"none"into the falsyFalse. Because it’s skipped,models-modestays the truthy string"none", the serializer wrongly enters the “write models folder” path, and it does so withMsrestModelSerializer(which has noget_properties_to_declare), crashing the render.Environment
@typespec/compiler1.12.0@azure-tools/typespec-python(emitter)0.62.1@typespec/http-client-python0.29.23.12.322.23.0Actual behavior
Generation aborts with the following traceback (paths abbreviated):
Note the emitter still enters
_serialize_and_write_models_foldereven though--models-mode=nonewas passed.Expected behavior
models-mode=noneshould generate a modelless client (nomodels/folder; operations take/return plainJSON = MutableMapping[str, Any]), exactly as the__setitem__normalization already implies - not crash.Root cause
In
pygen/__init__.py,class OptionsDict(MutableMapping):Because
__init__assignsself._datadirectly, options passed to the constructor never run through_validate_and_transform. Somodels-mode="none"is kept as the truthy string"none"instead ofFalse. Downstream serialization treats that as “emit models”, picksMsrestModelSerializer, and rendersmodel_dpg.py.jinja2, which callsget_properties_to_declare- a method that doesn’t exist onMsrestModelSerializer- producing the crash.Proposed fix
Route constructor-supplied options through the same
_validate_and_transformused by__setitem__:With this change,
OptionsDict({"models-mode": "none"})["models-mode"]returnsFalse, generation honorsmodels-mode=none, and a modelless client is produced successfully.Reproduction
Minimal reproduction (isolates the bug, no spec needed)
Output:
The two paths disagree. The emitter constructs
OptionsDict(options)with the CLI options (includingmodels-mode=none), so it always takes the buggy constructor path.Full reproduction (end-to-end)
Compile any ARM management-plane TypeSpec spec with
models-mode=none:Checklist