Skip to content

Commit

Permalink
Pydev.Debugger extension (#607)
Browse files Browse the repository at this point in the history
  • Loading branch information
omry authored Mar 16, 2021
1 parent dfe4dd3 commit e72ce68
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 1 deletion.
16 changes: 16 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -858,3 +858,19 @@ Creates a copy of a DictConfig that contains only specific keys.
a:
b: 10
<BLANKLINE>

Debugger integration
^^^^^^^^^^^^^^^^^^^^
OmegaConf is packaged with a PyDev.Debugger extension which enables better debugging experience in PyCharm,
VSCode and other `PyDev.Debugger <https://github.com/fabioz/PyDev.Debugger>`_ powered IDEs.

The debugger extension enables OmegaConf-aware object inspection:
- providing information about interpolations.
- properly handling missing values (``???``).

The plugin comes in two flavors:
- USER: Default behavior, useful when debugging your OmegaConf objects.
- DEV: Useful when debugging OmegaConf itself, shows the exact data model of OmegaConf.

The default flavor is ``USER``. You can select which flavor to use using the environment variable ``OC_PYDEVD_RESOLVER``,
Which takes the possible values ``USER``, ``DEV`` and ``DISABLE``.
1 change: 1 addition & 0 deletions news/214.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New PyDev.Debugger resolver plugin for easier debugging
6 changes: 6 additions & 0 deletions pydevd_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
try:
__import__("pkg_resources").declare_namespace(__name__)
except ImportError: # pragma: no cover
import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__) # type: ignore
6 changes: 6 additions & 0 deletions pydevd_plugins/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
try:
__import__("pkg_resources").declare_namespace(__name__)
except ImportError: # pragma: no cover
import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__) # type: ignore
107 changes: 107 additions & 0 deletions pydevd_plugins/extensions/pydevd_plugin_omegaconf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# based on https://github.com/fabioz/PyDev.Debugger/tree/main/pydevd_plugins/extensions
import os
import sys
from functools import lru_cache
from typing import Any, Dict, Sequence

from _pydevd_bundle.pydevd_extension_api import ( # type: ignore
StrPresentationProvider,
TypeResolveProvider,
)


@lru_cache(maxsize=128)
def find_mod_attr(mod_name: str, attr: str) -> Any:
mod = sys.modules.get(mod_name)
return getattr(mod, attr, None)


class OmegaConfDeveloperResolver(object):
def can_provide(self, type_object: Any, type_name: str) -> bool:
Node = find_mod_attr("omegaconf", "Node")
return Node is not None and issubclass(type_object, Node)

def resolve(self, obj: Any, attribute: str) -> Any:
return getattr(obj, attribute)

def get_dictionary(self, obj: Any) -> Any:
return obj.__dict__


class OmegaConfUserResolver(StrPresentationProvider): # type: ignore
def can_provide(self, type_object: Any, type_name: str) -> bool:
Node = find_mod_attr("omegaconf", "Node")
return Node is not None and issubclass(type_object, Node)

def resolve(self, obj: Any, attribute: Any) -> Any:
if isinstance(obj, Sequence) and isinstance(attribute, str):
attribute = int(attribute)
val = obj.__dict__["_content"][attribute]

return val

def _is_simple_value(self, val: Any) -> bool:
ValueNode = find_mod_attr("omegaconf", "ValueNode")
return (
isinstance(val, ValueNode)
and not val._is_none()
and not val._is_missing()
and not val._is_interpolation()
)

def get_dictionary(self, obj: Any) -> Dict[str, Any]:
ListConfig = find_mod_attr("omegaconf", "ListConfig")
DictConfig = find_mod_attr("omegaconf", "DictConfig")
Node = find_mod_attr("omegaconf", "Node")

if isinstance(obj, Node):
obj = obj._dereference_node(throw_on_resolution_failure=False)
if obj is None or obj._is_none() or obj._is_missing():
return {}

if isinstance(obj, DictConfig):
d = {}
for k, v in obj.__dict__["_content"].items():
if self._is_simple_value(v):
v = v._value()
d[k] = v
elif isinstance(obj, ListConfig):
d = {}
for idx, v in enumerate(obj.__dict__["_content"]):
if self._is_simple_value(v):
v = v._value()
d[str(idx)] = v
else:
d = {}

return d

def get_str(self, val: Any) -> str:
IRE = find_mod_attr("omegaconf.errors", "InterpolationResolutionError")

if val._is_missing():
return "??? <MISSING>"
if val._is_interpolation():
try:
dr = val._dereference_node()
except IRE as e:
dr = f"ERR: {e}"
return f"{val._value()} -> {dr}"
else:
return f"{val}"


# OC_PYDEVD_RESOLVER env can take:
# DISABLE: Do not install a pydevd resolver
# USER: Install a resolver for OmegaConf users (default)
# DEV: Install a resolver for OmegaConf developers. Shows underlying data-model in the debugger.
resolver = os.environ.get("OC_PYDEVD_RESOLVER", "USER").upper()
if resolver != "DISABLE": # pragma: no cover
if resolver == "USER":
TypeResolveProvider.register(OmegaConfUserResolver)
elif resolver == "DEV":
TypeResolveProvider.register(OmegaConfDeveloperResolver)
else:
sys.stderr.write(
f"OmegaConf pydev plugin: Not installing. Unknown mode {resolver}. Supported one of [USER, DEV, DISABLE]\n"
)
1 change: 1 addition & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ pytest-mock
sphinx
towncrier
twine
pydevd
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@
tests_require=["pytest"],
url="https://github.com/omry/omegaconf",
keywords="yaml configuration config",
packages=["omegaconf", "omegaconf.grammar", "omegaconf.grammar.gen"],
packages=[
"omegaconf",
"omegaconf.grammar",
"omegaconf.grammar.gen",
"pydevd_plugins",
"pydevd_plugins.extensions",
],
python_requires=">=3.6",
classifiers=[
"Programming Language :: Python :: 3.6",
Expand Down
Loading

0 comments on commit e72ce68

Please sign in to comment.