Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion python/ql/src/Variables/UndefinedExport.ql
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,27 @@ predicate contains_unknown_import_star(ModuleValue m) {
)
}

/** Holds if `m` can provide missing attributes through a module-level `__getattr__`. */
private predicate has_dynamic_module_attributes(ModuleValue m) {
major_version() = 3 and
minor_version() >= 7 and
m.hasAttribute("__getattr__") and
// If points-to cannot resolve the binding, do not assume it is non-callable.
not exists(Value getter, ClassValue cls |
getter = m.attr("__getattr__") and
cls = getter.getClass() and
not cls.isCallable() and
not cls.failedInference(_)
)
}

from ModuleValue m, StringLiteral name, string exported_name
where
declaredInAll(m.getScope(), name) and
exported_name = name.getText() and
not m.hasAttribute(exported_name) and
not is_exported_submodule_name(m, exported_name) and
not contains_unknown_import_star(m) and
not mutates_globals(m)
not mutates_globals(m) and
not has_dynamic_module_attributes(m)
select name, "The name '" + exported_name + "' is exported by __all__ but is not defined."
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---

* The `py/undefined-export` query no longer flags names listed in `__all__` when a Python 3.7 or newer module has a callable module-level `__getattr__` to provide attributes dynamically.
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
| decorated_exports.py:3:33:3:45 | StringLiteral | The name 'not_defined' is exported by __all__ but is not defined. |
| exports.py:1:57:1:64 | StringLiteral | The name 'nosuch' is exported by __all__ but is not defined. |
| nested_getattr.py:1:12:1:26 | StringLiteral | The name 'still_missing' is exported by __all__ but is not defined. |
| non_callable_getattr.py:1:12:1:26 | StringLiteral | The name 'still_missing' is exported by __all__ but is not defined. |
| rebound_getattr.py:1:12:1:26 | StringLiteral | The name 'still_missing' is exported by __all__ but is not defined. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from functools import lru_cache

__all__ = ["available_lazily"]


@lru_cache
def __getattr__(name):
return object()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__all__ = ["available_lazily"]


class DynamicAttribute:
def __init__(self, name):
self.name = name


__getattr__ = DynamicAttribute
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__all__ = ["available_lazily", "also_available_lazily"]


def __getattr__(name):
if name in __all__:
return object()
raise AttributeError(name)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__all__ = ["available_lazily"]


class DynamicAttributes:
def __call__(self, name):
return object()


__getattr__ = DynamicAttributes()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__all__ = ["available_lazily"]

__getattr__ = lambda name: object()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__all__ = ["still_missing"]


class DynamicObject:
def __getattr__(self, name):
return name
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = ["still_missing"]

# This does not enable dynamic module attributes because it is not callable.
__getattr__ = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__all__ = ["still_missing"]


def __getattr__(name):
return object()


__getattr__ = None
Loading